diff --git a/.core_files.yaml b/.core_files.yaml
index 6fd3a74df92..e49ca624393 100644
--- a/.core_files.yaml
+++ b/.core_files.yaml
@@ -79,7 +79,6 @@ components: &components
- homeassistant/components/group/**
- homeassistant/components/hassio/**
- homeassistant/components/homeassistant/**
- - homeassistant/components/homeassistant_hardware/**
- homeassistant/components/http/**
- homeassistant/components/image/**
- homeassistant/components/input_boolean/**
@@ -128,7 +127,6 @@ tests: &tests
- tests/*.py
- tests/auth/**
- tests/backports/**
- - tests/components/conftest.py
- tests/components/diagnostics/**
- tests/components/history/**
- tests/components/logbook/**
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 44c38afdec6..df92976fb76 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -58,13 +58,7 @@
],
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
- },
- "json.schemas": [
- {
- "fileMatch": ["homeassistant/components/*/manifest.json"],
- "url": "./script/json_schemas/manifest_schema.json"
- }
- ]
+ }
}
}
}
diff --git a/.dockerignore b/.dockerignore
index cf975f4215f..7fde7f33fa5 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -7,7 +7,6 @@ docs
# Development
.devcontainer
.vscode
-.tool-versions
# Test related files
tests
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 9deb34d20e9..ad3205c51c8 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1,2 @@
-custom: https://www.openhomefoundation.org
+custom: https://www.nabucasa.com
+github: balloob
diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml
index cc100c48fd8..f05fed50a0f 100644
--- a/.github/workflows/builder.yml
+++ b/.github/workflows/builder.yml
@@ -10,7 +10,7 @@ on:
env:
BUILD_TYPE: core
- DEFAULT_PYTHON: "3.13"
+ DEFAULT_PYTHON: "3.12"
PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
@@ -27,12 +27,12 @@ jobs:
publish: ${{ steps.version.outputs.publish }}
steps:
- name: Checkout the repository
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
with:
fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -69,7 +69,7 @@ jobs:
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: translations
path: translations.tar.gz
@@ -90,7 +90,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
@@ -116,7 +116,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev'
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -242,7 +242,7 @@ jobs:
- green
steps:
- name: Checkout the repository
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set build additional args
run: |
@@ -279,7 +279,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Initialize git
uses: home-assistant/actions/helpers/git-init@master
@@ -321,7 +321,7 @@ jobs:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps:
- name: Checkout the repository
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Install Cosign
uses: sigstore/cosign-installer@v3.7.0
@@ -451,10 +451,10 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -499,7 +499,7 @@ jobs:
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
@@ -531,7 +531,7 @@ jobs:
- name: Generate artifact attestation
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
- uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4
+ uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3
with:
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index fa05f6082a2..14e1a786526 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -37,12 +37,12 @@ on:
type: boolean
env:
- CACHE_VERSION: 11
+ CACHE_VERSION: 10
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9
- HA_SHORT_VERSION: "2024.12"
+ HA_SHORT_VERSION: "2024.11"
DEFAULT_PYTHON: "3.12"
- ALL_PYTHON_VERSIONS: "['3.12', '3.13']"
+ ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support
@@ -93,7 +93,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Generate partial Python venv restore key
id: generate_python_cache_key
run: |
@@ -231,16 +231,16 @@ jobs:
- info
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v4.1.2
+ uses: actions/cache@v4.1.1
with:
path: venv
key: >-
@@ -256,7 +256,7 @@ jobs:
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache@v4.1.2
+ uses: actions/cache@v4.1.1
with:
path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true
@@ -277,16 +277,16 @@ jobs:
- pre-commit
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -295,7 +295,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -317,16 +317,16 @@ jobs:
- pre-commit
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -335,7 +335,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -357,16 +357,16 @@ jobs:
- pre-commit
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -375,7 +375,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -447,7 +447,7 @@ jobs:
- script/hassfest/docker/Dockerfile
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Register hadolint problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
@@ -466,10 +466,10 @@ jobs:
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -482,7 +482,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache@v4.1.2
+ uses: actions/cache@v4.1.1
with:
path: venv
lookup-only: true
@@ -491,7 +491,7 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
- uses: actions/cache@v4.1.2
+ uses: actions/cache@v4.1.1
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
@@ -550,16 +550,16 @@ jobs:
sudo apt-get -y install \
libturbojpeg
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -583,16 +583,16 @@ jobs:
- base
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -615,41 +615,37 @@ jobs:
&& github.event.inputs.mypy-only != 'true'
|| github.event.inputs.audit-licenses-only == 'true')
&& needs.info.outputs.requirements == 'true'
- strategy:
- fail-fast: false
- matrix:
- python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
- - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/checkout@v4.2.1
+ - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
- python-version: ${{ matrix.python-version }}
+ python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- - name: Restore full Python ${{ matrix.python-version }} virtual environment
+ - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- - name: Extract license data
+ - name: Run pip-licenses
run: |
. venv/bin/activate
- python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json
+ pip-licenses --format=json --output-file=licenses.json
- name: Upload licenses
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
- name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
- path: licenses-${{ matrix.python-version }}.json
- - name: Check licenses
+ name: licenses
+ path: licenses.json
+ - name: Process licenses
run: |
. venv/bin/activate
- python -m script.licenses check licenses-${{ matrix.python-version }}.json
+ python -m script.licenses licenses.json
pylint:
name: Check pylint
@@ -664,16 +660,16 @@ jobs:
- base
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -711,16 +707,16 @@ jobs:
- base
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -756,10 +752,10 @@ jobs:
- base
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -772,7 +768,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -780,7 +776,7 @@ jobs:
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
- uses: actions/cache@v4.1.2
+ uses: actions/cache@v4.1.1
with:
path: .mypy_cache
key: >-
@@ -819,7 +815,11 @@ jobs:
needs:
- info
- base
- name: Split tests for full run
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
+ name: Split tests for full run Python ${{ matrix.python-version }}
steps:
- name: Install additional OS dependencies
run: |
@@ -831,16 +831,16 @@ jobs:
libturbojpeg \
libgammu-dev
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
- - name: Set up Python ${{ env.DEFAULT_PYTHON }}
+ uses: actions/checkout@v4.2.1
+ - name: Set up Python ${{ matrix.python-version }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
- python-version: ${{ env.DEFAULT_PYTHON }}
+ python-version: ${{ matrix.python-version }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -852,7 +852,7 @@ jobs:
. venv/bin/activate
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
- name: Upload pytest_buckets
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: pytest_buckets
path: pytest_buckets.txt
@@ -895,16 +895,16 @@ jobs:
libturbojpeg \
libgammu-dev
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -944,8 +944,7 @@ jobs:
-qq \
--timeout=9 \
--durations=10 \
- --numprocesses auto \
- --snapshot-details \
+ -n auto \
--dist=loadfile \
${cov_params[@]} \
-o console_output_style=count \
@@ -954,14 +953,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
@@ -1016,16 +1015,16 @@ jobs:
libturbojpeg \
libmariadb-dev-compat
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -1067,8 +1066,7 @@ jobs:
python3 -b -X dev -m pytest \
-qq \
--timeout=20 \
- --numprocesses 1 \
- --snapshot-details \
+ -n 1 \
${cov_params[@]} \
-o console_output_style=count \
--durations=10 \
@@ -1081,7 +1079,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1089,7 +1087,7 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1100,7 +1098,7 @@ jobs:
./script/check_dirty
pytest-postgres:
- runs-on: ubuntu-24.04
+ runs-on: ubuntu-22.04
services:
postgres:
image: ${{ matrix.postgresql-group }}
@@ -1140,21 +1138,19 @@ jobs:
sudo apt-get -y install \
bluez \
ffmpeg \
- libturbojpeg
- sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
- sudo apt-get -y install \
+ libturbojpeg \
postgresql-server-dev-14
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -1196,8 +1192,7 @@ jobs:
python3 -b -X dev -m pytest \
-qq \
--timeout=9 \
- --numprocesses 1 \
- --snapshot-details \
+ -n 1 \
${cov_params[@]} \
-o console_output_style=count \
--durations=0 \
@@ -1211,7 +1206,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1219,7 +1214,7 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1241,7 +1236,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.8
with:
@@ -1292,16 +1287,16 @@ jobs:
libturbojpeg \
libgammu-dev
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
- uses: actions/cache/restore@v4.1.2
+ uses: actions/cache/restore@v4.1.1
with:
path: venv
fail-on-cache-miss: true
@@ -1343,8 +1338,7 @@ jobs:
python3 -b -X dev -m pytest \
-qq \
--timeout=9 \
- --numprocesses auto \
- --snapshot-details \
+ -n auto \
${cov_params[@]} \
-o console_output_style=count \
--durations=0 \
@@ -1354,14 +1348,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
@@ -1380,7 +1374,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.8
with:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 48e37717232..020d91d5661 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -21,14 +21,14 @@ jobs:
steps:
- name: Check out code from GitHub
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Initialize CodeQL
- uses: github/codeql-action/init@v3.27.3
+ uses: github/codeql-action/init@v3.26.12
with:
languages: python
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3.27.3
+ uses: github/codeql-action/analyze@v3.26.12
with:
category: "/language:python"
diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml
index 3fffc41e60c..b90f38b69bc 100644
--- a/.github/workflows/translations.yml
+++ b/.github/workflows/translations.yml
@@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index b9f54bba081..1983282d53c 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -32,11 +32,11 @@ jobs:
architectures: ${{ steps.info.outputs.architectures }}
steps:
- name: Checkout the repository
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
- uses: actions/setup-python@v5.3.0
+ uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -79,7 +79,7 @@ jobs:
) > .env_file
- name: Upload env_file
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: env_file
path: ./.env_file
@@ -87,7 +87,7 @@ jobs:
overwrite: true
- name: Upload requirements_diff
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: requirements_diff
path: ./requirements_diff.txt
@@ -99,7 +99,7 @@ jobs:
python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels
- uses: actions/upload-artifact@v4.4.3
+ uses: actions/upload-artifact@v4.4.2
with:
name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt
@@ -112,11 +112,11 @@ jobs:
strategy:
fail-fast: false
matrix:
- abi: ["cp312", "cp313"]
+ abi: ["cp312"]
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Download env_file
uses: actions/download-artifact@v4.1.8
@@ -135,14 +135,14 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
- uses: home-assistant/wheels@2024.11.0
+ uses: home-assistant/wheels@2024.07.1
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
- apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev"
+ apk: "libffi-dev;openssl-dev;yaml-dev;nasm"
skip-binary: aiohttp;multidict;yarl
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
@@ -156,11 +156,11 @@ jobs:
strategy:
fail-fast: false
matrix:
- abi: ["cp312", "cp313"]
+ abi: ["cp312"]
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v4.2.1
- name: Download env_file
uses: actions/download-artifact@v4.1.8
@@ -198,7 +198,6 @@ jobs:
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt
- name: Create requirements for cython<3
- if: matrix.abi == 'cp312'
run: |
# Some dependencies still require 'cython<3'
# and don't yet use isolated build environments.
@@ -209,8 +208,7 @@ jobs:
cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt
- name: Build wheels (old cython)
- uses: home-assistant/wheels@2024.11.0
- if: matrix.abi == 'cp312'
+ uses: home-assistant/wheels@2024.07.1
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -225,43 +223,43 @@ jobs:
pip: "'cython<3'"
- name: Build wheels (part 1)
- uses: home-assistant/wheels@2024.11.0
+ uses: home-assistant/wheels@2024.07.1
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
- apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev"
- skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
+ apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
+ skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa"
- name: Build wheels (part 2)
- uses: home-assistant/wheels@2024.11.0
+ uses: home-assistant/wheels@2024.07.1
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
- apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev"
- skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
+ apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
+ skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab"
- name: Build wheels (part 3)
- uses: home-assistant/wheels@2024.11.0
+ uses: home-assistant/wheels@2024.07.1
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
- apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev"
- skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
+ apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
+ skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac"
diff --git a/.gitignore b/.gitignore
index 241255253c5..9bbf5bb81d4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,7 +79,6 @@ pytest-*.txt
.pydevproject
.python-version
-.tool-versions
# emacs auto backups
*~
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 56fbabe8087..af0fbd0af7f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.7.3
+ rev: v0.6.9
hooks:
- id: ruff
args:
@@ -90,7 +90,7 @@ repos:
pass_filenames: false
language: script
types: [text]
- files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$
+ files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml)$
- id: hassfest-mypy-config
name: hassfest-mypy-config
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config
diff --git a/.strict-typing b/.strict-typing
index b0fd74bce54..c0b65c0f3da 100644
--- a/.strict-typing
+++ b/.strict-typing
@@ -124,7 +124,6 @@ homeassistant.components.bryant_evolution.*
homeassistant.components.bthome.*
homeassistant.components.button.*
homeassistant.components.calendar.*
-homeassistant.components.cambridge_audio.*
homeassistant.components.camera.*
homeassistant.components.canary.*
homeassistant.components.cert_expiry.*
@@ -209,7 +208,6 @@ homeassistant.components.geo_location.*
homeassistant.components.geocaching.*
homeassistant.components.gios.*
homeassistant.components.glances.*
-homeassistant.components.go2rtc.*
homeassistant.components.goalzero.*
homeassistant.components.google.*
homeassistant.components.google_assistant_sdk.*
@@ -304,6 +302,7 @@ homeassistant.components.lookin.*
homeassistant.components.luftdaten.*
homeassistant.components.madvr.*
homeassistant.components.manual.*
+homeassistant.components.map.*
homeassistant.components.mastodon.*
homeassistant.components.matrix.*
homeassistant.components.matter.*
@@ -324,13 +323,11 @@ homeassistant.components.moon.*
homeassistant.components.mopeka.*
homeassistant.components.motionmount.*
homeassistant.components.mqtt.*
-homeassistant.components.music_assistant.*
homeassistant.components.my.*
homeassistant.components.mysensors.*
homeassistant.components.myuplink.*
homeassistant.components.nam.*
homeassistant.components.nanoleaf.*
-homeassistant.components.nasweb.*
homeassistant.components.neato.*
homeassistant.components.nest.*
homeassistant.components.netatmo.*
@@ -340,7 +337,6 @@ homeassistant.components.nfandroidtv.*
homeassistant.components.nightscout.*
homeassistant.components.nissan_leaf.*
homeassistant.components.no_ip.*
-homeassistant.components.nordpool.*
homeassistant.components.notify.*
homeassistant.components.notion.*
homeassistant.components.number.*
@@ -429,7 +425,6 @@ homeassistant.components.snooz.*
homeassistant.components.solarlog.*
homeassistant.components.sonarr.*
homeassistant.components.speedtestdotnet.*
-homeassistant.components.spotify.*
homeassistant.components.sql.*
homeassistant.components.squeezebox.*
homeassistant.components.ssdp.*
diff --git a/.vscode/settings.default.json b/.vscode/settings.default.json
index ace0a988bf5..681698d08b3 100644
--- a/.vscode/settings.default.json
+++ b/.vscode/settings.default.json
@@ -6,13 +6,5 @@
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
"python.testing.pytestEnabled": false,
// https://code.visualstudio.com/docs/python/linting#_general-settings
- "pylint.importStrategy": "fromEnvironment",
- "json.schemas": [
- {
- "fileMatch": [
- "homeassistant/components/*/manifest.json"
- ],
- "url": "./script/json_schemas/manifest_schema.json"
- }
- ]
+ "pylint.importStrategy": "fromEnvironment"
}
diff --git a/CODEOWNERS b/CODEOWNERS
index e204463695e..9a4379fc342 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -40,8 +40,6 @@ build.json @home-assistant/supervisor
# Integrations
/homeassistant/components/abode/ @shred86
/tests/components/abode/ @shred86
-/homeassistant/components/acaia/ @zweckj
-/tests/components/acaia/ @zweckj
/homeassistant/components/accuweather/ @bieniu
/tests/components/accuweather/ @bieniu
/homeassistant/components/acmeda/ @atmurray
@@ -498,8 +496,8 @@ build.json @home-assistant/supervisor
/tests/components/freebox/ @hacf-fr @Quentame
/homeassistant/components/freedompro/ @stefano055415
/tests/components/freedompro/ @stefano055415
-/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
-/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
+/homeassistant/components/fritz/ @mammuth @AaronDavidSchneider @chemelli74 @mib1185
+/tests/components/fritz/ @mammuth @AaronDavidSchneider @chemelli74 @mib1185
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
/tests/components/fritzbox/ @mib1185 @flabbamann
/homeassistant/components/fritzbox_callmonitor/ @cdce8p
@@ -619,8 +617,8 @@ build.json @home-assistant/supervisor
/tests/components/hlk_sw16/ @jameshilliard
/homeassistant/components/holiday/ @jrieger @gjohansson-ST
/tests/components/holiday/ @jrieger @gjohansson-ST
-/homeassistant/components/home_connect/ @DavidMStraub @Diegorro98
-/tests/components/home_connect/ @DavidMStraub @Diegorro98
+/homeassistant/components/home_connect/ @DavidMStraub
+/tests/components/home_connect/ @DavidMStraub
/homeassistant/components/homeassistant/ @home-assistant/core
/tests/components/homeassistant/ @home-assistant/core
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
@@ -661,8 +659,6 @@ build.json @home-assistant/supervisor
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/homeassistant/components/husqvarna_automower/ @Thomas55555
/tests/components/husqvarna_automower/ @Thomas55555
-/homeassistant/components/husqvarna_automower_ble/ @alistair23
-/tests/components/husqvarna_automower_ble/ @alistair23
/homeassistant/components/huum/ @frwickst
/tests/components/huum/ @frwickst
/homeassistant/components/hvv_departures/ @vigonotion
@@ -823,8 +819,6 @@ build.json @home-assistant/supervisor
/tests/components/lektrico/ @lektrico
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
/tests/components/lg_netcast/ @Drafteed @splinter98
-/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
-/tests/components/lg_thinq/ @LG-ThinQ-Integration
/homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob
/homeassistant/components/lifx/ @Djelibeybi
@@ -956,8 +950,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/msteams/ @peroyvind
/homeassistant/components/mullvad/ @meichthys
/tests/components/mullvad/ @meichthys
-/homeassistant/components/music_assistant/ @music-assistant
-/tests/components/music_assistant/ @music-assistant
/homeassistant/components/mutesync/ @currentoor
/tests/components/mutesync/ @currentoor
/homeassistant/components/my/ @home-assistant/core
@@ -972,8 +964,6 @@ build.json @home-assistant/supervisor
/tests/components/nam/ @bieniu
/homeassistant/components/nanoleaf/ @milanmeu @joostlek
/tests/components/nanoleaf/ @milanmeu @joostlek
-/homeassistant/components/nasweb/ @nasWebio
-/tests/components/nasweb/ @nasWebio
/homeassistant/components/neato/ @Santobert
/tests/components/neato/ @Santobert
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM
@@ -1014,8 +1004,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/noaa_tides/ @jdelaney72
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
/tests/components/nobo_hub/ @echoromeo @oyvindwe
-/homeassistant/components/nordpool/ @gjohansson-ST
-/tests/components/nordpool/ @gjohansson-ST
/homeassistant/components/notify/ @home-assistant/core
/tests/components/notify/ @home-assistant/core
/homeassistant/components/notify_events/ @matrozov @papajojo
@@ -1059,7 +1047,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/onewire/ @garbled1 @epenet
/tests/components/onewire/ @garbled1 @epenet
/homeassistant/components/onkyo/ @arturpragacz
-/tests/components/onkyo/ @arturpragacz
/homeassistant/components/onvif/ @hunterjm
/tests/components/onvif/ @hunterjm
/homeassistant/components/open_meteo/ @frenck
@@ -1101,10 +1088,10 @@ build.json @home-assistant/supervisor
/tests/components/ovo_energy/ @timmo001
/homeassistant/components/p1_monitor/ @klaasnicolaas
/tests/components/p1_monitor/ @klaasnicolaas
-/homeassistant/components/palazzetti/ @dotvav
-/tests/components/palazzetti/ @dotvav
/homeassistant/components/panel_custom/ @home-assistant/frontend
/tests/components/panel_custom/ @home-assistant/frontend
+/homeassistant/components/panel_iframe/ @home-assistant/frontend
+/tests/components/panel_iframe/ @home-assistant/frontend
/homeassistant/components/peco/ @IceBotYT
/tests/components/peco/ @IceBotYT
/homeassistant/components/pegel_online/ @mib1185
@@ -1252,8 +1239,8 @@ build.json @home-assistant/supervisor
/tests/components/roku/ @ctalkington
/homeassistant/components/romy/ @xeniter
/tests/components/romy/ @xeniter
-/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
-/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
+/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn @Xitee1 @Orhideous
+/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Xitee1 @Orhideous
/homeassistant/components/roon/ @pavoni
/tests/components/roon/ @pavoni
/homeassistant/components/rpi_power/ @shenxn @swetoast
@@ -1346,8 +1333,6 @@ build.json @home-assistant/supervisor
/tests/components/siren/ @home-assistant/core @raman325
/homeassistant/components/sisyphus/ @jkeljo
/homeassistant/components/sky_hub/ @rogerselwyn
-/homeassistant/components/sky_remote/ @dunnmj @saty9
-/tests/components/sky_remote/ @dunnmj @saty9
/homeassistant/components/skybell/ @tkdrob
/tests/components/skybell/ @tkdrob
/homeassistant/components/slack/ @tkdrob @fletcherau
@@ -1366,7 +1351,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/smarttub/ @mdz
/tests/components/smarttub/ @mdz
/homeassistant/components/smarty/ @z0mbieprocess
-/tests/components/smarty/ @z0mbieprocess
/homeassistant/components/smhi/ @gjohansson-ST
/tests/components/smhi/ @gjohansson-ST
/homeassistant/components/smlight/ @tl-sl
@@ -1430,8 +1414,8 @@ build.json @home-assistant/supervisor
/tests/components/stt/ @home-assistant/core
/homeassistant/components/subaru/ @G-Two
/tests/components/subaru/ @G-Two
-/homeassistant/components/suez_water/ @ooii @jb101010-2
-/tests/components/suez_water/ @ooii @jb101010-2
+/homeassistant/components/suez_water/ @ooii
+/tests/components/suez_water/ @ooii
/homeassistant/components/sun/ @Swamp-Ig
/tests/components/sun/ @Swamp-Ig
/homeassistant/components/sunweg/ @rokam
@@ -1489,8 +1473,8 @@ build.json @home-assistant/supervisor
/tests/components/tedee/ @patrickhilker @zweckj
/homeassistant/components/tellduslive/ @fredrike
/tests/components/tellduslive/ @fredrike
-/homeassistant/components/template/ @PhracturedBlue @home-assistant/core
-/tests/components/template/ @PhracturedBlue @home-assistant/core
+/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core
+/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
/homeassistant/components/tesla_fleet/ @Bre77
/tests/components/tesla_fleet/ @Bre77
/homeassistant/components/tesla_wall_connector/ @einarhauks
diff --git a/Dockerfile b/Dockerfile
index 15574192093..44edbdf8e3e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,13 +7,12 @@ FROM ${BUILD_FROM}
# Synchronize with homeassistant/core.py:async_stop
ENV \
S6_SERVICES_GRACETIME=240000 \
- UV_SYSTEM_PYTHON=true \
- UV_NO_CACHE=true
+ UV_SYSTEM_PYTHON=true
ARG QEMU_CPU
# Install uv
-RUN pip3 install uv==0.5.0
+RUN pip3 install uv==0.4.17
WORKDIR /usr/src
@@ -55,7 +54,7 @@ RUN \
"armv7") go2rtc_suffix='arm' ;; \
*) go2rtc_suffix=${BUILD_ARCH} ;; \
esac \
- && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.7/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
+ && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.4/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& chmod +x /bin/go2rtc \
# Verify go2rtc can be executed
&& go2rtc --version
diff --git a/Dockerfile.dev b/Dockerfile.dev
index 48f582a1581..d05c6df425c 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -35,9 +35,6 @@ RUN \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
-# Add go2rtc binary
-COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc
-
# Install uv
RUN pip3 install uv
diff --git a/build.yaml b/build.yaml
index a8755bbbf5c..13618740ab8 100644
--- a/build.yaml
+++ b/build.yaml
@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
- aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.11.0
- armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.11.0
- armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.11.0
- amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.11.0
- i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.11.0
+ aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.06.1
+ armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.06.1
+ armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.06.1
+ amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.06.1
+ i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.06.1
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io
diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py
index b9d98832705..4c870e94b24 100644
--- a/homeassistant/__main__.py
+++ b/homeassistant/__main__.py
@@ -9,7 +9,6 @@ import os
import sys
import threading
-from .backup_restore import restore_backup
from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
FAULT_LOG_FILENAME = "home-assistant.log.fault"
@@ -183,9 +182,6 @@ def main() -> int:
return scripts.run(args.script)
config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config))
- if restore_backup(config_dir):
- return RESTART_EXIT_CODE
-
ensure_config_path(config_dir)
# pylint: disable-next=import-outside-toplevel
diff --git a/homeassistant/backup_restore.py b/homeassistant/backup_restore.py
deleted file mode 100644
index 32991dfb2d3..00000000000
--- a/homeassistant/backup_restore.py
+++ /dev/null
@@ -1,126 +0,0 @@
-"""Home Assistant module to handle restoring backups."""
-
-from dataclasses import dataclass
-import json
-import logging
-from pathlib import Path
-import shutil
-import sys
-from tempfile import TemporaryDirectory
-
-from awesomeversion import AwesomeVersion
-import securetar
-
-from .const import __version__ as HA_VERSION
-
-RESTORE_BACKUP_FILE = ".HA_RESTORE"
-KEEP_PATHS = ("backups",)
-
-_LOGGER = logging.getLogger(__name__)
-
-
-@dataclass
-class RestoreBackupFileContent:
- """Definition for restore backup file content."""
-
- backup_file_path: Path
-
-
-def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent | None:
- """Return the contents of the restore backup file."""
- instruction_path = config_dir.joinpath(RESTORE_BACKUP_FILE)
- try:
- instruction_content = json.loads(instruction_path.read_text(encoding="utf-8"))
- return RestoreBackupFileContent(
- backup_file_path=Path(instruction_content["path"])
- )
- except (FileNotFoundError, json.JSONDecodeError):
- return None
-
-
-def _clear_configuration_directory(config_dir: Path) -> None:
- """Delete all files and directories in the config directory except for the backups directory."""
- keep_paths = [config_dir.joinpath(path) for path in KEEP_PATHS]
- config_contents = sorted(
- [entry for entry in config_dir.iterdir() if entry not in keep_paths]
- )
-
- for entry in config_contents:
- entrypath = config_dir.joinpath(entry)
-
- if entrypath.is_file():
- entrypath.unlink()
- elif entrypath.is_dir():
- shutil.rmtree(entrypath)
-
-
-def _extract_backup(config_dir: Path, backup_file_path: Path) -> None:
- """Extract the backup file to the config directory."""
- with (
- TemporaryDirectory() as tempdir,
- securetar.SecureTarFile(
- backup_file_path,
- gzip=False,
- mode="r",
- ) as ostf,
- ):
- ostf.extractall(
- path=Path(tempdir, "extracted"),
- members=securetar.secure_path(ostf),
- filter="fully_trusted",
- )
- backup_meta_file = Path(tempdir, "extracted", "backup.json")
- backup_meta = json.loads(backup_meta_file.read_text(encoding="utf8"))
-
- if (
- backup_meta_version := AwesomeVersion(
- backup_meta["homeassistant"]["version"]
- )
- ) > HA_VERSION:
- raise ValueError(
- f"You need at least Home Assistant version {backup_meta_version} to restore this backup"
- )
-
- with securetar.SecureTarFile(
- Path(
- tempdir,
- "extracted",
- f"homeassistant.tar{'.gz' if backup_meta["compressed"] else ''}",
- ),
- gzip=backup_meta["compressed"],
- mode="r",
- ) as istf:
- for member in istf.getmembers():
- if member.name == "data":
- continue
- member.name = member.name.replace("data/", "")
- _clear_configuration_directory(config_dir)
- istf.extractall(
- path=config_dir,
- members=[
- member
- for member in securetar.secure_path(istf)
- if member.name != "data"
- ],
- filter="fully_trusted",
- )
-
-
-def restore_backup(config_dir_path: str) -> bool:
- """Restore the backup file if any.
-
- Returns True if a restore backup file was found and restored, False otherwise.
- """
- config_dir = Path(config_dir_path)
- if not (restore_content := restore_backup_file_content(config_dir)):
- return False
-
- logging.basicConfig(stream=sys.stdout, level=logging.INFO)
- backup_file_path = restore_content.backup_file_path
- _LOGGER.info("Restoring %s", backup_file_path)
- try:
- _extract_backup(config_dir, backup_file_path)
- except FileNotFoundError as err:
- raise ValueError(f"Backup file {backup_file_path} does not exist") from err
- _LOGGER.info("Restore complete, restarting")
- return True
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index 1034223051c..742a293e4c4 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -70,7 +70,6 @@ from .const import (
REQUIRED_NEXT_PYTHON_VER,
SIGNAL_BOOTSTRAP_INTEGRATIONS,
)
-from .core_config import async_process_ha_core_config
from .exceptions import HomeAssistantError
from .helpers import (
area_registry,
@@ -480,7 +479,7 @@ async def async_from_config_dict(
core_config = config.get(core.DOMAIN, {})
try:
- await async_process_ha_core_config(hass, core_config)
+ await conf_util.async_process_ha_core_config(hass, core_config)
except vol.Invalid as config_err:
conf_util.async_log_schema_error(config_err, core.DOMAIN, core_config, hass)
async_notify_setup_error(hass, core.DOMAIN)
@@ -515,7 +514,7 @@ async def async_from_config_dict(
issue_registry.async_create_issue(
hass,
core.DOMAIN,
- f"python_version_{required_python_version}",
+ "python_version",
is_fixable=False,
severity=issue_registry.IssueSeverity.WARNING,
breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE,
diff --git a/homeassistant/brands/husqvarna.json b/homeassistant/brands/husqvarna.json
deleted file mode 100644
index a01eba75232..00000000000
--- a/homeassistant/brands/husqvarna.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "domain": "husqvarna",
- "name": "Husqvarna",
- "integrations": ["husqvarna_automower", "husqvarna_automower_ble"]
-}
diff --git a/homeassistant/brands/lg.json b/homeassistant/brands/lg.json
index 02bd58c0d1c..350db80b5f3 100644
--- a/homeassistant/brands/lg.json
+++ b/homeassistant/brands/lg.json
@@ -1,5 +1,5 @@
{
"domain": "lg",
"name": "LG",
- "integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
+ "integrations": ["lg_netcast", "lg_soundbar", "webostv"]
}
diff --git a/homeassistant/brands/sky.json b/homeassistant/brands/sky.json
deleted file mode 100644
index 3ab0cbbe5bd..00000000000
--- a/homeassistant/brands/sky.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "domain": "sky",
- "name": "Sky",
- "integrations": ["sky_hub", "sky_remote"]
-}
diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py
index 4ec59ca4c39..b58a4757785 100644
--- a/homeassistant/components/abode/alarm_control_panel.py
+++ b/homeassistant/components/abode/alarm_control_panel.py
@@ -7,9 +7,13 @@ from jaraco.abode.devices.alarm import Alarm
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -40,14 +44,14 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanelEntity):
_device: Alarm
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the device."""
if self._device.is_standby:
- return AlarmControlPanelState.DISARMED
+ return STATE_ALARM_DISARMED
if self._device.is_away:
- return AlarmControlPanelState.ARMED_AWAY
+ return STATE_ALARM_ARMED_AWAY
if self._device.is_home:
- return AlarmControlPanelState.ARMED_HOME
+ return STATE_ALARM_ARMED_HOME
return None
def alarm_disarm(self, code: str | None = None) -> None:
diff --git a/homeassistant/components/acaia/__init__.py b/homeassistant/components/acaia/__init__.py
deleted file mode 100644
index dfdb4cb935d..00000000000
--- a/homeassistant/components/acaia/__init__.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""Initialize the Acaia component."""
-
-from homeassistant.const import Platform
-from homeassistant.core import HomeAssistant
-
-from .coordinator import AcaiaConfigEntry, AcaiaCoordinator
-
-PLATFORMS = [
- Platform.BUTTON,
-]
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: AcaiaConfigEntry) -> bool:
- """Set up acaia as config entry."""
-
- coordinator = AcaiaCoordinator(hass, entry)
- await coordinator.async_config_entry_first_refresh()
-
- entry.runtime_data = coordinator
-
- await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
-
- return True
-
-
-async def async_unload_entry(hass: HomeAssistant, entry: AcaiaConfigEntry) -> bool:
- """Unload a config entry."""
-
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/acaia/button.py b/homeassistant/components/acaia/button.py
deleted file mode 100644
index 50671eecbba..00000000000
--- a/homeassistant/components/acaia/button.py
+++ /dev/null
@@ -1,61 +0,0 @@
-"""Button entities for Acaia scales."""
-
-from collections.abc import Callable, Coroutine
-from dataclasses import dataclass
-from typing import Any
-
-from aioacaia.acaiascale import AcaiaScale
-
-from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from .coordinator import AcaiaConfigEntry
-from .entity import AcaiaEntity
-
-
-@dataclass(kw_only=True, frozen=True)
-class AcaiaButtonEntityDescription(ButtonEntityDescription):
- """Description for acaia button entities."""
-
- press_fn: Callable[[AcaiaScale], Coroutine[Any, Any, None]]
-
-
-BUTTONS: tuple[AcaiaButtonEntityDescription, ...] = (
- AcaiaButtonEntityDescription(
- key="tare",
- translation_key="tare",
- press_fn=lambda scale: scale.tare(),
- ),
- AcaiaButtonEntityDescription(
- key="reset_timer",
- translation_key="reset_timer",
- press_fn=lambda scale: scale.reset_timer(),
- ),
- AcaiaButtonEntityDescription(
- key="start_stop",
- translation_key="start_stop",
- press_fn=lambda scale: scale.start_stop_timer(),
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: AcaiaConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up button entities and services."""
-
- coordinator = entry.runtime_data
- async_add_entities(AcaiaButton(coordinator, description) for description in BUTTONS)
-
-
-class AcaiaButton(AcaiaEntity, ButtonEntity):
- """Representation of an Acaia button."""
-
- entity_description: AcaiaButtonEntityDescription
-
- async def async_press(self) -> None:
- """Handle the button press."""
- await self.entity_description.press_fn(self._scale)
diff --git a/homeassistant/components/acaia/config_flow.py b/homeassistant/components/acaia/config_flow.py
deleted file mode 100644
index 36727059c8a..00000000000
--- a/homeassistant/components/acaia/config_flow.py
+++ /dev/null
@@ -1,149 +0,0 @@
-"""Config flow for Acaia integration."""
-
-import logging
-from typing import Any
-
-from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError, AcaiaUnknownDevice
-from aioacaia.helpers import is_new_scale
-import voluptuous as vol
-
-from homeassistant.components.bluetooth import (
- BluetoothServiceInfoBleak,
- async_discovered_service_info,
-)
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_ADDRESS, CONF_NAME
-from homeassistant.helpers.device_registry import format_mac
-from homeassistant.helpers.selector import (
- SelectOptionDict,
- SelectSelector,
- SelectSelectorConfig,
- SelectSelectorMode,
-)
-
-from .const import CONF_IS_NEW_STYLE_SCALE, DOMAIN
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
- """Handle a config flow for acaia."""
-
- def __init__(self) -> None:
- """Initialize the config flow."""
- self._discovered: dict[str, Any] = {}
- self._discovered_devices: dict[str, str] = {}
-
- async def async_step_user(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle a flow initialized by the user."""
-
- errors: dict[str, str] = {}
-
- if user_input is not None:
- mac = format_mac(user_input[CONF_ADDRESS])
- try:
- is_new_style_scale = await is_new_scale(mac)
- except AcaiaDeviceNotFound:
- errors["base"] = "device_not_found"
- except AcaiaError:
- _LOGGER.exception("Error occurred while connecting to the scale")
- errors["base"] = "unknown"
- except AcaiaUnknownDevice:
- return self.async_abort(reason="unsupported_device")
- else:
- await self.async_set_unique_id(mac)
- self._abort_if_unique_id_configured()
-
- if not errors:
- return self.async_create_entry(
- title=self._discovered_devices[user_input[CONF_ADDRESS]],
- data={
- CONF_ADDRESS: mac,
- CONF_IS_NEW_STYLE_SCALE: is_new_style_scale,
- },
- )
-
- for device in async_discovered_service_info(self.hass):
- self._discovered_devices[device.address] = device.name
-
- if not self._discovered_devices:
- return self.async_abort(reason="no_devices_found")
-
- options = [
- SelectOptionDict(
- value=device_mac,
- label=f"{device_name} ({device_mac})",
- )
- for device_mac, device_name in self._discovered_devices.items()
- ]
-
- return self.async_show_form(
- step_id="user",
- data_schema=vol.Schema(
- {
- vol.Required(CONF_ADDRESS): SelectSelector(
- SelectSelectorConfig(
- options=options,
- mode=SelectSelectorMode.DROPDOWN,
- )
- )
- }
- ),
- errors=errors,
- )
-
- async def async_step_bluetooth(
- self, discovery_info: BluetoothServiceInfoBleak
- ) -> ConfigFlowResult:
- """Handle a discovered Bluetooth device."""
-
- self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address)
- self._discovered[CONF_NAME] = discovery_info.name
-
- await self.async_set_unique_id(mac)
- self._abort_if_unique_id_configured()
-
- try:
- self._discovered[CONF_IS_NEW_STYLE_SCALE] = await is_new_scale(
- discovery_info.address
- )
- except AcaiaDeviceNotFound:
- _LOGGER.debug("Device not found during discovery")
- return self.async_abort(reason="device_not_found")
- except AcaiaError:
- _LOGGER.debug(
- "Error occurred while connecting to the scale during discovery",
- exc_info=True,
- )
- return self.async_abort(reason="unknown")
- except AcaiaUnknownDevice:
- _LOGGER.debug("Unsupported device during discovery")
- return self.async_abort(reason="unsupported_device")
-
- return await self.async_step_bluetooth_confirm()
-
- async def async_step_bluetooth_confirm(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle confirmation of Bluetooth discovery."""
-
- if user_input is not None:
- return self.async_create_entry(
- title=self._discovered[CONF_NAME],
- data={
- CONF_ADDRESS: self._discovered[CONF_ADDRESS],
- CONF_IS_NEW_STYLE_SCALE: self._discovered[CONF_IS_NEW_STYLE_SCALE],
- },
- )
-
- self.context["title_placeholders"] = placeholders = {
- CONF_NAME: self._discovered[CONF_NAME]
- }
-
- self._set_confirm_only()
- return self.async_show_form(
- step_id="bluetooth_confirm",
- description_placeholders=placeholders,
- )
diff --git a/homeassistant/components/acaia/const.py b/homeassistant/components/acaia/const.py
deleted file mode 100644
index c603578763d..00000000000
--- a/homeassistant/components/acaia/const.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""Constants for component."""
-
-DOMAIN = "acaia"
-CONF_IS_NEW_STYLE_SCALE = "is_new_style_scale"
diff --git a/homeassistant/components/acaia/coordinator.py b/homeassistant/components/acaia/coordinator.py
deleted file mode 100644
index bd915b42408..00000000000
--- a/homeassistant/components/acaia/coordinator.py
+++ /dev/null
@@ -1,86 +0,0 @@
-"""Coordinator for Acaia integration."""
-
-from __future__ import annotations
-
-from datetime import timedelta
-import logging
-
-from aioacaia.acaiascale import AcaiaScale
-from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_ADDRESS
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
-
-from .const import CONF_IS_NEW_STYLE_SCALE
-
-SCAN_INTERVAL = timedelta(seconds=15)
-
-_LOGGER = logging.getLogger(__name__)
-
-type AcaiaConfigEntry = ConfigEntry[AcaiaCoordinator]
-
-
-class AcaiaCoordinator(DataUpdateCoordinator[None]):
- """Class to handle fetching data from the scale."""
-
- config_entry: AcaiaConfigEntry
-
- def __init__(self, hass: HomeAssistant, entry: AcaiaConfigEntry) -> None:
- """Initialize coordinator."""
- super().__init__(
- hass,
- _LOGGER,
- name="acaia coordinator",
- update_interval=SCAN_INTERVAL,
- config_entry=entry,
- )
-
- self._scale = AcaiaScale(
- address_or_ble_device=entry.data[CONF_ADDRESS],
- name=entry.title,
- is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
- notify_callback=self.async_update_listeners,
- )
-
- @property
- def scale(self) -> AcaiaScale:
- """Return the scale object."""
- return self._scale
-
- async def _async_update_data(self) -> None:
- """Fetch data."""
-
- # scale is already connected, return
- if self._scale.connected:
- return
-
- # scale is not connected, try to connect
- try:
- await self._scale.connect(setup_tasks=False)
- except (AcaiaDeviceNotFound, AcaiaError, TimeoutError) as ex:
- _LOGGER.debug(
- "Could not connect to scale: %s, Error: %s",
- self.config_entry.data[CONF_ADDRESS],
- ex,
- )
- self._scale.device_disconnected_handler(notify=False)
- return
-
- # connected, set up background tasks
- if not self._scale.heartbeat_task or self._scale.heartbeat_task.done():
- self._scale.heartbeat_task = self.config_entry.async_create_background_task(
- hass=self.hass,
- target=self._scale.send_heartbeats(),
- name="acaia_heartbeat_task",
- )
-
- if not self._scale.process_queue_task or self._scale.process_queue_task.done():
- self._scale.process_queue_task = (
- self.config_entry.async_create_background_task(
- hass=self.hass,
- target=self._scale.process_queue(),
- name="acaia_process_queue_task",
- )
- )
diff --git a/homeassistant/components/acaia/entity.py b/homeassistant/components/acaia/entity.py
deleted file mode 100644
index 8a2108d2687..00000000000
--- a/homeassistant/components/acaia/entity.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""Base class for Acaia entities."""
-
-from dataclasses import dataclass
-
-from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.entity import EntityDescription
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from .const import DOMAIN
-from .coordinator import AcaiaCoordinator
-
-
-@dataclass
-class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]):
- """Common elements for all entities."""
-
- _attr_has_entity_name = True
-
- def __init__(
- self,
- coordinator: AcaiaCoordinator,
- entity_description: EntityDescription,
- ) -> None:
- """Initialize the entity."""
- super().__init__(coordinator)
- self.entity_description = entity_description
- self._scale = coordinator.scale
- self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}"
-
- self._attr_device_info = DeviceInfo(
- identifiers={(DOMAIN, self._scale.mac)},
- manufacturer="Acaia",
- model=self._scale.model,
- suggested_area="Kitchen",
- )
-
- @property
- def available(self) -> bool:
- """Returns whether entity is available."""
- return super().available and self._scale.connected
diff --git a/homeassistant/components/acaia/icons.json b/homeassistant/components/acaia/icons.json
deleted file mode 100644
index aeab07ee912..00000000000
--- a/homeassistant/components/acaia/icons.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "entity": {
- "button": {
- "tare": {
- "default": "mdi:scale-balance"
- },
- "reset_timer": {
- "default": "mdi:timer-refresh"
- },
- "start_stop": {
- "default": "mdi:timer-play"
- }
- }
- }
-}
diff --git a/homeassistant/components/acaia/manifest.json b/homeassistant/components/acaia/manifest.json
deleted file mode 100644
index c907a70a38e..00000000000
--- a/homeassistant/components/acaia/manifest.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "domain": "acaia",
- "name": "Acaia",
- "bluetooth": [
- {
- "manufacturer_id": 16962
- },
- {
- "local_name": "ACAIA*"
- },
- {
- "local_name": "PYXIS-*"
- },
- {
- "local_name": "LUNAR-*"
- },
- {
- "local_name": "PROCHBT001"
- }
- ],
- "codeowners": ["@zweckj"],
- "config_flow": true,
- "dependencies": ["bluetooth_adapters"],
- "documentation": "https://www.home-assistant.io/integrations/acaia",
- "integration_type": "device",
- "iot_class": "local_push",
- "loggers": ["aioacaia"],
- "requirements": ["aioacaia==0.1.6"]
-}
diff --git a/homeassistant/components/acaia/strings.json b/homeassistant/components/acaia/strings.json
deleted file mode 100644
index f6a1aeb66fd..00000000000
--- a/homeassistant/components/acaia/strings.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "config": {
- "flow_title": "{name}",
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
- "unsupported_device": "This device is not supported."
- },
- "error": {
- "device_not_found": "Device could not be found.",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- },
- "step": {
- "bluetooth_confirm": {
- "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
- },
- "user": {
- "description": "[%key:component::bluetooth::config::step::user::description%]",
- "data": {
- "address": "[%key:common::config_flow::data::device%]"
- }
- }
- }
- },
- "entity": {
- "button": {
- "tare": {
- "name": "Tare"
- },
- "reset_timer": {
- "name": "Reset timer"
- },
- "start_stop": {
- "name": "Start/stop timer"
- }
- }
- }
-}
diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py
index c046933d5d5..3d52df765e6 100644
--- a/homeassistant/components/accuweather/__init__.py
+++ b/homeassistant/components/accuweather/__init__.py
@@ -2,11 +2,13 @@
from __future__ import annotations
+from dataclasses import dataclass
import logging
from accuweather import AccuWeather
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -14,9 +16,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, UPDATE_INTERVAL_DAILY_FORECAST, UPDATE_INTERVAL_OBSERVATION
from .coordinator import (
- AccuWeatherConfigEntry,
AccuWeatherDailyForecastDataUpdateCoordinator,
- AccuWeatherData,
AccuWeatherObservationDataUpdateCoordinator,
)
@@ -25,6 +25,17 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
+@dataclass
+class AccuWeatherData:
+ """Data for AccuWeather integration."""
+
+ coordinator_observation: AccuWeatherObservationDataUpdateCoordinator
+ coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
+
+
+type AccuWeatherConfigEntry = ConfigEntry[AccuWeatherData]
+
+
async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) -> bool:
"""Set up AccuWeather as config entry."""
api_key: str = entry.data[CONF_API_KEY]
@@ -39,7 +50,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry)
coordinator_observation = AccuWeatherObservationDataUpdateCoordinator(
hass,
- entry,
accuweather,
name,
"observation",
@@ -48,7 +58,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry)
coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator(
hass,
- entry,
accuweather,
name,
"daily forecast",
diff --git a/homeassistant/components/accuweather/coordinator.py b/homeassistant/components/accuweather/coordinator.py
index 40ff3ad2c87..26fadd6806c 100644
--- a/homeassistant/components/accuweather/coordinator.py
+++ b/homeassistant/components/accuweather/coordinator.py
@@ -1,9 +1,6 @@
"""The AccuWeather coordinator."""
-from __future__ import annotations
-
from asyncio import timeout
-from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any
@@ -11,7 +8,6 @@ from typing import TYPE_CHECKING, Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp.client_exceptions import ClientConnectorError
-from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import (
@@ -27,17 +23,6 @@ EXCEPTIONS = (ApiError, ClientConnectorError, InvalidApiKeyError, RequestsExceed
_LOGGER = logging.getLogger(__name__)
-@dataclass
-class AccuWeatherData:
- """Data for AccuWeather integration."""
-
- coordinator_observation: AccuWeatherObservationDataUpdateCoordinator
- coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
-
-
-type AccuWeatherConfigEntry = ConfigEntry[AccuWeatherData]
-
-
class AccuWeatherObservationDataUpdateCoordinator(
DataUpdateCoordinator[dict[str, Any]]
):
@@ -46,7 +31,6 @@ class AccuWeatherObservationDataUpdateCoordinator(
def __init__(
self,
hass: HomeAssistant,
- config_entry: AccuWeatherConfigEntry,
accuweather: AccuWeather,
name: str,
coordinator_type: str,
@@ -64,7 +48,6 @@ class AccuWeatherObservationDataUpdateCoordinator(
super().__init__(
hass,
_LOGGER,
- config_entry=config_entry,
name=f"{name} ({coordinator_type})",
update_interval=update_interval,
)
@@ -90,7 +73,6 @@ class AccuWeatherDailyForecastDataUpdateCoordinator(
def __init__(
self,
hass: HomeAssistant,
- config_entry: AccuWeatherConfigEntry,
accuweather: AccuWeather,
name: str,
coordinator_type: str,
@@ -108,7 +90,6 @@ class AccuWeatherDailyForecastDataUpdateCoordinator(
super().__init__(
hass,
_LOGGER,
- config_entry=config_entry,
name=f"{name} ({coordinator_type})",
update_interval=update_interval,
)
diff --git a/homeassistant/components/accuweather/diagnostics.py b/homeassistant/components/accuweather/diagnostics.py
index 9f35c47b886..85c06a6140a 100644
--- a/homeassistant/components/accuweather/diagnostics.py
+++ b/homeassistant/components/accuweather/diagnostics.py
@@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
-from .coordinator import AccuWeatherConfigEntry, AccuWeatherData
+from . import AccuWeatherConfigEntry, AccuWeatherData
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE}
diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py
index 001edc5f197..2f6b10b296f 100644
--- a/homeassistant/components/accuweather/sensor.py
+++ b/homeassistant/components/accuweather/sensor.py
@@ -28,6 +28,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
+from . import AccuWeatherConfigEntry
from .const import (
API_METRIC,
ATTR_CATEGORY,
@@ -40,7 +41,6 @@ from .const import (
MAX_FORECAST_DAYS,
)
from .coordinator import (
- AccuWeatherConfigEntry,
AccuWeatherDailyForecastDataUpdateCoordinator,
AccuWeatherObservationDataUpdateCoordinator,
)
diff --git a/homeassistant/components/accuweather/system_health.py b/homeassistant/components/accuweather/system_health.py
index f5efaf3079f..eab16498248 100644
--- a/homeassistant/components/accuweather/system_health.py
+++ b/homeassistant/components/accuweather/system_health.py
@@ -9,8 +9,8 @@ from accuweather.const import ENDPOINT
from homeassistant.components import system_health
from homeassistant.core import HomeAssistant, callback
+from . import AccuWeatherConfigEntry
from .const import DOMAIN
-from .coordinator import AccuWeatherConfigEntry
@callback
diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py
index 7d754278d91..72d717f2703 100644
--- a/homeassistant/components/accuweather/weather.py
+++ b/homeassistant/components/accuweather/weather.py
@@ -33,6 +33,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utc_from_timestamp
+from . import AccuWeatherConfigEntry, AccuWeatherData
from .const import (
API_METRIC,
ATTR_DIRECTION,
@@ -42,9 +43,7 @@ from .const import (
CONDITION_MAP,
)
from .coordinator import (
- AccuWeatherConfigEntry,
AccuWeatherDailyForecastDataUpdateCoordinator,
- AccuWeatherData,
AccuWeatherObservationDataUpdateCoordinator,
)
diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py
index 6fd50967c22..c07967ec2c5 100644
--- a/homeassistant/components/adguard/config_flow.py
+++ b/homeassistant/components/adguard/config_flow.py
@@ -7,6 +7,7 @@ from typing import Any
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
import voluptuous as vol
+from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_HOST,
@@ -17,7 +18,6 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from .const import DOMAIN
diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py
index 8be1b719993..752c1ec26fc 100644
--- a/homeassistant/components/advantage_air/__init__.py
+++ b/homeassistant/components/advantage_air/__init__.py
@@ -55,7 +55,6 @@ async def async_setup_entry(
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name="Advantage Air",
update_method=async_get,
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py
index 29bc044c67d..e242d62a580 100644
--- a/homeassistant/components/aemet/__init__.py
+++ b/homeassistant/components/aemet/__init__.py
@@ -1,5 +1,6 @@
"""The AEMET OpenData component."""
+from dataclasses import dataclass
import logging
from aemet_opendata.exceptions import AemetError, TownNotFound
@@ -12,10 +13,20 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from .const import CONF_STATION_UPDATES, PLATFORMS
-from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator
+from .coordinator import WeatherUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
+type AemetConfigEntry = ConfigEntry[AemetData]
+
+
+@dataclass
+class AemetData:
+ """Aemet runtime data."""
+
+ name: str
+ coordinator: WeatherUpdateCoordinator
+
async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> bool:
"""Set up AEMET OpenData as config entry."""
@@ -35,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
except AemetError as err:
raise ConfigEntryNotReady(err) from err
- weather_coordinator = WeatherUpdateCoordinator(hass, entry, aemet)
+ weather_coordinator = WeatherUpdateCoordinator(hass, aemet)
await weather_coordinator.async_config_entry_first_refresh()
entry.runtime_data = AemetData(name=name, coordinator=weather_coordinator)
diff --git a/homeassistant/components/aemet/coordinator.py b/homeassistant/components/aemet/coordinator.py
index 2e8534c7466..8d179ccdb02 100644
--- a/homeassistant/components/aemet/coordinator.py
+++ b/homeassistant/components/aemet/coordinator.py
@@ -3,7 +3,6 @@
from __future__ import annotations
from asyncio import timeout
-from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any, Final, cast
@@ -20,7 +19,6 @@ from aemet_opendata.helpers import dict_nested_value
from aemet_opendata.interface import AEMET
from homeassistant.components.weather import Forecast
-from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -31,16 +29,6 @@ _LOGGER = logging.getLogger(__name__)
API_TIMEOUT: Final[int] = 120
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
-type AemetConfigEntry = ConfigEntry[AemetData]
-
-
-@dataclass
-class AemetData:
- """Aemet runtime data."""
-
- name: str
- coordinator: WeatherUpdateCoordinator
-
class WeatherUpdateCoordinator(DataUpdateCoordinator):
"""Weather data update coordinator."""
@@ -48,7 +36,6 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
def __init__(
self,
hass: HomeAssistant,
- entry: AemetConfigEntry,
aemet: AEMET,
) -> None:
"""Initialize coordinator."""
@@ -57,7 +44,6 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
super().__init__(
hass,
_LOGGER,
- config_entry=entry,
name=DOMAIN,
update_interval=WEATHER_UPDATE_INTERVAL,
)
diff --git a/homeassistant/components/aemet/diagnostics.py b/homeassistant/components/aemet/diagnostics.py
index bc366fc6d44..2379bd34bc0 100644
--- a/homeassistant/components/aemet/diagnostics.py
+++ b/homeassistant/components/aemet/diagnostics.py
@@ -15,7 +15,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
-from .coordinator import AemetConfigEntry
+from . import AemetConfigEntry
TO_REDACT_CONFIG = [
CONF_API_KEY,
diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py
index 88eb34b6f84..83d490f7fe2 100644
--- a/homeassistant/components/aemet/sensor.py
+++ b/homeassistant/components/aemet/sensor.py
@@ -55,6 +55,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
+from . import AemetConfigEntry
from .const import (
ATTR_API_CONDITION,
ATTR_API_FORECAST_CONDITION,
@@ -86,7 +87,7 @@ from .const import (
ATTR_API_WIND_SPEED,
CONDITIONS_MAP,
)
-from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator
+from .coordinator import WeatherUpdateCoordinator
from .entity import AemetEntity
@@ -248,7 +249,6 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
name="Rain",
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
- state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
key=ATTR_API_RAIN_PROB,
@@ -263,7 +263,6 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
name="Snow",
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
- state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
key=ATTR_API_SNOW_PROB,
diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py
index a156652eadd..341b81d71c4 100644
--- a/homeassistant/components/aemet/weather.py
+++ b/homeassistant/components/aemet/weather.py
@@ -27,8 +27,9 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from . import AemetConfigEntry
from .const import CONDITIONS_MAP
-from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator
+from .coordinator import WeatherUpdateCoordinator
from .entity import AemetEntity
diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py
index 23328315e42..f098184321f 100644
--- a/homeassistant/components/agent_dvr/alarm_control_panel.py
+++ b/homeassistant/components/agent_dvr/alarm_control_panel.py
@@ -5,7 +5,12 @@ from __future__ import annotations
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
+)
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
@@ -60,37 +65,37 @@ class AgentBaseStation(AlarmControlPanelEntity):
self._attr_available = self._client.is_available
armed = self._client.is_armed
if armed is None:
- self._attr_alarm_state = None
+ self._attr_state = None
return
if armed:
prof = (await self._client.get_active_profile()).lower()
- self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
+ self._attr_state = STATE_ALARM_ARMED_AWAY
if prof == CONF_HOME_MODE_NAME:
- self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
+ self._attr_state = STATE_ALARM_ARMED_HOME
elif prof == CONF_NIGHT_MODE_NAME:
- self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
+ self._attr_state = STATE_ALARM_ARMED_NIGHT
else:
- self._attr_alarm_state = AlarmControlPanelState.DISARMED
+ self._attr_state = STATE_ALARM_DISARMED
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
await self._client.disarm()
- self._attr_alarm_state = AlarmControlPanelState.DISARMED
+ self._attr_state = STATE_ALARM_DISARMED
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command. Uses custom mode."""
await self._client.arm()
await self._client.set_active_profile(CONF_AWAY_MODE_NAME)
- self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
+ self._attr_state = STATE_ALARM_ARMED_AWAY
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command. Uses custom mode."""
await self._client.arm()
await self._client.set_active_profile(CONF_HOME_MODE_NAME)
- self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
+ self._attr_state = STATE_ALARM_ARMED_HOME
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command. Uses custom mode."""
await self._client.arm()
await self._client.set_active_profile(CONF_NIGHT_MODE_NAME)
- self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
+ self._attr_state = STATE_ALARM_ARMED_NIGHT
diff --git a/homeassistant/components/agent_dvr/manifest.json b/homeassistant/components/agent_dvr/manifest.json
index 4ec14296363..9a6c528c336 100644
--- a/homeassistant/components/agent_dvr/manifest.json
+++ b/homeassistant/components/agent_dvr/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
"iot_class": "local_polling",
"loggers": ["agent"],
- "requirements": ["agent-py==0.0.24"]
+ "requirements": ["agent-py==0.0.23"]
}
diff --git a/homeassistant/components/airnow/config_flow.py b/homeassistant/components/airnow/config_flow.py
index d0ab16e9758..e839acdcb7b 100644
--- a/homeassistant/components/airnow/config_flow.py
+++ b/homeassistant/components/airnow/config_flow.py
@@ -1,7 +1,5 @@
"""Config flow for AirNow integration."""
-from __future__ import annotations
-
import logging
from typing import Any
@@ -14,6 +12,7 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
from homeassistant.core import HomeAssistant, callback
@@ -121,12 +120,12 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
- ) -> AirNowOptionsFlowHandler:
+ ) -> OptionsFlow:
"""Return the options flow."""
- return AirNowOptionsFlowHandler()
+ return AirNowOptionsFlowHandler(config_entry)
-class AirNowOptionsFlowHandler(OptionsFlow):
+class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle an options flow for AirNow."""
async def async_step_init(
@@ -137,7 +136,12 @@ class AirNowOptionsFlowHandler(OptionsFlow):
return self.async_create_entry(data=user_input)
options_schema = vol.Schema(
- {vol.Optional(CONF_RADIUS): vol.All(int, vol.Range(min=5))}
+ {
+ vol.Optional(CONF_RADIUS): vol.All(
+ int,
+ vol.Range(min=5),
+ ),
+ }
)
return self.async_show_form(
diff --git a/homeassistant/components/airthings/__init__.py b/homeassistant/components/airthings/__init__.py
index 14e2f28370f..22138c7d4fc 100644
--- a/homeassistant/components/airthings/__init__.py
+++ b/homeassistant/components/airthings/__init__.py
@@ -42,7 +42,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=DOMAIN,
update_method=_update_method,
update_interval=SCAN_INTERVAL,
diff --git a/homeassistant/components/airthings_ble/__init__.py b/homeassistant/components/airthings_ble/__init__.py
index 1c3c6084739..79384eed4ef 100644
--- a/homeassistant/components/airthings_ble/__init__.py
+++ b/homeassistant/components/airthings_ble/__init__.py
@@ -2,27 +2,75 @@
from __future__ import annotations
+from datetime import timedelta
+import logging
+
+from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
+from bleak_retry_connector import close_stale_connections_by_address
+
+from homeassistant.components import bluetooth
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+from homeassistant.util.unit_system import METRIC_SYSTEM
-from .const import MAX_RETRIES_AFTER_STARTUP
-from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
+from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MAX_RETRIES_AFTER_STARTUP
PLATFORMS: list[Platform] = [Platform.SENSOR]
+_LOGGER = logging.getLogger(__name__)
+
+AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator[AirthingsDevice]
+AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator]
+
async def async_setup_entry(
hass: HomeAssistant, entry: AirthingsBLEConfigEntry
) -> bool:
"""Set up Airthings BLE device from a config entry."""
- coordinator = AirthingsBLEDataUpdateCoordinator(hass, entry)
+ hass.data.setdefault(DOMAIN, {})
+ address = entry.unique_id
+
+ is_metric = hass.config.units is METRIC_SYSTEM
+ assert address is not None
+
+ await close_stale_connections_by_address(address)
+
+ ble_device = bluetooth.async_ble_device_from_address(hass, address)
+
+ if not ble_device:
+ raise ConfigEntryNotReady(
+ f"Could not find Airthings device with address {address}"
+ )
+
+ airthings = AirthingsBluetoothDeviceData(_LOGGER, is_metric)
+
+ async def _async_update_method() -> AirthingsDevice:
+ """Get data from Airthings BLE."""
+ try:
+ data = await airthings.update_device(ble_device)
+ except Exception as err:
+ raise UpdateFailed(f"Unable to fetch data: {err}") from err
+
+ return data
+
+ coordinator: AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=DOMAIN,
+ update_method=_async_update_method,
+ update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
+ )
+
await coordinator.async_config_entry_first_refresh()
# Once its setup and we know we are not going to delay
# the startup of Home Assistant, we can set the max attempts
# to a higher value. If the first connection attempt fails,
# Home Assistant's built-in retry logic will take over.
- coordinator.airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP)
+ airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP)
entry.runtime_data = coordinator
diff --git a/homeassistant/components/airthings_ble/coordinator.py b/homeassistant/components/airthings_ble/coordinator.py
deleted file mode 100644
index 81009dcea81..00000000000
--- a/homeassistant/components/airthings_ble/coordinator.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""The Airthings BLE integration."""
-
-from __future__ import annotations
-
-from datetime import timedelta
-import logging
-
-from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
-from bleak.backends.device import BLEDevice
-from bleak_retry_connector import close_stale_connections_by_address
-
-from homeassistant.components import bluetooth
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-from homeassistant.util.unit_system import METRIC_SYSTEM
-
-from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
-
-_LOGGER = logging.getLogger(__name__)
-
-type AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator]
-
-
-class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
- """Class to manage fetching Airthings BLE data."""
-
- ble_device: BLEDevice
- config_entry: AirthingsBLEConfigEntry
-
- def __init__(self, hass: HomeAssistant, entry: AirthingsBLEConfigEntry) -> None:
- """Initialize the coordinator."""
- self.airthings = AirthingsBluetoothDeviceData(
- _LOGGER, hass.config.units is METRIC_SYSTEM
- )
- super().__init__(
- hass,
- _LOGGER,
- config_entry=entry,
- name=DOMAIN,
- update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
- )
-
- async def _async_setup(self) -> None:
- """Set up the coordinator."""
- address = self.config_entry.unique_id
-
- assert address is not None
-
- await close_stale_connections_by_address(address)
-
- ble_device = bluetooth.async_ble_device_from_address(self.hass, address)
-
- if not ble_device:
- raise ConfigEntryNotReady(
- f"Could not find Airthings device with address {address}"
- )
- self.ble_device = ble_device
-
- async def _async_update_data(self) -> AirthingsDevice:
- """Get data from Airthings BLE."""
- try:
- data = await self.airthings.update_device(self.ble_device)
- except Exception as err:
- raise UpdateFailed(f"Unable to fetch data: {err}") from err
-
- return data
diff --git a/homeassistant/components/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json
index fe2cc0eeb36..6c00fe79e7b 100644
--- a/homeassistant/components/airthings_ble/manifest.json
+++ b/homeassistant/components/airthings_ble/manifest.json
@@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling",
- "requirements": ["airthings-ble==0.9.2"]
+ "requirements": ["airthings-ble==0.9.1"]
}
diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py
index 0dfd82a38c4..b1ae7d533d8 100644
--- a/homeassistant/components/airthings_ble/sensor.py
+++ b/homeassistant/components/airthings_ble/sensor.py
@@ -34,8 +34,8 @@ from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.unit_system import METRIC_SYSTEM
+from . import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE
-from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/airtouch5/__init__.py b/homeassistant/components/airtouch5/__init__.py
index f0c7ba8123c..8aab41d72cb 100644
--- a/homeassistant/components/airtouch5/__init__.py
+++ b/homeassistant/components/airtouch5/__init__.py
@@ -9,6 +9,8 @@ from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
+from .const import DOMAIN
+
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER]
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
@@ -17,6 +19,8 @@ type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
async def async_setup_entry(hass: HomeAssistant, entry: Airtouch5ConfigEntry) -> bool:
"""Set up Airtouch 5 from a config entry."""
+ hass.data.setdefault(DOMAIN, {})
+
# Create API instance
host = entry.data[CONF_HOST]
client = Airtouch5SimpleClient(host)
diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py
index d2e5e7169b9..dac34b170c9 100644
--- a/homeassistant/components/airvisual/__init__.py
+++ b/homeassistant/components/airvisual/__init__.py
@@ -204,7 +204,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
coordinator = DataUpdateCoordinator(
hass,
LOGGER,
- config_entry=entry,
name=async_get_geography_id(entry.data),
# We give a placeholder update interval in order to create the coordinator;
# then, below, we use the coordinator's presence (along with any other
diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py
index 7643d541070..8c012aca93d 100644
--- a/homeassistant/components/airvisual/config_flow.py
+++ b/homeassistant/components/airvisual/config_flow.py
@@ -16,12 +16,7 @@ from pyairvisual.cloud_api import (
from pyairvisual.errors import AirVisualError
import voluptuous as vol
-from homeassistant.config_entries import (
- SOURCE_REAUTH,
- ConfigEntry,
- ConfigFlow,
- ConfigFlowResult,
-)
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_API_KEY,
CONF_COUNTRY,
@@ -145,11 +140,8 @@ class AirVisualFlowHandler(ConfigFlow, domain=DOMAIN):
valid_keys.add(user_input[CONF_API_KEY])
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
- data_updates={CONF_API_KEY: user_input[CONF_API_KEY]},
- )
+ if existing_entry := await self.async_set_unique_id(self._geo_id):
+ return self.async_update_reload_and_abort(existing_entry, data=user_input)
return self.async_create_entry(
title=f"Cloud API ({self._geo_id})",
diff --git a/homeassistant/components/airvisual_pro/__init__.py b/homeassistant/components/airvisual_pro/__init__.py
index 3b3ac6df232..b95d0597bab 100644
--- a/homeassistant/components/airvisual_pro/__init__.py
+++ b/homeassistant/components/airvisual_pro/__init__.py
@@ -81,7 +81,6 @@ async def async_setup_entry(
coordinator = DataUpdateCoordinator(
hass,
LOGGER,
- config_entry=entry,
name="Node/Pro data",
update_interval=UPDATE_INTERVAL,
update_method=async_get_data,
diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py
index 5d1f9f051a3..754dfe90dce 100644
--- a/homeassistant/components/airzone/__init__.py
+++ b/homeassistant/components/airzone/__init__.py
@@ -24,7 +24,6 @@ PLATFORMS: list[Platform] = [
Platform.CLIMATE,
Platform.SELECT,
Platform.SENSOR,
- Platform.SWITCH,
Platform.WATER_HEATER,
]
diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py
index 6be7416bbb0..5e5e1c126de 100644
--- a/homeassistant/components/airzone/climate.py
+++ b/homeassistant/components/airzone/climate.py
@@ -85,7 +85,6 @@ HVAC_MODE_LIB_TO_HASS: Final[dict[OperationMode, HVACMode]] = {
OperationMode.HEATING: HVACMode.HEAT,
OperationMode.FAN: HVACMode.FAN_ONLY,
OperationMode.DRY: HVACMode.DRY,
- OperationMode.AUX_HEATING: HVACMode.HEAT,
OperationMode.AUTO: HVACMode.HEAT_COOL,
}
HVAC_MODE_HASS_TO_LIB: Final[dict[HVACMode, OperationMode]] = {
@@ -158,10 +157,9 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[
self.get_airzone_value(AZD_TEMP_UNIT)
]
- _attr_hvac_modes = [
+ self._attr_hvac_modes = [
HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES)
]
- self._attr_hvac_modes = list(dict.fromkeys(_attr_hvac_modes))
if (
self.get_airzone_value(AZD_SPEED) is not None
and self.get_airzone_value(AZD_SPEEDS) is not None
@@ -275,18 +273,12 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
self._attr_min_temp = self.get_airzone_value(AZD_TEMP_MIN)
if self.supported_features & ClimateEntityFeature.FAN_MODE:
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
- if (
- self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
- and self._attr_hvac_mode == HVACMode.HEAT_COOL
- ):
+ if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
self._attr_target_temperature_high = self.get_airzone_value(
AZD_COOL_TEMP_SET
)
self._attr_target_temperature_low = self.get_airzone_value(
AZD_HEAT_TEMP_SET
)
- self._attr_target_temperature = None
else:
- self._attr_target_temperature_high = None
- self._attr_target_temperature_low = None
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)
diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json
index 6bf374087a6..c40f4138b0a 100644
--- a/homeassistant/components/airzone/manifest.json
+++ b/homeassistant/components/airzone/manifest.json
@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
- "requirements": ["aioairzone==0.9.6"]
+ "requirements": ["aioairzone==0.9.3"]
}
diff --git a/homeassistant/components/airzone/switch.py b/homeassistant/components/airzone/switch.py
deleted file mode 100644
index 93136810604..00000000000
--- a/homeassistant/components/airzone/switch.py
+++ /dev/null
@@ -1,122 +0,0 @@
-"""Support for the Airzone switch."""
-
-from __future__ import annotations
-
-from dataclasses import dataclass
-from typing import Any, Final
-
-from aioairzone.const import API_ON, AZD_ON, AZD_ZONES
-
-from homeassistant.components.switch import (
- SwitchDeviceClass,
- SwitchEntity,
- SwitchEntityDescription,
-)
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import AirzoneConfigEntry
-from .coordinator import AirzoneUpdateCoordinator
-from .entity import AirzoneEntity, AirzoneZoneEntity
-
-
-@dataclass(frozen=True, kw_only=True)
-class AirzoneSwitchDescription(SwitchEntityDescription):
- """Class to describe an Airzone switch entity."""
-
- api_param: str
-
-
-ZONE_SWITCH_TYPES: Final[tuple[AirzoneSwitchDescription, ...]] = (
- AirzoneSwitchDescription(
- api_param=API_ON,
- device_class=SwitchDeviceClass.SWITCH,
- key=AZD_ON,
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: AirzoneConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Add Airzone switch from a config_entry."""
- coordinator = entry.runtime_data
-
- added_zones: set[str] = set()
-
- def _async_entity_listener() -> None:
- """Handle additions of switch."""
-
- zones_data = coordinator.data.get(AZD_ZONES, {})
- received_zones = set(zones_data)
- new_zones = received_zones - added_zones
- if new_zones:
- async_add_entities(
- AirzoneZoneSwitch(
- coordinator,
- description,
- entry,
- system_zone_id,
- zones_data.get(system_zone_id),
- )
- for system_zone_id in new_zones
- for description in ZONE_SWITCH_TYPES
- if description.key in zones_data.get(system_zone_id)
- )
- added_zones.update(new_zones)
-
- entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
- _async_entity_listener()
-
-
-class AirzoneBaseSwitch(AirzoneEntity, SwitchEntity):
- """Define an Airzone switch."""
-
- entity_description: AirzoneSwitchDescription
-
- @callback
- def _handle_coordinator_update(self) -> None:
- """Update attributes when the coordinator updates."""
- self._async_update_attrs()
- super()._handle_coordinator_update()
-
- @callback
- def _async_update_attrs(self) -> None:
- """Update switch attributes."""
- self._attr_is_on = self.get_airzone_value(self.entity_description.key)
-
-
-class AirzoneZoneSwitch(AirzoneZoneEntity, AirzoneBaseSwitch):
- """Define an Airzone Zone switch."""
-
- def __init__(
- self,
- coordinator: AirzoneUpdateCoordinator,
- description: AirzoneSwitchDescription,
- entry: ConfigEntry,
- system_zone_id: str,
- zone_data: dict[str, Any],
- ) -> None:
- """Initialize."""
- super().__init__(coordinator, entry, system_zone_id, zone_data)
-
- self._attr_name = None
- self._attr_unique_id = (
- f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
- )
- self.entity_description = description
-
- self._async_update_attrs()
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn the entity on."""
- param = self.entity_description.api_param
- await self._async_update_hvac_params({param: True})
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn the entity off."""
- param = self.entity_description.api_param
- await self._async_update_hvac_params({param: False})
diff --git a/homeassistant/components/airzone_cloud/__init__.py b/homeassistant/components/airzone_cloud/__init__.py
index 5baa0bcea10..b1d7900f2e8 100644
--- a/homeassistant/components/airzone_cloud/__init__.py
+++ b/homeassistant/components/airzone_cloud/__init__.py
@@ -17,7 +17,6 @@ PLATFORMS: list[Platform] = [
Platform.CLIMATE,
Platform.SELECT,
Platform.SENSOR,
- Platform.SWITCH,
Platform.WATER_HEATER,
]
diff --git a/homeassistant/components/airzone_cloud/climate.py b/homeassistant/components/airzone_cloud/climate.py
index d32b070ad8c..3658c073795 100644
--- a/homeassistant/components/airzone_cloud/climate.py
+++ b/homeassistant/components/airzone_cloud/climate.py
@@ -224,20 +224,14 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity):
self._attr_hvac_mode = HVACMode.OFF
self._attr_max_temp = self.get_airzone_value(AZD_TEMP_SET_MAX)
self._attr_min_temp = self.get_airzone_value(AZD_TEMP_SET_MIN)
- if (
- self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
- and self._attr_hvac_mode == HVACMode.HEAT_COOL
- ):
+ if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
self._attr_target_temperature_high = self.get_airzone_value(
AZD_TEMP_SET_COOL_AIR
)
self._attr_target_temperature_low = self.get_airzone_value(
AZD_TEMP_SET_HOT_AIR
)
- self._attr_target_temperature = None
else:
- self._attr_target_temperature_high = None
- self._attr_target_temperature_low = None
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)
@@ -310,10 +304,6 @@ class AirzoneDeviceClimate(AirzoneClimate):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
- hvac_mode = kwargs.get(ATTR_HVAC_MODE)
- if hvac_mode is not None:
- await self.async_set_hvac_mode(hvac_mode)
-
params: dict[str, Any] = {}
if ATTR_TEMPERATURE in kwargs:
params[API_SETPOINT] = {
@@ -337,6 +327,9 @@ class AirzoneDeviceClimate(AirzoneClimate):
}
await self._async_update_params(params)
+ if ATTR_HVAC_MODE in kwargs:
+ await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
+
class AirzoneDeviceGroupClimate(AirzoneClimate):
"""Define an Airzone Cloud DeviceGroup base class."""
@@ -367,10 +360,6 @@ class AirzoneDeviceGroupClimate(AirzoneClimate):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
- hvac_mode = kwargs.get(ATTR_HVAC_MODE)
- if hvac_mode is not None:
- await self.async_set_hvac_mode(hvac_mode)
-
params: dict[str, Any] = {}
if ATTR_TEMPERATURE in kwargs:
params[API_PARAMS] = {
@@ -381,6 +370,9 @@ class AirzoneDeviceGroupClimate(AirzoneClimate):
}
await self._async_update_params(params)
+ if ATTR_HVAC_MODE in kwargs:
+ await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
+
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set hvac mode."""
params: dict[str, Any] = {
diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json
index 0e21e57ec52..b1d3400c9be 100644
--- a/homeassistant/components/airzone_cloud/manifest.json
+++ b/homeassistant/components/airzone_cloud/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"],
- "requirements": ["aioairzone-cloud==0.6.10"]
+ "requirements": ["aioairzone-cloud==0.6.6"]
}
diff --git a/homeassistant/components/airzone_cloud/select.py b/homeassistant/components/airzone_cloud/select.py
index 895796a1073..9bc0bdd1f5b 100644
--- a/homeassistant/components/airzone_cloud/select.py
+++ b/homeassistant/components/airzone_cloud/select.py
@@ -2,19 +2,14 @@
from __future__ import annotations
-from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Final
-from aioairzone_cloud.common import AirQualityMode, OperationMode
+from aioairzone_cloud.common import AirQualityMode
from aioairzone_cloud.const import (
API_AQ_MODE_CONF,
- API_MODE,
API_VALUE,
AZD_AQ_MODE_CONF,
- AZD_MASTER,
- AZD_MODE,
- AZD_MODES,
AZD_ZONES,
)
@@ -33,10 +28,7 @@ class AirzoneSelectDescription(SelectEntityDescription):
"""Class to describe an Airzone select entity."""
api_param: str
- options_dict: dict[str, Any]
- options_fn: Callable[[dict[str, Any], dict[str, Any]], list[str]] = (
- lambda zone_data, value: list(value)
- )
+ options_dict: dict[str, str]
AIR_QUALITY_MAP: Final[dict[str, str]] = {
@@ -45,35 +37,6 @@ AIR_QUALITY_MAP: Final[dict[str, str]] = {
"auto": AirQualityMode.AUTO,
}
-MODE_MAP: Final[dict[str, int]] = {
- "cool": OperationMode.COOLING,
- "dry": OperationMode.DRY,
- "fan": OperationMode.VENTILATION,
- "heat": OperationMode.HEATING,
- "heat_cool": OperationMode.AUTO,
- "stop": OperationMode.STOP,
-}
-
-
-def main_zone_options(
- zone_data: dict[str, Any],
- options: dict[str, int],
-) -> list[str]:
- """Filter available modes."""
- modes = zone_data.get(AZD_MODES, [])
- return [k for k, v in options.items() if v in modes]
-
-
-MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
- AirzoneSelectDescription(
- api_param=API_MODE,
- key=AZD_MODE,
- options_dict=MODE_MAP,
- options_fn=main_zone_options,
- translation_key="modes",
- ),
-)
-
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
AirzoneSelectDescription(
@@ -96,19 +59,7 @@ async def async_setup_entry(
coordinator = entry.runtime_data
# Zones
- entities: list[AirzoneZoneSelect] = [
- AirzoneZoneSelect(
- coordinator,
- description,
- zone_id,
- zone_data,
- )
- for description in MAIN_ZONE_SELECT_TYPES
- for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items()
- if description.key in zone_data and zone_data.get(AZD_MASTER)
- ]
-
- entities.extend(
+ async_add_entities(
AirzoneZoneSelect(
coordinator,
description,
@@ -120,8 +71,6 @@ async def async_setup_entry(
if description.key in zone_data
)
- async_add_entities(entities)
-
class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
"""Define an Airzone Cloud select."""
@@ -161,11 +110,6 @@ class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
self._attr_unique_id = f"{zone_id}_{description.key}"
self.entity_description = description
-
- self._attr_options = self.entity_description.options_fn(
- zone_data, description.options_dict
- )
-
self.values_dict = {v: k for k, v in description.options_dict.items()}
self._async_update_attrs()
diff --git a/homeassistant/components/airzone_cloud/strings.json b/homeassistant/components/airzone_cloud/strings.json
index 6e0f9adcd66..523c43f4955 100644
--- a/homeassistant/components/airzone_cloud/strings.json
+++ b/homeassistant/components/airzone_cloud/strings.json
@@ -36,17 +36,6 @@
"on": "On",
"auto": "Auto"
}
- },
- "modes": {
- "name": "Mode",
- "state": {
- "cool": "[%key:component::climate::entity_component::_::state::cool%]",
- "dry": "[%key:component::climate::entity_component::_::state::dry%]",
- "fan": "[%key:component::climate::entity_component::_::state::fan_only%]",
- "heat": "[%key:component::climate::entity_component::_::state::heat%]",
- "heat_cool": "[%key:component::climate::entity_component::_::state::heat_cool%]",
- "stop": "Stop"
- }
}
},
"sensor": {
diff --git a/homeassistant/components/airzone_cloud/switch.py b/homeassistant/components/airzone_cloud/switch.py
deleted file mode 100644
index 0eb907ff792..00000000000
--- a/homeassistant/components/airzone_cloud/switch.py
+++ /dev/null
@@ -1,115 +0,0 @@
-"""Support for the Airzone Cloud switch."""
-
-from __future__ import annotations
-
-from dataclasses import dataclass
-from typing import Any, Final
-
-from aioairzone_cloud.const import API_POWER, API_VALUE, AZD_POWER, AZD_ZONES
-
-from homeassistant.components.switch import (
- SwitchDeviceClass,
- SwitchEntity,
- SwitchEntityDescription,
-)
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import AirzoneCloudConfigEntry
-from .coordinator import AirzoneUpdateCoordinator
-from .entity import AirzoneEntity, AirzoneZoneEntity
-
-
-@dataclass(frozen=True, kw_only=True)
-class AirzoneSwitchDescription(SwitchEntityDescription):
- """Class to describe an Airzone switch entity."""
-
- api_param: str
-
-
-ZONE_SWITCH_TYPES: Final[tuple[AirzoneSwitchDescription, ...]] = (
- AirzoneSwitchDescription(
- api_param=API_POWER,
- device_class=SwitchDeviceClass.SWITCH,
- key=AZD_POWER,
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: AirzoneCloudConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Add Airzone Cloud switch from a config_entry."""
- coordinator = entry.runtime_data
-
- # Zones
- async_add_entities(
- AirzoneZoneSwitch(
- coordinator,
- description,
- zone_id,
- zone_data,
- )
- for description in ZONE_SWITCH_TYPES
- for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items()
- if description.key in zone_data
- )
-
-
-class AirzoneBaseSwitch(AirzoneEntity, SwitchEntity):
- """Define an Airzone Cloud switch."""
-
- entity_description: AirzoneSwitchDescription
-
- @callback
- def _handle_coordinator_update(self) -> None:
- """Update attributes when the coordinator updates."""
- self._async_update_attrs()
- super()._handle_coordinator_update()
-
- @callback
- def _async_update_attrs(self) -> None:
- """Update switch attributes."""
- self._attr_is_on = self.get_airzone_value(self.entity_description.key)
-
-
-class AirzoneZoneSwitch(AirzoneZoneEntity, AirzoneBaseSwitch):
- """Define an Airzone Cloud Zone switch."""
-
- def __init__(
- self,
- coordinator: AirzoneUpdateCoordinator,
- description: AirzoneSwitchDescription,
- zone_id: str,
- zone_data: dict[str, Any],
- ) -> None:
- """Initialize."""
- super().__init__(coordinator, zone_id, zone_data)
-
- self._attr_name = None
- self._attr_unique_id = f"{zone_id}_{description.key}"
- self.entity_description = description
-
- self._async_update_attrs()
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn the entity on."""
- param = self.entity_description.api_param
- params: dict[str, Any] = {
- param: {
- API_VALUE: True,
- }
- }
- await self._async_update_params(params)
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn the entity off."""
- param = self.entity_description.api_param
- params: dict[str, Any] = {
- param: {
- API_VALUE: False,
- }
- }
- await self._async_update_params(params)
diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py
index a9e433a3650..e5c2745104d 100644
--- a/homeassistant/components/alarm_control_panel/__init__.py
+++ b/homeassistant/components/alarm_control_panel/__init__.py
@@ -2,11 +2,10 @@
from __future__ import annotations
-import asyncio
from datetime import timedelta
from functools import partial
import logging
-from typing import TYPE_CHECKING, Any, Final, final
+from typing import Any, Final, final
from propcache import cached_property
import voluptuous as vol
@@ -34,7 +33,6 @@ from homeassistant.helpers.deprecation import (
)
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
-from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
@@ -51,7 +49,6 @@ from .const import ( # noqa: F401
ATTR_CODE_ARM_REQUIRED,
DOMAIN,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
@@ -145,7 +142,6 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
"changed_by",
"code_arm_required",
"supported_features",
- "alarm_state",
}
@@ -153,7 +149,6 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
"""An abstract class for alarm control entities."""
entity_description: AlarmControlPanelEntityDescription
- _attr_alarm_state: AlarmControlPanelState | None = None
_attr_changed_by: str | None = None
_attr_code_arm_required: bool = True
_attr_code_format: CodeFormat | None = None
@@ -162,84 +157,6 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
)
_alarm_control_panel_option_default_code: str | None = None
- __alarm_legacy_state: bool = False
- __alarm_legacy_state_reported: bool = False
-
- def __init_subclass__(cls, **kwargs: Any) -> None:
- """Post initialisation processing."""
- super().__init_subclass__(**kwargs)
- if any(method in cls.__dict__ for method in ("_attr_state", "state")):
- # Integrations should use the 'alarm_state' property instead of
- # setting the state directly.
- cls.__alarm_legacy_state = True
-
- def __setattr__(self, __name: str, __value: Any) -> None:
- """Set attribute.
-
- Deprecation warning if setting '_attr_state' directly
- unless already reported.
- """
- if __name == "_attr_state":
- if self.__alarm_legacy_state_reported is not True:
- self._report_deprecated_alarm_state_handling()
- self.__alarm_legacy_state_reported = True
- return super().__setattr__(__name, __value)
-
- @callback
- def add_to_platform_start(
- self,
- hass: HomeAssistant,
- platform: EntityPlatform,
- parallel_updates: asyncio.Semaphore | None,
- ) -> None:
- """Start adding an entity to a platform."""
- super().add_to_platform_start(hass, platform, parallel_updates)
- if self.__alarm_legacy_state and not self.__alarm_legacy_state_reported:
- self._report_deprecated_alarm_state_handling()
-
- @callback
- def _report_deprecated_alarm_state_handling(self) -> None:
- """Report on deprecated handling of alarm state.
-
- Integrations should implement alarm_state instead of using state directly.
- """
- self.__alarm_legacy_state_reported = True
- if "custom_components" in type(self).__module__:
- # Do not report on core integrations as they have been fixed.
- report_issue = "report it to the custom integration author."
- _LOGGER.warning(
- "Entity %s (%s) is setting state directly"
- " which will stop working in HA Core 2025.11."
- " Entities should implement the 'alarm_state' property and"
- " return its state using the AlarmControlPanelState enum, please %s",
- self.entity_id,
- type(self),
- report_issue,
- )
-
- @final
- @property
- def state(self) -> str | None:
- """Return the current state."""
- if (alarm_state := self.alarm_state) is not None:
- return alarm_state
- if self._attr_state is not None:
- # Backwards compatibility for integrations that set state directly
- # Should be removed in 2025.11
- if TYPE_CHECKING:
- assert isinstance(self._attr_state, str)
- return self._attr_state
- return None
-
- @cached_property
- def alarm_state(self) -> AlarmControlPanelState | None:
- """Return the current alarm control panel entity state.
-
- Integrations should overwrite this or use the '_attr_alarm_state'
- attribute to set the alarm status using the 'AlarmControlPanelState' enum.
- """
- return self._attr_alarm_state
-
@final
@callback
def code_or_default_code(self, code: str | None) -> str | None:
diff --git a/homeassistant/components/alarm_control_panel/const.py b/homeassistant/components/alarm_control_panel/const.py
index f3218626ead..2e8fe98da3b 100644
--- a/homeassistant/components/alarm_control_panel/const.py
+++ b/homeassistant/components/alarm_control_panel/const.py
@@ -17,21 +17,6 @@ ATTR_CHANGED_BY: Final = "changed_by"
ATTR_CODE_ARM_REQUIRED: Final = "code_arm_required"
-class AlarmControlPanelState(StrEnum):
- """Alarm control panel entity states."""
-
- DISARMED = "disarmed"
- ARMED_HOME = "armed_home"
- ARMED_AWAY = "armed_away"
- ARMED_NIGHT = "armed_night"
- ARMED_VACATION = "armed_vacation"
- ARMED_CUSTOM_BYPASS = "armed_custom_bypass"
- PENDING = "pending"
- ARMING = "arming"
- DISARMING = "disarming"
- TRIGGERED = "triggered"
-
-
class CodeFormat(StrEnum):
"""Code formats for the Alarm Control Panel."""
diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py
index 6d343bbe605..227fc31413e 100644
--- a/homeassistant/components/alarm_control_panel/device_condition.py
+++ b/homeassistant/components/alarm_control_panel/device_condition.py
@@ -13,6 +13,13 @@ from homeassistant.const import (
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_TYPE,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
@@ -24,7 +31,7 @@ from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
-from . import DOMAIN, AlarmControlPanelState
+from . import DOMAIN
from .const import (
CONDITION_ARMED_AWAY,
CONDITION_ARMED_CUSTOM_BYPASS,
@@ -102,19 +109,19 @@ def async_condition_from_config(
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config[CONF_TYPE] == CONDITION_TRIGGERED:
- state = AlarmControlPanelState.TRIGGERED
+ state = STATE_ALARM_TRIGGERED
elif config[CONF_TYPE] == CONDITION_DISARMED:
- state = AlarmControlPanelState.DISARMED
+ state = STATE_ALARM_DISARMED
elif config[CONF_TYPE] == CONDITION_ARMED_HOME:
- state = AlarmControlPanelState.ARMED_HOME
+ state = STATE_ALARM_ARMED_HOME
elif config[CONF_TYPE] == CONDITION_ARMED_AWAY:
- state = AlarmControlPanelState.ARMED_AWAY
+ state = STATE_ALARM_ARMED_AWAY
elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT:
- state = AlarmControlPanelState.ARMED_NIGHT
+ state = STATE_ALARM_ARMED_NIGHT
elif config[CONF_TYPE] == CONDITION_ARMED_VACATION:
- state = AlarmControlPanelState.ARMED_VACATION
+ state = STATE_ALARM_ARMED_VACATION
elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS:
- state = AlarmControlPanelState.ARMED_CUSTOM_BYPASS
+ state = STATE_ALARM_ARMED_CUSTOM_BYPASS
registry = er.async_get(hass)
entity_id = er.async_resolve_entity_id(registry, config[ATTR_ENTITY_ID])
diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py
index a488cf10870..557666720e8 100644
--- a/homeassistant/components/alarm_control_panel/device_trigger.py
+++ b/homeassistant/components/alarm_control_panel/device_trigger.py
@@ -15,6 +15,13 @@ from homeassistant.const import (
CONF_FOR,
CONF_PLATFORM,
CONF_TYPE,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_registry as er
@@ -22,7 +29,7 @@ from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
-from . import DOMAIN, AlarmControlPanelState
+from . import DOMAIN
from .const import AlarmControlPanelEntityFeature
BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"}
@@ -122,19 +129,19 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE:
"""Attach a trigger."""
if config[CONF_TYPE] == "triggered":
- to_state = AlarmControlPanelState.TRIGGERED
+ to_state = STATE_ALARM_TRIGGERED
elif config[CONF_TYPE] == "disarmed":
- to_state = AlarmControlPanelState.DISARMED
+ to_state = STATE_ALARM_DISARMED
elif config[CONF_TYPE] == "arming":
- to_state = AlarmControlPanelState.ARMING
+ to_state = STATE_ALARM_ARMING
elif config[CONF_TYPE] == "armed_home":
- to_state = AlarmControlPanelState.ARMED_HOME
+ to_state = STATE_ALARM_ARMED_HOME
elif config[CONF_TYPE] == "armed_away":
- to_state = AlarmControlPanelState.ARMED_AWAY
+ to_state = STATE_ALARM_ARMED_AWAY
elif config[CONF_TYPE] == "armed_night":
- to_state = AlarmControlPanelState.ARMED_NIGHT
+ to_state = STATE_ALARM_ARMED_NIGHT
elif config[CONF_TYPE] == "armed_vacation":
- to_state = AlarmControlPanelState.ARMED_VACATION
+ to_state = STATE_ALARM_ARMED_VACATION
state_config = {
state_trigger.CONF_PLATFORM: "state",
diff --git a/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py
index 765514e98ec..5a3d79fe2ed 100644
--- a/homeassistant/components/alarm_control_panel/reproduce_state.py
+++ b/homeassistant/components/alarm_control_panel/reproduce_state.py
@@ -16,21 +16,28 @@ from homeassistant.const import (
SERVICE_ALARM_ARM_VACATION,
SERVICE_ALARM_DISARM,
SERVICE_ALARM_TRIGGER,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
)
from homeassistant.core import Context, HomeAssistant, State
-from . import DOMAIN, AlarmControlPanelState
+from . import DOMAIN
_LOGGER: Final = logging.getLogger(__name__)
VALID_STATES: Final[set[str]] = {
- AlarmControlPanelState.ARMED_AWAY,
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
- AlarmControlPanelState.ARMED_HOME,
- AlarmControlPanelState.ARMED_NIGHT,
- AlarmControlPanelState.ARMED_VACATION,
- AlarmControlPanelState.DISARMED,
- AlarmControlPanelState.TRIGGERED,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
}
@@ -58,19 +65,19 @@ async def _async_reproduce_state(
service_data = {ATTR_ENTITY_ID: state.entity_id}
- if state.state == AlarmControlPanelState.ARMED_AWAY:
+ if state.state == STATE_ALARM_ARMED_AWAY:
service = SERVICE_ALARM_ARM_AWAY
- elif state.state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS:
+ elif state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
service = SERVICE_ALARM_ARM_CUSTOM_BYPASS
- elif state.state == AlarmControlPanelState.ARMED_HOME:
+ elif state.state == STATE_ALARM_ARMED_HOME:
service = SERVICE_ALARM_ARM_HOME
- elif state.state == AlarmControlPanelState.ARMED_NIGHT:
+ elif state.state == STATE_ALARM_ARMED_NIGHT:
service = SERVICE_ALARM_ARM_NIGHT
- elif state.state == AlarmControlPanelState.ARMED_VACATION:
+ elif state.state == STATE_ALARM_ARMED_VACATION:
service = SERVICE_ALARM_ARM_VACATION
- elif state.state == AlarmControlPanelState.DISARMED:
+ elif state.state == STATE_ALARM_DISARMED:
service = SERVICE_ALARM_DISARM
- elif state.state == AlarmControlPanelState.TRIGGERED:
+ elif state.state == STATE_ALARM_TRIGGERED:
service = SERVICE_ALARM_TRIGGER
await hass.services.async_call(
diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py
index cf72133ea12..7375320f800 100644
--- a/homeassistant/components/alarmdecoder/alarm_control_panel.py
+++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py
@@ -7,10 +7,16 @@ import voluptuous as vol
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
-from homeassistant.const import ATTR_CODE
+from homeassistant.const import (
+ ATTR_CODE,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
@@ -100,15 +106,15 @@ class AlarmDecoderAlarmPanel(AlarmDecoderEntity, AlarmControlPanelEntity):
def _message_callback(self, message):
"""Handle received messages."""
if message.alarm_sounding or message.fire_alarm:
- self._attr_alarm_state = AlarmControlPanelState.TRIGGERED
+ self._attr_state = STATE_ALARM_TRIGGERED
elif message.armed_away:
- self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
+ self._attr_state = STATE_ALARM_ARMED_AWAY
elif message.armed_home and (message.entry_delay_off or message.perimeter_only):
- self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
+ self._attr_state = STATE_ALARM_ARMED_NIGHT
elif message.armed_home:
- self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
+ self._attr_state = STATE_ALARM_ARMED_HOME
else:
- self._attr_alarm_state = AlarmControlPanelState.DISARMED
+ self._attr_state = STATE_ALARM_DISARMED
self._attr_extra_state_attributes = {
"ac_power": message.ac_power,
diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py
index 09b461428ac..6633cda8a97 100644
--- a/homeassistant/components/alexa/capabilities.py
+++ b/homeassistant/components/alexa/capabilities.py
@@ -26,7 +26,6 @@ from homeassistant.components import (
)
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
from homeassistant.components.climate import HVACMode
@@ -37,6 +36,10 @@ from homeassistant.const import (
ATTR_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
STATE_IDLE,
STATE_OFF,
STATE_ON,
@@ -1314,13 +1317,13 @@ class AlexaSecurityPanelController(AlexaCapability):
raise UnsupportedProperty(name)
arm_state = self.entity.state
- if arm_state == AlarmControlPanelState.ARMED_HOME:
+ if arm_state == STATE_ALARM_ARMED_HOME:
return "ARMED_STAY"
- if arm_state == AlarmControlPanelState.ARMED_AWAY:
+ if arm_state == STATE_ALARM_ARMED_AWAY:
return "ARMED_AWAY"
- if arm_state == AlarmControlPanelState.ARMED_NIGHT:
+ if arm_state == STATE_ALARM_ARMED_NIGHT:
return "ARMED_NIGHT"
- if arm_state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS:
+ if arm_state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
return "ARMED_STAY"
return "DISARMED"
diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py
index 8ea61ddbceb..3571f436ff6 100644
--- a/homeassistant/components/alexa/handlers.py
+++ b/homeassistant/components/alexa/handlers.py
@@ -9,7 +9,6 @@ from typing import Any
from homeassistant import core as ha
from homeassistant.components import (
- alarm_control_panel,
button,
camera,
climate,
@@ -52,6 +51,7 @@ from homeassistant.const import (
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP,
+ STATE_ALARM_DISARMED,
UnitOfTemperature,
)
from homeassistant.helpers import network
@@ -1083,13 +1083,7 @@ async def async_api_arm(
arm_state = directive.payload["armState"]
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
- # Per Alexa Documentation: users are not allowed to switch from armed_away
- # directly to another armed state without first disarming the system.
- # https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-securitypanelcontroller.html#arming
- if (
- entity.state == alarm_control_panel.AlarmControlPanelState.ARMED_AWAY
- and arm_state != "ARMED_AWAY"
- ):
+ if entity.state != STATE_ALARM_DISARMED:
msg = "You must disarm the system before you can set the requested arm state."
raise AlexaSecurityPanelAuthorizationRequired(msg)
@@ -1139,7 +1133,7 @@ async def async_api_disarm(
# Per Alexa Documentation: If you receive a Disarm directive, and the
# system is already disarmed, respond with a success response,
# not an error response.
- if entity.state == alarm_control_panel.AlarmControlPanelState.DISARMED:
+ if entity.state == STATE_ALARM_DISARMED:
return response
payload = directive.payload
diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py
index b63475c80a4..c1141b40e4d 100644
--- a/homeassistant/components/analytics/analytics.py
+++ b/homeassistant/components/analytics/analytics.py
@@ -29,7 +29,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.entity_registry as er
-from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.storage import Store
from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.loader import (
@@ -137,7 +136,7 @@ class Analytics:
@property
def supervisor(self) -> bool:
"""Return bool if a supervisor is present."""
- return is_hassio(self.hass)
+ return hassio.is_hassio(self.hass)
async def load(self) -> None:
"""Load preferences."""
diff --git a/homeassistant/components/analytics/manifest.json b/homeassistant/components/analytics/manifest.json
index 5142a86ad97..955c4a813f4 100644
--- a/homeassistant/components/analytics/manifest.json
+++ b/homeassistant/components/analytics/manifest.json
@@ -1,7 +1,7 @@
{
"domain": "analytics",
"name": "Analytics",
- "after_dependencies": ["energy", "hassio", "recorder"],
+ "after_dependencies": ["energy", "recorder"],
"codeowners": ["@home-assistant/core", "@ludeeus"],
"dependencies": ["api", "websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/analytics",
diff --git a/homeassistant/components/analytics_insights/config_flow.py b/homeassistant/components/analytics_insights/config_flow.py
index c36755f5403..909290b1035 100644
--- a/homeassistant/components/analytics_insights/config_flow.py
+++ b/homeassistant/components/analytics_insights/config_flow.py
@@ -16,6 +16,7 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -26,7 +27,6 @@ from homeassistant.helpers.selector import (
)
from .const import (
- CONF_TRACKED_ADDONS,
CONF_TRACKED_CUSTOM_INTEGRATIONS,
CONF_TRACKED_INTEGRATIONS,
DOMAIN,
@@ -45,11 +45,9 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> HomeassistantAnalyticsOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
- return HomeassistantAnalyticsOptionsFlowHandler()
+ return HomeassistantAnalyticsOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -57,12 +55,8 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
- if all(
- [
- not user_input.get(CONF_TRACKED_ADDONS),
- not user_input.get(CONF_TRACKED_INTEGRATIONS),
- not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
- ]
+ if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
+ CONF_TRACKED_CUSTOM_INTEGRATIONS
):
errors["base"] = "no_integrations_selected"
else:
@@ -70,7 +64,6 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
title="Home Assistant Analytics Insights",
data={},
options={
- CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
CONF_TRACKED_INTEGRATIONS: user_input.get(
CONF_TRACKED_INTEGRATIONS, []
),
@@ -84,7 +77,6 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
session=async_get_clientsession(self.hass)
)
try:
- addons = await client.get_addons()
integrations = await client.get_integrations()
custom_integrations = await client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError:
@@ -107,13 +99,6 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
data_schema=vol.Schema(
{
- vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
- SelectSelectorConfig(
- options=list(addons),
- multiple=True,
- sort=True,
- )
- ),
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=options,
@@ -133,7 +118,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
)
-class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
+class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle Homeassistant Analytics options."""
async def async_step_init(
@@ -142,19 +127,14 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
"""Manage the options."""
errors: dict[str, str] = {}
if user_input is not None:
- if all(
- [
- not user_input.get(CONF_TRACKED_ADDONS),
- not user_input.get(CONF_TRACKED_INTEGRATIONS),
- not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
- ]
+ if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
+ CONF_TRACKED_CUSTOM_INTEGRATIONS
):
errors["base"] = "no_integrations_selected"
else:
return self.async_create_entry(
title="",
data={
- CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
CONF_TRACKED_INTEGRATIONS: user_input.get(
CONF_TRACKED_INTEGRATIONS, []
),
@@ -168,7 +148,6 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
session=async_get_clientsession(self.hass)
)
try:
- addons = await client.get_addons()
integrations = await client.get_integrations()
custom_integrations = await client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError:
@@ -189,13 +168,6 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
- vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
- SelectSelectorConfig(
- options=list(addons),
- multiple=True,
- sort=True,
- )
- ),
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=options,
@@ -212,6 +184,6 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
),
},
),
- self.config_entry.options,
+ self.options,
),
)
diff --git a/homeassistant/components/analytics_insights/const.py b/homeassistant/components/analytics_insights/const.py
index 1a01755f9ed..56ea3f59794 100644
--- a/homeassistant/components/analytics_insights/const.py
+++ b/homeassistant/components/analytics_insights/const.py
@@ -4,7 +4,6 @@ import logging
DOMAIN = "analytics_insights"
-CONF_TRACKED_ADDONS = "tracked_addons"
CONF_TRACKED_INTEGRATIONS = "tracked_integrations"
CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations"
diff --git a/homeassistant/components/analytics_insights/coordinator.py b/homeassistant/components/analytics_insights/coordinator.py
index 701f1a8dbd4..3a7c40dfa82 100644
--- a/homeassistant/components/analytics_insights/coordinator.py
+++ b/homeassistant/components/analytics_insights/coordinator.py
@@ -12,13 +12,11 @@ from python_homeassistant_analytics import (
HomeassistantAnalyticsConnectionError,
HomeassistantAnalyticsNotModifiedError,
)
-from python_homeassistant_analytics.models import Addon
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
- CONF_TRACKED_ADDONS,
CONF_TRACKED_CUSTOM_INTEGRATIONS,
CONF_TRACKED_INTEGRATIONS,
DOMAIN,
@@ -35,7 +33,6 @@ class AnalyticsData:
active_installations: int
reports_integrations: int
- addons: dict[str, int]
core_integrations: dict[str, int]
custom_integrations: dict[str, int]
@@ -56,7 +53,6 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
update_interval=timedelta(hours=12),
)
self._client = client
- self._tracked_addons = self.config_entry.options.get(CONF_TRACKED_ADDONS, [])
self._tracked_integrations = self.config_entry.options[
CONF_TRACKED_INTEGRATIONS
]
@@ -66,7 +62,6 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
async def _async_update_data(self) -> AnalyticsData:
try:
- addons_data = await self._client.get_addons()
data = await self._client.get_current_analytics()
custom_data = await self._client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError as err:
@@ -75,9 +70,6 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
) from err
except HomeassistantAnalyticsNotModifiedError:
return self.data
- addons = {
- addon: get_addon_value(addons_data, addon) for addon in self._tracked_addons
- }
core_integrations = {
integration: data.integrations.get(integration, 0)
for integration in self._tracked_integrations
@@ -89,19 +81,11 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
return AnalyticsData(
data.active_installations,
data.reports_integrations,
- addons,
core_integrations,
custom_integrations,
)
-def get_addon_value(data: dict[str, Addon], name_slug: str) -> int:
- """Get addon value."""
- if name_slug in data:
- return data[name_slug].total
- return 0
-
-
def get_custom_integration_value(
data: dict[str, CustomIntegration], domain: str
) -> int:
diff --git a/homeassistant/components/analytics_insights/sensor.py b/homeassistant/components/analytics_insights/sensor.py
index 324ca6991d2..264c34e75ef 100644
--- a/homeassistant/components/analytics_insights/sensor.py
+++ b/homeassistant/components/analytics_insights/sensor.py
@@ -29,20 +29,6 @@ class AnalyticsSensorEntityDescription(SensorEntityDescription):
value_fn: Callable[[AnalyticsData], StateType]
-def get_addon_entity_description(
- name_slug: str,
-) -> AnalyticsSensorEntityDescription:
- """Get addon entity description."""
- return AnalyticsSensorEntityDescription(
- key=f"addon_{name_slug}_active_installations",
- translation_key="addons",
- name=name_slug,
- state_class=SensorStateClass.TOTAL,
- native_unit_of_measurement="active installations",
- value_fn=lambda data: data.addons.get(name_slug),
- )
-
-
def get_core_integration_entity_description(
domain: str, name: str
) -> AnalyticsSensorEntityDescription:
@@ -103,13 +89,6 @@ async def async_setup_entry(
analytics_data.coordinator
)
entities: list[HomeassistantAnalyticsSensor] = []
- entities.extend(
- HomeassistantAnalyticsSensor(
- coordinator,
- get_addon_entity_description(addon_name_slug),
- )
- for addon_name_slug in coordinator.data.addons
- )
entities.extend(
HomeassistantAnalyticsSensor(
coordinator,
diff --git a/homeassistant/components/analytics_insights/strings.json b/homeassistant/components/analytics_insights/strings.json
index 10d3c19a2f6..b3445fdf47e 100644
--- a/homeassistant/components/analytics_insights/strings.json
+++ b/homeassistant/components/analytics_insights/strings.json
@@ -3,12 +3,10 @@
"step": {
"user": {
"data": {
- "tracked_addons": "Addons",
"tracked_integrations": "Integrations",
"tracked_custom_integrations": "Custom integrations"
},
"data_description": {
- "tracked_addons": "Select the addons you want to track",
"tracked_integrations": "Select the integrations you want to track",
"tracked_custom_integrations": "Select the custom integrations you want to track"
}
@@ -26,12 +24,10 @@
"step": {
"init": {
"data": {
- "tracked_addons": "[%key:component::analytics_insights::config::step::user::data::tracked_addons%]",
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]",
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]"
},
"data_description": {
- "tracked_addons": "[%key:component::analytics_insights::config::step::user::data_description::tracked_addons%]",
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]",
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]"
}
diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py
index 34c4212c913..34b324db169 100644
--- a/homeassistant/components/androidtv/__init__.py
+++ b/homeassistant/components/androidtv/__init__.py
@@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
-import logging
import os
from typing import Any
@@ -41,7 +40,6 @@ from .const import (
CONF_ADB_SERVER_IP,
CONF_ADB_SERVER_PORT,
CONF_ADBKEY,
- CONF_SCREENCAP_INTERVAL,
CONF_STATE_DETECTION_RULES,
DEFAULT_ADB_SERVER_PORT,
DEVICE_ANDROIDTV,
@@ -68,8 +66,6 @@ RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
-_LOGGER = logging.getLogger(__name__)
-
@dataclass
class AndroidTVRuntimeData:
@@ -161,32 +157,6 @@ async def async_connect_androidtv(
return aftv, None
-async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
- """Migrate old entry."""
- _LOGGER.debug(
- "Migrating configuration from version %s.%s", entry.version, entry.minor_version
- )
-
- if entry.version == 1:
- new_options = {**entry.options}
-
- # Migrate MinorVersion 1 -> MinorVersion 2: New option
- if entry.minor_version < 2:
- new_options = {**new_options, CONF_SCREENCAP_INTERVAL: 0}
-
- hass.config_entries.async_update_entry(
- entry, options=new_options, minor_version=2, version=1
- )
-
- _LOGGER.debug(
- "Migration to configuration version %s.%s successful",
- entry.version,
- entry.minor_version,
- )
-
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool:
"""Set up Android Debug Bridge platform."""
diff --git a/homeassistant/components/androidtv/config_flow.py b/homeassistant/components/androidtv/config_flow.py
index afaba5175da..e8350acc9cb 100644
--- a/homeassistant/components/androidtv/config_flow.py
+++ b/homeassistant/components/androidtv/config_flow.py
@@ -13,7 +13,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT
from homeassistant.core import callback
@@ -34,7 +34,7 @@ from .const import (
CONF_APPS,
CONF_EXCLUDE_UNNAMED_APPS,
CONF_GET_SOURCES,
- CONF_SCREENCAP_INTERVAL,
+ CONF_SCREENCAP,
CONF_STATE_DETECTION_RULES,
CONF_TURN_OFF_COMMAND,
CONF_TURN_ON_COMMAND,
@@ -43,7 +43,7 @@ from .const import (
DEFAULT_EXCLUDE_UNNAMED_APPS,
DEFAULT_GET_SOURCES,
DEFAULT_PORT,
- DEFAULT_SCREENCAP_INTERVAL,
+ DEFAULT_SCREENCAP,
DEVICE_CLASSES,
DOMAIN,
PROP_ETHMAC,
@@ -76,7 +76,6 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
VERSION = 1
- MINOR_VERSION = 2
@callback
def _show_setup_form(
@@ -186,14 +185,16 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN):
return OptionsFlowHandler(config_entry)
-class OptionsFlowHandler(OptionsFlow):
+class OptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle an option flow for Android Debug Bridge."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
- self._apps: dict[str, Any] = dict(config_entry.options.get(CONF_APPS, {}))
- self._state_det_rules: dict[str, Any] = dict(
- config_entry.options.get(CONF_STATE_DETECTION_RULES, {})
+ super().__init__(config_entry)
+
+ self._apps: dict[str, Any] = self.options.setdefault(CONF_APPS, {})
+ self._state_det_rules: dict[str, Any] = self.options.setdefault(
+ CONF_STATE_DETECTION_RULES, {}
)
self._conf_app_id: str | None = None
self._conf_rule_id: str | None = None
@@ -235,7 +236,7 @@ class OptionsFlowHandler(OptionsFlow):
SelectOptionDict(value=k, label=v) for k, v in apps_list.items()
]
rules = [RULES_NEW_ID, *self._state_det_rules]
- options = self.config_entry.options
+ options = self.options
data_schema = vol.Schema(
{
@@ -252,12 +253,10 @@ class OptionsFlowHandler(OptionsFlow):
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
),
): bool,
- vol.Required(
- CONF_SCREENCAP_INTERVAL,
- default=options.get(
- CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
- ),
- ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=15)),
+ vol.Optional(
+ CONF_SCREENCAP,
+ default=options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP),
+ ): bool,
vol.Optional(
CONF_TURN_OFF_COMMAND,
description={
diff --git a/homeassistant/components/androidtv/const.py b/homeassistant/components/androidtv/const.py
index 0d9bdc8f6c0..ee279c0fb3a 100644
--- a/homeassistant/components/androidtv/const.py
+++ b/homeassistant/components/androidtv/const.py
@@ -9,7 +9,6 @@ CONF_APPS = "apps"
CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps"
CONF_GET_SOURCES = "get_sources"
CONF_SCREENCAP = "screencap"
-CONF_SCREENCAP_INTERVAL = "screencap_interval"
CONF_STATE_DETECTION_RULES = "state_detection_rules"
CONF_TURN_OFF_COMMAND = "turn_off_command"
CONF_TURN_ON_COMMAND = "turn_on_command"
@@ -19,7 +18,7 @@ DEFAULT_DEVICE_CLASS = "auto"
DEFAULT_EXCLUDE_UNNAMED_APPS = False
DEFAULT_GET_SOURCES = True
DEFAULT_PORT = 5555
-DEFAULT_SCREENCAP_INTERVAL = 5
+DEFAULT_SCREENCAP = True
DEVICE_ANDROIDTV = "androidtv"
DEVICE_FIRETV = "firetv"
diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py
index 728411ddf42..6e338529ad4 100644
--- a/homeassistant/components/androidtv/media_player.py
+++ b/homeassistant/components/androidtv/media_player.py
@@ -2,9 +2,10 @@
from __future__ import annotations
-from datetime import datetime, timedelta
+from datetime import timedelta
import hashlib
import logging
+from typing import Any
from androidtv.constants import APPS, KEYS
from androidtv.setup_async import AndroidTVAsync, FireTVAsync
@@ -22,19 +23,19 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.util.dt import utcnow
+from homeassistant.util import Throttle
from . import AndroidTVConfigEntry
from .const import (
CONF_APPS,
CONF_EXCLUDE_UNNAMED_APPS,
CONF_GET_SOURCES,
- CONF_SCREENCAP_INTERVAL,
+ CONF_SCREENCAP,
CONF_TURN_OFF_COMMAND,
CONF_TURN_ON_COMMAND,
DEFAULT_EXCLUDE_UNNAMED_APPS,
DEFAULT_GET_SOURCES,
- DEFAULT_SCREENCAP_INTERVAL,
+ DEFAULT_SCREENCAP,
DEVICE_ANDROIDTV,
SIGNAL_CONFIG_ENTITY,
)
@@ -47,6 +48,8 @@ ATTR_DEVICE_PATH = "device_path"
ATTR_HDMI_INPUT = "hdmi_input"
ATTR_LOCAL_PATH = "local_path"
+MIN_TIME_BETWEEN_SCREENCAPS = timedelta(seconds=60)
+
SERVICE_ADB_COMMAND = "adb_command"
SERVICE_DOWNLOAD = "download"
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
@@ -122,8 +125,7 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
self._app_name_to_id: dict[str, str] = {}
self._get_sources = DEFAULT_GET_SOURCES
self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS
- self._screencap_delta: timedelta | None = None
- self._last_screencap: datetime | None = None
+ self._screencap = DEFAULT_SCREENCAP
self.turn_on_command: str | None = None
self.turn_off_command: str | None = None
@@ -157,13 +159,7 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
self._exclude_unnamed_apps = options.get(
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
)
- screencap_interval: int = options.get(
- CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
- )
- if screencap_interval > 0:
- self._screencap_delta = timedelta(minutes=screencap_interval)
- else:
- self._screencap_delta = None
+ self._screencap = options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP)
self.turn_off_command = options.get(CONF_TURN_OFF_COMMAND)
self.turn_on_command = options.get(CONF_TURN_ON_COMMAND)
@@ -187,7 +183,7 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
async def _async_get_screencap(self, prev_app_id: str | None = None) -> None:
"""Take a screen capture from the device when enabled."""
if (
- not self._screencap_delta
+ not self._screencap
or self.state in {MediaPlayerState.OFF, None}
or not self.available
):
@@ -197,18 +193,11 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
force: bool = prev_app_id is not None
if force:
force = prev_app_id != self._attr_app_id
- await self._adb_get_screencap(force)
+ await self._adb_get_screencap(no_throttle=force)
- async def _adb_get_screencap(self, force: bool = False) -> None:
- """Take a screen capture from the device every configured minutes."""
- time_elapsed = self._screencap_delta is not None and (
- self._last_screencap is None
- or (utcnow() - self._last_screencap) >= self._screencap_delta
- )
- if not (force or time_elapsed):
- return
-
- self._last_screencap = utcnow()
+ @Throttle(MIN_TIME_BETWEEN_SCREENCAPS)
+ async def _adb_get_screencap(self, **kwargs: Any) -> None:
+ """Take a screen capture from the device every 60 seconds."""
if media_data := await self._adb_screencap():
self._media_image = media_data, "image/png"
self._attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16]
diff --git a/homeassistant/components/androidtv/strings.json b/homeassistant/components/androidtv/strings.json
index b6f5d494d0f..3032e9ac6ef 100644
--- a/homeassistant/components/androidtv/strings.json
+++ b/homeassistant/components/androidtv/strings.json
@@ -31,7 +31,7 @@
"apps": "Configure applications list",
"get_sources": "Retrieve the running apps as the list of sources",
"exclude_unnamed_apps": "Exclude apps with unknown name from the sources list",
- "screencap_interval": "Interval in minutes between screen capture for album art (set 0 to disable)",
+ "screencap": "Use screen capture for album art",
"state_detection_rules": "Configure state detection rules",
"turn_off_command": "ADB shell turn off command (leave empty for default)",
"turn_on_command": "ADB shell turn on command (leave empty for default)"
diff --git a/homeassistant/components/androidtv_remote/config_flow.py b/homeassistant/components/androidtv_remote/config_flow.py
index 3500e4ff47b..40ecb64afc7 100644
--- a/homeassistant/components/androidtv_remote/config_flow.py
+++ b/homeassistant/components/androidtv_remote/config_flow.py
@@ -20,7 +20,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import callback
@@ -151,18 +151,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
if not (mac := discovery_info.properties.get("bt")):
return self.async_abort(reason="cannot_connect")
self.mac = mac
- existing_config_entry = await self.async_set_unique_id(format_mac(mac))
- # Sometimes, devices send an invalid zeroconf message with multiple addresses
- # and one of them, which could end up being in discovery_info.host, is from a
- # different device. If any of the discovery_info.ip_addresses matches the
- # existing host, don't update the host.
- if existing_config_entry and len(discovery_info.ip_addresses) > 1:
- existing_host = existing_config_entry.data[CONF_HOST]
- if existing_host != self.host:
- if existing_host in [
- str(ip_address) for ip_address in discovery_info.ip_addresses
- ]:
- self.host = existing_host
+ await self.async_set_unique_id(format_mac(self.mac))
self._abort_if_unique_id_configured(
updates={CONF_HOST: self.host, CONF_NAME: self.name}
)
@@ -221,12 +210,13 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
return AndroidTVRemoteOptionsFlowHandler(config_entry)
-class AndroidTVRemoteOptionsFlowHandler(OptionsFlow):
+class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Android TV Remote options flow."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
- self._apps: dict[str, Any] = dict(config_entry.options.get(CONF_APPS, {}))
+ super().__init__(config_entry)
+ self._apps: dict[str, Any] = self.options.setdefault(CONF_APPS, {})
self._conf_app_id: str | None = None
@callback
diff --git a/homeassistant/components/anova/__init__.py b/homeassistant/components/anova/__init__.py
index 4ae4750b9a9..02c468c1319 100644
--- a/homeassistant/components/anova/__init__.py
+++ b/homeassistant/components/anova/__init__.py
@@ -13,7 +13,7 @@ from anova_wifi import (
WebsocketFailure,
)
-from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME, Platform
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
@@ -71,25 +71,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: AnovaConfigEntry) -> bo
# Disconnect from WS
await entry.runtime_data.api.disconnect_websocket()
return unload_ok
-
-
-async def async_migrate_entry(hass: HomeAssistant, entry: AnovaConfigEntry) -> bool:
- """Migrate entry."""
- _LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
-
- if entry.version > 1:
- # This means the user has downgraded from a future version
- return False
-
- if entry.version == 1 and entry.minor_version == 1:
- new_data = {**entry.data}
- if CONF_DEVICES in new_data:
- new_data.pop(CONF_DEVICES)
-
- hass.config_entries.async_update_entry(entry, data=new_data, minor_version=2)
-
- _LOGGER.debug(
- "Migration to version %s:%s successful", entry.version, entry.minor_version
- )
-
- return True
diff --git a/homeassistant/components/anova/config_flow.py b/homeassistant/components/anova/config_flow.py
index bc4723b1dba..6e331ccf4a2 100644
--- a/homeassistant/components/anova/config_flow.py
+++ b/homeassistant/components/anova/config_flow.py
@@ -6,7 +6,7 @@ from anova_wifi import AnovaApi, InvalidLogin
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
@@ -16,7 +16,6 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
"""Sets up a config flow for Anova."""
VERSION = 1
- MINOR_VERSION = 2
async def async_step_user(
self, user_input: dict[str, str] | None = None
@@ -43,6 +42,8 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
data={
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
+ # this can be removed in a migration to 1.2 in 2024.11
+ CONF_DEVICES: [],
},
)
diff --git a/homeassistant/components/anthropic/config_flow.py b/homeassistant/components/anthropic/config_flow.py
index fa43a3c4bcc..5ea167090c6 100644
--- a/homeassistant/components/anthropic/config_flow.py
+++ b/homeassistant/components/anthropic/config_flow.py
@@ -121,6 +121,7 @@ class AnthropicOptionsFlow(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
+ self.config_entry = config_entry
self.last_rendered_recommended = config_entry.options.get(
CONF_RECOMMENDED, False
)
diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py
index 68d3f58a63a..0f781e0e1c6 100644
--- a/homeassistant/components/apache_kafka/__init__.py
+++ b/homeassistant/components/apache_kafka/__init__.py
@@ -38,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema(
vol.Required(CONF_TOPIC): cv.string,
vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA,
vol.Optional(CONF_SECURITY_PROTOCOL, default="PLAINTEXT"): vol.In(
- ["PLAINTEXT", "SSL", "SASL_SSL"]
+ ["PLAINTEXT", "SASL_SSL"]
),
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
@@ -94,7 +94,7 @@ class KafkaManager:
port: int,
topic: str,
entities_filter: EntityFilter,
- security_protocol: Literal["PLAINTEXT", "SSL", "SASL_SSL"],
+ security_protocol: Literal["PLAINTEXT", "SASL_SSL"],
username: str | None,
password: str | None,
) -> None:
diff --git a/homeassistant/components/apsystems/sensor.py b/homeassistant/components/apsystems/sensor.py
index f87bc0f3f26..afeb9d071ab 100644
--- a/homeassistant/components/apsystems/sensor.py
+++ b/homeassistant/components/apsystems/sensor.py
@@ -12,11 +12,12 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
+ StateType,
)
from homeassistant.const import UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import DiscoveryInfoType, StateType
+from homeassistant.helpers.typing import DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import ApSystemsConfigEntry, ApSystemsData
diff --git a/homeassistant/components/aranet/__init__.py b/homeassistant/components/aranet/__init__.py
index 81b3dae04de..3a2bc266653 100644
--- a/homeassistant/components/aranet/__init__.py
+++ b/homeassistant/components/aranet/__init__.py
@@ -15,14 +15,12 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
+from .const import DOMAIN
+
PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
-type AranetConfigEntry = ConfigEntry[
- PassiveBluetoothProcessorCoordinator[Aranet4Advertisement]
-]
-
def _service_info_to_adv(
service_info: BluetoothServiceInfoBleak,
@@ -30,25 +28,30 @@ def _service_info_to_adv(
return Aranet4Advertisement(service_info.device, service_info.advertisement)
-async def async_setup_entry(hass: HomeAssistant, entry: AranetConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Aranet from a config entry."""
address = entry.unique_id
assert address is not None
- coordinator = PassiveBluetoothProcessorCoordinator(
- hass,
- _LOGGER,
- address=address,
- mode=BluetoothScanningMode.PASSIVE,
- update_method=_service_info_to_adv,
+ coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
+ PassiveBluetoothProcessorCoordinator(
+ hass,
+ _LOGGER,
+ address=address,
+ mode=BluetoothScanningMode.PASSIVE,
+ update_method=_service_info_to_adv,
+ )
)
- entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
- # only start after all platforms have had a chance to subscribe
- entry.async_on_unload(coordinator.async_start())
+ entry.async_on_unload(
+ coordinator.async_start()
+ ) # only start after all platforms have had a chance to subscribe
return True
-async def async_unload_entry(hass: HomeAssistant, entry: AranetConfigEntry) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/aranet/sensor.py b/homeassistant/components/aranet/sensor.py
index d7fbd0e4b3b..1dc4b9f956e 100644
--- a/homeassistant/components/aranet/sensor.py
+++ b/homeassistant/components/aranet/sensor.py
@@ -8,10 +8,12 @@ from typing import Any
from aranet4.client import Aranet4Advertisement
from bleak.backends.device import BLEDevice
+from homeassistant import config_entries
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothDataProcessor,
PassiveBluetoothDataUpdate,
PassiveBluetoothEntityKey,
+ PassiveBluetoothProcessorCoordinator,
PassiveBluetoothProcessorEntity,
)
from homeassistant.components.sensor import (
@@ -36,8 +38,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import AranetConfigEntry
-from .const import ARANET_MANUFACTURER_NAME
+from .const import ARANET_MANUFACTURER_NAME, DOMAIN
@dataclass(frozen=True)
@@ -173,17 +174,20 @@ def sensor_update_to_bluetooth_data_update(
async def async_setup_entry(
hass: HomeAssistant,
- entry: AranetConfigEntry,
+ entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Aranet sensors."""
+ coordinator: PassiveBluetoothProcessorCoordinator[Aranet4Advertisement] = hass.data[
+ DOMAIN
+ ][entry.entry_id]
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
entry.async_on_unload(
processor.async_add_entities_listener(
Aranet4BluetoothSensorEntity, async_add_entities
)
)
- entry.async_on_unload(entry.runtime_data.async_register_processor(processor))
+ entry.async_on_unload(coordinator.async_register_processor(processor))
class Aranet4BluetoothSensorEntity(
diff --git a/homeassistant/components/arris_tg2492lg/manifest.json b/homeassistant/components/arris_tg2492lg/manifest.json
index c36423d287a..fa7673b4276 100644
--- a/homeassistant/components/arris_tg2492lg/manifest.json
+++ b/homeassistant/components/arris_tg2492lg/manifest.json
@@ -2,6 +2,7 @@
"domain": "arris_tg2492lg",
"name": "Arris TG2492LG",
"codeowners": ["@vanbalken"],
+ "dependencies": [],
"documentation": "https://www.home-assistant.io/integrations/arris_tg2492lg",
"integration_type": "hub",
"iot_class": "local_polling",
diff --git a/homeassistant/components/aseko_pool_live/config_flow.py b/homeassistant/components/aseko_pool_live/config_flow.py
index e93eb803d62..a07395742fe 100644
--- a/homeassistant/components/aseko_pool_live/config_flow.py
+++ b/homeassistant/components/aseko_pool_live/config_flow.py
@@ -29,7 +29,7 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
}
)
- async def get_account_info(self, email: str, password: str) -> dict[str, Any]:
+ async def get_account_info(self, email: str, password: str) -> dict:
"""Get account info from the mobile API and the web API."""
aseko = Aseko(email, password)
user = await aseko.login()
@@ -70,9 +70,7 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_store_credentials(self, info: dict[str, Any]) -> ConfigFlowResult:
"""Store validated credentials."""
- await self.async_set_unique_id(info[CONF_UNIQUE_ID])
if self.source == SOURCE_REAUTH:
- self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
self._get_reauth_entry(),
title=info[CONF_EMAIL],
@@ -82,6 +80,7 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
},
)
+ await self.async_set_unique_id(info[CONF_UNIQUE_ID])
self._abort_if_unique_id_configured()
return self.async_create_entry(
diff --git a/homeassistant/components/aseko_pool_live/strings.json b/homeassistant/components/aseko_pool_live/strings.json
index 2805b60cdfd..9f6a99b8d12 100644
--- a/homeassistant/components/aseko_pool_live/strings.json
+++ b/homeassistant/components/aseko_pool_live/strings.json
@@ -21,8 +21,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
- "unique_id_mismatch": "The user identifier does not match the previous identifier"
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"entity": {
diff --git a/homeassistant/components/assist_pipeline/audio_enhancer.py b/homeassistant/components/assist_pipeline/audio_enhancer.py
index 1fabc7790e7..ff2b122187a 100644
--- a/homeassistant/components/assist_pipeline/audio_enhancer.py
+++ b/homeassistant/components/assist_pipeline/audio_enhancer.py
@@ -22,8 +22,8 @@ class EnhancedAudioChunk:
timestamp_ms: int
"""Timestamp relative to start of audio stream (milliseconds)"""
- speech_probability: float | None
- """Probability that audio chunk contains speech (0-1), None if unknown"""
+ is_speech: bool | None
+ """True if audio chunk likely contains speech, False if not, None if unknown"""
class AudioEnhancer(ABC):
@@ -70,27 +70,27 @@ class MicroVadSpeexEnhancer(AudioEnhancer):
)
self.vad: MicroVad | None = None
+ self.threshold = 0.5
if self.is_vad_enabled:
self.vad = MicroVad()
- _LOGGER.debug("Initialized microVAD")
+ _LOGGER.debug("Initialized microVAD with threshold=%s", self.threshold)
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
- speech_probability: float | None = None
+ is_speech: bool | None = None
assert len(audio) == BYTES_PER_CHUNK
if self.vad is not None:
# Run VAD
- speech_probability = self.vad.Process10ms(audio)
+ speech_prob = self.vad.Process10ms(audio)
+ is_speech = speech_prob > self.threshold
if self.audio_processor is not None:
# Run noise suppression and auto gain
audio = self.audio_processor.Process10ms(audio).audio
return EnhancedAudioChunk(
- audio=audio,
- timestamp_ms=timestamp_ms,
- speech_probability=speech_probability,
+ audio=audio, timestamp_ms=timestamp_ms, is_speech=is_speech
)
diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py
index a55e23ae051..a4255e37756 100644
--- a/homeassistant/components/assist_pipeline/pipeline.py
+++ b/homeassistant/components/assist_pipeline/pipeline.py
@@ -780,9 +780,7 @@ class PipelineRun:
# speaking the voice command.
audio_chunks_for_stt.extend(
EnhancedAudioChunk(
- audio=chunk_ts[0],
- timestamp_ms=chunk_ts[1],
- speech_probability=None,
+ audio=chunk_ts[0], timestamp_ms=chunk_ts[1], is_speech=False
)
for chunk_ts in result.queued_audio
)
@@ -829,7 +827,7 @@ class PipelineRun:
if wake_word_vad is not None:
chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate
- if not wake_word_vad.process(chunk_seconds, chunk.speech_probability):
+ if not wake_word_vad.process(chunk_seconds, chunk.is_speech):
raise WakeWordTimeoutError(
code="wake-word-timeout", message="Wake word was not detected"
)
@@ -957,7 +955,7 @@ class PipelineRun:
if stt_vad is not None:
chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate
- if not stt_vad.process(chunk_seconds, chunk.speech_probability):
+ if not stt_vad.process(chunk_seconds, chunk.is_speech):
# Silence detected at the end of voice command
self.process_event(
PipelineEvent(
@@ -1223,7 +1221,7 @@ class PipelineRun:
yield EnhancedAudioChunk(
audio=sub_chunk,
timestamp_ms=timestamp_ms,
- speech_probability=None, # no VAD
+ is_speech=None, # no VAD
)
timestamp_ms += MS_PER_CHUNK
diff --git a/homeassistant/components/assist_pipeline/vad.py b/homeassistant/components/assist_pipeline/vad.py
index deae5b9b7b3..4782d14dee4 100644
--- a/homeassistant/components/assist_pipeline/vad.py
+++ b/homeassistant/components/assist_pipeline/vad.py
@@ -75,7 +75,7 @@ class AudioBuffer:
class VoiceCommandSegmenter:
"""Segments an audio stream into voice commands."""
- speech_seconds: float = 0.1
+ speech_seconds: float = 0.3
"""Seconds of speech before voice command has started."""
command_seconds: float = 1.0
@@ -96,12 +96,6 @@ class VoiceCommandSegmenter:
timed_out: bool = False
"""True a timeout occurred during voice command."""
- before_command_speech_threshold: float = 0.2
- """Probability threshold for speech before voice command."""
-
- in_command_speech_threshold: float = 0.5
- """Probability threshold for speech during voice command."""
-
_speech_seconds_left: float = 0.0
"""Seconds left before considering voice command as started."""
@@ -130,7 +124,7 @@ class VoiceCommandSegmenter:
self._reset_seconds_left = self.reset_seconds
self.in_command = False
- def process(self, chunk_seconds: float, speech_probability: float | None) -> bool:
+ def process(self, chunk_seconds: float, is_speech: bool | None) -> bool:
"""Process samples using external VAD.
Returns False when command is done.
@@ -148,12 +142,7 @@ class VoiceCommandSegmenter:
self.timed_out = True
return False
- if speech_probability is None:
- speech_probability = 0.0
-
if not self.in_command:
- # Before command
- is_speech = speech_probability > self.before_command_speech_threshold
if is_speech:
self._reset_seconds_left = self.reset_seconds
self._speech_seconds_left -= chunk_seconds
@@ -171,29 +160,24 @@ class VoiceCommandSegmenter:
if self._reset_seconds_left <= 0:
self._speech_seconds_left = self.speech_seconds
self._reset_seconds_left = self.reset_seconds
+ elif not is_speech:
+ # Silence in command
+ self._reset_seconds_left = self.reset_seconds
+ self._silence_seconds_left -= chunk_seconds
+ self._command_seconds_left -= chunk_seconds
+ if (self._silence_seconds_left <= 0) and (self._command_seconds_left <= 0):
+ # Command finished successfully
+ self.reset()
+ _LOGGER.debug("Voice command finished")
+ return False
else:
- # In command
- is_speech = speech_probability > self.in_command_speech_threshold
- if not is_speech:
- # Silence in command
+ # Speech in command.
+ # Reset silence counter if enough speech.
+ self._reset_seconds_left -= chunk_seconds
+ self._command_seconds_left -= chunk_seconds
+ if self._reset_seconds_left <= 0:
+ self._silence_seconds_left = self.silence_seconds
self._reset_seconds_left = self.reset_seconds
- self._silence_seconds_left -= chunk_seconds
- self._command_seconds_left -= chunk_seconds
- if (self._silence_seconds_left <= 0) and (
- self._command_seconds_left <= 0
- ):
- # Command finished successfully
- self.reset()
- _LOGGER.debug("Voice command finished")
- return False
- else:
- # Speech in command.
- # Reset silence counter if enough speech.
- self._reset_seconds_left -= chunk_seconds
- self._command_seconds_left -= chunk_seconds
- if self._reset_seconds_left <= 0:
- self._silence_seconds_left = self.silence_seconds
- self._reset_seconds_left = self.reset_seconds
return True
@@ -242,9 +226,6 @@ class VoiceActivityTimeout:
reset_seconds: float = 0.5
"""Seconds of speech before resetting timeout."""
- speech_threshold: float = 0.5
- """Threshold for speech."""
-
_silence_seconds_left: float = 0.0
"""Seconds left before considering voice command as stopped."""
@@ -260,15 +241,12 @@ class VoiceActivityTimeout:
self._silence_seconds_left = self.silence_seconds
self._reset_seconds_left = self.reset_seconds
- def process(self, chunk_seconds: float, speech_probability: float | None) -> bool:
+ def process(self, chunk_seconds: float, is_speech: bool | None) -> bool:
"""Process samples using external VAD.
Returns False when timeout is reached.
"""
- if speech_probability is None:
- speech_probability = 0.0
-
- if speech_probability > self.speech_threshold:
+ if is_speech:
# Speech
self._reset_seconds_left -= chunk_seconds
if self._reset_seconds_left <= 0:
diff --git a/homeassistant/components/aussie_broadband/config_flow.py b/homeassistant/components/aussie_broadband/config_flow.py
index 5bc6ed1aa5c..540c04f3993 100644
--- a/homeassistant/components/aussie_broadband/config_flow.py
+++ b/homeassistant/components/aussie_broadband/config_flow.py
@@ -99,9 +99,8 @@ class AussieBroadbandConfigFlow(ConfigFlow, domain=DOMAIN):
}
if not (errors := await self.async_auth(data)):
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=data
- )
+ entry = self._get_reauth_entry()
+ return self.async_update_reload_and_abort(entry, data=data)
return self.async_show_form(
step_id="reauth_confirm",
diff --git a/homeassistant/components/autarco/manifest.json b/homeassistant/components/autarco/manifest.json
index 0567aeba722..0058ab9af77 100644
--- a/homeassistant/components/autarco/manifest.json
+++ b/homeassistant/components/autarco/manifest.json
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/autarco",
"iot_class": "cloud_polling",
- "requirements": ["autarco==3.1.0"]
+ "requirements": ["autarco==3.0.0"]
}
diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml
index 11900708b19..ad9c6f0286b 100644
--- a/homeassistant/components/automation/blueprints/motion_light.yaml
+++ b/homeassistant/components/automation/blueprints/motion_light.yaml
@@ -35,24 +35,24 @@ blueprint:
mode: restart
max_exceeded: silent
-triggers:
- trigger: state
+trigger:
+ platform: state
entity_id: !input motion_entity
from: "off"
to: "on"
-actions:
+action:
- alias: "Turn on the light"
- action: light.turn_on
+ service: light.turn_on
target: !input light_target
- alias: "Wait until there is no motion from device"
wait_for_trigger:
- trigger: state
+ platform: state
entity_id: !input motion_entity
from: "on"
to: "off"
- alias: "Wait the number of seconds that has been set"
delay: !input no_motion_wait
- alias: "Turn off the light"
- action: light.turn_off
+ service: light.turn_off
target: !input light_target
diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
index e072aad2565..e1e3bd5b2f6 100644
--- a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
+++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
@@ -25,8 +25,8 @@ blueprint:
filter:
integration: mobile_app
-triggers:
- trigger: state
+trigger:
+ platform: state
entity_id: !input person_entity
variables:
@@ -36,13 +36,13 @@ variables:
person_entity: !input person_entity
person_name: "{{ states[person_entity].name }}"
-conditions:
+condition:
condition: template
# The first case handles leaving the Home zone which has a special state when zoning called 'home'.
# The second case handles leaving all other zones.
value_template: "{{ zone_entity == 'zone.home' and trigger.from_state.state == 'home' and trigger.to_state.state != 'home' or trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}"
-actions:
+action:
- alias: "Notify that a person has left the zone"
domain: mobile_app
type: notify
diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py
index 88985b0db10..8b40eacbafc 100644
--- a/homeassistant/components/awair/config_flow.py
+++ b/homeassistant/components/awair/config_flow.py
@@ -209,9 +209,10 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN):
_, error = await self._check_cloud_connection(access_token)
if error is None:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=user_input
- )
+ entry = await self.async_set_unique_id(self.unique_id)
+ assert entry
+ self.hass.config_entries.async_update_entry(entry, data=user_input)
+ return self.async_abort(reason="reauth_successful")
if error != "invalid_access_token":
return self.async_abort(reason=error)
diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json
index a0fbd350dab..25257bc3e1c 100644
--- a/homeassistant/components/awair/manifest.json
+++ b/homeassistant/components/awair/manifest.json
@@ -3,6 +3,11 @@
"name": "Awair",
"codeowners": ["@ahayworth", "@danielsjf"],
"config_flow": true,
+ "dhcp": [
+ {
+ "macaddress": "70886B1*"
+ }
+ ],
"documentation": "https://www.home-assistant.io/integrations/awair",
"iot_class": "local_polling",
"loggers": ["python_awair"],
diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py
index 592b1e2d41f..0434ed71a22 100644
--- a/homeassistant/components/axis/config_flow.py
+++ b/homeassistant/components/axis/config_flow.py
@@ -13,12 +13,10 @@ import voluptuous as vol
from homeassistant.components import dhcp, ssdp, zeroconf
from homeassistant.config_entries import (
SOURCE_IGNORE,
- SOURCE_REAUTH,
- SOURCE_RECONFIGURE,
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import (
CONF_HOST,
@@ -59,11 +57,9 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> AxisOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> AxisOptionsFlowHandler:
"""Get the options flow for this handler."""
- return AxisOptionsFlowHandler()
+ return AxisOptionsFlowHandler(config_entry)
def __init__(self) -> None:
"""Initialize the Axis config flow."""
@@ -91,30 +87,27 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
else:
serial = api.vapix.serial_number
- config = {
+ await self.async_set_unique_id(format_mac(serial))
+
+ self._abort_if_unique_id_configured(
+ updates={
+ CONF_PROTOCOL: user_input[CONF_PROTOCOL],
+ CONF_HOST: user_input[CONF_HOST],
+ CONF_PORT: user_input[CONF_PORT],
+ CONF_USERNAME: user_input[CONF_USERNAME],
+ CONF_PASSWORD: user_input[CONF_PASSWORD],
+ }
+ )
+
+ self.config = {
CONF_PROTOCOL: user_input[CONF_PROTOCOL],
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
+ CONF_MODEL: api.vapix.product_number,
}
- await self.async_set_unique_id(format_mac(serial))
-
- if self.source == SOURCE_REAUTH:
- self._abort_if_unique_id_mismatch()
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=config
- )
- if self.source == SOURCE_RECONFIGURE:
- self._abort_if_unique_id_mismatch()
- return self.async_update_reload_and_abort(
- self._get_reconfigure_entry(), data_updates=config
- )
- self._abort_if_unique_id_configured()
-
- self.config = config | {CONF_MODEL: api.vapix.product_number}
-
return await self._create_entry(serial)
data = self.discovery_schema or {
@@ -159,9 +152,8 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Trigger a reconfiguration flow."""
- return await self._redo_configuration(
- self._get_reconfigure_entry().data, keep_password=True
- )
+ entry = self._get_reconfigure_entry()
+ return await self._redo_configuration(entry.data, keep_password=True)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
@@ -266,7 +258,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
return await self.async_step_user()
-class AxisOptionsFlowHandler(OptionsFlow):
+class AxisOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle Axis device options."""
config_entry: AxisConfigEntry
@@ -284,7 +276,8 @@ class AxisOptionsFlowHandler(OptionsFlow):
) -> ConfigFlowResult:
"""Manage the Axis device stream options."""
if user_input is not None:
- return self.async_create_entry(data=self.config_entry.options | user_input)
+ self.options.update(user_input)
+ return self.async_create_entry(title="", data=self.options)
schema = {}
diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json
index d2265307d47..e028736f4ca 100644
--- a/homeassistant/components/axis/manifest.json
+++ b/homeassistant/components/axis/manifest.json
@@ -30,7 +30,7 @@
"iot_class": "local_push",
"loggers": ["axis"],
"quality_scale": "platinum",
- "requirements": ["axis==63"],
+ "requirements": ["axis==62"],
"ssdp": [
{
"manufacturer": "AXIS"
diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json
index da1963deacd..9534989305d 100644
--- a/homeassistant/components/axis/strings.json
+++ b/homeassistant/components/axis/strings.json
@@ -26,10 +26,7 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"link_local_address": "Link local addresses are not supported",
- "not_axis_device": "Discovered device not an Axis device",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
- "unique_id_mismatch": "The serial number of the device does not match the previous serial number"
+ "not_axis_device": "Discovered device not an Axis device"
}
},
"options": {
diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py
index 13666343e1d..995f9c5f5a1 100644
--- a/homeassistant/components/azure_devops/config_flow.py
+++ b/homeassistant/components/azure_devops/config_flow.py
@@ -42,6 +42,17 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors or {},
)
+ async def _show_reauth_form(self, errors: dict[str, str]) -> ConfigFlowResult:
+ """Show the reauth form to the user."""
+ return self.async_show_form(
+ step_id="reauth",
+ description_placeholders={
+ "project_url": f"{self._organization}/{self._project}"
+ },
+ data_schema=vol.Schema({vol.Required(CONF_PAT): str}),
+ errors=errors or {},
+ )
+
async def _check_setup(self) -> dict[str, str] | None:
"""Check the setup of the flow."""
errors: dict[str, str] = {}
@@ -95,33 +106,22 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN):
self.context["title_placeholders"] = {
"project_url": f"{self._organization}/{self._project}",
}
- return await self.async_step_reauth_confirm()
- async def async_step_reauth_confirm(
- self, user_input: dict[str, str] | None = None
- ) -> ConfigFlowResult:
- """Handle configuration by re-auth."""
- errors: dict[str, str] | None = None
- if user_input is not None:
- errors = await self._check_setup()
- if errors is None:
- self.hass.config_entries.async_update_entry(
- self._get_reauth_entry(),
- data={
- CONF_ORG: self._organization,
- CONF_PROJECT: self._project,
- CONF_PAT: self._pat,
- },
- )
- return self.async_abort(reason="reauth_successful")
- return self.async_show_form(
- step_id="reauth_confirm",
- description_placeholders={
- "project_url": f"{self._organization}/{self._project}"
+ await self.async_set_unique_id(f"{self._organization}_{self._project}")
+
+ errors = await self._check_setup()
+ if errors is not None:
+ return await self._show_reauth_form(errors)
+
+ self.hass.config_entries.async_update_entry(
+ self._get_reauth_entry(),
+ data={
+ CONF_ORG: self._organization,
+ CONF_PROJECT: self._project,
+ CONF_PAT: self._pat,
},
- data_schema=vol.Schema({vol.Required(CONF_PAT): str}),
- errors=errors or {},
)
+ return self.async_abort(reason="reauth_successful")
def _async_create_entry(self) -> ConfigFlowResult:
"""Handle create entry."""
diff --git a/homeassistant/components/azure_devops/strings.json b/homeassistant/components/azure_devops/strings.json
index f5fe5cd06a7..c5304270396 100644
--- a/homeassistant/components/azure_devops/strings.json
+++ b/homeassistant/components/azure_devops/strings.json
@@ -16,7 +16,7 @@
"description": "Set up an Azure DevOps instance to access your project. A Personal Access Token is only required for a private project.",
"title": "Add Azure DevOps Project"
},
- "reauth_confirm": {
+ "reauth": {
"data": {
"personal_access_token": "[%key:component::azure_devops::config::step::user::data::personal_access_token%]"
},
diff --git a/homeassistant/components/azure_event_hub/config_flow.py b/homeassistant/components/azure_event_hub/config_flow.py
index 60ac9bff8cd..046851e6926 100644
--- a/homeassistant/components/azure_event_hub/config_flow.py
+++ b/homeassistant/components/azure_event_hub/config_flow.py
@@ -124,9 +124,7 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
step_id=STEP_CONN_STRING,
data_schema=CONN_STRING_SCHEMA,
errors=errors,
- description_placeholders={
- "event_hub_instance_name": self._data[CONF_EVENT_HUB_INSTANCE_NAME]
- },
+ description_placeholders=self._data[CONF_EVENT_HUB_INSTANCE_NAME],
last_step=True,
)
@@ -146,9 +144,7 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
step_id=STEP_SAS,
data_schema=SAS_SCHEMA,
errors=errors,
- description_placeholders={
- "event_hub_instance_name": self._data[CONF_EVENT_HUB_INSTANCE_NAME]
- },
+ description_placeholders=self._data[CONF_EVENT_HUB_INSTANCE_NAME],
last_step=True,
)
diff --git a/homeassistant/components/backup/__init__.py b/homeassistant/components/backup/__init__.py
index 907fda4c7f8..ac37ef4ec59 100644
--- a/homeassistant/components/backup/__init__.py
+++ b/homeassistant/components/backup/__init__.py
@@ -1,8 +1,8 @@
"""The Backup integration."""
+from homeassistant.components.hassio import is_hassio
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.typing import ConfigType
from .const import DATA_MANAGER, DOMAIN, LOGGER
@@ -32,9 +32,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_handle_create_service(call: ServiceCall) -> None:
"""Service handler for creating backups."""
- await backup_manager.async_create_backup(on_progress=None)
- if backup_task := backup_manager.backup_task:
- await backup_task
+ await backup_manager.generate_backup()
hass.services.async_register(DOMAIN, "create", async_handle_create_service)
diff --git a/homeassistant/components/backup/const.py b/homeassistant/components/backup/const.py
index f613f7cc352..90faa33fc7f 100644
--- a/homeassistant/components/backup/const.py
+++ b/homeassistant/components/backup/const.py
@@ -17,7 +17,6 @@ LOGGER = getLogger(__package__)
EXCLUDE_FROM_BACKUP = [
"__pycache__/*",
".DS_Store",
- ".HA_RESTORE",
"*.db-shm",
"*.log.*",
"*.log",
diff --git a/homeassistant/components/backup/http.py b/homeassistant/components/backup/http.py
index 42693035bd3..793192aa623 100644
--- a/homeassistant/components/backup/http.py
+++ b/homeassistant/components/backup/http.py
@@ -2,26 +2,23 @@
from __future__ import annotations
-import asyncio
from http import HTTPStatus
-from typing import cast
-from aiohttp import BodyPartReader
from aiohttp.hdrs import CONTENT_DISPOSITION
from aiohttp.web import FileResponse, Request, Response
-from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
+from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import slugify
-from .const import DATA_MANAGER
+from .const import DOMAIN
+from .manager import BackupManager
@callback
def async_register_http_views(hass: HomeAssistant) -> None:
"""Register the http views."""
hass.http.register_view(DownloadBackupView)
- hass.http.register_view(UploadBackupView)
class DownloadBackupView(HomeAssistantView):
@@ -39,8 +36,8 @@ class DownloadBackupView(HomeAssistantView):
if not request["hass_user"].is_admin:
return Response(status=HTTPStatus.UNAUTHORIZED)
- manager = request.app[KEY_HASS].data[DATA_MANAGER]
- backup = await manager.async_get_backup(slug=slug)
+ manager: BackupManager = request.app[KEY_HASS].data[DOMAIN]
+ backup = await manager.get_backup(slug)
if backup is None or not backup.path.exists():
return Response(status=HTTPStatus.NOT_FOUND)
@@ -51,29 +48,3 @@ class DownloadBackupView(HomeAssistantView):
CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar"
},
)
-
-
-class UploadBackupView(HomeAssistantView):
- """Generate backup view."""
-
- url = "/api/backup/upload"
- name = "api:backup:upload"
-
- @require_admin
- async def post(self, request: Request) -> Response:
- """Upload a backup file."""
- manager = request.app[KEY_HASS].data[DATA_MANAGER]
- reader = await request.multipart()
- contents = cast(BodyPartReader, await reader.next())
-
- try:
- await manager.async_receive_backup(contents=contents)
- except OSError as err:
- return Response(
- body=f"Can't write backup file {err}",
- status=HTTPStatus.INTERNAL_SERVER_ERROR,
- )
- except asyncio.CancelledError:
- return Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)
-
- return Response(status=HTTPStatus.CREATED)
diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py
index ddc0a1eac3f..e3331836202 100644
--- a/homeassistant/components/backup/manager.py
+++ b/homeassistant/components/backup/manager.py
@@ -2,26 +2,19 @@
from __future__ import annotations
-import abc
import asyncio
-from collections.abc import Callable
from dataclasses import asdict, dataclass
import hashlib
import io
import json
from pathlib import Path
-from queue import SimpleQueue
-import shutil
import tarfile
from tarfile import TarError
-from tempfile import TemporaryDirectory
import time
from typing import Any, Protocol, cast
-import aiohttp
from securetar import SecureTarFile, atomic_contents_add
-from homeassistant.backup_restore import RESTORE_BACKUP_FILE
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -35,13 +28,6 @@ from .const import DOMAIN, EXCLUDE_FROM_BACKUP, LOGGER
BUF_SIZE = 2**20 * 4 # 4MB
-@dataclass(slots=True)
-class NewBackup:
- """New backup class."""
-
- slug: str
-
-
@dataclass(slots=True)
class Backup:
"""Backup class."""
@@ -57,15 +43,6 @@ class Backup:
return {**asdict(self), "path": self.path.as_posix()}
-@dataclass(slots=True)
-class BackupProgress:
- """Backup progress class."""
-
- done: bool
- stage: str | None
- success: bool | None
-
-
class BackupPlatformProtocol(Protocol):
"""Define the format that backup platforms can have."""
@@ -76,16 +53,18 @@ class BackupPlatformProtocol(Protocol):
"""Perform operations after a backup finishes."""
-class BaseBackupManager(abc.ABC):
- """Define the format that backup managers can have."""
+class BackupManager:
+ """Backup manager for the Backup integration."""
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the backup manager."""
self.hass = hass
- self.backup_task: asyncio.Task | None = None
+ self.backup_dir = Path(hass.config.path("backups"))
+ self.backing_up = False
self.backups: dict[str, Backup] = {}
- self.loaded_platforms = False
self.platforms: dict[str, BackupPlatformProtocol] = {}
+ self.loaded_backups = False
+ self.loaded_platforms = False
@callback
def _add_platform(
@@ -105,7 +84,7 @@ class BaseBackupManager(abc.ABC):
return
self.platforms[integration_domain] = platform
- async def async_pre_backup_actions(self, **kwargs: Any) -> None:
+ async def pre_backup_actions(self) -> None:
"""Perform pre backup actions."""
if not self.loaded_platforms:
await self.load_platforms()
@@ -121,7 +100,7 @@ class BaseBackupManager(abc.ABC):
if isinstance(result, Exception):
raise result
- async def async_post_backup_actions(self, **kwargs: Any) -> None:
+ async def post_backup_actions(self) -> None:
"""Perform post backup actions."""
if not self.loaded_platforms:
await self.load_platforms()
@@ -137,6 +116,13 @@ class BaseBackupManager(abc.ABC):
if isinstance(result, Exception):
raise result
+ async def load_backups(self) -> None:
+ """Load data of stored backup files."""
+ backups = await self.hass.async_add_executor_job(self._read_backups)
+ LOGGER.debug("Loaded %s backups", len(backups))
+ self.backups = backups
+ self.loaded_backups = True
+
async def load_platforms(self) -> None:
"""Load backup platforms."""
await integration_platform.async_process_integration_platforms(
@@ -145,60 +131,6 @@ class BaseBackupManager(abc.ABC):
LOGGER.debug("Loaded %s platforms", len(self.platforms))
self.loaded_platforms = True
- @abc.abstractmethod
- async def async_restore_backup(self, slug: str, **kwargs: Any) -> None:
- """Restore a backup."""
-
- @abc.abstractmethod
- async def async_create_backup(
- self,
- *,
- on_progress: Callable[[BackupProgress], None] | None,
- **kwargs: Any,
- ) -> NewBackup:
- """Generate a backup."""
-
- @abc.abstractmethod
- async def async_get_backups(self, **kwargs: Any) -> dict[str, Backup]:
- """Get backups.
-
- Return a dictionary of Backup instances keyed by their slug.
- """
-
- @abc.abstractmethod
- async def async_get_backup(self, *, slug: str, **kwargs: Any) -> Backup | None:
- """Get a backup."""
-
- @abc.abstractmethod
- async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None:
- """Remove a backup."""
-
- @abc.abstractmethod
- async def async_receive_backup(
- self,
- *,
- contents: aiohttp.BodyPartReader,
- **kwargs: Any,
- ) -> None:
- """Receive and store a backup file from upload."""
-
-
-class BackupManager(BaseBackupManager):
- """Backup manager for the Backup integration."""
-
- def __init__(self, hass: HomeAssistant) -> None:
- """Initialize the backup manager."""
- super().__init__(hass=hass)
- self.backup_dir = Path(hass.config.path("backups"))
- self.loaded_backups = False
-
- async def load_backups(self) -> None:
- """Load data of stored backup files."""
- backups = await self.hass.async_add_executor_job(self._read_backups)
- LOGGER.debug("Loaded %s backups", len(backups))
- self.backups = backups
- self.loaded_backups = True
-
def _read_backups(self) -> dict[str, Backup]:
"""Read backups from disk."""
backups: dict[str, Backup] = {}
@@ -219,14 +151,14 @@ class BackupManager(BaseBackupManager):
LOGGER.warning("Unable to read backup %s: %s", backup_path, err)
return backups
- async def async_get_backups(self, **kwargs: Any) -> dict[str, Backup]:
+ async def get_backups(self) -> dict[str, Backup]:
"""Return backups."""
if not self.loaded_backups:
await self.load_backups()
return self.backups
- async def async_get_backup(self, *, slug: str, **kwargs: Any) -> Backup | None:
+ async def get_backup(self, slug: str) -> Backup | None:
"""Return a backup."""
if not self.loaded_backups:
await self.load_backups()
@@ -248,102 +180,26 @@ class BackupManager(BaseBackupManager):
return backup
- async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None:
+ async def remove_backup(self, slug: str) -> None:
"""Remove a backup."""
- if (backup := await self.async_get_backup(slug=slug)) is None:
+ if (backup := await self.get_backup(slug)) is None:
return
await self.hass.async_add_executor_job(backup.path.unlink, True)
LOGGER.debug("Removed backup located at %s", backup.path)
self.backups.pop(slug)
- async def async_receive_backup(
- self,
- *,
- contents: aiohttp.BodyPartReader,
- **kwargs: Any,
- ) -> None:
- """Receive and store a backup file from upload."""
- queue: SimpleQueue[tuple[bytes, asyncio.Future[None] | None] | None] = (
- SimpleQueue()
- )
- temp_dir_handler = await self.hass.async_add_executor_job(TemporaryDirectory)
- target_temp_file = Path(
- temp_dir_handler.name, contents.filename or "backup.tar"
- )
-
- def _sync_queue_consumer() -> None:
- with target_temp_file.open("wb") as file_handle:
- while True:
- if (_chunk_future := queue.get()) is None:
- break
- _chunk, _future = _chunk_future
- if _future is not None:
- self.hass.loop.call_soon_threadsafe(_future.set_result, None)
- file_handle.write(_chunk)
-
- fut: asyncio.Future[None] | None = None
- try:
- fut = self.hass.async_add_executor_job(_sync_queue_consumer)
- megabytes_sending = 0
- while chunk := await contents.read_chunk(BUF_SIZE):
- megabytes_sending += 1
- if megabytes_sending % 5 != 0:
- queue.put_nowait((chunk, None))
- continue
-
- chunk_future = self.hass.loop.create_future()
- queue.put_nowait((chunk, chunk_future))
- await asyncio.wait(
- (fut, chunk_future),
- return_when=asyncio.FIRST_COMPLETED,
- )
- if fut.done():
- # The executor job failed
- break
-
- queue.put_nowait(None) # terminate queue consumer
- finally:
- if fut is not None:
- await fut
-
- def _move_and_cleanup() -> None:
- shutil.move(target_temp_file, self.backup_dir / target_temp_file.name)
- temp_dir_handler.cleanup()
-
- await self.hass.async_add_executor_job(_move_and_cleanup)
- await self.load_backups()
-
- async def async_create_backup(
- self,
- *,
- on_progress: Callable[[BackupProgress], None] | None,
- **kwargs: Any,
- ) -> NewBackup:
+ async def generate_backup(self) -> Backup:
"""Generate a backup."""
- if self.backup_task:
+ if self.backing_up:
raise HomeAssistantError("Backup already in progress")
- backup_name = f"Core {HAVERSION}"
- date_str = dt_util.now().isoformat()
- slug = _generate_slug(date_str, backup_name)
- self.backup_task = self.hass.async_create_task(
- self._async_create_backup(backup_name, date_str, slug, on_progress),
- name="backup_manager_create_backup",
- eager_start=False, # To ensure the task is not started before we return
- )
- return NewBackup(slug=slug)
- async def _async_create_backup(
- self,
- backup_name: str,
- date_str: str,
- slug: str,
- on_progress: Callable[[BackupProgress], None] | None,
- ) -> Backup:
- """Generate a backup."""
- success = False
try:
- await self.async_pre_backup_actions()
+ self.backing_up = True
+ await self.pre_backup_actions()
+ backup_name = f"Core {HAVERSION}"
+ date_str = dt_util.now().isoformat()
+ slug = _generate_slug(date_str, backup_name)
backup_data = {
"slug": slug,
@@ -370,13 +226,10 @@ class BackupManager(BaseBackupManager):
if self.loaded_backups:
self.backups[slug] = backup
LOGGER.debug("Generated new backup with slug %s", slug)
- success = True
return backup
finally:
- if on_progress:
- on_progress(BackupProgress(done=True, stage=None, success=success))
- self.backup_task = None
- await self.async_post_backup_actions()
+ self.backing_up = False
+ await self.post_backup_actions()
def _mkdir_and_generate_backup_contents(
self,
@@ -410,25 +263,6 @@ class BackupManager(BaseBackupManager):
return tar_file_path.stat().st_size
- async def async_restore_backup(self, slug: str, **kwargs: Any) -> None:
- """Restore a backup.
-
- This will write the restore information to .HA_RESTORE which
- will be handled during startup by the restore_backup module.
- """
- if (backup := await self.async_get_backup(slug=slug)) is None:
- raise HomeAssistantError(f"Backup {slug} not found")
-
- def _write_restore_file() -> None:
- """Write the restore file."""
- Path(self.hass.config.path(RESTORE_BACKUP_FILE)).write_text(
- json.dumps({"path": backup.path.as_posix()}),
- encoding="utf-8",
- )
-
- await self.hass.async_add_executor_job(_write_restore_file)
- await self.hass.services.async_call("homeassistant", "restart", {})
-
def _generate_slug(date: str, name: str) -> str:
"""Generate a backup slug."""
diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py
index a7c61b7c66c..dd42fe06afc 100644
--- a/homeassistant/components/backup/websocket.py
+++ b/homeassistant/components/backup/websocket.py
@@ -8,7 +8,6 @@ from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from .const import DATA_MANAGER, LOGGER
-from .manager import BackupProgress
@callback
@@ -19,11 +18,9 @@ def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) ->
websocket_api.async_register_command(hass, handle_backup_start)
return
- websocket_api.async_register_command(hass, handle_details)
websocket_api.async_register_command(hass, handle_info)
websocket_api.async_register_command(hass, handle_create)
websocket_api.async_register_command(hass, handle_remove)
- websocket_api.async_register_command(hass, handle_restore)
@websocket_api.require_admin
@@ -36,35 +33,12 @@ async def handle_info(
) -> None:
"""List all stored backups."""
manager = hass.data[DATA_MANAGER]
- backups = await manager.async_get_backups()
+ backups = await manager.get_backups()
connection.send_result(
msg["id"],
{
"backups": list(backups.values()),
- "backing_up": manager.backup_task is not None,
- },
- )
-
-
-@websocket_api.require_admin
-@websocket_api.websocket_command(
- {
- vol.Required("type"): "backup/details",
- vol.Required("slug"): str,
- }
-)
-@websocket_api.async_response
-async def handle_details(
- hass: HomeAssistant,
- connection: websocket_api.ActiveConnection,
- msg: dict[str, Any],
-) -> None:
- """Get backup details for a specific slug."""
- backup = await hass.data[DATA_MANAGER].async_get_backup(slug=msg["slug"])
- connection.send_result(
- msg["id"],
- {
- "backup": backup,
+ "backing_up": manager.backing_up,
},
)
@@ -83,25 +57,7 @@ async def handle_remove(
msg: dict[str, Any],
) -> None:
"""Remove a backup."""
- await hass.data[DATA_MANAGER].async_remove_backup(slug=msg["slug"])
- connection.send_result(msg["id"])
-
-
-@websocket_api.require_admin
-@websocket_api.websocket_command(
- {
- vol.Required("type"): "backup/restore",
- vol.Required("slug"): str,
- }
-)
-@websocket_api.async_response
-async def handle_restore(
- hass: HomeAssistant,
- connection: websocket_api.ActiveConnection,
- msg: dict[str, Any],
-) -> None:
- """Restore a backup."""
- await hass.data[DATA_MANAGER].async_restore_backup(msg["slug"])
+ await hass.data[DATA_MANAGER].remove_backup(msg["slug"])
connection.send_result(msg["id"])
@@ -114,11 +70,7 @@ async def handle_create(
msg: dict[str, Any],
) -> None:
"""Generate a backup."""
-
- def on_progress(progress: BackupProgress) -> None:
- connection.send_message(websocket_api.event_message(msg["id"], progress))
-
- backup = await hass.data[DATA_MANAGER].async_create_backup(on_progress=on_progress)
+ backup = await hass.data[DATA_MANAGER].generate_backup()
connection.send_result(msg["id"], backup)
@@ -132,10 +84,11 @@ async def handle_backup_start(
) -> None:
"""Backup start notification."""
manager = hass.data[DATA_MANAGER]
+ manager.backing_up = True
LOGGER.debug("Backup start notification")
try:
- await manager.async_pre_backup_actions()
+ await manager.pre_backup_actions()
except Exception as err: # noqa: BLE001
connection.send_error(msg["id"], "pre_backup_actions_failed", str(err))
return
@@ -153,10 +106,11 @@ async def handle_backup_end(
) -> None:
"""Backup end notification."""
manager = hass.data[DATA_MANAGER]
+ manager.backing_up = False
LOGGER.debug("Backup end notification")
try:
- await manager.async_post_backup_actions()
+ await manager.post_backup_actions()
except Exception as err: # noqa: BLE001
connection.send_error(msg["id"], "post_backup_actions_failed", str(err))
return
diff --git a/homeassistant/components/balboa/__init__.py b/homeassistant/components/balboa/__init__.py
index 7838db16820..7e220bd46f8 100644
--- a/homeassistant/components/balboa/__init__.py
+++ b/homeassistant/components/balboa/__init__.py
@@ -14,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.event import async_track_time_interval
import homeassistant.util.dt as dt_util
-from .const import CONF_SYNC_TIME, DEFAULT_SYNC_TIME
+from .const import CONF_SYNC_TIME, DEFAULT_SYNC_TIME, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -30,10 +30,8 @@ PLATFORMS = [
KEEP_ALIVE_INTERVAL = timedelta(minutes=1)
SYNC_TIME_INTERVAL = timedelta(hours=1)
-type BalboaConfigEntry = ConfigEntry[SpaClient]
-
-async def async_setup_entry(hass: HomeAssistant, entry: BalboaConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Balboa Spa from a config entry."""
host = entry.data[CONF_HOST]
@@ -46,34 +44,41 @@ async def async_setup_entry(hass: HomeAssistant, entry: BalboaConfigEntry) -> bo
_LOGGER.error("Failed to get spa info at %s", host)
raise ConfigEntryNotReady("Unable to configure")
- entry.runtime_data = spa
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = spa
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await async_setup_time_sync(hass, entry)
entry.async_on_unload(entry.add_update_listener(update_listener))
- entry.async_on_unload(spa.disconnect)
return True
-async def async_unload_entry(hass: HomeAssistant, entry: BalboaConfigEntry) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ _LOGGER.debug("Disconnecting from spa")
+ spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
+
+ if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ await spa.disconnect()
+
+ return unload_ok
-async def update_listener(hass: HomeAssistant, entry: BalboaConfigEntry) -> None:
+async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
-async def async_setup_time_sync(hass: HomeAssistant, entry: BalboaConfigEntry) -> None:
+async def async_setup_time_sync(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Set up the time sync."""
if not entry.options.get(CONF_SYNC_TIME, DEFAULT_SYNC_TIME):
return
_LOGGER.debug("Setting up daily time sync")
- spa = entry.runtime_data
+ spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
async def sync_time(now: datetime) -> None:
now = dt_util.as_local(now)
diff --git a/homeassistant/components/balboa/binary_sensor.py b/homeassistant/components/balboa/binary_sensor.py
index b8c62ce8abf..d3352208cd9 100644
--- a/homeassistant/components/balboa/binary_sensor.py
+++ b/homeassistant/components/balboa/binary_sensor.py
@@ -12,20 +12,19 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BalboaConfigEntry
+from .const import DOMAIN
from .entity import BalboaEntity
async def async_setup_entry(
- hass: HomeAssistant,
- entry: BalboaConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the spa's binary sensors."""
- spa = entry.runtime_data
+ spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
entities = [
BalboaBinarySensorEntity(spa, description)
for description in BINARY_SENSOR_DESCRIPTIONS
diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py
index d27fd459676..8cd9e93e539 100644
--- a/homeassistant/components/balboa/climate.py
+++ b/homeassistant/components/balboa/climate.py
@@ -14,6 +14,7 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TEMPERATURE,
PRECISION_HALVES,
@@ -23,7 +24,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BalboaConfigEntry
from .const import DOMAIN
from .entity import BalboaEntity
@@ -45,12 +45,10 @@ TEMPERATURE_UNIT_MAP = {
async def async_setup_entry(
- hass: HomeAssistant,
- entry: BalboaConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the spa climate entity."""
- async_add_entities([BalboaClimateEntity(entry.runtime_data)])
+ async_add_entities([BalboaClimateEntity(hass.data[DOMAIN][entry.entry_id])])
class BalboaClimateEntity(BalboaEntity, ClimateEntity):
diff --git a/homeassistant/components/balboa/fan.py b/homeassistant/components/balboa/fan.py
index 67c1d9a9a62..bf7425f0e64 100644
--- a/homeassistant/components/balboa/fan.py
+++ b/homeassistant/components/balboa/fan.py
@@ -5,10 +5,11 @@ from __future__ import annotations
import math
from typing import Any, cast
-from pybalboa import SpaControl
+from pybalboa import SpaClient, SpaControl
from pybalboa.enums import OffOnState, UnknownState
from homeassistant.components.fan import FanEntity, FanEntityFeature
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.percentage import (
@@ -16,17 +17,15 @@ from homeassistant.util.percentage import (
ranged_value_to_percentage,
)
-from . import BalboaConfigEntry
+from .const import DOMAIN
from .entity import BalboaEntity
async def async_setup_entry(
- hass: HomeAssistant,
- entry: BalboaConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the spa's pumps."""
- spa = entry.runtime_data
+ spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
async_add_entities(BalboaPumpFanEntity(control) for control in spa.pumps)
diff --git a/homeassistant/components/balboa/light.py b/homeassistant/components/balboa/light.py
index 21e4dfc5e08..5dc8d48ef9d 100644
--- a/homeassistant/components/balboa/light.py
+++ b/homeassistant/components/balboa/light.py
@@ -4,24 +4,23 @@ from __future__ import annotations
from typing import Any, cast
-from pybalboa import SpaControl
+from pybalboa import SpaClient, SpaControl
from pybalboa.enums import OffOnState, UnknownState
from homeassistant.components.light import ColorMode, LightEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BalboaConfigEntry
+from .const import DOMAIN
from .entity import BalboaEntity
async def async_setup_entry(
- hass: HomeAssistant,
- entry: BalboaConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the spa's lights."""
- spa = entry.runtime_data
+ spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
async_add_entities(BalboaLightEntity(control) for control in spa.lights)
diff --git a/homeassistant/components/balboa/select.py b/homeassistant/components/balboa/select.py
index e88e40ab063..9c3074350c5 100644
--- a/homeassistant/components/balboa/select.py
+++ b/homeassistant/components/balboa/select.py
@@ -1,23 +1,22 @@
"""Support for Spa Client selects."""
-from pybalboa import SpaControl
+from pybalboa import SpaClient, SpaControl
from pybalboa.enums import LowHighRange
from homeassistant.components.select import SelectEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BalboaConfigEntry
+from .const import DOMAIN
from .entity import BalboaEntity
async def async_setup_entry(
- hass: HomeAssistant,
- entry: BalboaConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the spa select entity."""
- spa = entry.runtime_data
+ spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
async_add_entities([BalboaTempRangeSelectEntity(spa.temperature_range)])
diff --git a/homeassistant/components/bang_olufsen/__init__.py b/homeassistant/components/bang_olufsen/__init__.py
index c8ba1f1c3dc..e11df6ad5ed 100644
--- a/homeassistant/components/bang_olufsen/__init__.py
+++ b/homeassistant/components/bang_olufsen/__init__.py
@@ -31,12 +31,10 @@ class BangOlufsenData:
client: MozartClient
-type BangOlufsenConfigEntry = ConfigEntry[BangOlufsenData]
-
PLATFORMS = [Platform.MEDIA_PLAYER]
-async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
# Remove casts to str
@@ -69,7 +67,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry)
websocket = BangOlufsenWebsocket(hass, entry, client)
# Add the websocket and API client
- entry.runtime_data = BangOlufsenData(websocket, client)
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BangOlufsenData(
+ websocket,
+ client,
+ )
# Start WebSocket connection
await client.connect_notifications(remote_control=True, reconnect=True)
@@ -79,12 +80,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry)
return True
-async def async_unload_entry(
- hass: HomeAssistant, entry: BangOlufsenConfigEntry
-) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
# Close the API client and WebSocket notification listener
- entry.runtime_data.client.disconnect_notifications()
- await entry.runtime_data.client.close_api_client()
+ hass.data[DOMAIN][entry.entry_id].client.disconnect_notifications()
+ await hass.data[DOMAIN][entry.entry_id].client.close_api_client()
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/bang_olufsen/const.py b/homeassistant/components/bang_olufsen/const.py
index 209311d3e8a..64ee4cf275d 100644
--- a/homeassistant/components/bang_olufsen/const.py
+++ b/homeassistant/components/bang_olufsen/const.py
@@ -7,19 +7,20 @@ from typing import Final
from mozart_api.models import Source, SourceArray, SourceTypeEnum
-from homeassistant.components.media_player import (
- MediaPlayerState,
- MediaType,
- RepeatMode,
-)
+from homeassistant.components.media_player import MediaPlayerState, MediaType
class BangOlufsenSource:
"""Class used for associating device source ids with friendly names. May not include all sources."""
+ URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
+ BLUETOOTH: Final[Source] = Source(name="Bluetooth", id="bluetooth")
+ CHROMECAST: Final[Source] = Source(name="Chromecast built-in", id="chromeCast")
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
- URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
+ NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
+ DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
+ TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
@@ -35,17 +36,6 @@ BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
"unknown": MediaPlayerState.IDLE,
}
-# Dict used for translating Home Assistant settings to device repeat settings.
-BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
- RepeatMode.ALL: "all",
- RepeatMode.ONE: "track",
- RepeatMode.OFF: "none",
-}
-# Dict used for translating device repeat settings to Home Assistant settings.
-BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
- value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
-}
-
# Media types for play_media
class BangOlufsenMediaType(StrEnum):
@@ -133,6 +123,20 @@ VALID_MEDIA_TYPES: Final[tuple] = (
MediaType.CHANNEL,
)
+# Sources on the device that should not be selectable by the user
+HIDDEN_SOURCE_IDS: Final[tuple] = (
+ "airPlay",
+ "bluetooth",
+ "chromeCast",
+ "generator",
+ "local",
+ "dlna",
+ "qplay",
+ "wpl",
+ "pl",
+ "beolink",
+ "usbIn",
+)
# Fallback sources to use in case of API failure.
FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
@@ -140,26 +144,23 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
Source(
id="uriStreamer",
is_enabled=True,
- is_playable=True,
+ is_playable=False,
name="Audio Streamer",
type=SourceTypeEnum(value="uriStreamer"),
- is_seekable=False,
),
Source(
id="bluetooth",
is_enabled=True,
- is_playable=True,
+ is_playable=False,
name="Bluetooth",
type=SourceTypeEnum(value="bluetooth"),
- is_seekable=False,
),
Source(
id="spotify",
is_enabled=True,
- is_playable=True,
+ is_playable=False,
name="Spotify Connect",
type=SourceTypeEnum(value="spotify"),
- is_seekable=True,
),
Source(
id="lineIn",
@@ -167,7 +168,6 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True,
name="Line-In",
type=SourceTypeEnum(value="lineIn"),
- is_seekable=False,
),
Source(
id="spdif",
@@ -175,7 +175,6 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True,
name="Optical",
type=SourceTypeEnum(value="spdif"),
- is_seekable=False,
),
Source(
id="netRadio",
@@ -183,7 +182,6 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True,
name="B&O Radio",
type=SourceTypeEnum(value="netRadio"),
- is_seekable=False,
),
Source(
id="deezer",
@@ -191,7 +189,6 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True,
name="Deezer",
type=SourceTypeEnum(value="deezer"),
- is_seekable=True,
),
Source(
id="tidalConnect",
@@ -199,7 +196,6 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True,
name="Tidal Connect",
type=SourceTypeEnum(value="tidalConnect"),
- is_seekable=True,
),
]
)
diff --git a/homeassistant/components/bang_olufsen/icons.json b/homeassistant/components/bang_olufsen/icons.json
deleted file mode 100644
index fec0bf20937..00000000000
--- a/homeassistant/components/bang_olufsen/icons.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "services": {
- "beolink_join": { "service": "mdi:location-enter" },
- "beolink_expand": { "service": "mdi:location-enter" },
- "beolink_unexpand": { "service": "mdi:location-exit" },
- "beolink_leave": { "service": "mdi:close-circle-outline" },
- "beolink_allstandby": { "service": "mdi:close-circle-multiple-outline" }
- }
-}
diff --git a/homeassistant/components/bang_olufsen/manifest.json b/homeassistant/components/bang_olufsen/manifest.json
index b4a92d4da25..a93a6e7a624 100644
--- a/homeassistant/components/bang_olufsen/manifest.json
+++ b/homeassistant/components/bang_olufsen/manifest.json
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
"integration_type": "device",
"iot_class": "local_push",
- "requirements": ["mozart-api==4.1.1.116.0"],
+ "requirements": ["mozart-api==3.4.1.8.8"],
"zeroconf": ["_bangolufsen._tcp.local."]
}
diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py
index 56aa66d32e8..ecf571d5456 100644
--- a/homeassistant/components/bang_olufsen/media_player.py
+++ b/homeassistant/components/bang_olufsen/media_player.py
@@ -3,15 +3,12 @@
from __future__ import annotations
from collections.abc import Callable
-import contextlib
-from datetime import timedelta
import json
import logging
from typing import TYPE_CHECKING, Any, cast
-from aiohttp import ClientConnectorError
from mozart_api import __version__ as MOZART_API_VERSION
-from mozart_api.exceptions import ApiException, NotFoundException
+from mozart_api.exceptions import ApiException
from mozart_api.models import (
Action,
Art,
@@ -25,7 +22,6 @@ from mozart_api.models import (
PlaybackProgress,
PlayQueueItem,
PlayQueueItemType,
- PlayQueueSettings,
RenderingState,
SceneProperties,
SoftwareUpdateState,
@@ -38,7 +34,6 @@ from mozart_api.models import (
VolumeState,
)
from mozart_api.mozart_client import MozartClient, get_highest_resolution_artwork
-import voluptuous as vol
from homeassistant.components import media_source
from homeassistant.components.media_player import (
@@ -49,35 +44,26 @@ from homeassistant.components.media_player import (
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
- RepeatMode,
async_process_play_media_url,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODEL, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
-from homeassistant.helpers import (
- config_validation as cv,
- device_registry as dr,
- entity_registry as er,
-)
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity_platform import (
- AddEntitiesCallback,
- async_get_current_platform,
-)
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utcnow
-from . import BangOlufsenConfigEntry
+from . import BangOlufsenData
from .const import (
- BANG_OLUFSEN_REPEAT_FROM_HA,
- BANG_OLUFSEN_REPEAT_TO_HA,
BANG_OLUFSEN_STATES,
CONF_BEOLINK_JID,
CONNECTION_STATUS,
DOMAIN,
FALLBACK_SOURCES,
+ HIDDEN_SOURCE_IDS,
VALID_MEDIA_TYPES,
BangOlufsenMediaType,
BangOlufsenSource,
@@ -86,8 +72,6 @@ from .const import (
from .entity import BangOlufsenEntity
from .util import get_serial_number_from_jid
-SCAN_INTERVAL = timedelta(seconds=30)
-
_LOGGER = logging.getLogger(__name__)
BANG_OLUFSEN_FEATURES = (
@@ -100,9 +84,8 @@ BANG_OLUFSEN_FEATURES = (
| MediaPlayerEntityFeature.PLAY
| MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.PREVIOUS_TRACK
- | MediaPlayerEntityFeature.REPEAT_SET
+ | MediaPlayerEntityFeature.SEEK
| MediaPlayerEntityFeature.SELECT_SOURCE
- | MediaPlayerEntityFeature.SHUFFLE_SET
| MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.VOLUME_MUTE
@@ -113,68 +96,14 @@ BANG_OLUFSEN_FEATURES = (
async def async_setup_entry(
hass: HomeAssistant,
- config_entry: BangOlufsenConfigEntry,
+ config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a Media Player entity from config entry."""
+ data: BangOlufsenData = hass.data[DOMAIN][config_entry.entry_id]
+
# Add MediaPlayer entity
- async_add_entities(
- new_entities=[
- BangOlufsenMediaPlayer(config_entry, config_entry.runtime_data.client)
- ]
- )
-
- # Register actions.
- platform = async_get_current_platform()
-
- jid_regex = vol.Match(
- r"(^\d{4})[.](\d{7})[.](\d{8})(@products\.bang-olufsen\.com)$"
- )
-
- platform.async_register_entity_service(
- name="beolink_join",
- schema={vol.Optional("beolink_jid"): jid_regex},
- func="async_beolink_join",
- )
-
- platform.async_register_entity_service(
- name="beolink_expand",
- schema={
- vol.Exclusive("all_discovered", "devices", ""): cv.boolean,
- vol.Exclusive(
- "beolink_jids",
- "devices",
- "Define either specific Beolink JIDs or all discovered",
- ): vol.All(
- cv.ensure_list,
- [jid_regex],
- ),
- },
- func="async_beolink_expand",
- )
-
- platform.async_register_entity_service(
- name="beolink_unexpand",
- schema={
- vol.Required("beolink_jids"): vol.All(
- cv.ensure_list,
- [jid_regex],
- ),
- },
- func="async_beolink_unexpand",
- )
-
- platform.async_register_entity_service(
- name="beolink_leave",
- schema=None,
- func="async_beolink_leave",
- )
-
- platform.async_register_entity_service(
- name="beolink_allstandby",
- schema=None,
- func="async_beolink_allstandby",
- )
+ async_add_entities(new_entities=[BangOlufsenMediaPlayer(config_entry, data.client)])
class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
@@ -183,6 +112,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
_attr_icon = "mdi:speaker-wireless"
_attr_name = None
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
+ _attr_supported_features = BANG_OLUFSEN_FEATURES
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
"""Initialize the media player."""
@@ -199,7 +129,6 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
serial_number=self._unique_id,
)
self._attr_unique_id = self._unique_id
- self._attr_should_poll = True
# Misc. variables.
self._audio_sources: dict[str, str] = {}
@@ -216,8 +145,6 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# Beolink compatible sources
self._beolink_sources: dict[str, bool] = {}
self._remote_leader: BeolinkLeader | None = None
- # Extra state attributes for showing Beolink: peer(s), listener(s), leader and self
- self._beolink_attributes: dict[str, dict[str, dict[str, str]]] = {}
async def async_added_to_hass(self) -> None:
"""Turn on the dispatchers."""
@@ -227,11 +154,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
CONNECTION_STATUS: self._async_update_connection_state,
WebsocketNotification.ACTIVE_LISTENING_MODE: self._async_update_sound_modes,
WebsocketNotification.BEOLINK: self._async_update_beolink,
- WebsocketNotification.CONFIGURATION: self._async_update_name_and_beolink,
WebsocketNotification.PLAYBACK_ERROR: self._async_update_playback_error,
WebsocketNotification.PLAYBACK_METADATA: self._async_update_playback_metadata_and_beolink,
WebsocketNotification.PLAYBACK_PROGRESS: self._async_update_playback_progress,
- WebsocketNotification.PLAYBACK_SOURCE: self._async_update_sources,
WebsocketNotification.PLAYBACK_STATE: self._async_update_playback_state,
WebsocketNotification.REMOTE_MENU_CHANGED: self._async_update_sources,
WebsocketNotification.SOURCE_CHANGE: self._async_update_source_change,
@@ -293,23 +218,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
await self._async_update_sound_modes()
- # Update beolink attributes and device name.
- await self._async_update_name_and_beolink()
-
- async def async_update(self) -> None:
- """Update queue settings."""
- # The WebSocket event listener is the main handler for connection state.
- # The polling updates do therefore not set the device as available or unavailable
- with contextlib.suppress(ApiException, ClientConnectorError, TimeoutError):
- queue_settings = await self._client.get_settings_queue(_request_timeout=5)
-
- if queue_settings.repeat is not None:
- self._attr_repeat = BANG_OLUFSEN_REPEAT_TO_HA[queue_settings.repeat]
-
- if queue_settings.shuffle is not None:
- self._attr_shuffle = queue_settings.shuffle
-
- async def _async_update_sources(self, _: Source | None = None) -> None:
+ async def _async_update_sources(self) -> None:
"""Get sources for the specific product."""
# Audio sources
@@ -336,7 +245,10 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self._audio_sources = {
source.id: source.name
for source in cast(list[Source], sources.items)
- if source.is_enabled and source.id and source.name and source.is_playable
+ if source.is_enabled
+ and source.id
+ and source.name
+ and source.id not in HIDDEN_SOURCE_IDS
}
# Some sources are not Beolink expandable, meaning that they can't be joined by
@@ -438,44 +350,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self.async_write_ha_state()
- async def _async_update_name_and_beolink(self) -> None:
- """Update the device friendly name."""
- beolink_self = await self._client.get_beolink_self()
-
- # Update device name
- device_registry = dr.async_get(self.hass)
- assert self.device_entry is not None
-
- device_registry.async_update_device(
- device_id=self.device_entry.id,
- name=beolink_self.friendly_name,
- )
-
- await self._async_update_beolink()
-
async def _async_update_beolink(self) -> None:
"""Update the current Beolink leader, listeners, peers and self."""
- self._beolink_attributes = {}
-
- assert self.device_entry is not None
- assert self.device_entry.name is not None
-
- # Add Beolink self
- self._beolink_attributes = {
- "beolink": {"self": {self.device_entry.name: self._beolink_jid}}
- }
-
- # Add Beolink peers
- peers = await self._client.get_beolink_peers()
-
- if len(peers) > 0:
- self._beolink_attributes["beolink"]["peers"] = {}
- for peer in peers:
- self._beolink_attributes["beolink"]["peers"][peer.friendly_name] = (
- peer.jid
- )
-
# Add Beolink listeners / leader
self._remote_leader = self._playback_metadata.remote_leader
@@ -495,14 +372,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# Add self
group_members.append(self.entity_id)
- self._beolink_attributes["beolink"]["leader"] = {
- self._remote_leader.friendly_name: self._remote_leader.jid,
- }
-
# If not listener, check if leader.
else:
beolink_listeners = await self._client.get_beolink_listeners()
- beolink_listeners_attribute = {}
# Check if the device is a leader.
if len(beolink_listeners) > 0:
@@ -523,18 +395,6 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
for beolink_listener in beolink_listeners
]
)
- # Update Beolink attributes
- for beolink_listener in beolink_listeners:
- for peer in peers:
- if peer.jid == beolink_listener.jid:
- # Get the friendly names for the listeners from the peers
- beolink_listeners_attribute[peer.friendly_name] = (
- beolink_listener.jid
- )
- break
- self._beolink_attributes["beolink"]["listeners"] = (
- beolink_listeners_attribute
- )
self._attr_group_members = group_members
@@ -602,17 +462,6 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self.async_write_ha_state()
- @property
- def supported_features(self) -> MediaPlayerEntityFeature:
- """Flag media player features that are supported."""
- features = BANG_OLUFSEN_FEATURES
-
- # Add seeking if supported by the current source
- if self._source_change.is_seekable is True:
- features |= MediaPlayerEntityFeature.SEEK
-
- return features
-
@property
def state(self) -> MediaPlayerState:
"""Return the current state of the media player."""
@@ -688,19 +537,38 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
@property
def source(self) -> str | None:
"""Return the current audio source."""
+
+ # Try to fix some of the source_change chromecast weirdness.
+ if hasattr(self._playback_metadata, "title"):
+ # source_change is chromecast but line in is selected.
+ if self._playback_metadata.title == BangOlufsenSource.LINE_IN.name:
+ return BangOlufsenSource.LINE_IN.name
+
+ # source_change is chromecast but bluetooth is selected.
+ if self._playback_metadata.title == BangOlufsenSource.BLUETOOTH.name:
+ return BangOlufsenSource.BLUETOOTH.name
+
+ # source_change is line in, bluetooth or optical but stale metadata is sent through the WebSocket,
+ # And the source has not changed.
+ if self._source_change.id in (
+ BangOlufsenSource.BLUETOOTH.id,
+ BangOlufsenSource.LINE_IN.id,
+ BangOlufsenSource.SPDIF.id,
+ ):
+ return BangOlufsenSource.CHROMECAST.name
+
+ # source_change is chromecast and there is metadata but no artwork. Bluetooth does support metadata but not artwork
+ # So i assume that it is bluetooth and not chromecast
+ if (
+ hasattr(self._playback_metadata, "art")
+ and self._playback_metadata.art is not None
+ and len(self._playback_metadata.art) == 0
+ and self._source_change.id == BangOlufsenSource.CHROMECAST.id
+ ):
+ return BangOlufsenSource.BLUETOOTH.name
+
return self._source_change.name
- @property
- def extra_state_attributes(self) -> dict[str, Any] | None:
- """Return information that is not returned anywhere else."""
- attributes: dict[str, Any] = {}
-
- # Add Beolink attributes
- if self._beolink_attributes:
- attributes.update(self._beolink_attributes)
-
- return attributes
-
async def async_turn_off(self) -> None:
"""Set the device to "networkStandby"."""
await self._client.post_standby()
@@ -740,12 +608,17 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
async def async_media_seek(self, position: float) -> None:
"""Seek to position in ms."""
- await self._client.seek_to_position(position_ms=int(position * 1000))
- # Try to prevent the playback progress from bouncing in the UI.
- self._attr_media_position_updated_at = utcnow()
- self._playback_progress = PlaybackProgress(progress=int(position))
+ if self._source_change.id == BangOlufsenSource.DEEZER.id:
+ await self._client.seek_to_position(position_ms=int(position * 1000))
+ # Try to prevent the playback progress from bouncing in the UI.
+ self._attr_media_position_updated_at = utcnow()
+ self._playback_progress = PlaybackProgress(progress=int(position))
- self.async_write_ha_state()
+ self.async_write_ha_state()
+ else:
+ raise HomeAssistantError(
+ translation_domain=DOMAIN, translation_key="non_deezer_seeking"
+ )
async def async_media_previous_track(self) -> None:
"""Send the previous track command."""
@@ -755,20 +628,6 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
"""Clear the current playback queue."""
await self._client.post_clear_queue()
- async def async_set_repeat(self, repeat: RepeatMode) -> None:
- """Set playback queues to repeat."""
- await self._client.set_settings_queue(
- play_queue_settings=PlayQueueSettings(
- repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
- )
- )
-
- async def async_set_shuffle(self, shuffle: bool) -> None:
- """Set playback queues to shuffle."""
- await self._client.set_settings_queue(
- play_queue_settings=PlayQueueSettings(shuffle=shuffle),
- )
-
async def async_select_source(self, source: str) -> None:
"""Select an input source."""
if source not in self._sources.values():
@@ -972,30 +831,23 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# Beolink compatible B&O device.
# Repeated presses / calls will cycle between compatible playing devices.
if len(group_members) == 0:
- await self.async_beolink_join()
+ await self._async_beolink_join()
return
# Get JID for each group member
jids = [self._get_beolink_jid(group_member) for group_member in group_members]
- await self.async_beolink_expand(jids)
+ await self._async_beolink_expand(jids)
async def async_unjoin_player(self) -> None:
"""Unjoin Beolink session. End session if leader."""
- await self.async_beolink_leave()
+ await self._async_beolink_leave()
- # Custom actions:
- async def async_beolink_join(self, beolink_jid: str | None = None) -> None:
+ async def _async_beolink_join(self) -> None:
"""Join a Beolink multi-room experience."""
- if beolink_jid is None:
- await self._client.join_latest_beolink_experience()
- else:
- await self._client.join_beolink_peer(jid=beolink_jid)
+ await self._client.join_latest_beolink_experience()
- async def async_beolink_expand(
- self, beolink_jids: list[str] | None = None, all_discovered: bool = False
- ) -> None:
+ async def _async_beolink_expand(self, beolink_jids: list[str]) -> None:
"""Expand a Beolink multi-room experience with a device or devices."""
-
# Ensure that the current source is expandable
if not self._beolink_sources[cast(str, self._source_change.id)]:
raise ServiceValidationError(
@@ -1007,37 +859,10 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
},
)
- # Expand to all discovered devices
- if all_discovered:
- peers = await self._client.get_beolink_peers()
-
- for peer in peers:
- try:
- await self._client.post_beolink_expand(jid=peer.jid)
- except NotFoundException:
- _LOGGER.warning("Unable to expand to %s", peer.jid)
-
# Try to expand to all defined devices
- elif beolink_jids:
- for beolink_jid in beolink_jids:
- try:
- await self._client.post_beolink_expand(jid=beolink_jid)
- except NotFoundException:
- _LOGGER.warning(
- "Unable to expand to %s. Is the device available on the network?",
- beolink_jid,
- )
-
- async def async_beolink_unexpand(self, beolink_jids: list[str]) -> None:
- """Unexpand a Beolink multi-room experience with a device or devices."""
- # Unexpand all defined devices
for beolink_jid in beolink_jids:
- await self._client.post_beolink_unexpand(jid=beolink_jid)
+ await self._client.post_beolink_expand(jid=beolink_jid)
- async def async_beolink_leave(self) -> None:
+ async def _async_beolink_leave(self) -> None:
"""Leave the current Beolink experience."""
await self._client.post_beolink_leave()
-
- async def async_beolink_allstandby(self) -> None:
- """Set all connected Beolink devices to standby."""
- await self._client.post_beolink_allstandby()
diff --git a/homeassistant/components/bang_olufsen/services.yaml b/homeassistant/components/bang_olufsen/services.yaml
deleted file mode 100644
index e5d61420dff..00000000000
--- a/homeassistant/components/bang_olufsen/services.yaml
+++ /dev/null
@@ -1,79 +0,0 @@
-beolink_allstandby:
- target:
- entity:
- integration: bang_olufsen
- domain: media_player
- device:
- integration: bang_olufsen
-
-beolink_expand:
- target:
- entity:
- integration: bang_olufsen
- domain: media_player
- device:
- integration: bang_olufsen
- fields:
- all_discovered:
- required: false
- example: false
- selector:
- boolean:
- jid_options:
- collapsed: false
- fields:
- beolink_jids:
- required: false
- example: >-
- [
- 1111.2222222.33333333@products.bang-olufsen.com,
- 4444.5555555.66666666@products.bang-olufsen.com
- ]
- selector:
- object:
-
-beolink_join:
- target:
- entity:
- integration: bang_olufsen
- domain: media_player
- device:
- integration: bang_olufsen
- fields:
- jid_options:
- collapsed: false
- fields:
- beolink_jid:
- required: false
- example: 1111.2222222.33333333@products.bang-olufsen.com
- selector:
- text:
-
-beolink_leave:
- target:
- entity:
- integration: bang_olufsen
- domain: media_player
- device:
- integration: bang_olufsen
-
-beolink_unexpand:
- target:
- entity:
- integration: bang_olufsen
- domain: media_player
- device:
- integration: bang_olufsen
- fields:
- jid_options:
- collapsed: false
- fields:
- beolink_jids:
- required: true
- example: >-
- [
- 1111.2222222.33333333@products.bang-olufsen.com,
- 4444.5555555.66666666@products.bang-olufsen.com
- ]
- selector:
- object:
diff --git a/homeassistant/components/bang_olufsen/strings.json b/homeassistant/components/bang_olufsen/strings.json
index aef6f953524..b0cb88985d2 100644
--- a/homeassistant/components/bang_olufsen/strings.json
+++ b/homeassistant/components/bang_olufsen/strings.json
@@ -1,8 +1,4 @@
{
- "common": {
- "jid_options_name": "JID options",
- "jid_options_description": "Advanced grouping options, where devices' unique Beolink IDs (Called JIDs) are used directly. JIDs can be found in the state attributes of the media player entity."
- },
"config": {
"error": {
"api_exception": "[%key:common::config_flow::error::cannot_connect%]",
@@ -29,72 +25,13 @@
}
}
},
- "services": {
- "beolink_allstandby": {
- "name": "Beolink all standby",
- "description": "Set all Connected Beolink devices to standby."
- },
- "beolink_expand": {
- "name": "Beolink expand",
- "description": "Expand current Beolink experience.",
- "fields": {
- "all_discovered": {
- "name": "All discovered",
- "description": "Expand Beolink experience to all discovered devices."
- },
- "beolink_jids": {
- "name": "Beolink JIDs",
- "description": "Specify which Beolink JIDs will join current Beolink experience."
- }
- },
- "sections": {
- "jid_options": {
- "name": "[%key:component::bang_olufsen::common::jid_options_name%]",
- "description": "[%key:component::bang_olufsen::common::jid_options_description%]"
- }
- }
- },
- "beolink_join": {
- "name": "Beolink join",
- "description": "Join a Beolink experience.",
- "fields": {
- "beolink_jid": {
- "name": "Beolink JID",
- "description": "Manually specify Beolink JID to join."
- }
- },
- "sections": {
- "jid_options": {
- "name": "[%key:component::bang_olufsen::common::jid_options_name%]",
- "description": "[%key:component::bang_olufsen::common::jid_options_description%]"
- }
- }
- },
- "beolink_leave": {
- "name": "Beolink leave",
- "description": "Leave a Beolink experience."
- },
- "beolink_unexpand": {
- "name": "Beolink unexpand",
- "description": "Unexpand from current Beolink experience.",
- "fields": {
- "beolink_jids": {
- "name": "Beolink JIDs",
- "description": "Specify which Beolink JIDs will leave from current Beolink experience."
- }
- },
- "sections": {
- "jid_options": {
- "name": "[%key:component::bang_olufsen::common::jid_options_name%]",
- "description": "[%key:component::bang_olufsen::common::jid_options_description%]"
- }
- }
- }
- },
"exceptions": {
"m3u_invalid_format": {
"message": "Media sources with the .m3u extension are not supported."
},
+ "non_deezer_seeking": {
+ "message": "Seeking is currently only supported when using Deezer"
+ },
"invalid_source": {
"message": "Invalid source: {invalid_source}. Valid sources are: {valid_sources}"
},
diff --git a/homeassistant/components/bang_olufsen/websocket.py b/homeassistant/components/bang_olufsen/websocket.py
index 913f7cb3241..3519fcd9a48 100644
--- a/homeassistant/components/bang_olufsen/websocket.py
+++ b/homeassistant/components/bang_olufsen/websocket.py
@@ -63,9 +63,6 @@ class BangOlufsenWebsocket(BangOlufsenBase):
self._client.get_playback_progress_notifications(
self.on_playback_progress_notification
)
- self._client.get_playback_source_notifications(
- self.on_playback_source_notification
- )
self._client.get_playback_state_notifications(
self.on_playback_state_notification
)
@@ -120,11 +117,6 @@ class BangOlufsenWebsocket(BangOlufsenBase):
self.hass,
f"{self._unique_id}_{WebsocketNotification.BEOLINK}",
)
- elif notification_type is WebsocketNotification.CONFIGURATION:
- async_dispatcher_send(
- self.hass,
- f"{self._unique_id}_{WebsocketNotification.CONFIGURATION}",
- )
elif notification_type is WebsocketNotification.REMOTE_MENU_CHANGED:
async_dispatcher_send(
self.hass,
@@ -165,14 +157,6 @@ class BangOlufsenWebsocket(BangOlufsenBase):
notification,
)
- def on_playback_source_notification(self, notification: Source) -> None:
- """Send playback_source dispatch."""
- async_dispatcher_send(
- self.hass,
- f"{self._unique_id}_{WebsocketNotification.PLAYBACK_SOURCE}",
- notification,
- )
-
def on_source_change_notification(self, notification: Source) -> None:
"""Send source_change dispatch."""
async_dispatcher_send(
diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py
index 983f5750036..89d0d5fb146 100644
--- a/homeassistant/components/blebox/__init__.py
+++ b/homeassistant/components/blebox/__init__.py
@@ -17,11 +17,9 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
-from .const import DEFAULT_SETUP_TIMEOUT
+from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT
from .helpers import get_maybe_authenticated_session
-type BleBoxConfigEntry = ConfigEntry[Box]
-
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
@@ -37,7 +35,7 @@ PLATFORMS = [
PARALLEL_UPDATES = 0
-async def async_setup_entry(hass: HomeAssistant, entry: BleBoxConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up BleBox devices from a config entry."""
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
@@ -57,13 +55,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: BleBoxConfigEntry) -> bo
_LOGGER.error("Identify failed at %s:%d (%s)", api_host.host, api_host.port, ex)
raise ConfigEntryNotReady from ex
- entry.runtime_data = product
+ domain = hass.data.setdefault(DOMAIN, {})
+ domain_entry = domain.setdefault(entry.entry_id, {})
+ product = domain_entry.setdefault(PRODUCT, product)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
-async def async_unload_entry(hass: HomeAssistant, entry: BleBoxConfigEntry) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/blebox/binary_sensor.py b/homeassistant/components/blebox/binary_sensor.py
index 2aa86059ee2..7f909fd9a7b 100644
--- a/homeassistant/components/blebox/binary_sensor.py
+++ b/homeassistant/components/blebox/binary_sensor.py
@@ -1,16 +1,18 @@
"""BleBox binary sensor entities."""
from blebox_uniapi.binary_sensor import BinarySensor as BinarySensorFeature
+from blebox_uniapi.box import Box
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BleBoxConfigEntry
+from .const import DOMAIN, PRODUCT
from .entity import BleBoxEntity
BINARY_SENSOR_TYPES = (
@@ -23,13 +25,15 @@ BINARY_SENSOR_TYPES = (
async def async_setup_entry(
hass: HomeAssistant,
- config_entry: BleBoxConfigEntry,
+ config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a BleBox entry."""
+
+ product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
entities = [
BleBoxBinarySensorEntity(feature, description)
- for feature in config_entry.runtime_data.features.get("binary_sensors", [])
+ for feature in product.features.get("binary_sensors", [])
for description in BINARY_SENSOR_TYPES
if description.key == feature.device_class
]
diff --git a/homeassistant/components/blebox/button.py b/homeassistant/components/blebox/button.py
index 90356c8ae14..24b09306de7 100644
--- a/homeassistant/components/blebox/button.py
+++ b/homeassistant/components/blebox/button.py
@@ -2,25 +2,28 @@
from __future__ import annotations
+from blebox_uniapi.box import Box
import blebox_uniapi.button
from homeassistant.components.button import ButtonEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BleBoxConfigEntry
+from .const import DOMAIN, PRODUCT
from .entity import BleBoxEntity
async def async_setup_entry(
hass: HomeAssistant,
- config_entry: BleBoxConfigEntry,
+ config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a BleBox button entry."""
+ product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
+
entities = [
- BleBoxButtonEntity(feature)
- for feature in config_entry.runtime_data.features.get("buttons", [])
+ BleBoxButtonEntity(feature) for feature in product.features.get("buttons", [])
]
async_add_entities(entities, True)
diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py
index e04503974b7..d4834ebbc28 100644
--- a/homeassistant/components/blebox/climate.py
+++ b/homeassistant/components/blebox/climate.py
@@ -3,6 +3,7 @@
from datetime import timedelta
from typing import Any
+from blebox_uniapi.box import Box
import blebox_uniapi.climate
from homeassistant.components.climate import (
@@ -11,11 +12,12 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BleBoxConfigEntry
+from .const import DOMAIN, PRODUCT
from .entity import BleBoxEntity
SCAN_INTERVAL = timedelta(seconds=5)
@@ -37,13 +39,14 @@ BLEBOX_TO_HVACACTION = {
async def async_setup_entry(
hass: HomeAssistant,
- config_entry: BleBoxConfigEntry,
+ config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a BleBox climate entity."""
+ product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
+
entities = [
- BleBoxClimateEntity(feature)
- for feature in config_entry.runtime_data.features.get("climates", [])
+ BleBoxClimateEntity(feature) for feature in product.features.get("climates", [])
]
async_add_entities(entities, True)
diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py
index e9ea1922302..ff6a6b33af6 100644
--- a/homeassistant/components/blebox/const.py
+++ b/homeassistant/components/blebox/const.py
@@ -1,6 +1,7 @@
"""Constants for the BleBox devices integration."""
DOMAIN = "blebox"
+PRODUCT = "product"
DEFAULT_SETUP_TIMEOUT = 10
diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py
index 4f2a7eeef11..19a216ea2b2 100644
--- a/homeassistant/components/blebox/cover.py
+++ b/homeassistant/components/blebox/cover.py
@@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Any
+from blebox_uniapi.box import Box
import blebox_uniapi.cover
from blebox_uniapi.cover import BleboxCoverState
@@ -15,10 +16,11 @@ from homeassistant.components.cover import (
CoverEntityFeature,
CoverState,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BleBoxConfigEntry
+from .const import DOMAIN, PRODUCT
from .entity import BleBoxEntity
BLEBOX_TO_COVER_DEVICE_CLASSES = {
@@ -44,13 +46,13 @@ BLEBOX_TO_HASS_COVER_STATES = {
async def async_setup_entry(
hass: HomeAssistant,
- config_entry: BleBoxConfigEntry,
+ config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a BleBox entry."""
+ product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
entities = [
- BleBoxCoverEntity(feature)
- for feature in config_entry.runtime_data.features.get("covers", [])
+ BleBoxCoverEntity(feature) for feature in product.features.get("covers", [])
]
async_add_entities(entities, True)
diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py
index 33fff1d71da..650b8c057de 100644
--- a/homeassistant/components/blebox/light.py
+++ b/homeassistant/components/blebox/light.py
@@ -6,6 +6,7 @@ from datetime import timedelta
import logging
from typing import Any
+from blebox_uniapi.box import Box
import blebox_uniapi.light
from blebox_uniapi.light import BleboxColorMode
@@ -20,10 +21,11 @@ from homeassistant.components.light import (
LightEntity,
LightEntityFeature,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BleBoxConfigEntry
+from .const import DOMAIN, PRODUCT
from .entity import BleBoxEntity
_LOGGER = logging.getLogger(__name__)
@@ -33,13 +35,13 @@ SCAN_INTERVAL = timedelta(seconds=5)
async def async_setup_entry(
hass: HomeAssistant,
- config_entry: BleBoxConfigEntry,
+ config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a BleBox entry."""
+ product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
entities = [
- BleBoxLightEntity(feature)
- for feature in config_entry.runtime_data.features.get("lights", [])
+ BleBoxLightEntity(feature) for feature in product.features.get("lights", [])
]
async_add_entities(entities, True)
diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py
index c0abff31257..c60387c97b1 100644
--- a/homeassistant/components/blebox/sensor.py
+++ b/homeassistant/components/blebox/sensor.py
@@ -1,5 +1,6 @@
"""BleBox sensor entities."""
+from blebox_uniapi.box import Box
import blebox_uniapi.sensor
from homeassistant.components.sensor import (
@@ -8,6 +9,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
LIGHT_LUX,
@@ -25,7 +27,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BleBoxConfigEntry
+from .const import DOMAIN, PRODUCT
from .entity import BleBoxEntity
SENSOR_TYPES = (
@@ -115,13 +117,14 @@ SENSOR_TYPES = (
async def async_setup_entry(
hass: HomeAssistant,
- config_entry: BleBoxConfigEntry,
+ config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a BleBox entry."""
+ product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
entities = [
BleBoxSensorEntity(feature, description)
- for feature in config_entry.runtime_data.features.get("sensors", [])
+ for feature in product.features.get("sensors", [])
for description in SENSOR_TYPES
if description.key == feature.device_class
]
diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py
index c6f439e27c5..93c8df0030c 100644
--- a/homeassistant/components/blebox/switch.py
+++ b/homeassistant/components/blebox/switch.py
@@ -3,13 +3,15 @@
from datetime import timedelta
from typing import Any
+from blebox_uniapi.box import Box
import blebox_uniapi.switch
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BleBoxConfigEntry
+from .const import DOMAIN, PRODUCT
from .entity import BleBoxEntity
SCAN_INTERVAL = timedelta(seconds=5)
@@ -17,13 +19,13 @@ SCAN_INTERVAL = timedelta(seconds=5)
async def async_setup_entry(
hass: HomeAssistant,
- config_entry: BleBoxConfigEntry,
+ config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a BleBox switch entity."""
+ product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
entities = [
- BleBoxSwitchEntity(feature)
- for feature in config_entry.runtime_data.features.get("switches", [])
+ BleBoxSwitchEntity(feature) for feature in product.features.get("switches", [])
]
async_add_entities(entities, True)
diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py
index f6516434cd2..d21994ecc8f 100644
--- a/homeassistant/components/blink/__init__.py
+++ b/homeassistant/components/blink/__init__.py
@@ -2,7 +2,6 @@
from copy import deepcopy
import logging
-from typing import Any
from aiohttp import ClientError
from blinkpy.auth import Auth
@@ -10,6 +9,7 @@ from blinkpy.blinkpy import Blink
import voluptuous as vol
from homeassistant.components import persistent_notification
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.const import (
CONF_FILE_PATH,
CONF_FILENAME,
@@ -24,7 +24,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS
-from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
+from .coordinator import BlinkUpdateCoordinator
from .services import setup_services
_LOGGER = logging.getLogger(__name__)
@@ -40,11 +40,13 @@ SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
-async def _reauth_flow_wrapper(
- hass: HomeAssistant, entry: BlinkConfigEntry, data: dict[str, Any]
-) -> None:
+async def _reauth_flow_wrapper(hass, data):
"""Reauth flow wrapper."""
- entry.async_start_reauth(hass, data=data)
+ hass.add_job(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_REAUTH}, data=data
+ )
+ )
persistent_notification.async_create(
hass,
(
@@ -55,16 +57,16 @@ async def _reauth_flow_wrapper(
)
-async def async_migrate_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> bool:
+async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle migration of a previous version config entry."""
_LOGGER.debug("Migrating from version %s", entry.version)
data = {**entry.data}
if entry.version == 1:
data.pop("login_response", None)
- await _reauth_flow_wrapper(hass, entry, data)
+ await _reauth_flow_wrapper(hass, data)
return False
if entry.version == 2:
- await _reauth_flow_wrapper(hass, entry, data)
+ await _reauth_flow_wrapper(hass, data)
return False
return True
@@ -77,8 +79,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
-async def async_setup_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Blink via config entry."""
+ hass.data.setdefault(DOMAIN, {})
+
_async_import_options_from_data_if_missing(hass, entry)
session = async_get_clientsession(hass)
blink = Blink(session=session)
@@ -100,8 +104,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> boo
raise ConfigEntryNotReady
await coordinator.async_config_entry_first_refresh()
-
- entry.runtime_data = coordinator
+ hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -110,7 +113,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> boo
@callback
def _async_import_options_from_data_if_missing(
- hass: HomeAssistant, entry: BlinkConfigEntry
+ hass: HomeAssistant, entry: ConfigEntry
) -> None:
options = dict(entry.options)
if CONF_SCAN_INTERVAL not in entry.options:
@@ -120,6 +123,8 @@ def _async_import_options_from_data_if_missing(
hass.config_entries.async_update_entry(entry, options=options)
-async def async_unload_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Blink entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
+ hass.data[DOMAIN].pop(entry.entry_id)
+ return unload_ok
diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py
index bfb8aa9a3a0..0ad15cf0d31 100644
--- a/homeassistant/components/blink/alarm_control_panel.py
+++ b/homeassistant/components/blink/alarm_control_panel.py
@@ -9,9 +9,13 @@ from blinkpy.blinkpy import Blink, BlinkSyncModule
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
-from homeassistant.const import ATTR_ATTRIBUTION
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ ATTR_ATTRIBUTION,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_DISARMED,
+)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
@@ -19,18 +23,16 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN
-from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
+from .coordinator import BlinkUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: BlinkConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Blink Alarm Control Panels."""
- coordinator = config_entry.runtime_data
+ coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
sync_modules = []
for sync_name, sync_module in coordinator.api.sync.items():
@@ -78,10 +80,8 @@ class BlinkSyncModuleHA(
self.sync.attributes["associated_cameras"] = list(self.sync.cameras)
self.sync.attributes[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION
self._attr_extra_state_attributes = self.sync.attributes
- self._attr_alarm_state = (
- AlarmControlPanelState.ARMED_AWAY
- if self.sync.arm
- else AlarmControlPanelState.DISARMED
+ self._attr_state = (
+ STATE_ALARM_ARMED_AWAY if self.sync.arm else STATE_ALARM_DISARMED
)
async def async_alarm_disarm(self, code: str | None = None) -> None:
diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py
index c11d4cfea23..2f0a56a901c 100644
--- a/homeassistant/components/blink/binary_sensor.py
+++ b/homeassistant/components/blink/binary_sensor.py
@@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
@@ -22,7 +23,7 @@ from .const import (
TYPE_CAMERA_ARMED,
TYPE_MOTION_DETECTED,
)
-from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
+from .coordinator import BlinkUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -46,13 +47,11 @@ BINARY_SENSORS_TYPES: tuple[BinarySensorEntityDescription, ...] = (
async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: BlinkConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the blink binary sensors."""
- coordinator = config_entry.runtime_data
+ coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
entities = [
BlinkBinarySensor(coordinator, camera, description)
diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py
index 56a84135a9b..cce9100a0bd 100644
--- a/homeassistant/components/blink/camera.py
+++ b/homeassistant/components/blink/camera.py
@@ -10,6 +10,7 @@ from requests.exceptions import ChunkedEncodingError
import voluptuous as vol
from homeassistant.components.camera import Camera
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
@@ -27,7 +28,7 @@ from .const import (
SERVICE_SAVE_VIDEO,
SERVICE_TRIGGER,
)
-from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
+from .coordinator import BlinkUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -37,13 +38,11 @@ PARALLEL_UPDATES = 1
async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: BlinkConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up a Blink Camera."""
- coordinator = config_entry.runtime_data
+ coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
entities = [
BlinkCamera(coordinator, name, camera)
for name, camera in coordinator.api.cameras.items()
diff --git a/homeassistant/components/blink/coordinator.py b/homeassistant/components/blink/coordinator.py
index 7278dabe083..e71ff4e449e 100644
--- a/homeassistant/components/blink/coordinator.py
+++ b/homeassistant/components/blink/coordinator.py
@@ -8,7 +8,6 @@ from typing import Any
from blinkpy.blinkpy import Blink
-from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@@ -17,8 +16,6 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = 300
-type BlinkConfigEntry = ConfigEntry[BlinkUpdateCoordinator]
-
class BlinkUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""BlinkUpdateCoordinator - In charge of downloading the data for a site."""
diff --git a/homeassistant/components/blink/diagnostics.py b/homeassistant/components/blink/diagnostics.py
index 255f58fc369..88ff2aff928 100644
--- a/homeassistant/components/blink/diagnostics.py
+++ b/homeassistant/components/blink/diagnostics.py
@@ -4,21 +4,24 @@ from __future__ import annotations
from typing import Any
+from blinkpy.blinkpy import Blink
+
from homeassistant.components.diagnostics import async_redact_data
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
-from .coordinator import BlinkConfigEntry
+from .const import DOMAIN
TO_REDACT = {"serial", "macaddress", "username", "password", "token", "unique_id"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
- config_entry: BlinkConfigEntry,
+ config_entry: ConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
- api = config_entry.runtime_data.api
+ api: Blink = hass.data[DOMAIN][config_entry.entry_id].api
data = {
camera.name: dict(camera.attributes.items())
diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py
index e0b5989cc80..8a807b9303e 100644
--- a/homeassistant/components/blink/sensor.py
+++ b/homeassistant/components/blink/sensor.py
@@ -10,18 +10,15 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
-from homeassistant.const import (
- SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
- EntityCategory,
- UnitOfTemperature,
-)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import EntityCategory, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DEFAULT_BRAND, DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH
-from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
+from .coordinator import BlinkUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -36,8 +33,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=TYPE_WIFI_STRENGTH,
translation_key="wifi_strength",
- native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
- device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -45,13 +40,11 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: BlinkConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Initialize a Blink sensor."""
- coordinator = config_entry.runtime_data
+ coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
entities = [
BlinkSensor(coordinator, camera, description)
for camera in coordinator.api.cameras
diff --git a/homeassistant/components/blink/services.py b/homeassistant/components/blink/services.py
index 5f51598e721..bb2cbf575dd 100644
--- a/homeassistant/components/blink/services.py
+++ b/homeassistant/components/blink/services.py
@@ -11,7 +11,6 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_SEND_PIN
-from .coordinator import BlinkConfigEntry
SERVICE_UPDATE_SCHEMA = vol.Schema(
{
@@ -31,7 +30,6 @@ def setup_services(hass: HomeAssistant) -> None:
async def send_pin(call: ServiceCall):
"""Call blink to send new pin."""
- config_entry: BlinkConfigEntry | None
for entry_id in call.data[ATTR_CONFIG_ENTRY_ID]:
if not (config_entry := hass.config_entries.async_get_entry(entry_id)):
raise ServiceValidationError(
@@ -45,7 +43,7 @@ def setup_services(hass: HomeAssistant) -> None:
translation_key="not_loaded",
translation_placeholders={"target": config_entry.title},
)
- coordinator = config_entry.runtime_data
+ coordinator = hass.data[DOMAIN][entry_id]
await coordinator.api.auth.send_auth_key(
coordinator.api,
call.data[CONF_PIN],
diff --git a/homeassistant/components/blink/switch.py b/homeassistant/components/blink/switch.py
index 8eabd5c0e59..ab9b825ded1 100644
--- a/homeassistant/components/blink/switch.py
+++ b/homeassistant/components/blink/switch.py
@@ -9,6 +9,7 @@ from homeassistant.components.switch import (
SwitchEntity,
SwitchEntityDescription,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
@@ -16,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DEFAULT_BRAND, DOMAIN, TYPE_CAMERA_ARMED
-from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
+from .coordinator import BlinkUpdateCoordinator
SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
SwitchEntityDescription(
@@ -29,11 +30,11 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
- config_entry: BlinkConfigEntry,
+ config: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Blink switches."""
- coordinator = config_entry.runtime_data
+ coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
async_add_entities(
BlinkSwitch(coordinator, camera, description)
diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py
new file mode 100644
index 00000000000..c2a46baaeb3
--- /dev/null
+++ b/homeassistant/components/bloomsky/__init__.py
@@ -0,0 +1,83 @@
+"""Support for BloomSky weather station."""
+
+from datetime import timedelta
+from http import HTTPStatus
+import logging
+
+import requests
+import voluptuous as vol
+
+from homeassistant.const import CONF_API_KEY, Platform
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import discovery
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.typing import ConfigType
+from homeassistant.util import Throttle
+from homeassistant.util.unit_system import METRIC_SYSTEM
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.SENSOR]
+
+DOMAIN = "bloomsky"
+
+# The BloomSky only updates every 5-8 minutes as per the API spec so there's
+# no point in polling the API more frequently
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
+
+CONFIG_SCHEMA = vol.Schema(
+ {DOMAIN: vol.Schema({vol.Required(CONF_API_KEY): cv.string})}, extra=vol.ALLOW_EXTRA
+)
+
+
+def setup(hass: HomeAssistant, config: ConfigType) -> bool:
+ """Set up the BloomSky integration."""
+ api_key = config[DOMAIN][CONF_API_KEY]
+
+ try:
+ bloomsky = BloomSky(api_key, hass.config.units is METRIC_SYSTEM)
+ except RuntimeError:
+ return False
+
+ hass.data[DOMAIN] = bloomsky
+
+ for platform in PLATFORMS:
+ discovery.load_platform(hass, platform, DOMAIN, {}, config)
+
+ return True
+
+
+class BloomSky:
+ """Handle all communication with the BloomSky API."""
+
+ # API documentation at http://weatherlution.com/bloomsky-api/
+ API_URL = "http://api.bloomsky.com/api/skydata"
+
+ def __init__(self, api_key, is_metric):
+ """Initialize the BookSky."""
+ self._api_key = api_key
+ self._endpoint_argument = "unit=intl" if is_metric else ""
+ self.devices = {}
+ self.is_metric = is_metric
+ _LOGGER.debug("Initial BloomSky device load")
+ self.refresh_devices()
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def refresh_devices(self):
+ """Use the API to retrieve a list of devices."""
+ _LOGGER.debug("Fetching BloomSky update")
+ response = requests.get(
+ f"{self.API_URL}?{self._endpoint_argument}",
+ headers={"Authorization": self._api_key},
+ timeout=10,
+ )
+ if response.status_code == HTTPStatus.UNAUTHORIZED:
+ raise RuntimeError("Invalid API_KEY")
+ if response.status_code == HTTPStatus.METHOD_NOT_ALLOWED:
+ _LOGGER.error("You have no bloomsky devices configured")
+ return
+ if response.status_code != HTTPStatus.OK:
+ _LOGGER.error("Invalid HTTP response: %s", response.status_code)
+ return
+ # Create dictionary keyed off of the device unique id
+ self.devices.update({device["DeviceID"]: device for device in response.json()})
diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py
new file mode 100644
index 00000000000..12d55f971e1
--- /dev/null
+++ b/homeassistant/components/bloomsky/binary_sensor.py
@@ -0,0 +1,68 @@
+"""Support the binary sensors of a BloomSky weather station."""
+
+from __future__ import annotations
+
+import voluptuous as vol
+
+from homeassistant.components.binary_sensor import (
+ PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA,
+ BinarySensorDeviceClass,
+ BinarySensorEntity,
+)
+from homeassistant.const import CONF_MONITORED_CONDITIONS
+from homeassistant.core import HomeAssistant
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+
+from . import DOMAIN
+
+SENSOR_TYPES = {"Rain": BinarySensorDeviceClass.MOISTURE, "Night": None}
+
+PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All(
+ cv.ensure_list, [vol.In(SENSOR_TYPES)]
+ )
+ }
+)
+
+
+def setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
+ """Set up the available BloomSky weather binary sensors."""
+ # Default needed in case of discovery
+ if discovery_info is not None:
+ return
+
+ sensors = config[CONF_MONITORED_CONDITIONS]
+ bloomsky = hass.data[DOMAIN]
+
+ for device in bloomsky.devices.values():
+ for variable in sensors:
+ add_entities([BloomSkySensor(bloomsky, device, variable)], True)
+
+
+class BloomSkySensor(BinarySensorEntity):
+ """Representation of a single binary sensor in a BloomSky device."""
+
+ def __init__(self, bs, device, sensor_name):
+ """Initialize a BloomSky binary sensor."""
+ self._bloomsky = bs
+ self._device_id = device["DeviceID"]
+ self._sensor_name = sensor_name
+ self._attr_name = f"{device['DeviceName']} {sensor_name}"
+ self._attr_unique_id = f"{self._device_id}-{sensor_name}"
+ self._attr_device_class = SENSOR_TYPES.get(sensor_name)
+
+ def update(self) -> None:
+ """Request an update from the BloomSky API."""
+ self._bloomsky.refresh_devices()
+
+ self._attr_is_on = self._bloomsky.devices[self._device_id]["Data"][
+ self._sensor_name
+ ]
diff --git a/homeassistant/components/bloomsky/camera.py b/homeassistant/components/bloomsky/camera.py
new file mode 100644
index 00000000000..f07dd1e9d14
--- /dev/null
+++ b/homeassistant/components/bloomsky/camera.py
@@ -0,0 +1,67 @@
+"""Support for a camera of a BloomSky weather station."""
+
+from __future__ import annotations
+
+import logging
+
+import requests
+
+from homeassistant.components.camera import Camera
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+
+from . import DOMAIN
+
+
+def setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
+ """Set up access to BloomSky cameras."""
+ if discovery_info is not None:
+ return
+
+ bloomsky = hass.data[DOMAIN]
+
+ for device in bloomsky.devices.values():
+ add_entities([BloomSkyCamera(bloomsky, device)])
+
+
+class BloomSkyCamera(Camera):
+ """Representation of the images published from the BloomSky's camera."""
+
+ def __init__(self, bs, device):
+ """Initialize access to the BloomSky camera images."""
+ super().__init__()
+ self._attr_name = device["DeviceName"]
+ self._id = device["DeviceID"]
+ self._bloomsky = bs
+ self._url = ""
+ self._last_url = ""
+ # last_image will store images as they are downloaded so that the
+ # frequent updates in home-assistant don't keep poking the server
+ # to download the same image over and over.
+ self._last_image = ""
+ self._logger = logging.getLogger(__name__)
+ self._attr_unique_id = self._id
+
+ def camera_image(
+ self, width: int | None = None, height: int | None = None
+ ) -> bytes | None:
+ """Update the camera's image if it has changed."""
+ try:
+ self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"]
+ self._bloomsky.refresh_devices()
+ # If the URL hasn't changed then the image hasn't changed.
+ if self._url != self._last_url:
+ response = requests.get(self._url, timeout=10)
+ self._last_url = self._url
+ self._last_image = response.content
+ except requests.exceptions.RequestException as error:
+ self._logger.error("Error getting bloomsky image: %s", error)
+ return None
+
+ return self._last_image
diff --git a/homeassistant/components/bloomsky/manifest.json b/homeassistant/components/bloomsky/manifest.json
new file mode 100644
index 00000000000..65d302df239
--- /dev/null
+++ b/homeassistant/components/bloomsky/manifest.json
@@ -0,0 +1,7 @@
+{
+ "domain": "bloomsky",
+ "name": "BloomSky",
+ "codeowners": [],
+ "documentation": "https://www.home-assistant.io/integrations/bloomsky",
+ "iot_class": "cloud_polling"
+}
diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py
new file mode 100644
index 00000000000..6d99506bd44
--- /dev/null
+++ b/homeassistant/components/bloomsky/sensor.py
@@ -0,0 +1,115 @@
+"""Support the sensor of a BloomSky weather station."""
+
+from __future__ import annotations
+
+import voluptuous as vol
+
+from homeassistant.components.sensor import (
+ PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
+ SensorDeviceClass,
+ SensorEntity,
+)
+from homeassistant.const import (
+ AREA_SQUARE_METERS,
+ CONF_MONITORED_CONDITIONS,
+ PERCENTAGE,
+ UnitOfElectricPotential,
+ UnitOfPressure,
+ UnitOfTemperature,
+)
+from homeassistant.core import HomeAssistant
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+
+from . import DOMAIN
+
+# These are the available sensors
+SENSOR_TYPES = [
+ "Temperature",
+ "Humidity",
+ "Pressure",
+ "Luminance",
+ "UVIndex",
+ "Voltage",
+]
+
+# Sensor units - these do not currently align with the API documentation
+SENSOR_UNITS_IMPERIAL = {
+ "Temperature": UnitOfTemperature.FAHRENHEIT,
+ "Humidity": PERCENTAGE,
+ "Pressure": UnitOfPressure.INHG,
+ "Luminance": f"cd/{AREA_SQUARE_METERS}",
+ "Voltage": UnitOfElectricPotential.MILLIVOLT,
+}
+
+# Metric units
+SENSOR_UNITS_METRIC = {
+ "Temperature": UnitOfTemperature.CELSIUS,
+ "Humidity": PERCENTAGE,
+ "Pressure": UnitOfPressure.MBAR,
+ "Luminance": f"cd/{AREA_SQUARE_METERS}",
+ "Voltage": UnitOfElectricPotential.MILLIVOLT,
+}
+
+# Device class
+SENSOR_DEVICE_CLASS = {
+ "Temperature": SensorDeviceClass.TEMPERATURE,
+ "Humidity": SensorDeviceClass.HUMIDITY,
+ "Pressure": SensorDeviceClass.PRESSURE,
+ "Voltage": SensorDeviceClass.VOLTAGE,
+}
+
+# Which sensors to format numerically
+FORMAT_NUMBERS = ["Temperature", "Pressure", "Voltage"]
+
+PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES): vol.All(
+ cv.ensure_list, [vol.In(SENSOR_TYPES)]
+ )
+ }
+)
+
+
+def setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
+ """Set up the available BloomSky weather sensors."""
+ # Default needed in case of discovery
+ if discovery_info is not None:
+ return
+
+ sensors = config[CONF_MONITORED_CONDITIONS]
+ bloomsky = hass.data[DOMAIN]
+
+ for device in bloomsky.devices.values():
+ for variable in sensors:
+ add_entities([BloomSkySensor(bloomsky, device, variable)], True)
+
+
+class BloomSkySensor(SensorEntity):
+ """Representation of a single sensor in a BloomSky device."""
+
+ def __init__(self, bs, device, sensor_name):
+ """Initialize a BloomSky sensor."""
+ self._bloomsky = bs
+ self._device_id = device["DeviceID"]
+ self._sensor_name = sensor_name
+ self._attr_name = f"{device['DeviceName']} {sensor_name}"
+ self._attr_unique_id = f"{self._device_id}-{sensor_name}"
+ self._attr_device_class = SENSOR_DEVICE_CLASS.get(sensor_name)
+ self._attr_native_unit_of_measurement = SENSOR_UNITS_IMPERIAL.get(sensor_name)
+ if self._bloomsky.is_metric:
+ self._attr_native_unit_of_measurement = SENSOR_UNITS_METRIC.get(sensor_name)
+
+ def update(self) -> None:
+ """Request an update from the BloomSky API."""
+ self._bloomsky.refresh_devices()
+ state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name]
+ self._attr_native_value = (
+ f"{state:.2f}" if self._sensor_name in FORMAT_NUMBERS else state
+ )
diff --git a/homeassistant/components/blue_current/__init__.py b/homeassistant/components/blue_current/__init__.py
index 6d0ccd7b6db..e852dfc8c6e 100644
--- a/homeassistant/components/blue_current/__init__.py
+++ b/homeassistant/components/blue_current/__init__.py
@@ -22,8 +22,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import DOMAIN, EVSE_ID, LOGGER, MODEL_TYPE
-type BlueCurrentConfigEntry = ConfigEntry[Connector]
-
PLATFORMS = [Platform.SENSOR]
CHARGE_POINTS = "CHARGE_POINTS"
DATA = "data"
@@ -34,10 +32,9 @@ OBJECT = "object"
VALUE_TYPES = ["CH_STATUS"]
-async def async_setup_entry(
- hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
-) -> bool:
+async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up Blue Current as a config entry."""
+ hass.data.setdefault(DOMAIN, {})
client = Client()
api_token = config_entry.data[CONF_API_TOKEN]
connector = Connector(hass, config_entry, client)
@@ -53,25 +50,29 @@ async def async_setup_entry(
)
await client.wait_for_charge_points()
- config_entry.runtime_data = connector
+ hass.data[DOMAIN][config_entry.entry_id] = connector
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
-async def async_unload_entry(
- hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
-) -> bool:
+async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload the Blue Current config entry."""
- return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
+ unload_ok = await hass.config_entries.async_unload_platforms(
+ config_entry, PLATFORMS
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+
+ return unload_ok
class Connector:
"""Define a class that connects to the Blue Current websocket API."""
def __init__(
- self, hass: HomeAssistant, config: BlueCurrentConfigEntry, client: Client
+ self, hass: HomeAssistant, config: ConfigEntry, client: Client
) -> None:
"""Initialize."""
self.config = config
diff --git a/homeassistant/components/blue_current/sensor.py b/homeassistant/components/blue_current/sensor.py
index be39e9571ec..4c590544984 100644
--- a/homeassistant/components/blue_current/sensor.py
+++ b/homeassistant/components/blue_current/sensor.py
@@ -8,6 +8,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CURRENCY_EURO,
UnitOfElectricCurrent,
@@ -18,7 +19,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BlueCurrentConfigEntry, Connector
+from . import Connector
from .const import DOMAIN
from .entity import BlueCurrentEntity, ChargepointEntity
@@ -210,12 +211,10 @@ PARALLEL_UPDATES = 1
async def async_setup_entry(
- hass: HomeAssistant,
- entry: BlueCurrentConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Blue Current sensors."""
- connector = entry.runtime_data
+ connector: Connector = hass.data[DOMAIN][entry.entry_id]
sensor_list: list[SensorEntity] = [
ChargePointSensor(connector, sensor, evse_id)
for evse_id in connector.charge_points
diff --git a/homeassistant/components/bluemaestro/__init__.py b/homeassistant/components/bluemaestro/__init__.py
index 3d358148fab..c25ceb44759 100644
--- a/homeassistant/components/bluemaestro/__init__.py
+++ b/homeassistant/components/bluemaestro/__init__.py
@@ -14,26 +14,27 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
+from .const import DOMAIN
+
PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
-type BlueMaestroConfigEntry = ConfigEntry[PassiveBluetoothProcessorCoordinator]
-
-async def async_setup_entry(hass: HomeAssistant, entry: BlueMaestroConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up BlueMaestro BLE device from a config entry."""
address = entry.unique_id
assert address is not None
data = BlueMaestroBluetoothDeviceData()
- coordinator = PassiveBluetoothProcessorCoordinator(
- hass,
- _LOGGER,
- address=address,
- mode=BluetoothScanningMode.PASSIVE,
- update_method=data.update,
+ coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
+ PassiveBluetoothProcessorCoordinator(
+ hass,
+ _LOGGER,
+ address=address,
+ mode=BluetoothScanningMode.PASSIVE,
+ update_method=data.update,
+ )
)
- entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(
coordinator.async_start()
@@ -41,8 +42,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BlueMaestroConfigEntry)
return True
-async def async_unload_entry(
- hass: HomeAssistant, entry: BlueMaestroConfigEntry
-) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/bluemaestro/sensor.py b/homeassistant/components/bluemaestro/sensor.py
index 57702d4ff31..75d448c9b9d 100644
--- a/homeassistant/components/bluemaestro/sensor.py
+++ b/homeassistant/components/bluemaestro/sensor.py
@@ -8,9 +8,11 @@ from bluemaestro_ble import (
Units,
)
+from homeassistant import config_entries
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothDataProcessor,
PassiveBluetoothDataUpdate,
+ PassiveBluetoothProcessorCoordinator,
PassiveBluetoothProcessorEntity,
)
from homeassistant.components.sensor import (
@@ -30,7 +32,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
-from . import BlueMaestroConfigEntry
+from .const import DOMAIN
from .device import device_key_to_bluetooth_entity_key
SENSOR_DESCRIPTIONS = {
@@ -115,11 +117,13 @@ def sensor_update_to_bluetooth_data_update(
async def async_setup_entry(
hass: HomeAssistant,
- entry: BlueMaestroConfigEntry,
+ entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the BlueMaestro BLE sensors."""
- coordinator = entry.runtime_data
+ coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
+ entry.entry_id
+ ]
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
entry.async_on_unload(
processor.async_add_entities_listener(
diff --git a/homeassistant/components/bluesound/__init__.py b/homeassistant/components/bluesound/__init__.py
index 82fe9b00d57..da74ed042be 100644
--- a/homeassistant/components/bluesound/__init__.py
+++ b/homeassistant/components/bluesound/__init__.py
@@ -14,7 +14,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
-from .services import setup_services
+from .media_player import setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
diff --git a/homeassistant/components/bluesound/const.py b/homeassistant/components/bluesound/const.py
index b1be33f6770..b7da4e31702 100644
--- a/homeassistant/components/bluesound/const.py
+++ b/homeassistant/components/bluesound/const.py
@@ -2,5 +2,9 @@
DOMAIN = "bluesound"
INTEGRATION_TITLE = "Bluesound"
+SERVICE_CLEAR_TIMER = "clear_sleep_timer"
+SERVICE_JOIN = "join"
+SERVICE_SET_TIMER = "set_sleep_timer"
+SERVICE_UNJOIN = "unjoin"
ATTR_BLUESOUND_GROUP = "bluesound_group"
ATTR_MASTER = "master"
diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json
index 462112a8b78..4d92a5f7fc0 100644
--- a/homeassistant/components/bluesound/manifest.json
+++ b/homeassistant/components/bluesound/manifest.json
@@ -6,7 +6,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bluesound",
"iot_class": "local_polling",
- "requirements": ["pyblu==1.0.4"],
+ "requirements": ["pyblu==1.0.3"],
"zeroconf": [
{
"type": "_musc._tcp.local."
diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py
index 97985a74300..1e2a537cd62 100644
--- a/homeassistant/components/bluesound/media_player.py
+++ b/homeassistant/components/bluesound/media_player.py
@@ -7,7 +7,7 @@ from asyncio import CancelledError, Task
from contextlib import suppress
from datetime import datetime, timedelta
import logging
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, NamedTuple, cast
from pyblu import Input, Player, Preset, Status, SyncStatus
from pyblu.errors import PlayerUnreachableError
@@ -24,8 +24,18 @@ from homeassistant.components.media_player import (
async_process_play_media_url,
)
from homeassistant.config_entries import SOURCE_IMPORT
-from homeassistant.const import CONF_HOST, CONF_HOSTS, CONF_NAME, CONF_PORT
-from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_HOST,
+ CONF_HOSTS,
+ CONF_NAME,
+ CONF_PORT,
+)
+from homeassistant.core import (
+ DOMAIN as HOMEASSISTANT_DOMAIN,
+ HomeAssistant,
+ ServiceCall,
+)
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, issue_registry as ir
@@ -38,7 +48,16 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.dt as dt_util
-from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN, INTEGRATION_TITLE
+from .const import (
+ ATTR_BLUESOUND_GROUP,
+ ATTR_MASTER,
+ DOMAIN,
+ INTEGRATION_TITLE,
+ SERVICE_CLEAR_TIMER,
+ SERVICE_JOIN,
+ SERVICE_SET_TIMER,
+ SERVICE_UNJOIN,
+)
from .utils import format_unique_id
if TYPE_CHECKING:
@@ -73,6 +92,29 @@ PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
}
)
+BS_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
+
+BS_JOIN_SCHEMA = BS_SCHEMA.extend({vol.Required(ATTR_MASTER): cv.entity_id})
+
+
+class ServiceMethodDetails(NamedTuple):
+ """Details for SERVICE_TO_METHOD mapping."""
+
+ method: str
+ schema: vol.Schema
+
+
+SERVICE_TO_METHOD = {
+ SERVICE_JOIN: ServiceMethodDetails(method="async_join", schema=BS_JOIN_SCHEMA),
+ SERVICE_UNJOIN: ServiceMethodDetails(method="async_unjoin", schema=BS_SCHEMA),
+ SERVICE_SET_TIMER: ServiceMethodDetails(
+ method="async_increase_timer", schema=BS_SCHEMA
+ ),
+ SERVICE_CLEAR_TIMER: ServiceMethodDetails(
+ method="async_clear_timer", schema=BS_SCHEMA
+ ),
+}
+
async def _async_import(hass: HomeAssistant, config: ConfigType) -> None:
"""Import config entry from configuration.yaml."""
@@ -117,6 +159,33 @@ async def _async_import(hass: HomeAssistant, config: ConfigType) -> None:
)
+def setup_services(hass: HomeAssistant) -> None:
+ """Set up services for Bluesound component."""
+
+ async def async_service_handler(service: ServiceCall) -> None:
+ """Map services to method of Bluesound devices."""
+ if not (method := SERVICE_TO_METHOD.get(service.service)):
+ return
+
+ params = {
+ key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID
+ }
+ if entity_ids := service.data.get(ATTR_ENTITY_ID):
+ target_players = [
+ player for player in hass.data[DOMAIN] if player.entity_id in entity_ids
+ ]
+ else:
+ target_players = hass.data[DOMAIN]
+
+ for player in target_players:
+ await getattr(player, method.method)(**params)
+
+ for service, method in SERVICE_TO_METHOD.items():
+ hass.services.async_register(
+ DOMAIN, service, async_service_handler, schema=method.schema
+ )
+
+
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BluesoundConfigEntry,
@@ -364,13 +433,12 @@ class BluesoundPlayer(MediaPlayerEntity):
if self.is_grouped and not self.is_master:
return MediaPlayerState.IDLE
- match self._status.state:
- case "pause":
- return MediaPlayerState.PAUSED
- case "stream" | "play":
- return MediaPlayerState.PLAYING
- case _:
- return MediaPlayerState.IDLE
+ status = self._status.state
+ if status in ("pause", "stop"):
+ return MediaPlayerState.PAUSED
+ if status in ("stream", "play"):
+ return MediaPlayerState.PLAYING
+ return MediaPlayerState.IDLE
@property
def media_title(self) -> str | None:
@@ -425,8 +493,6 @@ class BluesoundPlayer(MediaPlayerEntity):
return None
position = self._status.seconds
- if position is None:
- return None
if mediastate == MediaPlayerState.PLAYING:
position += (dt_util.utcnow() - self._last_status_update).total_seconds()
@@ -487,11 +553,6 @@ class BluesoundPlayer(MediaPlayerEntity):
"""Return the device name as returned by the device."""
return self._bluesound_device_name
- @property
- def sync_status(self) -> SyncStatus:
- """Return the sync status."""
- return self._sync_status
-
@property
def source_list(self) -> list[str] | None:
"""List of available input sources."""
@@ -630,7 +691,7 @@ class BluesoundPlayer(MediaPlayerEntity):
reverse=True,
)
return [
- entity.sync_status.name
+ cast(str, entity.name)
for entity in sorted_entities
if entity.bluesound_device_name in device_group
]
@@ -770,7 +831,7 @@ class BluesoundPlayer(MediaPlayerEntity):
async def async_set_volume_level(self, volume: float) -> None:
"""Send volume_up command to media player."""
- volume = int(round(volume * 100))
+ volume = int(volume * 100)
volume = min(100, volume)
volume = max(0, volume)
diff --git a/homeassistant/components/bluesound/services.py b/homeassistant/components/bluesound/services.py
deleted file mode 100644
index 06a507420f8..00000000000
--- a/homeassistant/components/bluesound/services.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""Support for Bluesound devices."""
-
-from __future__ import annotations
-
-from typing import NamedTuple
-
-import voluptuous as vol
-
-from homeassistant.const import ATTR_ENTITY_ID
-from homeassistant.core import HomeAssistant, ServiceCall
-from homeassistant.helpers import config_validation as cv
-
-from .const import ATTR_MASTER, DOMAIN
-
-SERVICE_CLEAR_TIMER = "clear_sleep_timer"
-SERVICE_JOIN = "join"
-SERVICE_SET_TIMER = "set_sleep_timer"
-SERVICE_UNJOIN = "unjoin"
-
-BS_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
-
-BS_JOIN_SCHEMA = BS_SCHEMA.extend({vol.Required(ATTR_MASTER): cv.entity_id})
-
-
-class ServiceMethodDetails(NamedTuple):
- """Details for SERVICE_TO_METHOD mapping."""
-
- method: str
- schema: vol.Schema
-
-
-SERVICE_TO_METHOD = {
- SERVICE_JOIN: ServiceMethodDetails(method="async_join", schema=BS_JOIN_SCHEMA),
- SERVICE_UNJOIN: ServiceMethodDetails(method="async_unjoin", schema=BS_SCHEMA),
- SERVICE_SET_TIMER: ServiceMethodDetails(
- method="async_increase_timer", schema=BS_SCHEMA
- ),
- SERVICE_CLEAR_TIMER: ServiceMethodDetails(
- method="async_clear_timer", schema=BS_SCHEMA
- ),
-}
-
-
-def setup_services(hass: HomeAssistant) -> None:
- """Set up services for Bluesound component."""
-
- async def async_service_handler(service: ServiceCall) -> None:
- """Map services to method of Bluesound devices."""
- if not (method := SERVICE_TO_METHOD.get(service.service)):
- return
-
- params = {
- key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID
- }
- if entity_ids := service.data.get(ATTR_ENTITY_ID):
- target_players = [
- player for player in hass.data[DOMAIN] if player.entity_id in entity_ids
- ]
- else:
- target_players = hass.data[DOMAIN]
-
- for player in target_players:
- await getattr(player, method.method)(**params)
-
- for service, method in SERVICE_TO_METHOD.items():
- hass.services.async_register(
- DOMAIN, service, async_service_handler, schema=method.schema
- )
diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json
index fe16bd73a9e..81602359c88 100644
--- a/homeassistant/components/bluetooth/manifest.json
+++ b/homeassistant/components/bluetooth/manifest.json
@@ -20,6 +20,6 @@
"bluetooth-auto-recovery==1.4.2",
"bluetooth-data-tools==1.20.0",
"dbus-fast==2.24.3",
- "habluetooth==3.6.0"
+ "habluetooth==3.5.0"
]
}
diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py
index 409bfdca6f1..37ff1eb374c 100644
--- a/homeassistant/components/bmw_connected_drive/config_flow.py
+++ b/homeassistant/components/bmw_connected_drive/config_flow.py
@@ -7,11 +7,7 @@ from typing import Any
from bimmer_connected.api.authentication import MyBMWAuthentication
from bimmer_connected.api.regions import get_region_from_name
-from bimmer_connected.models import (
- MyBMWAPIError,
- MyBMWAuthError,
- MyBMWCaptchaMissingError,
-)
+from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError
from httpx import RequestError
import voluptuous as vol
@@ -21,7 +17,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
@@ -58,8 +54,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
try:
await auth.login()
- except MyBMWCaptchaMissingError as ex:
- raise MissingCaptcha from ex
except MyBMWAuthError as ex:
raise InvalidAuth from ex
except (MyBMWAPIError, RequestError) as ex:
@@ -104,8 +98,6 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
CONF_GCID: info.get(CONF_GCID),
}
- except MissingCaptcha:
- errors["base"] = "missing_captcha"
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
@@ -153,10 +145,10 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> BMWOptionsFlow:
"""Return a MyBMW option flow."""
- return BMWOptionsFlow()
+ return BMWOptionsFlow(config_entry)
-class BMWOptionsFlow(OptionsFlow):
+class BMWOptionsFlow(OptionsFlowWithConfigEntry):
"""Handle a option flow for MyBMW."""
async def async_step_init(
@@ -200,7 +192,3 @@ class CannotConnect(HomeAssistantError):
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
-
-
-class MissingCaptcha(HomeAssistantError):
- """Error to indicate the captcha token is missing."""
diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py
index d38b7ffacc2..992e7dea6b2 100644
--- a/homeassistant/components/bmw_connected_drive/coordinator.py
+++ b/homeassistant/components/bmw_connected_drive/coordinator.py
@@ -7,12 +7,7 @@ import logging
from bimmer_connected.account import MyBMWAccount
from bimmer_connected.api.regions import get_region_from_name
-from bimmer_connected.models import (
- GPSPosition,
- MyBMWAPIError,
- MyBMWAuthError,
- MyBMWCaptchaMissingError,
-)
+from bimmer_connected.models import GPSPosition, MyBMWAPIError, MyBMWAuthError
from httpx import RequestError
from homeassistant.config_entries import ConfigEntry
@@ -66,12 +61,6 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
try:
await self.account.get_vehicles()
- except MyBMWCaptchaMissingError as err:
- # If a captcha is required (user/password login flow), always trigger the reauth flow
- raise ConfigEntryAuthFailed(
- translation_domain=DOMAIN,
- translation_key="missing_captcha",
- ) from err
except MyBMWAuthError as err:
# Allow one retry interval before raising AuthFailed to avoid flaky API issues
if self.last_update_success:
diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json
index 584eb1eebb5..6bc9027ac19 100644
--- a/homeassistant/components/bmw_connected_drive/manifest.json
+++ b/homeassistant/components/bmw_connected_drive/manifest.json
@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["bimmer_connected"],
"quality_scale": "platinum",
- "requirements": ["bimmer-connected[china]==0.16.4"]
+ "requirements": ["bimmer-connected[china]==0.16.3"]
}
diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py
index e24e2dd75f6..fe0e835622b 100644
--- a/homeassistant/components/bmw_connected_drive/sensor.py
+++ b/homeassistant/components/bmw_connected_drive/sensor.py
@@ -80,6 +80,7 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
BMWSensorEntityDescription(
key="fuel_and_battery.charging_target",
translation_key="charging_target",
+ device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=0,
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json
index 0e7a4a32ef4..fed71f85e35 100644
--- a/homeassistant/components/bmw_connected_drive/strings.json
+++ b/homeassistant/components/bmw_connected_drive/strings.json
@@ -11,8 +11,7 @@
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
- "missing_captcha": "Captcha validation missing"
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
@@ -201,9 +200,6 @@
"exceptions": {
"invalid_poi": {
"message": "Invalid data for point of interest: {poi_exception}"
- },
- "missing_captcha": {
- "message": "Login requires captcha validation"
}
}
}
diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py
index 42915c7dc0b..a2d88bc6f6a 100644
--- a/homeassistant/components/bond/button.py
+++ b/homeassistant/components/bond/button.py
@@ -237,20 +237,6 @@ BUTTONS: tuple[BondButtonEntityDescription, ...] = (
mutually_exclusive=Action.SET_POSITION,
argument=STEP_SIZE,
),
- BondButtonEntityDescription(
- key=Action.OPEN_NEXT,
- name="Open Next",
- translation_key="open_next",
- mutually_exclusive=None,
- argument=None,
- ),
- BondButtonEntityDescription(
- key=Action.CLOSE_NEXT,
- name="Close Next",
- translation_key="close_next",
- mutually_exclusive=None,
- argument=None,
- ),
)
diff --git a/homeassistant/components/bond/icons.json b/homeassistant/components/bond/icons.json
index b150d1c1fa3..48b351b1c76 100644
--- a/homeassistant/components/bond/icons.json
+++ b/homeassistant/components/bond/icons.json
@@ -84,12 +84,6 @@
},
"decrease_position": {
"default": "mdi:minus-box"
- },
- "open_next": {
- "default": "mdi:plus-box"
- },
- "close_next": {
- "default": "mdi:minus-box"
}
},
"light": {
diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py
index 58601152da5..a8896414a4f 100644
--- a/homeassistant/components/bosch_shc/config_flow.py
+++ b/homeassistant/components/bosch_shc/config_flow.py
@@ -39,21 +39,16 @@ HOST_SCHEMA = vol.Schema(
)
-def write_tls_asset(
- hass: HomeAssistant, folder: str, filename: str, asset: bytes
-) -> None:
+def write_tls_asset(hass: HomeAssistant, filename: str, asset: bytes) -> None:
"""Write the tls assets to disk."""
- makedirs(hass.config.path(DOMAIN, folder), exist_ok=True)
- with open(
- hass.config.path(DOMAIN, folder, filename), "w", encoding="utf8"
- ) as file_handle:
+ makedirs(hass.config.path(DOMAIN), exist_ok=True)
+ with open(hass.config.path(DOMAIN, filename), "w", encoding="utf8") as file_handle:
file_handle.write(asset.decode("utf-8"))
def create_credentials_and_validate(
hass: HomeAssistant,
host: str,
- unique_id: str,
user_input: dict[str, Any],
zeroconf_instance: zeroconf.HaZeroconf,
) -> dict[str, Any] | None:
@@ -62,15 +57,13 @@ def create_credentials_and_validate(
result = helper.register(host, "HomeAssistant")
if result is not None:
- # Save key/certificate pair for each registered host separately
- # otherwise only the last registered host is accessible.
- write_tls_asset(hass, unique_id, CONF_SHC_CERT, result["cert"])
- write_tls_asset(hass, unique_id, CONF_SHC_KEY, result["key"])
+ write_tls_asset(hass, CONF_SHC_CERT, result["cert"])
+ write_tls_asset(hass, CONF_SHC_KEY, result["key"])
session = SHCSession(
host,
- hass.config.path(DOMAIN, unique_id, CONF_SHC_CERT),
- hass.config.path(DOMAIN, unique_id, CONF_SHC_KEY),
+ hass.config.path(DOMAIN, CONF_SHC_CERT),
+ hass.config.path(DOMAIN, CONF_SHC_KEY),
True,
zeroconf_instance,
)
@@ -150,16 +143,11 @@ class BoschSHCConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input is not None:
zeroconf_instance = await zeroconf.async_get_instance(self.hass)
- # unique_id uniquely identifies the registered controller and is used
- # to save the key/certificate pair for each controller separately
- unique_id = self.info["unique_id"]
- assert unique_id
try:
result = await self.hass.async_add_executor_job(
create_credentials_and_validate,
self.hass,
self.host,
- unique_id,
user_input,
zeroconf_instance,
)
@@ -179,18 +167,13 @@ class BoschSHCConfigFlow(ConfigFlow, domain=DOMAIN):
else:
assert result
entry_data = {
- # Each host has its own key/certificate pair
- CONF_SSL_CERTIFICATE: self.hass.config.path(
- DOMAIN, unique_id, CONF_SHC_CERT
- ),
- CONF_SSL_KEY: self.hass.config.path(
- DOMAIN, unique_id, CONF_SHC_KEY
- ),
+ CONF_SSL_CERTIFICATE: self.hass.config.path(DOMAIN, CONF_SHC_CERT),
+ CONF_SSL_KEY: self.hass.config.path(DOMAIN, CONF_SHC_KEY),
CONF_HOST: self.host,
CONF_TOKEN: result["token"],
CONF_HOSTNAME: result["token"].split(":", 1)[1],
}
- existing_entry = await self.async_set_unique_id(unique_id)
+ existing_entry = await self.async_set_unique_id(self.info["unique_id"])
if existing_entry:
return self.async_update_reload_and_abort(
existing_entry,
diff --git a/homeassistant/components/bring/icons.json b/homeassistant/components/bring/icons.json
index c670ef87700..7a4775066cf 100644
--- a/homeassistant/components/bring/icons.json
+++ b/homeassistant/components/bring/icons.json
@@ -12,13 +12,6 @@
},
"list_language": {
"default": "mdi:earth"
- },
- "list_access": {
- "default": "mdi:account-lock",
- "state": {
- "shared": "mdi:account-group",
- "invitation": "mdi:account-multiple-plus"
- }
}
},
"todo": {
diff --git a/homeassistant/components/bring/manifest.json b/homeassistant/components/bring/manifest.json
index ff24a991350..79336c086ed 100644
--- a/homeassistant/components/bring/manifest.json
+++ b/homeassistant/components/bring/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bring",
"integration_type": "service",
"iot_class": "cloud_polling",
- "requirements": ["bring-api==0.9.1"]
+ "requirements": ["bring-api==0.9.0"]
}
diff --git a/homeassistant/components/bring/sensor.py b/homeassistant/components/bring/sensor.py
index 746ed397e1b..edc1da3d59b 100644
--- a/homeassistant/components/bring/sensor.py
+++ b/homeassistant/components/bring/sensor.py
@@ -40,7 +40,6 @@ class BringSensor(StrEnum):
CONVENIENT = "convenient"
DISCOUNTED = "discounted"
LIST_LANGUAGE = "list_language"
- LIST_ACCESS = "list_access"
SENSOR_DESCRIPTIONS: tuple[BringSensorEntityDescription, ...] = (
@@ -74,14 +73,6 @@ SENSOR_DESCRIPTIONS: tuple[BringSensorEntityDescription, ...] = (
options=[x.lower() for x in BRING_SUPPORTED_LOCALES],
device_class=SensorDeviceClass.ENUM,
),
- BringSensorEntityDescription(
- key=BringSensor.LIST_ACCESS,
- translation_key=BringSensor.LIST_ACCESS,
- value_fn=lambda lst, _: lst["status"].lower(),
- entity_category=EntityCategory.DIAGNOSTIC,
- options=["registered", "shared", "invitation"],
- device_class=SensorDeviceClass.ENUM,
- ),
)
diff --git a/homeassistant/components/bring/strings.json b/homeassistant/components/bring/strings.json
index 9a93881b5d2..bce18fc6a92 100644
--- a/homeassistant/components/bring/strings.json
+++ b/homeassistant/components/bring/strings.json
@@ -61,14 +61,6 @@
"sv-se": "Sweden",
"tr-tr": "Türkiye"
}
- },
- "list_access": {
- "name": "List access",
- "state": {
- "registered": "Private",
- "shared": "Shared",
- "invitation": "Invitation pending"
- }
}
}
},
diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py
index 75b6236a473..2518cd65bd3 100644
--- a/homeassistant/components/broadlink/device.py
+++ b/homeassistant/components/broadlink/device.py
@@ -15,7 +15,7 @@ from broadlink.exceptions import (
)
from typing_extensions import TypeVar
-from homeassistant.config_entries import ConfigEntry
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
@@ -200,4 +200,10 @@ class BroadlinkDevice(Generic[_ApiT]):
self.api.host[0],
)
- self.config.async_start_reauth(self.hass, data={CONF_NAME: self.name})
+ self.hass.async_create_task(
+ self.hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_REAUTH},
+ data={CONF_NAME: self.name, **self.config.data},
+ )
+ )
diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py
index d9130b96300..ffc2b3bfa8a 100644
--- a/homeassistant/components/brother/config_flow.py
+++ b/homeassistant/components/brother/config_flow.py
@@ -141,6 +141,12 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle a reconfiguration flow initialized by the user."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfiguration flow initialized by the user."""
entry = self._get_reconfigure_entry()
@@ -164,7 +170,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=self.add_suggested_values_to_schema(
data_schema=RECONFIGURE_SCHEMA,
suggested_values=entry.data | (user_input or {}),
diff --git a/homeassistant/components/brother/strings.json b/homeassistant/components/brother/strings.json
index 3b5b38ce9a0..d7f8f4a1b89 100644
--- a/homeassistant/components/brother/strings.json
+++ b/homeassistant/components/brother/strings.json
@@ -18,7 +18,7 @@
"type": "[%key:component::brother::config::step::user::data::type%]"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"description": "Update configuration for {printer_name}.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py
index c488c813b3b..bec281d1902 100644
--- a/homeassistant/components/brunt/__init__.py
+++ b/homeassistant/components/brunt/__init__.py
@@ -2,22 +2,79 @@
from __future__ import annotations
+from asyncio import timeout
+import logging
+
+from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError
+from brunt import BruntClientAsync, Thing
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-from .const import PLATFORMS
-from .coordinator import BruntConfigEntry, BruntCoordinator
+from .const import DATA_BAPI, DATA_COOR, DOMAIN, PLATFORMS, REGULAR_INTERVAL
+
+_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: BruntConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Brunt using config flow."""
- coordinator = BruntCoordinator(hass, entry)
+ session = async_get_clientsession(hass)
+ bapi = BruntClientAsync(
+ username=entry.data[CONF_USERNAME],
+ password=entry.data[CONF_PASSWORD],
+ session=session,
+ )
+ try:
+ await bapi.async_login()
+ except ServerDisconnectedError as exc:
+ raise ConfigEntryNotReady("Brunt not ready to connect.") from exc
+ except ClientResponseError as exc:
+ raise ConfigEntryAuthFailed(
+ f"Brunt could not connect with username: {entry.data[CONF_USERNAME]}."
+ ) from exc
+
+ async def async_update_data() -> dict[str | None, Thing]:
+ """Fetch data from the Brunt endpoint for all Things.
+
+ Error 403 is the API response for any kind of authentication error (failed password or email)
+ Error 401 is the API response for things that are not part of the account, could happen when a device is deleted from the account.
+ """
+ try:
+ async with timeout(10):
+ things = await bapi.async_get_things(force=True)
+ return {thing.serial: thing for thing in things}
+ except ServerDisconnectedError as err:
+ raise UpdateFailed(f"Error communicating with API: {err}") from err
+ except ClientResponseError as err:
+ if err.status == 403:
+ raise ConfigEntryAuthFailed from err
+ if err.status == 401:
+ _LOGGER.warning("Device not found, will reload Brunt integration")
+ await hass.config_entries.async_reload(entry.entry_id)
+ raise UpdateFailed from err
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name="brunt",
+ update_method=async_update_data,
+ update_interval=REGULAR_INTERVAL,
+ )
await coordinator.async_config_entry_first_refresh()
- entry.runtime_data = coordinator
+ hass.data.setdefault(DOMAIN, {})
+ hass.data[DOMAIN][entry.entry_id] = {DATA_BAPI: bapi, DATA_COOR: coordinator}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
-async def async_unload_entry(hass: HomeAssistant, entry: BruntConfigEntry) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+ return unload_ok
diff --git a/homeassistant/components/brunt/config_flow.py b/homeassistant/components/brunt/config_flow.py
index 3baea9b98cc..dd119a402d8 100644
--- a/homeassistant/components/brunt/config_flow.py
+++ b/homeassistant/components/brunt/config_flow.py
@@ -12,7 +12,7 @@ from brunt import BruntClientAsync
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import DOMAIN
@@ -92,10 +92,7 @@ class BruntConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="reauth_confirm",
data_schema=REAUTH_SCHEMA,
- description_placeholders={
- CONF_USERNAME: username,
- CONF_NAME: reauth_entry.title,
- },
+ description_placeholders={"username": username},
)
user_input[CONF_USERNAME] = username
errors = await validate_input(user_input)
@@ -104,10 +101,7 @@ class BruntConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="reauth_confirm",
data_schema=REAUTH_SCHEMA,
errors=errors,
- description_placeholders={
- CONF_USERNAME: username,
- CONF_NAME: reauth_entry.title,
- },
+ description_placeholders={"username": username},
)
return self.async_update_reload_and_abort(reauth_entry, data=user_input)
diff --git a/homeassistant/components/brunt/const.py b/homeassistant/components/brunt/const.py
index 0d9323cbf07..4c246d28d64 100644
--- a/homeassistant/components/brunt/const.py
+++ b/homeassistant/components/brunt/const.py
@@ -10,6 +10,8 @@ NOTIFICATION_ID = "brunt_notification"
NOTIFICATION_TITLE = "Brunt Cover Setup"
ATTRIBUTION = "Based on an unofficial Brunt SDK."
PLATFORMS = [Platform.COVER]
+DATA_BAPI = "bapi"
+DATA_COOR = "coordinator"
CLOSED_POSITION = 0
OPEN_POSITION = 100
diff --git a/homeassistant/components/brunt/coordinator.py b/homeassistant/components/brunt/coordinator.py
deleted file mode 100644
index b07ec2c0c88..00000000000
--- a/homeassistant/components/brunt/coordinator.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""The brunt component."""
-
-from __future__ import annotations
-
-from asyncio import timeout
-import logging
-
-from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError
-from brunt import BruntClientAsync, Thing
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-
-from .const import REGULAR_INTERVAL
-
-_LOGGER = logging.getLogger(__name__)
-
-type BruntConfigEntry = ConfigEntry[BruntCoordinator]
-
-
-class BruntCoordinator(DataUpdateCoordinator[dict[str | None, Thing]]):
- """Config entry data."""
-
- bapi: BruntClientAsync
- config_entry: BruntConfigEntry
-
- def __init__(
- self,
- hass: HomeAssistant,
- config_entry: BruntConfigEntry,
- ) -> None:
- """Initialize the Brunt coordinator."""
- super().__init__(
- hass,
- _LOGGER,
- config_entry=config_entry,
- name="brunt",
- update_interval=REGULAR_INTERVAL,
- )
-
- async def _async_setup(self) -> None:
- session = async_get_clientsession(self.hass)
-
- self.bapi = BruntClientAsync(
- username=self.config_entry.data[CONF_USERNAME],
- password=self.config_entry.data[CONF_PASSWORD],
- session=session,
- )
- try:
- await self.bapi.async_login()
- except ServerDisconnectedError as exc:
- raise ConfigEntryNotReady("Brunt not ready to connect.") from exc
- except ClientResponseError as exc:
- raise ConfigEntryAuthFailed(
- f"Brunt could not connect with username: {self.config_entry.data[CONF_USERNAME]}."
- ) from exc
-
- async def _async_update_data(self) -> dict[str | None, Thing]:
- """Fetch data from the Brunt endpoint for all Things.
-
- Error 403 is the API response for any kind of authentication error (failed password or email)
- Error 401 is the API response for things that are not part of the account, could happen when a device is deleted from the account.
- """
- try:
- async with timeout(10):
- things = await self.bapi.async_get_things(force=True)
- return {thing.serial: thing for thing in things}
- except ServerDisconnectedError as err:
- raise UpdateFailed(f"Error communicating with API: {err}") from err
- except ClientResponseError as err:
- if err.status == 403:
- raise ConfigEntryAuthFailed from err
- if err.status == 401:
- _LOGGER.warning("Device not found, will reload Brunt integration")
- await self.hass.config_entries.async_reload(self.config_entry.entry_id)
- raise UpdateFailed from err
diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py
index bb97f42bd36..519885fe542 100644
--- a/homeassistant/components/brunt/cover.py
+++ b/homeassistant/components/brunt/cover.py
@@ -5,7 +5,7 @@ from __future__ import annotations
from typing import Any
from aiohttp.client_exceptions import ClientResponseError
-from brunt import Thing
+from brunt import BruntClientAsync, Thing
from homeassistant.components.cover import (
ATTR_POSITION,
@@ -13,39 +13,49 @@ from homeassistant.components.cover import (
CoverEntity,
CoverEntityFeature,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
from .const import (
ATTR_REQUEST_POSITION,
ATTRIBUTION,
CLOSED_POSITION,
+ DATA_BAPI,
+ DATA_COOR,
DOMAIN,
FAST_INTERVAL,
OPEN_POSITION,
REGULAR_INTERVAL,
)
-from .coordinator import BruntConfigEntry, BruntCoordinator
async def async_setup_entry(
hass: HomeAssistant,
- entry: BruntConfigEntry,
+ entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the brunt platform."""
- coordinator = entry.runtime_data
+ bapi: BruntClientAsync = hass.data[DOMAIN][entry.entry_id][DATA_BAPI]
+ coordinator: DataUpdateCoordinator[dict[str | None, Thing]] = hass.data[DOMAIN][
+ entry.entry_id
+ ][DATA_COOR]
async_add_entities(
- BruntDevice(coordinator, serial, thing, entry.entry_id)
+ BruntDevice(coordinator, serial, thing, bapi, entry.entry_id)
for serial, thing in coordinator.data.items()
)
-class BruntDevice(CoordinatorEntity[BruntCoordinator], CoverEntity):
+class BruntDevice(
+ CoordinatorEntity[DataUpdateCoordinator[dict[str | None, Thing]]], CoverEntity
+):
"""Representation of a Brunt cover device.
Contains the common logic for all Brunt devices.
@@ -63,14 +73,16 @@ class BruntDevice(CoordinatorEntity[BruntCoordinator], CoverEntity):
def __init__(
self,
- coordinator: BruntCoordinator,
+ coordinator: DataUpdateCoordinator[dict[str | None, Thing]],
serial: str | None,
thing: Thing,
+ bapi: BruntClientAsync,
entry_id: str,
) -> None:
"""Init the Brunt device."""
super().__init__(coordinator)
self._attr_unique_id = serial
+ self._bapi = bapi
self._thing = thing
self._entry_id = entry_id
@@ -155,7 +167,7 @@ class BruntDevice(CoordinatorEntity[BruntCoordinator], CoverEntity):
async def _async_update_cover(self, position: int) -> None:
"""Set the cover to the new position and wait for the update to be reflected."""
try:
- await self.coordinator.bapi.async_change_request_position(
+ await self._bapi.async_change_request_position(
position, thing_uri=self._thing.thing_uri
)
except ClientResponseError as exc:
@@ -170,7 +182,7 @@ class BruntDevice(CoordinatorEntity[BruntCoordinator], CoverEntity):
"""Update the update interval after each refresh."""
if (
self.request_cover_position
- == self.coordinator.bapi.last_requested_positions[self._thing.thing_uri]
+ == self._bapi.last_requested_positions[self._thing.thing_uri]
and self.move_state == 0
):
self.coordinator.update_interval = REGULAR_INTERVAL
diff --git a/homeassistant/components/bryant_evolution/config_flow.py b/homeassistant/components/bryant_evolution/config_flow.py
index 2e5a094948d..9e115bd69ee 100644
--- a/homeassistant/components/bryant_evolution/config_flow.py
+++ b/homeassistant/components/bryant_evolution/config_flow.py
@@ -61,6 +61,12 @@ class BryantConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle integration reconfiguration."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle integration reconfiguration."""
errors: dict[str, str] = {}
@@ -76,7 +82,7 @@ class BryantConfigFlow(ConfigFlow, domain=DOMAIN):
)
errors["base"] = "cannot_connect"
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
)
diff --git a/homeassistant/components/bryant_evolution/strings.json b/homeassistant/components/bryant_evolution/strings.json
index ec816d3d961..11ce4bc6ce7 100644
--- a/homeassistant/components/bryant_evolution/strings.json
+++ b/homeassistant/components/bryant_evolution/strings.json
@@ -1,7 +1,7 @@
{
"config": {
"step": {
- "reconfigure": {
+ "reconfigure_confirm": {
"data": {
"filename": "[%key:component::bryant_evolution::config::step::user::data::filename%]"
}
diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py
index 4d3c6ee2073..79447c6cff5 100644
--- a/homeassistant/components/bsblan/__init__.py
+++ b/homeassistant/components/bsblan/__init__.py
@@ -15,13 +15,11 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import CONF_PASSKEY
+from .const import CONF_PASSKEY, DOMAIN
from .coordinator import BSBLanUpdateCoordinator
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
-type BSBLanConfigEntry = ConfigEntry[BSBLanData]
-
@dataclasses.dataclass
class BSBLanData:
@@ -34,7 +32,7 @@ class BSBLanData:
static: StaticState
-async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up BSB-Lan from a config entry."""
# create config using BSBLANConfig
@@ -59,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
info = await bsblan.info()
static = await bsblan.static_values()
- entry.runtime_data = BSBLanData(
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BSBLanData(
client=bsblan,
coordinator=coordinator,
device=device,
@@ -72,6 +70,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
return True
-async def async_unload_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload BSBLAN config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
+ # Cleanup
+ del hass.data[DOMAIN][entry.entry_id]
+ if not hass.data[DOMAIN]:
+ del hass.data[DOMAIN]
+ return unload_ok
diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py
index fcbe88f2fac..3a204a9e0c2 100644
--- a/homeassistant/components/bsblan/climate.py
+++ b/homeassistant/components/bsblan/climate.py
@@ -15,6 +15,7 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
HVACMode,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
@@ -22,7 +23,7 @@ from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.enum import try_parse_enum
-from . import BSBLanConfigEntry, BSBLanData
+from . import BSBLanData
from .const import ATTR_TARGET_TEMPERATURE, DOMAIN
from .entity import BSBLanEntity
@@ -42,12 +43,18 @@ PRESET_MODES = [
async def async_setup_entry(
hass: HomeAssistant,
- entry: BSBLanConfigEntry,
+ entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up BSBLAN device based on a config entry."""
- data = entry.runtime_data
- async_add_entities([BSBLANClimate(data)])
+ data: BSBLanData = hass.data[DOMAIN][entry.entry_id]
+ async_add_entities(
+ [
+ BSBLANClimate(
+ data,
+ )
+ ]
+ )
class BSBLANClimate(BSBLanEntity, ClimateEntity):
diff --git a/homeassistant/components/bsblan/coordinator.py b/homeassistant/components/bsblan/coordinator.py
index 1a4299fe72f..508f2c898c3 100644
--- a/homeassistant/components/bsblan/coordinator.py
+++ b/homeassistant/components/bsblan/coordinator.py
@@ -54,9 +54,6 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
async def _async_update_data(self) -> BSBLanCoordinatorData:
"""Get state and sensor data from BSB-Lan device."""
try:
- # initialize the client, this is cached and will only be called once
- await self.client.initialize()
-
state = await self.client.state()
sensor = await self.client.sensor()
except BSBLANConnectionError as err:
diff --git a/homeassistant/components/bsblan/diagnostics.py b/homeassistant/components/bsblan/diagnostics.py
index 5a8e5c1c4c5..88418f306c8 100644
--- a/homeassistant/components/bsblan/diagnostics.py
+++ b/homeassistant/components/bsblan/diagnostics.py
@@ -4,16 +4,18 @@ from __future__ import annotations
from typing import Any
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
-from . import BSBLanConfigEntry
+from . import BSBLanData
+from .const import DOMAIN
async def async_get_config_entry_diagnostics(
- hass: HomeAssistant, entry: BSBLanConfigEntry
+ hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
- data = entry.runtime_data
+ data: BSBLanData = hass.data[DOMAIN][entry.entry_id]
return {
"info": data.info.to_dict(),
diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json
index aa9c03abf4a..6cd8608c42d 100644
--- a/homeassistant/components/bsblan/manifest.json
+++ b/homeassistant/components/bsblan/manifest.json
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bsblan"],
- "requirements": ["python-bsblan==1.2.1"]
+ "requirements": ["python-bsblan==0.6.2"]
}
diff --git a/homeassistant/components/bsblan/sensor.py b/homeassistant/components/bsblan/sensor.py
index eab03d7a50c..346f972ea9a 100644
--- a/homeassistant/components/bsblan/sensor.py
+++ b/homeassistant/components/bsblan/sensor.py
@@ -11,12 +11,14 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
-from . import BSBLanConfigEntry, BSBLanData
+from . import BSBLanData
+from .const import DOMAIN
from .coordinator import BSBLanCoordinatorData
from .entity import BSBLanEntity
@@ -50,11 +52,11 @@ SENSOR_TYPES: tuple[BSBLanSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
- entry: BSBLanConfigEntry,
+ entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up BSB-Lan sensor based on a config entry."""
- data = entry.runtime_data
+ data: BSBLanData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(BSBLanSensor(data, description) for description in SENSOR_TYPES)
diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py
index 417df9f5068..64e6d61cefb 100644
--- a/homeassistant/components/bthome/sensor.py
+++ b/homeassistant/components/bthome/sensor.py
@@ -364,7 +364,7 @@ SENSOR_DESCRIPTIONS = {
): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.CONDUCTIVITY}_{Units.CONDUCTIVITY}",
device_class=SensorDeviceClass.CONDUCTIVITY,
- native_unit_of_measurement=UnitOfConductivity.MICROSIEMENS_PER_CM,
+ native_unit_of_measurement=UnitOfConductivity.MICROSIEMENS,
state_class=SensorStateClass.MEASUREMENT,
),
}
diff --git a/homeassistant/components/buienradar/__init__.py b/homeassistant/components/buienradar/__init__.py
index bea0102be40..3bf593b2dab 100644
--- a/homeassistant/components/buienradar/__init__.py
+++ b/homeassistant/components/buienradar/__init__.py
@@ -6,26 +6,25 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
-from .util import BrData
+from .const import DOMAIN
PLATFORMS = [Platform.CAMERA, Platform.SENSOR, Platform.WEATHER]
-type BuienRadarConfigEntry = ConfigEntry[dict[Platform, BrData]]
-
-async def async_setup_entry(hass: HomeAssistant, entry: BuienRadarConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up buienradar from a config entry."""
- entry.runtime_data = {}
+ hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_update_options))
return True
-async def async_unload_entry(hass: HomeAssistant, entry: BuienRadarConfigEntry) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
+ entry_data = hass.data[DOMAIN].pop(entry.entry_id)
for platform in PLATFORMS:
- if (data := entry.runtime_data.get(platform)) and (
+ if (data := entry_data.get(platform)) and (
unsub := data.unsub_schedule_update
):
unsub()
@@ -33,8 +32,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: BuienRadarConfigEntry)
return unload_ok
-async def async_update_options(
- hass: HomeAssistant, config_entry: BuienRadarConfigEntry
-) -> None:
+async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Update options."""
await hass.config_entries.async_reload(config_entry.entry_id)
diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py
index 45ff2d6de52..e9a7d2517cb 100644
--- a/homeassistant/components/buienradar/camera.py
+++ b/homeassistant/components/buienradar/camera.py
@@ -10,13 +10,13 @@ import aiohttp
import voluptuous as vol
from homeassistant.components.camera import Camera
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_COUNTRY_CODE, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
-from . import BuienRadarConfigEntry
from .const import CONF_DELTA, DEFAULT_COUNTRY, DEFAULT_DELTA, DEFAULT_DIMENSION
_LOGGER = logging.getLogger(__name__)
@@ -29,9 +29,7 @@ SUPPORTED_COUNTRY_CODES = ["NL", "BE"]
async def async_setup_entry(
- hass: HomeAssistant,
- entry: BuienRadarConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up buienradar radar-loop camera component."""
config = entry.data
diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py
index afce293402e..c61d8e10b85 100644
--- a/homeassistant/components/buienradar/sensor.py
+++ b/homeassistant/components/buienradar/sensor.py
@@ -28,6 +28,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_LATITUDE,
@@ -48,10 +49,10 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
-from . import BuienRadarConfigEntry
from .const import (
CONF_TIMEFRAME,
DEFAULT_TIMEFRAME,
+ DOMAIN,
STATE_CONDITION_CODES,
STATE_CONDITIONS,
STATE_DETAILED_CONDITIONS,
@@ -689,9 +690,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
async def async_setup_entry(
- hass: HomeAssistant,
- entry: BuienRadarConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Create the buienradar sensor."""
config = entry.data
@@ -724,7 +723,7 @@ async def async_setup_entry(
# create weather data:
data = BrData(hass, coordinates, timeframe, entities)
- entry.runtime_data[Platform.SENSOR] = data
+ hass.data[DOMAIN][entry.entry_id][Platform.SENSOR] = data
await data.async_update()
async_add_entities(entities)
diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py
index 8b71032bace..2af66982fab 100644
--- a/homeassistant/components/buienradar/weather.py
+++ b/homeassistant/components/buienradar/weather.py
@@ -39,6 +39,7 @@ from homeassistant.components.weather import (
WeatherEntity,
WeatherEntityFeature,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
@@ -53,8 +54,8 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import BuienRadarConfigEntry
-from .const import DEFAULT_TIMEFRAME
+# Reuse data and API logic from the sensor implementation
+from .const import DEFAULT_TIMEFRAME, DOMAIN
from .util import BrData
_LOGGER = logging.getLogger(__name__)
@@ -92,9 +93,7 @@ CONDITION_MAP = {
async def async_setup_entry(
- hass: HomeAssistant,
- entry: BuienRadarConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the buienradar platform."""
config = entry.data
@@ -114,7 +113,7 @@ async def async_setup_entry(
# create weather data:
data = BrData(hass, coordinates, DEFAULT_TIMEFRAME, entities)
- entry.runtime_data[Platform.WEATHER] = data
+ hass.data[DOMAIN][entry.entry_id][Platform.WEATHER] = data
await data.async_update()
async_add_entities(entities)
diff --git a/homeassistant/components/caldav/__init__.py b/homeassistant/components/caldav/__init__.py
index 1d50e6d309a..beb03cec554 100644
--- a/homeassistant/components/caldav/__init__.py
+++ b/homeassistant/components/caldav/__init__.py
@@ -17,7 +17,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
-type CalDavConfigEntry = ConfigEntry[caldav.DAVClient]
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -25,8 +25,10 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.CALENDAR, Platform.TODO]
-async def async_setup_entry(hass: HomeAssistant, entry: CalDavConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up CalDAV from a config entry."""
+ hass.data.setdefault(DOMAIN, {})
+
client = caldav.DAVClient(
entry.data[CONF_URL],
username=entry.data[CONF_USERNAME],
@@ -48,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CalDavConfigEntry) -> bo
except DAVError as err:
raise ConfigEntryNotReady("CalDAV client error") from err
- entry.runtime_data = client
+ hass.data[DOMAIN][entry.entry_id] = client
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py
index fb53947a723..7591722b1ab 100644
--- a/homeassistant/components/caldav/calendar.py
+++ b/homeassistant/components/caldav/calendar.py
@@ -15,6 +15,7 @@ from homeassistant.components.calendar import (
CalendarEvent,
is_offset_reached,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
@@ -29,8 +30,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from . import CalDavConfigEntry
from .api import async_get_calendars
+from .const import DOMAIN
from .coordinator import CalDavUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -109,7 +110,6 @@ async def async_setup_platform(
entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
coordinator = CalDavUpdateCoordinator(
hass,
- None,
calendar=calendar,
days=days,
include_all_day=True,
@@ -127,7 +127,6 @@ async def async_setup_platform(
entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
coordinator = CalDavUpdateCoordinator(
hass,
- None,
calendar=calendar,
days=days,
include_all_day=False,
@@ -142,11 +141,12 @@ async def async_setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
- entry: CalDavConfigEntry,
+ entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the CalDav calendar platform for a config entry."""
- calendars = await async_get_calendars(hass, entry.runtime_data, SUPPORTED_COMPONENT)
+ client: caldav.DAVClient = hass.data[DOMAIN][entry.entry_id]
+ calendars = await async_get_calendars(hass, client, SUPPORTED_COMPONENT)
async_add_entities(
(
WebDavCalendarEntity(
@@ -154,7 +154,6 @@ async def async_setup_entry(
async_generate_entity_id(ENTITY_ID_FORMAT, calendar.name, hass=hass),
CalDavUpdateCoordinator(
hass,
- entry,
calendar=calendar,
days=CONFIG_ENTRY_DEFAULT_DAYS,
include_all_day=True,
@@ -207,8 +206,7 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
if self._supports_offset:
self._attr_extra_state_attributes = {
"offset_reached": is_offset_reached(
- self._event.start_datetime_local,
- self.coordinator.offset, # type: ignore[arg-type]
+ self._event.start_datetime_local, self.coordinator.offset
)
if self._event
else False
diff --git a/homeassistant/components/caldav/coordinator.py b/homeassistant/components/caldav/coordinator.py
index eb09e3f5452..3a10b567167 100644
--- a/homeassistant/components/caldav/coordinator.py
+++ b/homeassistant/components/caldav/coordinator.py
@@ -6,9 +6,6 @@ from datetime import date, datetime, time, timedelta
from functools import partial
import logging
import re
-from typing import TYPE_CHECKING
-
-import caldav
from homeassistant.components.calendar import CalendarEvent, extract_offset
from homeassistant.core import HomeAssistant
@@ -17,9 +14,6 @@ from homeassistant.util import dt as dt_util
from .api import get_attr_value
-if TYPE_CHECKING:
- from . import CalDavConfigEntry
-
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
@@ -29,20 +23,11 @@ OFFSET = "!!"
class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
"""Class to utilize the calendar dav client object to get next event."""
- def __init__(
- self,
- hass: HomeAssistant,
- entry: CalDavConfigEntry | None,
- calendar: caldav.Calendar,
- days: int,
- include_all_day: bool,
- search: str | None,
- ) -> None:
+ def __init__(self, hass, calendar, days, include_all_day, search):
"""Set up how we are going to search the WebDav calendar."""
super().__init__(
hass,
_LOGGER,
- config_entry=entry,
name=f"CalDAV {calendar.name}",
update_interval=MIN_TIME_BETWEEN_UPDATES,
)
@@ -50,7 +35,7 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
self.days = days
self.include_all_day = include_all_day
self.search = search
- self.offset: timedelta | None = None
+ self.offset = None
async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
@@ -124,7 +109,7 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
_start_of_tomorrow = start_of_tomorrow
if _start_of_today <= start_dt < _start_of_tomorrow:
new_event = event.copy()
- new_vevent = new_event.instance.vevent # type: ignore[attr-defined]
+ new_vevent = new_event.instance.vevent
if hasattr(new_vevent, "dtend"):
dur = new_vevent.dtend.value - new_vevent.dtstart.value
new_vevent.dtend.value = start_dt + dur
diff --git a/homeassistant/components/caldav/todo.py b/homeassistant/components/caldav/todo.py
index cbd7963b595..e8cd4fc9334 100644
--- a/homeassistant/components/caldav/todo.py
+++ b/homeassistant/components/caldav/todo.py
@@ -18,13 +18,14 @@ from homeassistant.components.todo import (
TodoListEntity,
TodoListEntityFeature,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
-from . import CalDavConfigEntry
from .api import async_get_calendars, get_attr_value
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -45,11 +46,12 @@ TODO_STATUS_MAP_INV: dict[TodoItemStatus, str] = {
async def async_setup_entry(
hass: HomeAssistant,
- entry: CalDavConfigEntry,
+ entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the CalDav todo platform for a config entry."""
- calendars = await async_get_calendars(hass, entry.runtime_data, SUPPORTED_COMPONENT)
+ client: caldav.DAVClient = hass.data[DOMAIN][entry.entry_id]
+ calendars = await async_get_calendars(hass, client, SUPPORTED_COMPONENT)
async_add_entities(
(
WebDavTodoListEntity(
diff --git a/homeassistant/components/calendar/icons.json b/homeassistant/components/calendar/icons.json
index a28adcf317e..9b8df3ec6d3 100644
--- a/homeassistant/components/calendar/icons.json
+++ b/homeassistant/components/calendar/icons.json
@@ -14,6 +14,9 @@
},
"get_events": {
"service": "mdi:calendar-month"
+ },
+ "list_events": {
+ "service": "mdi:calendar-month"
}
}
}
diff --git a/homeassistant/components/calendar/services.yaml b/homeassistant/components/calendar/services.yaml
index 9701293c0be..2e926fbdeed 100644
--- a/homeassistant/components/calendar/services.yaml
+++ b/homeassistant/components/calendar/services.yaml
@@ -36,6 +36,22 @@ create_event:
example: "Conference Room - F123, Bldg. 002"
selector:
text:
+list_events:
+ target:
+ entity:
+ domain: calendar
+ fields:
+ start_date_time:
+ example: "2022-03-22 20:00:00"
+ selector:
+ datetime:
+ end_date_time:
+ example: "2022-03-22 22:00:00"
+ selector:
+ datetime:
+ duration:
+ selector:
+ duration:
get_events:
target:
entity:
diff --git a/homeassistant/components/calendar/strings.json b/homeassistant/components/calendar/strings.json
index 76e6c42b666..5b76a33f7c3 100644
--- a/homeassistant/components/calendar/strings.json
+++ b/homeassistant/components/calendar/strings.json
@@ -89,6 +89,24 @@
"description": "Returns active events from start_date_time until the specified duration."
}
}
+ },
+ "list_events": {
+ "name": "List event",
+ "description": "Lists events on a calendar within a time range.",
+ "fields": {
+ "start_date_time": {
+ "name": "[%key:component::calendar::services::get_events::fields::start_date_time::name%]",
+ "description": "[%key:component::calendar::services::get_events::fields::start_date_time::description%]"
+ },
+ "end_date_time": {
+ "name": "[%key:component::calendar::services::get_events::fields::end_date_time::name%]",
+ "description": "[%key:component::calendar::services::get_events::fields::end_date_time::description%]"
+ },
+ "duration": {
+ "name": "[%key:component::calendar::services::get_events::fields::duration::name%]",
+ "description": "[%key:component::calendar::services::get_events::fields::duration::description%]"
+ }
+ }
}
},
"issues": {
diff --git a/homeassistant/components/cambridge_audio/__init__.py b/homeassistant/components/cambridge_audio/__init__.py
index a584f0db6c1..5060d12cfe1 100644
--- a/homeassistant/components/cambridge_audio/__init__.py
+++ b/homeassistant/components/cambridge_audio/__init__.py
@@ -13,9 +13,9 @@ from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
-from .const import CONNECT_TIMEOUT, DOMAIN, STREAM_MAGIC_EXCEPTIONS
+from .const import CONNECT_TIMEOUT, STREAM_MAGIC_EXCEPTIONS
-PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.SELECT, Platform.SWITCH]
+PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER]
_LOGGER = logging.getLogger(__name__)
@@ -45,13 +45,7 @@ async def async_setup_entry(
async with asyncio.timeout(CONNECT_TIMEOUT):
await client.connect()
except STREAM_MAGIC_EXCEPTIONS as err:
- raise ConfigEntryNotReady(
- translation_domain=DOMAIN,
- translation_key="entry_cannot_connect",
- translation_placeholders={
- "host": client.host,
- },
- ) from err
+ raise ConfigEntryNotReady(f"Error while connecting to {client.host}") from err
entry.runtime_data = client
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
diff --git a/homeassistant/components/cambridge_audio/const.py b/homeassistant/components/cambridge_audio/const.py
index eae417ffe39..5a4e5a1f2e0 100644
--- a/homeassistant/components/cambridge_audio/const.py
+++ b/homeassistant/components/cambridge_audio/const.py
@@ -17,7 +17,3 @@ STREAM_MAGIC_EXCEPTIONS = (
)
CONNECT_TIMEOUT = 5
-
-CAMBRIDGE_MEDIA_TYPE_PRESET = "preset"
-CAMBRIDGE_MEDIA_TYPE_AIRABLE = "airable"
-CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO = "internet_radio"
diff --git a/homeassistant/components/cambridge_audio/diagnostics.py b/homeassistant/components/cambridge_audio/diagnostics.py
index a670b1f32eb..b4295e7c885 100644
--- a/homeassistant/components/cambridge_audio/diagnostics.py
+++ b/homeassistant/components/cambridge_audio/diagnostics.py
@@ -2,22 +2,20 @@
from typing import Any
+from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
+from homeassistant.helpers.redact import async_redact_data
from . import CambridgeAudioConfigEntry
+TO_REDACT = {CONF_HOST}
+
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: CambridgeAudioConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for the provided config entry."""
client = entry.runtime_data
- return {
- "display": client.display.to_dict(),
- "info": client.info.to_dict(),
- "now_playing": client.now_playing.to_dict(),
- "play_state": client.play_state.to_dict(),
- "presets_list": client.preset_list.to_dict(),
- "sources": [s.to_dict() for s in client.sources],
- "update": client.update.to_dict(),
- }
+ return async_redact_data(
+ {"info": client.info, "sources": client.sources}, TO_REDACT
+ )
diff --git a/homeassistant/components/cambridge_audio/entity.py b/homeassistant/components/cambridge_audio/entity.py
index de7a3e31765..ac43a673725 100644
--- a/homeassistant/components/cambridge_audio/entity.py
+++ b/homeassistant/components/cambridge_audio/entity.py
@@ -26,12 +26,7 @@ def command[_EntityT: CambridgeAudioEntity, **_P](
await func(self, *args, **kwargs)
except STREAM_MAGIC_EXCEPTIONS as exc:
raise HomeAssistantError(
- translation_domain=DOMAIN,
- translation_key="command_error",
- translation_placeholders={
- "function_name": func.__name__,
- "entity_id": self.entity_id,
- },
+ f"Error executing {func.__name__} on entity {self.entity_id},"
) from exc
return decorator
@@ -67,4 +62,4 @@ class CambridgeAudioEntity(Entity):
async def async_will_remove_from_hass(self) -> None:
"""Remove callbacks."""
- self.client.unregister_state_update_callbacks(self._state_update_callback)
+ await self.client.unregister_state_update_callbacks(self._state_update_callback)
diff --git a/homeassistant/components/cambridge_audio/icons.json b/homeassistant/components/cambridge_audio/icons.json
deleted file mode 100644
index b4346a7fe8e..00000000000
--- a/homeassistant/components/cambridge_audio/icons.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "entity": {
- "select": {
- "display_brightness": {
- "default": "mdi:brightness-7",
- "state": {
- "bright": "mdi:brightness-7",
- "dim": "mdi:brightness-6",
- "off": "mdi:brightness-3"
- }
- },
- "audio_output": {
- "default": "mdi:audio-input-stereo-minijack"
- }
- },
- "switch": {
- "pre_amp": {
- "default": "mdi:volume-high",
- "state": {
- "off": "mdi:volume-low"
- }
- },
- "early_update": {
- "default": "mdi:update"
- }
- }
- }
-}
diff --git a/homeassistant/components/cambridge_audio/manifest.json b/homeassistant/components/cambridge_audio/manifest.json
index c359ca14a21..232e3d8e2aa 100644
--- a/homeassistant/components/cambridge_audio/manifest.json
+++ b/homeassistant/components/cambridge_audio/manifest.json
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["aiostreammagic"],
- "requirements": ["aiostreammagic==2.8.5"],
+ "requirements": ["aiostreammagic==2.5.0"],
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
}
diff --git a/homeassistant/components/cambridge_audio/media_player.py b/homeassistant/components/cambridge_audio/media_player.py
index 5e340cdd21e..1c490cd6ac9 100644
--- a/homeassistant/components/cambridge_audio/media_player.py
+++ b/homeassistant/components/cambridge_audio/media_player.py
@@ -3,7 +3,6 @@
from __future__ import annotations
from datetime import datetime
-from typing import Any
from aiostreammagic import (
RepeatMode as CambridgeRepeatMode,
@@ -22,22 +21,14 @@ from homeassistant.components.media_player import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from .const import (
- CAMBRIDGE_MEDIA_TYPE_AIRABLE,
- CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO,
- CAMBRIDGE_MEDIA_TYPE_PRESET,
- DOMAIN,
-)
from .entity import CambridgeAudioEntity, command
BASE_FEATURES = (
MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.TURN_ON
- | MediaPlayerEntityFeature.PLAY_MEDIA
)
PREAMP_FEATURES = (
@@ -177,9 +168,12 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
return volume / 100
@property
- def shuffle(self) -> bool:
+ def shuffle(self) -> bool | None:
"""Current shuffle configuration."""
- return self.client.play_state.mode_shuffle != ShuffleMode.OFF
+ mode_shuffle = self.client.play_state.mode_shuffle
+ if not mode_shuffle:
+ return False
+ return mode_shuffle != ShuffleMode.OFF
@property
def repeat(self) -> RepeatMode | None:
@@ -291,48 +285,3 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
if repeat in {RepeatMode.ALL, RepeatMode.ONE}:
repeat_mode = CambridgeRepeatMode.ALL
await self.client.set_repeat(repeat_mode)
-
- @command
- async def async_play_media(
- self, media_type: MediaType | str, media_id: str, **kwargs: Any
- ) -> None:
- """Play media on the Cambridge Audio device."""
-
- if media_type not in {
- CAMBRIDGE_MEDIA_TYPE_PRESET,
- CAMBRIDGE_MEDIA_TYPE_AIRABLE,
- CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO,
- }:
- raise HomeAssistantError(
- translation_domain=DOMAIN,
- translation_key="unsupported_media_type",
- translation_placeholders={"media_type": media_type},
- )
-
- if media_type == CAMBRIDGE_MEDIA_TYPE_PRESET:
- try:
- preset_id = int(media_id)
- except ValueError as ve:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="preset_non_integer",
- translation_placeholders={"preset_id": media_id},
- ) from ve
- preset = None
- for _preset in self.client.preset_list.presets:
- if _preset.preset_id == preset_id:
- preset = _preset
- if not preset:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="missing_preset",
- translation_placeholders={"preset_id": media_id},
- )
- await self.client.recall_preset(preset.preset_id)
-
- if media_type == CAMBRIDGE_MEDIA_TYPE_AIRABLE:
- preset_id = int(media_id)
- await self.client.play_radio_airable("Radio", preset_id)
-
- if media_type == CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO:
- await self.client.play_radio_url("Radio", media_id)
diff --git a/homeassistant/components/cambridge_audio/select.py b/homeassistant/components/cambridge_audio/select.py
deleted file mode 100644
index c99abc853e5..00000000000
--- a/homeassistant/components/cambridge_audio/select.py
+++ /dev/null
@@ -1,121 +0,0 @@
-"""Support for Cambridge Audio select entities."""
-
-from collections.abc import Awaitable, Callable
-from dataclasses import dataclass, field
-
-from aiostreammagic import StreamMagicClient
-from aiostreammagic.models import DisplayBrightness
-
-from homeassistant.components.select import SelectEntity, SelectEntityDescription
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import EntityCategory
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from .entity import CambridgeAudioEntity
-
-
-@dataclass(frozen=True, kw_only=True)
-class CambridgeAudioSelectEntityDescription(SelectEntityDescription):
- """Describes Cambridge Audio select entity."""
-
- options_fn: Callable[[StreamMagicClient], list[str]] = field(default=lambda _: [])
- load_fn: Callable[[StreamMagicClient], bool] = field(default=lambda _: True)
- value_fn: Callable[[StreamMagicClient], str | None]
- set_value_fn: Callable[[StreamMagicClient, str], Awaitable[None]]
-
-
-async def _audio_output_set_value_fn(client: StreamMagicClient, value: str) -> None:
- """Set the audio output using the display name."""
- audio_output_id = next(
- (output.id for output in client.audio_output.outputs if value == output.name),
- None,
- )
- assert audio_output_id is not None
- await client.set_audio_output(audio_output_id)
-
-
-def _audio_output_value_fn(client: StreamMagicClient) -> str | None:
- """Convert the current audio output id to name."""
- return next(
- (
- output.name
- for output in client.audio_output.outputs
- if client.state.audio_output == output.id
- ),
- None,
- )
-
-
-CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = (
- CambridgeAudioSelectEntityDescription(
- key="display_brightness",
- translation_key="display_brightness",
- options=[
- DisplayBrightness.BRIGHT.value,
- DisplayBrightness.DIM.value,
- DisplayBrightness.OFF.value,
- ],
- entity_category=EntityCategory.CONFIG,
- load_fn=lambda client: client.display.brightness != DisplayBrightness.NONE,
- value_fn=lambda client: client.display.brightness,
- set_value_fn=lambda client, value: client.set_display_brightness(
- DisplayBrightness(value)
- ),
- ),
- CambridgeAudioSelectEntityDescription(
- key="audio_output",
- translation_key="audio_output",
- entity_category=EntityCategory.CONFIG,
- options_fn=lambda client: [
- output.name for output in client.audio_output.outputs
- ],
- load_fn=lambda client: len(client.audio_output.outputs) > 0,
- value_fn=_audio_output_value_fn,
- set_value_fn=_audio_output_set_value_fn,
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up Cambridge Audio select entities based on a config entry."""
-
- client: StreamMagicClient = entry.runtime_data
- entities: list[CambridgeAudioSelect] = [
- CambridgeAudioSelect(client, description)
- for description in CONTROL_ENTITIES
- if description.load_fn(client)
- ]
- async_add_entities(entities)
-
-
-class CambridgeAudioSelect(CambridgeAudioEntity, SelectEntity):
- """Defines a Cambridge Audio select entity."""
-
- entity_description: CambridgeAudioSelectEntityDescription
-
- def __init__(
- self,
- client: StreamMagicClient,
- description: CambridgeAudioSelectEntityDescription,
- ) -> None:
- """Initialize Cambridge Audio select."""
- super().__init__(client)
- self.entity_description = description
- self._attr_unique_id = f"{client.info.unit_id}-{description.key}"
- options_fn = description.options_fn(client)
- if options_fn:
- self._attr_options = options_fn
-
- @property
- def current_option(self) -> str | None:
- """Return the state of the select."""
- return self.entity_description.value_fn(self.client)
-
- async def async_select_option(self, option: str) -> None:
- """Change the selected option."""
- await self.entity_description.set_value_fn(self.client, option)
diff --git a/homeassistant/components/cambridge_audio/strings.json b/homeassistant/components/cambridge_audio/strings.json
index c368ba060a7..fa27dc452de 100644
--- a/homeassistant/components/cambridge_audio/strings.json
+++ b/homeassistant/components/cambridge_audio/strings.json
@@ -22,45 +22,5 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
- },
- "entity": {
- "select": {
- "display_brightness": {
- "name": "Display brightness",
- "state": {
- "bright": "Bright",
- "dim": "Dim",
- "off": "[%key:common::state::off%]"
- }
- },
- "audio_output": {
- "name": "Audio output"
- }
- },
- "switch": {
- "pre_amp": {
- "name": "Pre-Amp"
- },
- "early_update": {
- "name": "Early update"
- }
- }
- },
- "exceptions": {
- "unsupported_media_type": {
- "message": "Unsupported media type for Cambridge Audio device: {media_type}"
- },
- "missing_preset": {
- "message": "Missing preset for media_id: {preset_id}"
- },
- "preset_non_integer": {
- "message": "Preset must be an integer, got: {preset_id}"
- },
- "entry_cannot_connect": {
- "message": "Error while connecting to {host}"
- },
- "command_error": {
- "message": "Error executing {function_name} on entity {entity_id}"
- }
}
}
diff --git a/homeassistant/components/cambridge_audio/switch.py b/homeassistant/components/cambridge_audio/switch.py
deleted file mode 100644
index 3209b275d46..00000000000
--- a/homeassistant/components/cambridge_audio/switch.py
+++ /dev/null
@@ -1,82 +0,0 @@
-"""Support for Cambridge Audio switch entities."""
-
-from collections.abc import Awaitable, Callable
-from dataclasses import dataclass
-from typing import Any
-
-from aiostreammagic import StreamMagicClient
-
-from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import EntityCategory
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from .entity import CambridgeAudioEntity
-
-
-@dataclass(frozen=True, kw_only=True)
-class CambridgeAudioSwitchEntityDescription(SwitchEntityDescription):
- """Describes Cambridge Audio switch entity."""
-
- value_fn: Callable[[StreamMagicClient], bool]
- set_value_fn: Callable[[StreamMagicClient, bool], Awaitable[None]]
-
-
-CONTROL_ENTITIES: tuple[CambridgeAudioSwitchEntityDescription, ...] = (
- CambridgeAudioSwitchEntityDescription(
- key="pre_amp",
- translation_key="pre_amp",
- entity_category=EntityCategory.CONFIG,
- value_fn=lambda client: client.state.pre_amp_mode,
- set_value_fn=lambda client, value: client.set_pre_amp_mode(value),
- ),
- CambridgeAudioSwitchEntityDescription(
- key="early_update",
- translation_key="early_update",
- entity_category=EntityCategory.CONFIG,
- value_fn=lambda client: client.update.early_update,
- set_value_fn=lambda client, value: client.set_early_update(value),
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up Cambridge Audio switch entities based on a config entry."""
- async_add_entities(
- CambridgeAudioSwitch(entry.runtime_data, description)
- for description in CONTROL_ENTITIES
- )
-
-
-class CambridgeAudioSwitch(CambridgeAudioEntity, SwitchEntity):
- """Defines a Cambridge Audio switch entity."""
-
- entity_description: CambridgeAudioSwitchEntityDescription
-
- def __init__(
- self,
- client: StreamMagicClient,
- description: CambridgeAudioSwitchEntityDescription,
- ) -> None:
- """Initialize Cambridge Audio switch."""
- super().__init__(client)
- self.entity_description = description
- self._attr_unique_id = f"{client.info.unit_id}-{description.key}"
-
- @property
- def is_on(self) -> bool:
- """Return the state of the switch."""
- return self.entity_description.value_fn(self.client)
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn the switch on."""
- await self.entity_description.set_value_fn(self.client, True)
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn the switch off."""
- await self.entity_description.set_value_fn(self.client, False)
diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py
index d31d21d424c..1f1ac881b26 100644
--- a/homeassistant/components/camera/__init__.py
+++ b/homeassistant/components/camera/__init__.py
@@ -4,9 +4,9 @@ from __future__ import annotations
import asyncio
import collections
-from collections.abc import Awaitable, Callable, Coroutine
+from collections.abc import Awaitable, Callable
from contextlib import suppress
-from dataclasses import asdict, dataclass
+from dataclasses import asdict
from datetime import datetime, timedelta
from enum import IntFlag
from functools import partial
@@ -18,9 +18,8 @@ from typing import Any, Final, final
from aiohttp import hdrs, web
import attr
-from propcache import cached_property, under_cached_property
+from propcache import cached_property
import voluptuous as vol
-from webrtc_models import RTCIceCandidate, RTCIceServer
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
@@ -50,7 +49,7 @@ from homeassistant.const import (
)
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import config_validation as cv, issue_registry as ir
+from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.deprecation import (
DeprecatedConstantEnum,
all_with_deprecated_constants,
@@ -86,20 +85,13 @@ from .img_util import scale_jpeg_camera_image
from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
from .webrtc import (
DATA_ICE_SERVERS,
- CameraWebRTCLegacyProvider,
CameraWebRTCProvider,
- WebRTCAnswer,
- WebRTCCandidate, # noqa: F401
+ RTCIceServer,
WebRTCClientConfiguration,
- WebRTCError,
- WebRTCMessage, # noqa: F401
- WebRTCSendMessage,
- async_get_supported_legacy_provider,
- async_get_supported_provider,
- async_register_ice_servers,
+ async_get_supported_providers,
async_register_rtsp_to_web_rtc_provider, # noqa: F401
- async_register_webrtc_provider, # noqa: F401
- async_register_ws,
+ register_ice_server,
+ ws_get_client_config,
)
_LOGGER = logging.getLogger(__name__)
@@ -177,13 +169,6 @@ class Image:
content: bytes = attr.ib()
-@dataclass(frozen=True)
-class CameraCapabilities:
- """Camera capabilities."""
-
- frontend_stream_types: set[StreamType]
-
-
@bind_hass
async def async_request_stream(hass: HomeAssistant, entity_id: str, fmt: str) -> str:
"""Request a stream for a camera entity."""
@@ -357,10 +342,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.http.register_view(CameraMjpegStream(component))
websocket_api.async_register_command(hass, ws_camera_stream)
+ websocket_api.async_register_command(hass, ws_camera_web_rtc_offer)
websocket_api.async_register_command(hass, websocket_get_prefs)
websocket_api.async_register_command(hass, websocket_update_prefs)
- websocket_api.async_register_command(hass, ws_camera_capabilities)
- async_register_ws(hass)
+ websocket_api.async_register_command(hass, ws_get_client_config)
await component.async_setup(config)
@@ -416,20 +401,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
SERVICE_RECORD, CAMERA_SERVICE_RECORD, async_handle_record_service
)
- @callback
- def get_ice_servers() -> list[RTCIceServer]:
- if hass.config.webrtc.ice_servers:
- return hass.config.webrtc.ice_servers
- return [
- RTCIceServer(
- urls=[
- "stun:stun.home-assistant.io:80",
- "stun:stun.home-assistant.io:3478",
- ]
- ),
- ]
+ async def get_ice_server() -> RTCIceServer:
+ return RTCIceServer(urls="stun:stun.home-assistant.io:3478")
- async_register_ice_servers(hass, get_ice_servers)
+ register_ice_server(hass, get_ice_server)
return True
@@ -476,11 +451,8 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_attr_state: None = None # State is determined by is_on
_attr_supported_features: CameraEntityFeature = CameraEntityFeature(0)
- __supports_stream: CameraEntityFeature | None = None
-
def __init__(self) -> None:
"""Initialize a camera."""
- self._cache: dict[str, Any] = {}
self.stream: Stream | None = None
self.stream_options: dict[str, str | bool | float] = {}
self.content_type: str = DEFAULT_CONTENT_TYPE
@@ -488,15 +460,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self._warned_old_signature = False
self.async_update_token()
self._create_stream_lock: asyncio.Lock | None = None
- self._webrtc_provider: CameraWebRTCProvider | None = None
- self._legacy_webrtc_provider: CameraWebRTCLegacyProvider | None = None
- self._supports_native_sync_webrtc = (
- type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
- )
- self._supports_native_async_webrtc = (
- type(self).async_handle_async_webrtc_offer
- != Camera.async_handle_async_webrtc_offer
- )
+ self._webrtc_providers: list[CameraWebRTCProvider] = []
@cached_property
def entity_picture(self) -> str:
@@ -570,7 +534,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return self._attr_frontend_stream_type
if CameraEntityFeature.STREAM not in self.supported_features_compat:
return None
- if self._webrtc_provider or self._legacy_webrtc_provider:
+ if self._webrtc_providers:
return StreamType.WEB_RTC
return StreamType.HLS
@@ -620,66 +584,12 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
Integrations can override with a native WebRTC implementation.
"""
-
- async def async_handle_async_webrtc_offer(
- self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
- ) -> None:
- """Handle the async WebRTC offer.
-
- Async means that it could take some time to process the offer and responses/message
- will be sent with the send_message callback.
- This method is used by cameras with CameraEntityFeature.STREAM and StreamType.WEB_RTC.
- An integration overriding this method must also implement async_on_webrtc_candidate.
-
- Integrations can override with a native WebRTC implementation.
- """
- if self._supports_native_sync_webrtc:
- try:
- answer = await self.async_handle_web_rtc_offer(offer_sdp)
- except ValueError as ex:
- _LOGGER.error("Error handling WebRTC offer: %s", ex)
- send_message(
- WebRTCError(
- "webrtc_offer_failed",
- str(ex),
- )
- )
- except TimeoutError:
- # This catch was already here and should stay through the deprecation
- _LOGGER.error("Timeout handling WebRTC offer")
- send_message(
- WebRTCError(
- "webrtc_offer_failed",
- "Timeout handling WebRTC offer",
- )
- )
- else:
- if answer:
- send_message(WebRTCAnswer(answer))
- else:
- _LOGGER.error("Error handling WebRTC offer: No answer")
- send_message(
- WebRTCError(
- "webrtc_offer_failed",
- "No answer on WebRTC offer",
- )
- )
- return
-
- if self._webrtc_provider:
- await self._webrtc_provider.async_handle_async_webrtc_offer(
- self, offer_sdp, session_id, send_message
- )
- return
-
- if self._legacy_webrtc_provider and (
- answer := await self._legacy_webrtc_provider.async_handle_web_rtc_offer(
- self, offer_sdp
- )
- ):
- send_message(WebRTCAnswer(answer))
- else:
- raise HomeAssistantError("Camera does not support WebRTC")
+ for provider in self._webrtc_providers:
+ if answer := await provider.async_handle_web_rtc_offer(self, offer_sdp):
+ return answer
+ raise HomeAssistantError(
+ "WebRTC offer was not accepted by the supported providers"
+ )
def camera_image(
self, width: int | None = None, height: int | None = None
@@ -789,133 +699,55 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
async def async_internal_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_internal_added_to_hass()
- self.__supports_stream = (
- self.supported_features_compat & CameraEntityFeature.STREAM
- )
- await self.async_refresh_providers(write_state=False)
+ # Avoid calling async_refresh_providers() in here because it
+ # it will write state a second time since state is always
+ # written when an entity is added to hass.
+ self._webrtc_providers = await self._async_get_supported_webrtc_providers()
- async def async_refresh_providers(self, *, write_state: bool = True) -> None:
+ async def async_refresh_providers(self) -> None:
"""Determine if any of the registered providers are suitable for this entity.
This affects state attributes, so it should be invoked any time the registered
providers or inputs to the state attributes change.
+
+ Returns True if any state was updated (and needs to be written)
"""
- old_provider = self._webrtc_provider
- old_legacy_provider = self._legacy_webrtc_provider
- new_provider = None
- new_legacy_provider = None
+ old_providers = self._webrtc_providers
+ new_providers = await self._async_get_supported_webrtc_providers()
+ self._webrtc_providers = new_providers
+ if old_providers != new_providers:
+ self.async_write_ha_state()
- # Skip all providers if the camera has a native WebRTC implementation
- if not (
- self._supports_native_sync_webrtc or self._supports_native_async_webrtc
- ):
- # Camera doesn't have a native WebRTC implementation
- new_provider = await self._async_get_supported_webrtc_provider(
- async_get_supported_provider
- )
-
- if new_provider is None:
- # Only add the legacy provider if the new provider is not available
- new_legacy_provider = await self._async_get_supported_webrtc_provider(
- async_get_supported_legacy_provider
- )
-
- if old_provider != new_provider or old_legacy_provider != new_legacy_provider:
- self._webrtc_provider = new_provider
- self._legacy_webrtc_provider = new_legacy_provider
- self._invalidate_camera_capabilities_cache()
- if write_state:
- self.async_write_ha_state()
-
- async def _async_get_supported_webrtc_provider[_T](
- self, fn: Callable[[HomeAssistant, Camera], Coroutine[None, None, _T | None]]
- ) -> _T | None:
- """Get first provider that supports this camera."""
+ async def _async_get_supported_webrtc_providers(
+ self,
+ ) -> list[CameraWebRTCProvider]:
+ """Get the all providers that supports this camera."""
if CameraEntityFeature.STREAM not in self.supported_features_compat:
- return None
+ return []
- return await fn(self.hass, self)
+ return await async_get_supported_providers(self.hass, self)
- @callback
- def _async_get_webrtc_client_configuration(self) -> WebRTCClientConfiguration:
+ @property
+ def webrtc_providers(self) -> list[CameraWebRTCProvider]:
+ """Return the WebRTC providers."""
+ return self._webrtc_providers
+
+ async def _async_get_webrtc_client_configuration(self) -> WebRTCClientConfiguration:
"""Return the WebRTC client configuration adjustable per integration."""
return WebRTCClientConfiguration()
@final
- @callback
- def async_get_webrtc_client_configuration(self) -> WebRTCClientConfiguration:
+ async def async_get_webrtc_client_configuration(self) -> WebRTCClientConfiguration:
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
- config = self._async_get_webrtc_client_configuration()
+ config = await self._async_get_webrtc_client_configuration()
- if not self._supports_native_sync_webrtc:
- # Until 2024.11, the frontend was not resolving any ice servers
- # The async approach was added 2024.11 and new integrations need to use it
- ice_servers = [
- server
- for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
- for server in servers()
- ]
- config.configuration.ice_servers.extend(ice_servers)
-
- config.get_candidates_upfront = (
- self._supports_native_sync_webrtc
- or self._legacy_webrtc_provider is not None
+ ice_servers = await asyncio.gather(
+ *[server() for server in self.hass.data.get(DATA_ICE_SERVERS, [])]
)
+ config.configuration.ice_servers.extend(ice_servers)
return config
- async def async_on_webrtc_candidate(
- self, session_id: str, candidate: RTCIceCandidate
- ) -> None:
- """Handle a WebRTC candidate."""
- if self._webrtc_provider:
- await self._webrtc_provider.async_on_webrtc_candidate(session_id, candidate)
- else:
- raise HomeAssistantError("Cannot handle WebRTC candidate")
-
- @callback
- def close_webrtc_session(self, session_id: str) -> None:
- """Close a WebRTC session."""
- if self._webrtc_provider:
- self._webrtc_provider.async_close_session(session_id)
-
- @callback
- def _invalidate_camera_capabilities_cache(self) -> None:
- """Invalidate the camera capabilities cache."""
- self._cache.pop("camera_capabilities", None)
-
- @final
- @under_cached_property
- def camera_capabilities(self) -> CameraCapabilities:
- """Return the camera capabilities."""
- frontend_stream_types = set()
- if CameraEntityFeature.STREAM in self.supported_features_compat:
- if self._supports_native_sync_webrtc or self._supports_native_async_webrtc:
- # The camera has a native WebRTC implementation
- frontend_stream_types.add(StreamType.WEB_RTC)
- else:
- frontend_stream_types.add(StreamType.HLS)
-
- if self._webrtc_provider:
- frontend_stream_types.add(StreamType.WEB_RTC)
-
- return CameraCapabilities(frontend_stream_types)
-
- @callback
- def async_write_ha_state(self) -> None:
- """Write the state to the state machine.
-
- Schedules async_refresh_providers if support of streams have changed.
- """
- super().async_write_ha_state()
- if self.__supports_stream != (
- supports_stream := self.supported_features_compat
- & CameraEntityFeature.STREAM
- ):
- self.__supports_stream = supports_stream
- self._invalidate_camera_capabilities_cache()
- self.hass.async_create_task(self.async_refresh_providers())
-
class CameraView(HomeAssistantView):
"""Base CameraView."""
@@ -1006,24 +838,6 @@ class CameraMjpegStream(CameraView):
raise web.HTTPBadRequest from err
-@websocket_api.websocket_command(
- {
- vol.Required("type"): "camera/capabilities",
- vol.Required("entity_id"): cv.entity_id,
- }
-)
-@websocket_api.async_response
-async def ws_camera_capabilities(
- hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
-) -> None:
- """Handle get camera capabilities websocket command.
-
- Async friendly.
- """
- camera = get_camera_from_entity_id(hass, msg["entity_id"])
- connection.send_result(msg["id"], asdict(camera.camera_capabilities))
-
-
@websocket_api.websocket_command(
{
vol.Required("type"): "camera/stream",
@@ -1054,6 +868,53 @@ async def ws_camera_stream(
)
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "camera/web_rtc_offer",
+ vol.Required("entity_id"): cv.entity_id,
+ vol.Required("offer"): str,
+ }
+)
+@websocket_api.async_response
+async def ws_camera_web_rtc_offer(
+ hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
+) -> None:
+ """Handle the signal path for a WebRTC stream.
+
+ This signal path is used to route the offer created by the client to the
+ camera device through the integration for negotiation on initial setup,
+ which returns an answer. The actual streaming is handled entirely between
+ the client and camera device.
+
+ Async friendly.
+ """
+ entity_id = msg["entity_id"]
+ offer = msg["offer"]
+ camera = get_camera_from_entity_id(hass, entity_id)
+ if camera.frontend_stream_type != StreamType.WEB_RTC:
+ connection.send_error(
+ msg["id"],
+ "web_rtc_offer_failed",
+ (
+ "Camera does not support WebRTC,"
+ f" frontend_stream_type={camera.frontend_stream_type}"
+ ),
+ )
+ return
+ try:
+ answer = await camera.async_handle_web_rtc_offer(offer)
+ except (HomeAssistantError, ValueError) as ex:
+ _LOGGER.error("Error handling WebRTC offer: %s", ex)
+ connection.send_error(msg["id"], "web_rtc_offer_failed", str(ex))
+ except TimeoutError:
+ _LOGGER.error("Timeout handling WebRTC offer")
+ connection.send_error(
+ msg["id"], "web_rtc_offer_failed", "Timeout handling WebRTC offer"
+ )
+ else:
+ connection.send_result(msg["id"], {"answer": answer})
+
+
@websocket_api.websocket_command(
{vol.Required("type"): "camera/get_prefs", vol.Required("entity_id"): cv.entity_id}
)
@@ -1096,46 +957,6 @@ async def websocket_update_prefs(
connection.send_result(msg["id"], entity_prefs)
-class _TemplateCameraEntity:
- """Class to warn when the `entity_id` template variable is accessed.
-
- Can be removed in HA Core 2025.6.
- """
-
- def __init__(self, camera: Camera, service: str) -> None:
- """Initialize."""
- self._camera = camera
- self._entity_id = camera.entity_id
- self._hass = camera.hass
- self._service = service
-
- def _report_issue(self) -> None:
- """Create a repair issue."""
- ir.async_create_issue(
- self._hass,
- DOMAIN,
- f"deprecated_filename_template_{self._entity_id}_{self._service}",
- breaks_in_ha_version="2025.6.0",
- is_fixable=True,
- severity=ir.IssueSeverity.WARNING,
- translation_key="deprecated_filename_template",
- translation_placeholders={
- "entity_id": self._entity_id,
- "service": f"{DOMAIN}.{self._service}",
- },
- )
-
- def __getattr__(self, name: str) -> Any:
- """Forward to the camera entity."""
- self._report_issue()
- return getattr(self._camera, name)
-
- def __str__(self) -> str:
- """Forward to the camera entity."""
- self._report_issue()
- return str(self._camera)
-
-
async def async_handle_snapshot_service(
camera: Camera, service_call: ServiceCall
) -> None:
@@ -1143,9 +964,7 @@ async def async_handle_snapshot_service(
hass = camera.hass
filename: Template = service_call.data[ATTR_FILENAME]
- snapshot_file = filename.async_render(
- variables={ATTR_ENTITY_ID: _TemplateCameraEntity(camera, SERVICE_SNAPSHOT)}
- )
+ snapshot_file = filename.async_render(variables={ATTR_ENTITY_ID: camera})
# check if we allow to access to that file
if not hass.config.is_allowed_path(snapshot_file):
@@ -1221,9 +1040,7 @@ async def async_handle_record_service(
raise HomeAssistantError(f"{camera.entity_id} does not support record service")
filename = service_call.data[CONF_FILENAME]
- video_path = filename.async_render(
- variables={ATTR_ENTITY_ID: _TemplateCameraEntity(camera, SERVICE_RECORD)}
- )
+ video_path = filename.async_render(variables={ATTR_ENTITY_ID: camera})
await stream.async_record(
video_path,
diff --git a/homeassistant/components/camera/strings.json b/homeassistant/components/camera/strings.json
index 4a7e9aafc6e..90b053ec087 100644
--- a/homeassistant/components/camera/strings.json
+++ b/homeassistant/components/camera/strings.json
@@ -35,23 +35,6 @@
}
}
},
- "issues": {
- "deprecated_filename_template": {
- "title": "Detected use of deprecated template variable",
- "fix_flow": {
- "step": {
- "confirm": {
- "title": "[%key:component::camera::issues::deprecated_filename_template::title%]",
- "description": "The pre-defined template variable `entity_id` was used when performing action `{service}` targeting camera entity `{entity_id}`. The pre-defined template variable `entity_id` is being removed from the `filename` parameter of `{service}`.\n\nPlease update your automations and scripts to use a manually defined variable instead and select **Submit** to close this issue."
- }
- }
- }
- },
- "legacy_webrtc_provider": {
- "title": "Detected use of legacy WebRTC provider registered by {legacy_integration}",
- "description": "The {legacy_integration} integration has registered a legacy WebRTC provider. Home Assistant prefers using the built-in modern WebRTC provider registered by the {builtin_integration} integration.\n\nBenefits of the built-in integration are:\n\n- The camera stream is started faster.\n- More camera devices are supported.\n\nTo fix this issue, you can either keep using the built-in modern WebRTC provider and remove the {legacy_integration} integration or remove the {builtin_integration} integration to use the legacy provider, and then restart Home Assistant."
- }
- },
"services": {
"turn_off": {
"name": "[%key:common::action::turn_off%]",
@@ -75,7 +58,7 @@
"fields": {
"filename": {
"name": "Filename",
- "description": "Full path to filename."
+ "description": "Template of a filename. Variable available is `entity_id`."
}
}
},
@@ -99,7 +82,7 @@
"fields": {
"filename": {
"name": "[%key:component::camera::services::snapshot::fields::filename::name%]",
- "description": "Full path to filename. Must be mp4."
+ "description": "Template of a filename. Variable available is `entity_id`. Must be mp4."
},
"duration": {
"name": "Duration",
diff --git a/homeassistant/components/camera/webrtc.py b/homeassistant/components/camera/webrtc.py
index d627a888169..05924855bc4 100644
--- a/homeassistant/components/camera/webrtc.py
+++ b/homeassistant/components/camera/webrtc.py
@@ -2,23 +2,20 @@
from __future__ import annotations
-from abc import ABC, abstractmethod
import asyncio
-from collections.abc import Awaitable, Callable, Iterable
-from dataclasses import asdict, dataclass, field
-from functools import cache, partial, wraps
-import logging
+from collections.abc import Awaitable, Callable, Coroutine
+from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Protocol
+from mashumaro import field_options
+from mashumaro.config import BaseConfig
+from mashumaro.mixins.dict import DataClassDictMixin
import voluptuous as vol
-from webrtc_models import RTCConfiguration, RTCIceCandidate, RTCIceServer
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import config_validation as cv, issue_registry as ir
+from homeassistant.helpers import config_validation as cv
from homeassistant.util.hass_dict import HassKey
-from homeassistant.util.ulid import ulid
from .const import DATA_COMPONENT, DOMAIN, StreamType
from .helper import get_camera_from_entity_id
@@ -26,137 +23,64 @@ from .helper import get_camera_from_entity_id
if TYPE_CHECKING:
from . import Camera
-_LOGGER = logging.getLogger(__name__)
-
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
- "camera_webrtc_providers"
+ "camera_web_rtc_providers"
)
-DATA_WEBRTC_LEGACY_PROVIDERS: HassKey[dict[str, CameraWebRTCLegacyProvider]] = HassKey(
- "camera_webrtc_legacy_providers"
-)
-DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
- "camera_webrtc_ice_servers"
+DATA_ICE_SERVERS: HassKey[list[Callable[[], Coroutine[Any, Any, RTCIceServer]]]] = (
+ HassKey("camera_web_rtc_ice_servers")
)
-_WEBRTC = "WebRTC"
+class _RTCBaseModel(DataClassDictMixin):
+ """Base class for RTC models."""
+
+ class Config(BaseConfig):
+ """Mashumaro config."""
+
+ # Serialize to spec conform names and omit default values
+ omit_default = True
+ serialize_by_alias = True
-@dataclass(frozen=True)
-class WebRTCMessage:
- """Base class for WebRTC messages."""
+@dataclass
+class RTCIceServer(_RTCBaseModel):
+ """RTC Ice Server.
- @classmethod
- @cache
- def _get_type(cls) -> str:
- _, _, name = cls.__name__.partition(_WEBRTC)
- return name.lower()
+ See https://www.w3.org/TR/webrtc/#rtciceserver-dictionary
+ """
- def as_dict(self) -> dict[str, Any]:
- """Return a dict representation of the message."""
- data = asdict(self)
- data["type"] = self._get_type()
- return data
+ urls: list[str] | str
+ username: str | None = None
+ credential: str | None = None
-@dataclass(frozen=True)
-class WebRTCSession(WebRTCMessage):
- """WebRTC session."""
+@dataclass
+class RTCConfiguration(_RTCBaseModel):
+ """RTC Configuration.
- session_id: str
+ See https://www.w3.org/TR/webrtc/#rtcconfiguration-dictionary
+ """
-
-@dataclass(frozen=True)
-class WebRTCAnswer(WebRTCMessage):
- """WebRTC answer."""
-
- answer: str
-
-
-@dataclass(frozen=True)
-class WebRTCCandidate(WebRTCMessage):
- """WebRTC candidate."""
-
- candidate: RTCIceCandidate
-
- def as_dict(self) -> dict[str, Any]:
- """Return a dict representation of the message."""
- return {
- "type": self._get_type(),
- "candidate": self.candidate.candidate,
- }
-
-
-@dataclass(frozen=True)
-class WebRTCError(WebRTCMessage):
- """WebRTC error."""
-
- code: str
- message: str
-
-
-type WebRTCSendMessage = Callable[[WebRTCMessage], None]
+ ice_servers: list[RTCIceServer] = field(
+ metadata=field_options(alias="iceServers"), default_factory=list
+ )
@dataclass(kw_only=True)
-class WebRTCClientConfiguration:
+class WebRTCClientConfiguration(_RTCBaseModel):
"""WebRTC configuration for the client.
Not part of the spec, but required to configure client.
"""
configuration: RTCConfiguration = field(default_factory=RTCConfiguration)
- data_channel: str | None = None
- get_candidates_upfront: bool = False
-
- def to_frontend_dict(self) -> dict[str, Any]:
- """Return a dict that can be used by the frontend."""
- data: dict[str, Any] = {
- "configuration": self.configuration.to_dict(),
- "getCandidatesUpfront": self.get_candidates_upfront,
- }
- if self.data_channel is not None:
- data["dataChannel"] = self.data_channel
- return data
+ data_channel: str | None = field(
+ metadata=field_options(alias="dataChannel"), default=None
+ )
-class CameraWebRTCProvider(ABC):
- """WebRTC provider."""
-
- @property
- @abstractmethod
- def domain(self) -> str:
- """Return the integration domain of the provider."""
-
- @callback
- @abstractmethod
- def async_is_supported(self, stream_source: str) -> bool:
- """Determine if the provider supports the stream source."""
-
- @abstractmethod
- async def async_handle_async_webrtc_offer(
- self,
- camera: Camera,
- offer_sdp: str,
- session_id: str,
- send_message: WebRTCSendMessage,
- ) -> None:
- """Handle the WebRTC offer and return the answer via the provided callback."""
-
- @abstractmethod
- async def async_on_webrtc_candidate(
- self, session_id: str, candidate: RTCIceCandidate
- ) -> None:
- """Handle the WebRTC candidate."""
-
- @callback
- def async_close_session(self, session_id: str) -> None:
- """Close the session."""
- return ## This is an optional method so we need a default here.
-
-
-class CameraWebRTCLegacyProvider(Protocol):
+class CameraWebRTCProvider(Protocol):
"""WebRTC provider."""
async def async_is_supported(self, stream_source: str) -> bool:
@@ -168,7 +92,6 @@ class CameraWebRTCLegacyProvider(Protocol):
"""Handle the WebRTC offer and return an answer."""
-@callback
def async_register_webrtc_provider(
hass: HomeAssistant,
provider: CameraWebRTCProvider,
@@ -180,7 +103,9 @@ def async_register_webrtc_provider(
if DOMAIN not in hass.data:
raise ValueError("Unexpected state, camera not loaded")
- providers = hass.data.setdefault(DATA_WEBRTC_PROVIDERS, set())
+ providers: set[CameraWebRTCProvider] = hass.data.setdefault(
+ DATA_WEBRTC_PROVIDERS, set()
+ )
@callback
def remove_provider() -> None:
@@ -197,7 +122,6 @@ def async_register_webrtc_provider(
async def _async_refresh_providers(hass: HomeAssistant) -> None:
"""Check all cameras for any state changes for registered providers."""
- _async_check_conflicting_legacy_provider(hass)
component = hass.data[DATA_COMPONENT]
await asyncio.gather(
@@ -205,103 +129,6 @@ async def _async_refresh_providers(hass: HomeAssistant) -> None:
)
-type WsCommandWithCamera = Callable[
- [websocket_api.ActiveConnection, dict[str, Any], Camera],
- Awaitable[None],
-]
-
-
-def require_webrtc_support(
- error_code: str,
-) -> Callable[[WsCommandWithCamera], websocket_api.AsyncWebSocketCommandHandler]:
- """Validate that the camera supports WebRTC."""
-
- def decorate(
- func: WsCommandWithCamera,
- ) -> websocket_api.AsyncWebSocketCommandHandler:
- """Decorate func."""
-
- @wraps(func)
- async def validate(
- hass: HomeAssistant,
- connection: websocket_api.ActiveConnection,
- msg: dict[str, Any],
- ) -> None:
- """Validate that the camera supports WebRTC."""
- entity_id = msg["entity_id"]
- camera = get_camera_from_entity_id(hass, entity_id)
- if camera.frontend_stream_type != StreamType.WEB_RTC:
- connection.send_error(
- msg["id"],
- error_code,
- (
- "Camera does not support WebRTC,"
- f" frontend_stream_type={camera.frontend_stream_type}"
- ),
- )
- return
-
- await func(connection, msg, camera)
-
- return validate
-
- return decorate
-
-
-@websocket_api.websocket_command(
- {
- vol.Required("type"): "camera/webrtc/offer",
- vol.Required("entity_id"): cv.entity_id,
- vol.Required("offer"): str,
- }
-)
-@websocket_api.async_response
-@require_webrtc_support("webrtc_offer_failed")
-async def ws_webrtc_offer(
- connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera
-) -> None:
- """Handle the signal path for a WebRTC stream.
-
- This signal path is used to route the offer created by the client to the
- camera device through the integration for negotiation on initial setup.
- The ws endpoint returns a subscription id, where ice candidates and the
- final answer will be returned.
- The actual streaming is handled entirely between the client and camera device.
-
- Async friendly.
- """
- offer = msg["offer"]
- session_id = ulid()
- connection.subscriptions[msg["id"]] = partial(
- camera.close_webrtc_session, session_id
- )
-
- connection.send_message(websocket_api.result_message(msg["id"]))
-
- @callback
- def send_message(message: WebRTCMessage) -> None:
- """Push a value to websocket."""
- connection.send_message(
- websocket_api.event_message(
- msg["id"],
- message.as_dict(),
- )
- )
-
- send_message(WebRTCSession(session_id))
-
- try:
- await camera.async_handle_async_webrtc_offer(offer, session_id, send_message)
- except HomeAssistantError as ex:
- _LOGGER.error("Error handling WebRTC offer: %s", ex)
- send_message(
- WebRTCError(
- "webrtc_offer_failed",
- str(ex),
- )
- )
-
-
@websocket_api.websocket_command(
{
vol.Required("type"): "camera/webrtc/get_client_config",
@@ -309,81 +136,49 @@ async def ws_webrtc_offer(
}
)
@websocket_api.async_response
-@require_webrtc_support("webrtc_get_client_config_failed")
async def ws_get_client_config(
- connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera
+ hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle get WebRTC client config websocket command."""
- config = camera.async_get_webrtc_client_configuration().to_frontend_dict()
+ entity_id = msg["entity_id"]
+ camera = get_camera_from_entity_id(hass, entity_id)
+ if camera.frontend_stream_type != StreamType.WEB_RTC:
+ connection.send_error(
+ msg["id"],
+ "web_rtc_offer_failed",
+ (
+ "Camera does not support WebRTC,"
+ f" frontend_stream_type={camera.frontend_stream_type}"
+ ),
+ )
+ return
+
+ config = (await camera.async_get_webrtc_client_configuration()).to_dict()
connection.send_result(
msg["id"],
config,
)
-@websocket_api.websocket_command(
- {
- vol.Required("type"): "camera/webrtc/candidate",
- vol.Required("entity_id"): cv.entity_id,
- vol.Required("session_id"): str,
- vol.Required("candidate"): str,
- }
-)
-@websocket_api.async_response
-@require_webrtc_support("webrtc_candidate_failed")
-async def ws_candidate(
- connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera
-) -> None:
- """Handle WebRTC candidate websocket command."""
- await camera.async_on_webrtc_candidate(
- msg["session_id"], RTCIceCandidate(msg["candidate"])
- )
- connection.send_message(websocket_api.result_message(msg["id"]))
-
-
-@callback
-def async_register_ws(hass: HomeAssistant) -> None:
- """Register camera webrtc ws endpoints."""
-
- websocket_api.async_register_command(hass, ws_webrtc_offer)
- websocket_api.async_register_command(hass, ws_get_client_config)
- websocket_api.async_register_command(hass, ws_candidate)
-
-
-async def async_get_supported_provider(
+async def async_get_supported_providers(
hass: HomeAssistant, camera: Camera
-) -> CameraWebRTCProvider | None:
- """Return the first supported provider for the camera."""
+) -> list[CameraWebRTCProvider]:
+ """Return a list of supported providers for the camera."""
providers = hass.data.get(DATA_WEBRTC_PROVIDERS)
if not providers or not (stream_source := await camera.stream_source()):
- return None
+ return []
- for provider in providers:
- if provider.async_is_supported(stream_source):
- return provider
-
- return None
-
-
-async def async_get_supported_legacy_provider(
- hass: HomeAssistant, camera: Camera
-) -> CameraWebRTCLegacyProvider | None:
- """Return the first supported provider for the camera."""
- providers = hass.data.get(DATA_WEBRTC_LEGACY_PROVIDERS)
- if not providers or not (stream_source := await camera.stream_source()):
- return None
-
- for provider in providers.values():
- if await provider.async_is_supported(stream_source):
- return provider
-
- return None
+ return [
+ provider
+ for provider in providers
+ if await provider.async_is_supported(stream_source)
+ ]
@callback
-def async_register_ice_servers(
+def register_ice_server(
hass: HomeAssistant,
- get_ice_server_fn: Callable[[], Iterable[RTCIceServer]],
+ get_ice_server_fn: Callable[[], Coroutine[Any, Any, RTCIceServer]],
) -> Callable[[], None]:
"""Register a ICE server.
@@ -412,7 +207,7 @@ _RTSP_PREFIXES = {"rtsp://", "rtsps://", "rtmp://"}
type RtspToWebRtcProviderType = Callable[[str, str, str], Awaitable[str | None]]
-class _CameraRtspToWebRTCProvider(CameraWebRTCLegacyProvider):
+class _CameraRtspToWebRTCProvider(CameraWebRTCProvider):
def __init__(self, fn: RtspToWebRtcProviderType) -> None:
"""Initialize the RTSP to WebRTC provider."""
self._fn = fn
@@ -440,49 +235,5 @@ def async_register_rtsp_to_web_rtc_provider(
The first provider to satisfy the offer will be used.
"""
- if DOMAIN not in hass.data:
- raise ValueError("Unexpected state, camera not loaded")
-
- legacy_providers = hass.data.setdefault(DATA_WEBRTC_LEGACY_PROVIDERS, {})
-
- if domain in legacy_providers:
- raise ValueError("Provider already registered")
-
provider_instance = _CameraRtspToWebRTCProvider(provider)
-
- @callback
- def remove_provider() -> None:
- legacy_providers.pop(domain)
- hass.async_create_task(_async_refresh_providers(hass))
-
- legacy_providers[domain] = provider_instance
- hass.async_create_task(_async_refresh_providers(hass))
-
- return remove_provider
-
-
-@callback
-def _async_check_conflicting_legacy_provider(hass: HomeAssistant) -> None:
- """Check if a legacy provider is registered together with the builtin provider."""
- builtin_provider_domain = "go2rtc"
- if (
- (legacy_providers := hass.data.get(DATA_WEBRTC_LEGACY_PROVIDERS))
- and (providers := hass.data.get(DATA_WEBRTC_PROVIDERS))
- and any(provider.domain == builtin_provider_domain for provider in providers)
- ):
- for domain in legacy_providers:
- ir.async_create_issue(
- hass,
- DOMAIN,
- f"legacy_webrtc_provider_{domain}",
- is_fixable=False,
- is_persistent=False,
- issue_domain=domain,
- learn_more_url="https://www.home-assistant.io/integrations/go2rtc/",
- severity=ir.IssueSeverity.WARNING,
- translation_key="legacy_webrtc_provider",
- translation_placeholders={
- "legacy_integration": domain,
- "builtin_integration": builtin_provider_domain,
- },
- )
+ return async_register_webrtc_provider(hass, provider_instance)
diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py
index 69600e4bbc7..a7d5dc8ab98 100644
--- a/homeassistant/components/canary/alarm_control_panel.py
+++ b/homeassistant/components/canary/alarm_control_panel.py
@@ -10,9 +10,14 @@ from canary.model import Location
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -65,18 +70,18 @@ class CanaryAlarm(
return self.coordinator.data["locations"][self._location_id]
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the device."""
if self.location.is_private:
- return AlarmControlPanelState.DISARMED
+ return STATE_ALARM_DISARMED
mode = self.location.mode
if mode.name == LOCATION_MODE_AWAY:
- return AlarmControlPanelState.ARMED_AWAY
+ return STATE_ALARM_ARMED_AWAY
if mode.name == LOCATION_MODE_HOME:
- return AlarmControlPanelState.ARMED_HOME
+ return STATE_ALARM_ARMED_HOME
if mode.name == LOCATION_MODE_NIGHT:
- return AlarmControlPanelState.ARMED_NIGHT
+ return STATE_ALARM_ARMED_NIGHT
return None
diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py
index 2dd3a678b5d..5af7142af8f 100644
--- a/homeassistant/components/canary/config_flow.py
+++ b/homeassistant/components/canary/config_flow.py
@@ -52,7 +52,7 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
- return CanaryOptionsFlowHandler()
+ return CanaryOptionsFlowHandler(config_entry)
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Handle a flow initiated by configuration file."""
@@ -104,6 +104,10 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN):
class CanaryOptionsFlowHandler(OptionsFlow):
"""Handle Canary client options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py
index 03a3f2ea1f8..4f7dd59e83e 100644
--- a/homeassistant/components/cast/config_flow.py
+++ b/homeassistant/components/cast/config_flow.py
@@ -41,18 +41,24 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> CastOptionsFlowHandler:
"""Get the options flow for this handler."""
- return CastOptionsFlowHandler()
+ return CastOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
return await self.async_step_config()
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle a flow initialized by zeroconf discovery."""
+ if self._async_in_progress() or self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
await self.async_set_unique_id(DOMAIN)
return await self.async_step_confirm()
@@ -109,8 +115,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
class CastOptionsFlowHandler(OptionsFlow):
"""Handle Google Cast options."""
- def __init__(self) -> None:
+ def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize Google Cast options flow."""
+ self.config_entry = config_entry
self.updated_config: dict[str, Any] = {}
async def async_step_init(self, user_input: None = None) -> ConfigFlowResult:
diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json
index 0650f267544..72b2f799d18 100644
--- a/homeassistant/components/cast/manifest.json
+++ b/homeassistant/components/cast/manifest.json
@@ -14,7 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/cast",
"iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"],
- "requirements": ["PyChromecast==14.0.5"],
- "single_config_entry": true,
+ "requirements": ["PyChromecast==14.0.4"],
"zeroconf": ["_googlecast._tcp.local."]
}
diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json
index 12f2edeee9a..ce622e48aae 100644
--- a/homeassistant/components/cast/strings.json
+++ b/homeassistant/components/cast/strings.json
@@ -12,6 +12,9 @@
}
}
},
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ },
"error": {
"invalid_known_hosts": "Known hosts must be a comma separated list of hosts."
}
diff --git a/homeassistant/components/cisco_webex_teams/__init__.py b/homeassistant/components/cisco_webex_teams/__init__.py
index 5932f2ed680..0a8714806a1 100644
--- a/homeassistant/components/cisco_webex_teams/__init__.py
+++ b/homeassistant/components/cisco_webex_teams/__init__.py
@@ -1 +1 @@
-"""Component to integrate the Cisco Webex cloud."""
+"""Component to integrate the Cisco Webex Teams cloud."""
diff --git a/homeassistant/components/cisco_webex_teams/manifest.json b/homeassistant/components/cisco_webex_teams/manifest.json
index 3da31a0b453..822919213c2 100644
--- a/homeassistant/components/cisco_webex_teams/manifest.json
+++ b/homeassistant/components/cisco_webex_teams/manifest.json
@@ -2,8 +2,9 @@
"domain": "cisco_webex_teams",
"name": "Cisco Webex Teams",
"codeowners": ["@fbradyirl"],
+ "disabled": "Integration library not compatible with Python 3.12",
"documentation": "https://www.home-assistant.io/integrations/cisco_webex_teams",
"iot_class": "cloud_push",
- "loggers": ["webexpythonsdk"],
- "requirements": ["webexpythonsdk==2.0.1"]
+ "loggers": ["webexteamssdk"],
+ "requirements": ["webexteamssdk==1.1.1;python_version<'3.12'"]
}
diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py
index 74d033c62d4..b93ebb273dd 100644
--- a/homeassistant/components/cisco_webex_teams/notify.py
+++ b/homeassistant/components/cisco_webex_teams/notify.py
@@ -1,11 +1,11 @@
-"""Cisco Webex notify component."""
+"""Cisco Webex Teams notify component."""
from __future__ import annotations
import logging
import voluptuous as vol
-from webexpythonsdk import ApiError, WebexAPI, exceptions
+from webexteamssdk import ApiError, WebexTeamsAPI, exceptions
from homeassistant.components.notify import (
ATTR_TITLE,
@@ -30,9 +30,9 @@ def get_service(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
-) -> CiscoWebexNotificationService | None:
- """Get the Cisco Webex notification service."""
- client = WebexAPI(access_token=config[CONF_TOKEN])
+) -> CiscoWebexTeamsNotificationService | None:
+ """Get the CiscoWebexTeams notification service."""
+ client = WebexTeamsAPI(access_token=config[CONF_TOKEN])
try:
# Validate the token & room_id
client.rooms.get(config[CONF_ROOM_ID])
@@ -40,11 +40,11 @@ def get_service(
_LOGGER.error(error)
return None
- return CiscoWebexNotificationService(client, config[CONF_ROOM_ID])
+ return CiscoWebexTeamsNotificationService(client, config[CONF_ROOM_ID])
-class CiscoWebexNotificationService(BaseNotificationService):
- """The Cisco Webex Notification Service."""
+class CiscoWebexTeamsNotificationService(BaseNotificationService):
+ """The Cisco Webex Teams Notification Service."""
def __init__(self, client, room):
"""Initialize the service."""
@@ -62,5 +62,5 @@ class CiscoWebexNotificationService(BaseNotificationService):
self.client.messages.create(roomId=self.room, html=f"{title}{message}")
except ApiError as api_error:
_LOGGER.error(
- "Could not send Cisco Webex notification. Error: %s", api_error
+ "Could not send CiscoWebexTeams notification. Error: %s", api_error
)
diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py
index ee46fa42125..01c8de77156 100644
--- a/homeassistant/components/cloud/client.py
+++ b/homeassistant/components/cloud/client.py
@@ -3,7 +3,6 @@
from __future__ import annotations
import asyncio
-from collections.abc import Callable
from datetime import datetime
from http import HTTPStatus
import logging
@@ -12,14 +11,12 @@ from typing import Any, Literal
import aiohttp
from hass_nabucasa.client import CloudClient as Interface, RemoteActivationNotAllowed
-from webrtc_models import RTCIceServer
from homeassistant.components import google_assistant, persistent_notification, webhook
from homeassistant.components.alexa import (
errors as alexa_errors,
smart_home as alexa_smart_home,
)
-from homeassistant.components.camera.webrtc import async_register_ice_servers
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import Context, HassJob, HomeAssistant, callback
@@ -30,7 +27,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
from homeassistant.util.aiohttp import MockRequest, serialize_response
from . import alexa_config, google_config
-from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN, PREF_ENABLE_CLOUD_ICE_SERVERS
+from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN
from .prefs import CloudPreferences
_LOGGER = logging.getLogger(__name__)
@@ -63,7 +60,6 @@ class CloudClient(Interface):
self._alexa_config_init_lock = asyncio.Lock()
self._google_config_init_lock = asyncio.Lock()
self._relayer_region: str | None = None
- self._cloud_ice_servers_listener: Callable[[], None] | None = None
@property
def base_path(self) -> Path:
@@ -191,49 +187,6 @@ class CloudClient(Interface):
if is_new_user:
await gconf.async_sync_entities(gconf.agent_user_id)
- async def setup_cloud_ice_servers(_: datetime) -> None:
- async def register_cloud_ice_server(
- ice_servers: list[RTCIceServer],
- ) -> Callable[[], None]:
- """Register cloud ice server."""
-
- def get_ice_servers() -> list[RTCIceServer]:
- return ice_servers
-
- return async_register_ice_servers(self._hass, get_ice_servers)
-
- async def async_register_cloud_ice_servers_listener(
- prefs: CloudPreferences,
- ) -> None:
- is_cloud_ice_servers_enabled = (
- self.cloud.is_logged_in
- and not self.cloud.subscription_expired
- and prefs.cloud_ice_servers_enabled
- )
- if is_cloud_ice_servers_enabled:
- if self._cloud_ice_servers_listener is None:
- self._cloud_ice_servers_listener = await self.cloud.ice_servers.async_register_ice_servers_listener(
- register_cloud_ice_server
- )
- elif self._cloud_ice_servers_listener:
- self._cloud_ice_servers_listener()
- self._cloud_ice_servers_listener = None
-
- async def async_prefs_updated(prefs: CloudPreferences) -> None:
- updated_prefs = prefs.last_updated
-
- if (
- updated_prefs is None
- or PREF_ENABLE_CLOUD_ICE_SERVERS not in updated_prefs
- ):
- return
-
- await async_register_cloud_ice_servers_listener(prefs)
-
- await async_register_cloud_ice_servers_listener(self._prefs)
-
- self._prefs.async_listen_updates(async_prefs_updated)
-
tasks = []
if self._prefs.alexa_enabled and self._prefs.alexa_report_state:
@@ -242,8 +195,6 @@ class CloudClient(Interface):
if self._prefs.google_enabled:
tasks.append(enable_google)
- tasks.append(setup_cloud_ice_servers)
-
if tasks:
await asyncio.gather(*(task(None) for task in tasks))
@@ -271,10 +222,6 @@ class CloudClient(Interface):
self._google_config.async_deinitialize()
self._google_config = None
- if self._cloud_ice_servers_listener:
- self._cloud_ice_servers_listener()
- self._cloud_ice_servers_listener = None
-
@callback
def user_message(self, identifier: str, title: str, message: str) -> None:
"""Create a message for user to UI."""
diff --git a/homeassistant/components/cloud/config_flow.py b/homeassistant/components/cloud/config_flow.py
index 92fbf78378b..932291c2bfa 100644
--- a/homeassistant/components/cloud/config_flow.py
+++ b/homeassistant/components/cloud/config_flow.py
@@ -18,4 +18,6 @@ class CloudConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the system step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="Home Assistant Cloud", data={})
diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py
index 4392bf94827..5e9fb2e9dc7 100644
--- a/homeassistant/components/cloud/const.py
+++ b/homeassistant/components/cloud/const.py
@@ -43,7 +43,6 @@ PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version"
PREF_TTS_DEFAULT_VOICE = "tts_default_voice"
PREF_GOOGLE_CONNECTED = "google_connected"
PREF_REMOTE_ALLOW_REMOTE_ENABLE = "remote_allow_remote_enable"
-PREF_ENABLE_CLOUD_ICE_SERVERS = "cloud_ice_servers_enabled"
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "JennyNeural")
DEFAULT_DISABLE_2FA = False
DEFAULT_ALEXA_REPORT_STATE = True
diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py
index 4f2ad0ddcf7..b1931515745 100644
--- a/homeassistant/components/cloud/http_api.py
+++ b/homeassistant/components/cloud/http_api.py
@@ -42,7 +42,6 @@ from .const import (
PREF_ALEXA_REPORT_STATE,
PREF_DISABLE_2FA,
PREF_ENABLE_ALEXA,
- PREF_ENABLE_CLOUD_ICE_SERVERS,
PREF_ENABLE_GOOGLE,
PREF_GOOGLE_REPORT_STATE,
PREF_GOOGLE_SECURE_DEVICES_PIN,
@@ -440,16 +439,15 @@ def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]:
@websocket_api.websocket_command(
{
vol.Required("type"): "cloud/update_prefs",
- vol.Optional(PREF_ALEXA_REPORT_STATE): bool,
- vol.Optional(PREF_ENABLE_ALEXA): bool,
- vol.Optional(PREF_ENABLE_CLOUD_ICE_SERVERS): bool,
vol.Optional(PREF_ENABLE_GOOGLE): bool,
+ vol.Optional(PREF_ENABLE_ALEXA): bool,
+ vol.Optional(PREF_ALEXA_REPORT_STATE): bool,
vol.Optional(PREF_GOOGLE_REPORT_STATE): bool,
vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
- vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool,
vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All(
vol.Coerce(tuple), validate_language_voice
),
+ vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool,
}
)
@websocket_api.async_response
diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json
index 4201cb1b2d4..529f4fb9be9 100644
--- a/homeassistant/components/cloud/manifest.json
+++ b/homeassistant/components/cloud/manifest.json
@@ -8,6 +8,5 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["hass_nabucasa"],
- "requirements": ["hass-nabucasa==0.84.0"],
- "single_config_entry": true
+ "requirements": ["hass-nabucasa==0.81.1"]
}
diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py
index ae4b2794e1b..9f76c16a113 100644
--- a/homeassistant/components/cloud/prefs.py
+++ b/homeassistant/components/cloud/prefs.py
@@ -32,7 +32,6 @@ from .const import (
PREF_CLOUD_USER,
PREF_CLOUDHOOKS,
PREF_ENABLE_ALEXA,
- PREF_ENABLE_CLOUD_ICE_SERVERS,
PREF_ENABLE_GOOGLE,
PREF_ENABLE_REMOTE,
PREF_GOOGLE_CONNECTED,
@@ -163,21 +162,20 @@ class CloudPreferences:
async def async_update(
self,
*,
- alexa_enabled: bool | UndefinedType = UNDEFINED,
- alexa_report_state: bool | UndefinedType = UNDEFINED,
- alexa_settings_version: int | UndefinedType = UNDEFINED,
- cloud_ice_servers_enabled: bool | UndefinedType = UNDEFINED,
- cloud_user: str | UndefinedType = UNDEFINED,
- cloudhooks: dict[str, dict[str, str | bool]] | UndefinedType = UNDEFINED,
- google_connected: bool | UndefinedType = UNDEFINED,
google_enabled: bool | UndefinedType = UNDEFINED,
- google_report_state: bool | UndefinedType = UNDEFINED,
- google_secure_devices_pin: str | None | UndefinedType = UNDEFINED,
- google_settings_version: int | UndefinedType = UNDEFINED,
- remote_allow_remote_enable: bool | UndefinedType = UNDEFINED,
- remote_domain: str | None | UndefinedType = UNDEFINED,
+ alexa_enabled: bool | UndefinedType = UNDEFINED,
remote_enabled: bool | UndefinedType = UNDEFINED,
+ google_secure_devices_pin: str | None | UndefinedType = UNDEFINED,
+ cloudhooks: dict[str, dict[str, str | bool]] | UndefinedType = UNDEFINED,
+ cloud_user: str | UndefinedType = UNDEFINED,
+ alexa_report_state: bool | UndefinedType = UNDEFINED,
+ google_report_state: bool | UndefinedType = UNDEFINED,
tts_default_voice: tuple[str, str] | UndefinedType = UNDEFINED,
+ remote_domain: str | None | UndefinedType = UNDEFINED,
+ alexa_settings_version: int | UndefinedType = UNDEFINED,
+ google_settings_version: int | UndefinedType = UNDEFINED,
+ google_connected: bool | UndefinedType = UNDEFINED,
+ remote_allow_remote_enable: bool | UndefinedType = UNDEFINED,
) -> None:
"""Update user preferences."""
prefs = {**self._prefs}
@@ -186,21 +184,20 @@ class CloudPreferences:
{
key: value
for key, value in (
- (PREF_ALEXA_REPORT_STATE, alexa_report_state),
- (PREF_ALEXA_SETTINGS_VERSION, alexa_settings_version),
- (PREF_CLOUD_USER, cloud_user),
- (PREF_CLOUDHOOKS, cloudhooks),
- (PREF_ENABLE_ALEXA, alexa_enabled),
- (PREF_ENABLE_CLOUD_ICE_SERVERS, cloud_ice_servers_enabled),
(PREF_ENABLE_GOOGLE, google_enabled),
+ (PREF_ENABLE_ALEXA, alexa_enabled),
(PREF_ENABLE_REMOTE, remote_enabled),
- (PREF_GOOGLE_CONNECTED, google_connected),
- (PREF_GOOGLE_REPORT_STATE, google_report_state),
(PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin),
+ (PREF_CLOUDHOOKS, cloudhooks),
+ (PREF_CLOUD_USER, cloud_user),
+ (PREF_ALEXA_REPORT_STATE, alexa_report_state),
+ (PREF_GOOGLE_REPORT_STATE, google_report_state),
+ (PREF_ALEXA_SETTINGS_VERSION, alexa_settings_version),
(PREF_GOOGLE_SETTINGS_VERSION, google_settings_version),
- (PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable),
- (PREF_REMOTE_DOMAIN, remote_domain),
(PREF_TTS_DEFAULT_VOICE, tts_default_voice),
+ (PREF_REMOTE_DOMAIN, remote_domain),
+ (PREF_GOOGLE_CONNECTED, google_connected),
+ (PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable),
)
if value is not UNDEFINED
}
@@ -242,7 +239,6 @@ class CloudPreferences:
PREF_ALEXA_REPORT_STATE: self.alexa_report_state,
PREF_CLOUDHOOKS: self.cloudhooks,
PREF_ENABLE_ALEXA: self.alexa_enabled,
- PREF_ENABLE_CLOUD_ICE_SERVERS: self.cloud_ice_servers_enabled,
PREF_ENABLE_GOOGLE: self.google_enabled,
PREF_ENABLE_REMOTE: self.remote_enabled,
PREF_GOOGLE_DEFAULT_EXPOSE: self.google_default_expose,
@@ -366,14 +362,6 @@ class CloudPreferences:
"""
return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE) # type: ignore[no-any-return]
- @property
- def cloud_ice_servers_enabled(self) -> bool:
- """Return if cloud ICE servers are enabled."""
- cloud_ice_servers_enabled: bool = self._prefs.get(
- PREF_ENABLE_CLOUD_ICE_SERVERS, True
- )
- return cloud_ice_servers_enabled
-
async def get_cloud_user(self) -> str:
"""Return ID of Home Assistant Cloud system user."""
user = await self._load_cloud_user()
@@ -421,7 +409,6 @@ class CloudPreferences:
PREF_ENABLE_ALEXA: True,
PREF_ENABLE_GOOGLE: True,
PREF_ENABLE_REMOTE: False,
- PREF_ENABLE_CLOUD_ICE_SERVERS: True,
PREF_GOOGLE_CONNECTED: False,
PREF_GOOGLE_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
PREF_GOOGLE_ENTITY_CONFIGS: {},
diff --git a/homeassistant/components/cloud/strings.json b/homeassistant/components/cloud/strings.json
index 9f7e0dbadcd..fe36159e5eb 100644
--- a/homeassistant/components/cloud/strings.json
+++ b/homeassistant/components/cloud/strings.json
@@ -1,4 +1,10 @@
{
+ "config": {
+ "step": {},
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ }
+ },
"system_health": {
"info": {
"can_reach_cert_server": "Reach certificate server",
diff --git a/homeassistant/components/cloud/system_health.py b/homeassistant/components/cloud/system_health.py
index ac50c2fb49b..0e65aa93eaf 100644
--- a/homeassistant/components/cloud/system_health.py
+++ b/homeassistant/components/cloud/system_health.py
@@ -33,7 +33,6 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
data["remote_connected"] = cloud.remote.is_connected
data["alexa_enabled"] = client.prefs.alexa_enabled
data["google_enabled"] = client.prefs.google_enabled
- data["cloud_ice_servers_enabled"] = client.prefs.cloud_ice_servers_enabled
data["remote_server"] = cloud.remote.snitun_server
data["certificate_status"] = cloud.remote.certificate_status
data["instance_id"] = client.prefs.instance_id
diff --git a/homeassistant/components/cloudflare/config_flow.py b/homeassistant/components/cloudflare/config_flow.py
index c3845a447e4..a4276cf9dd3 100644
--- a/homeassistant/components/cloudflare/config_flow.py
+++ b/homeassistant/components/cloudflare/config_flow.py
@@ -118,6 +118,9 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initiated by the user."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
persistent_notification.async_dismiss(self.hass, "cloudflare_setup")
errors: dict[str, str] = {}
diff --git a/homeassistant/components/cloudflare/manifest.json b/homeassistant/components/cloudflare/manifest.json
index 8529a0b9bad..0f689aa3e03 100644
--- a/homeassistant/components/cloudflare/manifest.json
+++ b/homeassistant/components/cloudflare/manifest.json
@@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/cloudflare",
"iot_class": "cloud_push",
"loggers": ["pycfdns"],
- "requirements": ["pycfdns==3.0.0"],
- "single_config_entry": true
+ "requirements": ["pycfdns==3.0.0"]
}
diff --git a/homeassistant/components/cloudflare/strings.json b/homeassistant/components/cloudflare/strings.json
index 8c8ec57b074..c72953211f0 100644
--- a/homeassistant/components/cloudflare/strings.json
+++ b/homeassistant/components/cloudflare/strings.json
@@ -34,7 +34,8 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"services": {
diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py
index 0d357cce199..3313d01be85 100644
--- a/homeassistant/components/co2signal/config_flow.py
+++ b/homeassistant/components/co2signal/config_flow.py
@@ -13,7 +13,7 @@ from aioelectricitymaps import (
)
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_API_KEY,
CONF_COUNTRY_CODE,
@@ -42,6 +42,7 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
_data: dict | None
+ _reauth_entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -127,6 +128,9 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle the reauth step."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -161,14 +165,16 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
except ElectricityMapsError:
errors["base"] = "unknown"
else:
- if self.source == SOURCE_REAUTH:
+ if self._reauth_entry:
return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
- data_updates={CONF_API_KEY: data[CONF_API_KEY]},
+ self._reauth_entry,
+ data={
+ CONF_API_KEY: data[CONF_API_KEY],
+ },
)
return self.async_create_entry(
- title=get_extra_name(data) or "Electricity Maps",
+ title=get_extra_name(data) or "CO2 Signal",
data=data,
)
diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py
index 8b7b4b9e313..616fdaf8f7a 100644
--- a/homeassistant/components/coinbase/config_flow.py
+++ b/homeassistant/components/coinbase/config_flow.py
@@ -158,12 +158,16 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Handle a option flow for Coinbase."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/comelit/alarm_control_panel.py b/homeassistant/components/comelit/alarm_control_panel.py
index b3bd6664bf8..b325de25e97 100644
--- a/homeassistant/components/comelit/alarm_control_panel.py
+++ b/homeassistant/components/comelit/alarm_control_panel.py
@@ -10,12 +10,21 @@ from aiocomelit.const import ALARM_AREAS, AlarmAreaState
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_DISARMING,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
@@ -103,7 +112,7 @@ class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanel
return super().available
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> StateType:
"""Return the state of the alarm."""
_LOGGER.debug(
@@ -114,16 +123,16 @@ class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanel
)
if self._area.human_status == AlarmAreaState.ARMED:
if self._area.armed == ALARM_AREA_ARMED_STATUS[AWAY]:
- return AlarmControlPanelState.ARMED_AWAY
+ return STATE_ALARM_ARMED_AWAY
if self._area.armed == ALARM_AREA_ARMED_STATUS[NIGHT]:
- return AlarmControlPanelState.ARMED_NIGHT
- return AlarmControlPanelState.ARMED_HOME
+ return STATE_ALARM_ARMED_NIGHT
+ return STATE_ALARM_ARMED_HOME
return {
- AlarmAreaState.DISARMED: AlarmControlPanelState.DISARMED,
- AlarmAreaState.ENTRY_DELAY: AlarmControlPanelState.DISARMING,
- AlarmAreaState.EXIT_DELAY: AlarmControlPanelState.ARMING,
- AlarmAreaState.TRIGGERED: AlarmControlPanelState.TRIGGERED,
+ AlarmAreaState.DISARMED: STATE_ALARM_DISARMED,
+ AlarmAreaState.ENTRY_DELAY: STATE_ALARM_DISARMING,
+ AlarmAreaState.EXIT_DELAY: STATE_ALARM_ARMING,
+ AlarmAreaState.TRIGGERED: STATE_ALARM_TRIGGERED,
}.get(self._area.human_status)
async def async_alarm_disarm(self, code: str | None = None) -> None:
diff --git a/homeassistant/components/comelit/diagnostics.py b/homeassistant/components/comelit/diagnostics.py
deleted file mode 100644
index afa57831eae..00000000000
--- a/homeassistant/components/comelit/diagnostics.py
+++ /dev/null
@@ -1,93 +0,0 @@
-"""Diagnostics support for Comelit integration."""
-
-from __future__ import annotations
-
-from typing import Any
-
-from aiocomelit import (
- ComelitSerialBridgeObject,
- ComelitVedoAreaObject,
- ComelitVedoZoneObject,
-)
-from aiocomelit.const import BRIDGE
-
-from homeassistant.components.diagnostics import async_redact_data
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_PIN, CONF_TYPE
-from homeassistant.core import HomeAssistant
-
-from .const import DOMAIN
-from .coordinator import ComelitBaseCoordinator
-
-TO_REDACT = {CONF_PIN}
-
-
-async def async_get_config_entry_diagnostics(
- hass: HomeAssistant, entry: ConfigEntry
-) -> dict[str, Any]:
- """Return diagnostics for a config entry."""
-
- coordinator: ComelitBaseCoordinator = hass.data[DOMAIN][entry.entry_id]
-
- dev_list: list[dict[str, Any]] = []
- dev_type_list: list[dict[int, Any]] = []
-
- for dev_type in coordinator.data:
- dev_type_list = []
- for sensor_data in coordinator.data[dev_type].values():
- if isinstance(sensor_data, ComelitSerialBridgeObject):
- dev_type_list.append(
- {
- sensor_data.index: {
- "name": sensor_data.name,
- "status": sensor_data.status,
- "human_status": sensor_data.human_status,
- "protected": sensor_data.protected,
- "val": sensor_data.val,
- "zone": sensor_data.zone,
- "power": sensor_data.power,
- "power_unit": sensor_data.power_unit,
- }
- }
- )
- if isinstance(sensor_data, ComelitVedoAreaObject):
- dev_type_list.append(
- {
- sensor_data.index: {
- "name": sensor_data.name,
- "human_status": sensor_data.human_status.value,
- "p1": sensor_data.p1,
- "p2": sensor_data.p2,
- "ready": sensor_data.ready,
- "armed": sensor_data.armed,
- "alarm": sensor_data.alarm,
- "alarm_memory": sensor_data.alarm_memory,
- "sabotage": sensor_data.sabotage,
- "anomaly": sensor_data.anomaly,
- "in_time": sensor_data.in_time,
- "out_time": sensor_data.out_time,
- }
- }
- )
- if isinstance(sensor_data, ComelitVedoZoneObject):
- dev_type_list.append(
- {
- sensor_data.index: {
- "name": sensor_data.name,
- "human_status": sensor_data.human_status.value,
- "status": sensor_data.status,
- "status_api": sensor_data.status_api,
- }
- }
- )
- dev_list.append({dev_type: dev_type_list})
-
- return {
- "entry": async_redact_data(entry.as_dict(), TO_REDACT),
- "type": entry.data.get(CONF_TYPE, BRIDGE),
- "device_info": {
- "last_update success": coordinator.last_update_success,
- "last_exception": repr(coordinator.last_exception),
- "devices": dev_list,
- },
- }
diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json
index d25d5c1d7d5..b9264d16f69 100644
--- a/homeassistant/components/comelit/manifest.json
+++ b/homeassistant/components/comelit/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "silver",
- "requirements": ["aiocomelit==0.9.1"]
+ "requirements": ["aiocomelit==0.9.0"]
}
diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json
index 775bde3c859..caae9190bca 100644
--- a/homeassistant/components/compensation/manifest.json
+++ b/homeassistant/components/compensation/manifest.json
@@ -4,5 +4,5 @@
"codeowners": ["@Petro31"],
"documentation": "https://www.home-assistant.io/integrations/compensation",
"iot_class": "calculated",
- "requirements": ["numpy==2.1.3"]
+ "requirements": ["numpy==1.26.4"]
}
diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py
index 02453b56376..661a2beacc0 100644
--- a/homeassistant/components/concord232/alarm_control_panel.py
+++ b/homeassistant/components/concord232/alarm_control_panel.py
@@ -13,10 +13,18 @@ from homeassistant.components.alarm_control_panel import (
PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA,
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
-from homeassistant.const import CONF_CODE, CONF_HOST, CONF_MODE, CONF_NAME, CONF_PORT
+from homeassistant.const import (
+ CONF_CODE,
+ CONF_HOST,
+ CONF_MODE,
+ CONF_NAME,
+ CONF_PORT,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -67,6 +75,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
"""Representation of the Concord232-based alarm panel."""
_attr_code_format = CodeFormat.NUMBER
+ _attr_state: str | None
_attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
@@ -98,21 +107,21 @@ class Concord232Alarm(AlarmControlPanelEntity):
return
if part["arming_level"] == "Off":
- self._attr_alarm_state = AlarmControlPanelState.DISARMED
+ self._attr_state = STATE_ALARM_DISARMED
elif "Home" in part["arming_level"]:
- self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
+ self._attr_state = STATE_ALARM_ARMED_HOME
else:
- self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
+ self._attr_state = STATE_ALARM_ARMED_AWAY
def alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
- if not self._validate_code(code, AlarmControlPanelState.DISARMED):
+ if not self._validate_code(code, STATE_ALARM_DISARMED):
return
self._alarm.disarm(code)
def alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
- if not self._validate_code(code, AlarmControlPanelState.ARMED_HOME):
+ if not self._validate_code(code, STATE_ALARM_ARMED_HOME):
return
if self._mode == "silent":
self._alarm.arm("stay", "silent")
@@ -121,7 +130,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
def alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
- if not self._validate_code(code, AlarmControlPanelState.ARMED_AWAY):
+ if not self._validate_code(code, STATE_ALARM_ARMED_AWAY):
return
self._alarm.arm("away")
@@ -129,7 +138,10 @@ class Concord232Alarm(AlarmControlPanelEntity):
"""Validate given code."""
if self._code is None:
return True
- alarm_code = self._code
+ if isinstance(self._code, str):
+ alarm_code = self._code
+ else:
+ alarm_code = self._code.render(from_state=self._attr_state, to_state=state)
check = not alarm_code or code == alarm_code
if not check:
_LOGGER.warning("Invalid code given for %s", state)
diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py
index 19fae1ef7ca..77ae2c98c7d 100644
--- a/homeassistant/components/control4/config_flow.py
+++ b/homeassistant/components/control4/config_flow.py
@@ -154,12 +154,16 @@ class Control4ConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Handle a option flow for Control4."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py
index 4838d19537a..155909d5fe3 100644
--- a/homeassistant/components/conversation/default_agent.py
+++ b/homeassistant/components/conversation/default_agent.py
@@ -17,10 +17,9 @@ from hassil.intents import Intents, SlotList, TextSlotList, WildcardSlotList
from hassil.recognize import (
MISSING_ENTITY,
RecognizeResult,
+ UnmatchedTextEntity,
recognize_all,
- recognize_best,
)
-from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
from hassil.util import merge_dict
from home_assistant_intents import ErrorKey, get_intents, get_languages
import yaml
@@ -294,7 +293,7 @@ class DefaultAgent(ConversationEntity):
self.hass, language, DOMAIN, [DOMAIN]
)
response_text = translations.get(
- f"component.{DOMAIN}.conversation.agent.done", "Done"
+ f"component.{DOMAIN}.agent.done", "Done"
)
response.async_set_speech(response_text)
@@ -438,68 +437,69 @@ class DefaultAgent(ConversationEntity):
language: str,
) -> RecognizeResult | None:
"""Search intents for a match to user input."""
- strict_result = self._recognize_strict(
- user_input, lang_intents, slot_lists, intent_context, language
- )
-
- if strict_result is not None:
- # Successful strict match
- return strict_result
-
- # Try again with all entities (including unexposed)
- entity_registry = er.async_get(self.hass)
- all_entity_names: list[tuple[str, str, dict[str, Any]]] = []
-
- for state in self.hass.states.async_all():
- context = {"domain": state.domain}
- if state.attributes:
- # Include some attributes
- for attr in DEFAULT_EXPOSED_ATTRIBUTES:
- if attr not in state.attributes:
- continue
- context[attr] = state.attributes[attr]
-
- if entity := entity_registry.async_get(state.entity_id):
- # Skip config/hidden entities
- if (entity.entity_category is not None) or (
- entity.hidden_by is not None
+ custom_result: RecognizeResult | None = None
+ name_result: RecognizeResult | None = None
+ best_results: list[RecognizeResult] = []
+ best_text_chunks_matched: int | None = None
+ for result in recognize_all(
+ user_input.text,
+ lang_intents.intents,
+ slot_lists=slot_lists,
+ intent_context=intent_context,
+ language=language,
+ ):
+ # User intents have highest priority
+ if (result.intent_metadata is not None) and result.intent_metadata.get(
+ METADATA_CUSTOM_SENTENCE
+ ):
+ if (custom_result is None) or (
+ result.text_chunks_matched > custom_result.text_chunks_matched
):
- continue
+ custom_result = result
- if entity.aliases:
- # Also add aliases
- for alias in entity.aliases:
- if not alias.strip():
- continue
+ # Clear builtin results
+ best_results = []
+ name_result = None
+ continue
- all_entity_names.append((alias, alias, context))
+ # Prioritize results with a "name" slot, but still prefer ones with
+ # more literal text matched.
+ if (
+ ("name" in result.entities)
+ and (not result.entities["name"].is_wildcard)
+ and (
+ (name_result is None)
+ or (result.text_chunks_matched > name_result.text_chunks_matched)
+ )
+ ):
+ name_result = result
- # Default name
- all_entity_names.append((state.name, state.name, context))
+ if (best_text_chunks_matched is None) or (
+ result.text_chunks_matched > best_text_chunks_matched
+ ):
+ # Only overwrite if more literal text was matched.
+ # This causes wildcards to match last.
+ best_results = [result]
+ best_text_chunks_matched = result.text_chunks_matched
+ elif result.text_chunks_matched == best_text_chunks_matched:
+ # Accumulate results with the same number of literal text matched.
+ # We will resolve the ambiguity below.
+ best_results.append(result)
- slot_lists = {
- **slot_lists,
- "name": TextSlotList.from_tuples(all_entity_names, allow_template=False),
- }
+ if custom_result is not None:
+ # Prioritize user intents
+ return custom_result
- strict_result = self._recognize_strict(
- user_input,
- lang_intents,
- slot_lists,
- intent_context,
- language,
- )
+ if name_result is not None:
+ # Prioritize matches with entity names above area names
+ return name_result
- if strict_result is not None:
- # Not a successful match, but useful for an error message.
- # This should fail the intent handling phase (async_match_targets).
- return strict_result
+ if best_results:
+ # Successful strict match
+ return best_results[0]
# Try again with missing entities enabled
maybe_result: RecognizeResult | None = None
- best_num_matched_entities = 0
- best_num_unmatched_entities = 0
- best_num_unmatched_ranges = 0
for result in recognize_all(
user_input.text,
lang_intents.intents,
@@ -512,80 +512,36 @@ class DefaultAgent(ConversationEntity):
continue
# Don't count missing entities that couldn't be filled from context
- num_matched_entities = 0
- for matched_entity in result.entities_list:
- if matched_entity.name not in result.unmatched_entities:
- num_matched_entities += 1
-
num_unmatched_entities = 0
- num_unmatched_ranges = 0
- for unmatched_entity in result.unmatched_entities_list:
- if isinstance(unmatched_entity, UnmatchedTextEntity):
- if unmatched_entity.text != MISSING_ENTITY:
+ for entity in result.unmatched_entities_list:
+ if isinstance(entity, UnmatchedTextEntity):
+ if entity.text != MISSING_ENTITY:
num_unmatched_entities += 1
- elif isinstance(unmatched_entity, UnmatchedRangeEntity):
- num_unmatched_ranges += 1
- num_unmatched_entities += 1
else:
num_unmatched_entities += 1
- if (
- (maybe_result is None) # first result
- or (num_matched_entities > best_num_matched_entities)
- or (
- # Fewer unmatched entities
- (num_matched_entities == best_num_matched_entities)
- and (num_unmatched_entities < best_num_unmatched_entities)
- )
- or (
- # Prefer unmatched ranges
- (num_matched_entities == best_num_matched_entities)
- and (num_unmatched_entities == best_num_unmatched_entities)
- and (num_unmatched_ranges > best_num_unmatched_ranges)
- )
- or (
- # More literal text matched
- (num_matched_entities == best_num_matched_entities)
- and (num_unmatched_entities == best_num_unmatched_entities)
- and (num_unmatched_ranges == best_num_unmatched_ranges)
- and (result.text_chunks_matched > maybe_result.text_chunks_matched)
- )
- or (
- # Prefer match failures with entities
- (result.text_chunks_matched == maybe_result.text_chunks_matched)
- and (num_unmatched_entities == best_num_unmatched_entities)
- and (num_unmatched_ranges == best_num_unmatched_ranges)
- and (
- ("name" in result.entities)
- or ("name" in result.unmatched_entities)
- )
- )
- ):
+ if maybe_result is None:
+ # First result
maybe_result = result
- best_num_matched_entities = num_matched_entities
best_num_unmatched_entities = num_unmatched_entities
- best_num_unmatched_ranges = num_unmatched_ranges
+ elif num_unmatched_entities < best_num_unmatched_entities:
+ # Fewer unmatched entities
+ maybe_result = result
+ best_num_unmatched_entities = num_unmatched_entities
+ elif num_unmatched_entities == best_num_unmatched_entities:
+ if (result.text_chunks_matched > maybe_result.text_chunks_matched) or (
+ (result.text_chunks_matched == maybe_result.text_chunks_matched)
+ and ("name" in result.unmatched_entities) # prefer entities
+ ):
+ # More literal text chunks matched, but prefer entities to areas, etc.
+ maybe_result = result
- return maybe_result
+ if (maybe_result is not None) and maybe_result.unmatched_entities:
+ # Failed to match, but we have more information about why in unmatched_entities
+ return maybe_result
- def _recognize_strict(
- self,
- user_input: ConversationInput,
- lang_intents: LanguageIntents,
- slot_lists: dict[str, SlotList],
- intent_context: dict[str, Any] | None,
- language: str,
- ) -> RecognizeResult | None:
- """Search intents for a strict match to user input."""
- return recognize_best(
- user_input.text,
- lang_intents.intents,
- slot_lists=slot_lists,
- intent_context=intent_context,
- language=language,
- best_metadata_key=METADATA_CUSTOM_SENTENCE,
- best_slot_name="name",
- )
+ # Complete match failure
+ return None
async def _build_speech(
self,
@@ -868,18 +824,20 @@ class DefaultAgent(ConversationEntity):
start = time.monotonic()
entity_registry = er.async_get(self.hass)
+ states = [
+ state
+ for state in self.hass.states.async_all()
+ if async_should_expose(self.hass, DOMAIN, state.entity_id)
+ ]
- # Gather entity names, keeping track of exposed names.
- # We try intent recognition with only exposed names first, then all names.
+ # Gather exposed entity names.
#
# NOTE: We do not pass entity ids in here because multiple entities may
# have the same name. The intent matcher doesn't gather all matching
# values for a list, just the first. So we will need to match by name no
# matter what.
- exposed_entity_names = []
- for state in self.hass.states.async_all():
- is_exposed = async_should_expose(self.hass, DOMAIN, state.entity_id)
-
+ entity_names = []
+ for state in states:
# Checked against "requires_context" and "excludes_context" in hassil
context = {"domain": state.domain}
if state.attributes:
@@ -889,23 +847,24 @@ class DefaultAgent(ConversationEntity):
continue
context[attr] = state.attributes[attr]
- if (
- entity := entity_registry.async_get(state.entity_id)
- ) and entity.aliases:
+ entity = entity_registry.async_get(state.entity_id)
+
+ if not entity:
+ # Default name
+ entity_names.append((state.name, state.name, context))
+ continue
+
+ if entity.aliases:
for alias in entity.aliases:
if not alias.strip():
continue
- name_tuple = (alias, alias, context)
- if is_exposed:
- exposed_entity_names.append(name_tuple)
+ entity_names.append((alias, alias, context))
# Default name
- name_tuple = (state.name, state.name, context)
- if is_exposed:
- exposed_entity_names.append(name_tuple)
+ entity_names.append((state.name, state.name, context))
- _LOGGER.debug("Exposed entities: %s", exposed_entity_names)
+ _LOGGER.debug("Exposed entities: %s", entity_names)
# Expose all areas.
areas = ar.async_get(self.hass)
@@ -939,9 +898,7 @@ class DefaultAgent(ConversationEntity):
self._slot_lists = {
"area": TextSlotList.from_tuples(area_names, allow_template=False),
- "name": TextSlotList.from_tuples(
- exposed_entity_names, allow_template=False
- ),
+ "name": TextSlotList.from_tuples(entity_names, allow_template=False),
"floor": TextSlotList.from_tuples(floor_names, allow_template=False),
}
@@ -1135,10 +1092,6 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
if matched_area_entity := result.entities.get("area"):
matched_area = matched_area_entity.text.strip()
- matched_floor: str | None = None
- if matched_floor_entity := result.entities.get("floor"):
- matched_floor = matched_floor_entity.text.strip()
-
if unmatched_name := unmatched_text.get("name"):
if matched_area:
# device in area
@@ -1146,12 +1099,6 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
"entity": unmatched_name,
"area": matched_area,
}
- if matched_floor:
- # device on floor
- return ErrorKey.NO_ENTITY_IN_FLOOR, {
- "entity": unmatched_name,
- "floor": matched_floor,
- }
# device only
return ErrorKey.NO_ENTITY, {"entity": unmatched_name}
@@ -1234,62 +1181,17 @@ def _get_match_error_response(
if reason == intent.MatchFailedReason.STATE:
# Entity is not in correct state
- assert constraints.states
- state = next(iter(constraints.states))
- if constraints.domains:
+ assert match_error.constraints.states
+ state = next(iter(match_error.constraints.states))
+ if match_error.constraints.domains:
# Translate if domain is available
- domain = next(iter(constraints.domains))
+ domain = next(iter(match_error.constraints.domains))
state = translation.async_translate_state(
hass, state, domain, None, None, None
)
return ErrorKey.ENTITY_WRONG_STATE, {"state": state}
- if reason == intent.MatchFailedReason.ASSISTANT:
- # Not exposed
- if constraints.name:
- if constraints.area_name:
- return ErrorKey.NO_ENTITY_IN_AREA_EXPOSED, {
- "entity": constraints.name,
- "area": constraints.area_name,
- }
- if constraints.floor_name:
- return ErrorKey.NO_ENTITY_IN_FLOOR_EXPOSED, {
- "entity": constraints.name,
- "floor": constraints.floor_name,
- }
- return ErrorKey.NO_ENTITY_EXPOSED, {"entity": constraints.name}
-
- if constraints.device_classes:
- device_class = next(iter(constraints.device_classes))
-
- if constraints.area_name:
- return ErrorKey.NO_DEVICE_CLASS_IN_AREA_EXPOSED, {
- "device_class": device_class,
- "area": constraints.area_name,
- }
- if constraints.floor_name:
- return ErrorKey.NO_DEVICE_CLASS_IN_FLOOR_EXPOSED, {
- "device_class": device_class,
- "floor": constraints.floor_name,
- }
- return ErrorKey.NO_DEVICE_CLASS_EXPOSED, {"device_class": device_class}
-
- if constraints.domains:
- domain = next(iter(constraints.domains))
-
- if constraints.area_name:
- return ErrorKey.NO_DOMAIN_IN_AREA_EXPOSED, {
- "domain": domain,
- "area": constraints.area_name,
- }
- if constraints.floor_name:
- return ErrorKey.NO_DOMAIN_IN_FLOOR_EXPOSED, {
- "domain": domain,
- "floor": constraints.floor_name,
- }
- return ErrorKey.NO_DOMAIN_EXPOSED, {"domain": domain}
-
# Default error
return ErrorKey.NO_INTENT, {}
diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py
index 5e5800ad6f1..df1ffc7f74f 100644
--- a/homeassistant/components/conversation/http.py
+++ b/homeassistant/components/conversation/http.py
@@ -6,8 +6,12 @@ from collections.abc import Iterable
from typing import Any
from aiohttp import web
-from hassil.recognize import MISSING_ENTITY, RecognizeResult
-from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
+from hassil.recognize import (
+ MISSING_ENTITY,
+ RecognizeResult,
+ UnmatchedRangeEntity,
+ UnmatchedTextEntity,
+)
import voluptuous as vol
from homeassistant.components import http, websocket_api
diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json
index 1676cdf8254..c2168ce7152 100644
--- a/homeassistant/components/conversation/manifest.json
+++ b/homeassistant/components/conversation/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
- "requirements": ["hassil==2.0.1", "home-assistant-intents==2024.11.13"]
+ "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.10.2"]
}
diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py
index a4f64ffbad9..ec7ecc76da0 100644
--- a/homeassistant/components/conversation/trigger.py
+++ b/homeassistant/components/conversation/trigger.py
@@ -4,8 +4,7 @@ from __future__ import annotations
from typing import Any
-from hassil.recognize import RecognizeResult
-from hassil.util import PUNCTUATION_ALL
+from hassil.recognize import PUNCTUATION, RecognizeResult
import voluptuous as vol
from homeassistant.const import CONF_COMMAND, CONF_PLATFORM
@@ -21,7 +20,7 @@ from .const import DATA_DEFAULT_ENTITY, DOMAIN
def has_no_punctuation(value: list[str]) -> list[str]:
"""Validate result does not contain punctuation."""
for sentence in value:
- if PUNCTUATION_ALL.search(sentence):
+ if PUNCTUATION.search(sentence):
raise vol.Invalid("sentence should not contain punctuation")
return value
diff --git a/homeassistant/components/crownstone/config_flow.py b/homeassistant/components/crownstone/config_flow.py
index bf6e9204714..7d86fbbd7fb 100644
--- a/homeassistant/components/crownstone/config_flow.py
+++ b/homeassistant/components/crownstone/config_flow.py
@@ -213,19 +213,18 @@ class CrownstoneOptionsFlowHandler(BaseCrownstoneFlowHandler, OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize Crownstone options."""
super().__init__(OPTIONS_FLOW, self.async_create_new_entry)
- self.options = config_entry.options.copy()
+ self.entry = config_entry
+ self.updated_options = config_entry.options.copy()
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage Crownstone options."""
- self.cloud: CrownstoneCloud = self.hass.data[DOMAIN][
- self.config_entry.entry_id
- ].cloud
+ self.cloud: CrownstoneCloud = self.hass.data[DOMAIN][self.entry.entry_id].cloud
spheres = {sphere.name: sphere.cloud_id for sphere in self.cloud.cloud_data}
- usb_path = self.config_entry.options.get(CONF_USB_PATH)
- usb_sphere = self.config_entry.options.get(CONF_USB_SPHERE)
+ usb_path = self.entry.options.get(CONF_USB_PATH)
+ usb_sphere = self.entry.options.get(CONF_USB_SPHERE)
options_schema = vol.Schema(
{vol.Optional(CONF_USE_USB_OPTION, default=usb_path is not None): bool}
@@ -244,14 +243,14 @@ class CrownstoneOptionsFlowHandler(BaseCrownstoneFlowHandler, OptionsFlow):
if user_input[CONF_USE_USB_OPTION] and usb_path is None:
return await self.async_step_usb_config()
if not user_input[CONF_USE_USB_OPTION] and usb_path is not None:
- self.options[CONF_USB_PATH] = None
- self.options[CONF_USB_SPHERE] = None
+ self.updated_options[CONF_USB_PATH] = None
+ self.updated_options[CONF_USB_SPHERE] = None
elif (
CONF_USB_SPHERE_OPTION in user_input
and spheres[user_input[CONF_USB_SPHERE_OPTION]] != usb_sphere
):
sphere_id = spheres[user_input[CONF_USB_SPHERE_OPTION]]
- self.options[CONF_USB_SPHERE] = sphere_id
+ self.updated_options[CONF_USB_SPHERE] = sphere_id
return self.async_create_new_entry()
@@ -261,7 +260,7 @@ class CrownstoneOptionsFlowHandler(BaseCrownstoneFlowHandler, OptionsFlow):
"""Create a new entry."""
# these attributes will only change when a usb was configured
if self.usb_path is not None and self.usb_sphere_id is not None:
- self.options[CONF_USB_PATH] = self.usb_path
- self.options[CONF_USB_SPHERE] = self.usb_sphere_id
+ self.updated_options[CONF_USB_PATH] = self.usb_path
+ self.updated_options[CONF_USB_SPHERE] = self.usb_sphere_id
- return super().async_create_entry(title="", data=self.options)
+ return super().async_create_entry(title="", data=self.updated_options)
diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py
index 39e92ab1921..f1fc0473115 100644
--- a/homeassistant/components/daikin/climate.py
+++ b/homeassistant/components/daikin/climate.py
@@ -159,7 +159,6 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
if values:
await self.device.set(values)
- await self.coordinator.async_refresh()
@property
def unique_id(self) -> str:
@@ -262,7 +261,6 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
await self.device.set_advanced_mode(
HA_PRESET_TO_DAIKIN[PRESET_ECO], ATTR_STATE_OFF
)
- await self.coordinator.async_refresh()
@property
def preset_modes(self) -> list[str]:
@@ -277,11 +275,9 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
async def async_turn_on(self) -> None:
"""Turn device on."""
await self.device.set({})
- await self.coordinator.async_refresh()
async def async_turn_off(self) -> None:
"""Turn device off."""
await self.device.set(
{HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_STATE_TO_DAIKIN[HVACMode.OFF]}
)
- await self.coordinator.async_refresh()
diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py
index 669048ac45e..23517d085d2 100644
--- a/homeassistant/components/daikin/switch.py
+++ b/homeassistant/components/daikin/switch.py
@@ -63,12 +63,10 @@ class DaikinZoneSwitch(DaikinEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the zone on."""
await self.device.set_zone(self._zone_id, "zone_onoff", "1")
- await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the zone off."""
await self.device.set_zone(self._zone_id, "zone_onoff", "0")
- await self.coordinator.async_refresh()
class DaikinStreamerSwitch(DaikinEntity, SwitchEntity):
@@ -90,12 +88,10 @@ class DaikinStreamerSwitch(DaikinEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the zone on."""
await self.device.set_streamer("on")
- await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the zone off."""
await self.device.set_streamer("off")
- await self.coordinator.async_refresh()
class DaikinToggleSwitch(DaikinEntity, SwitchEntity):
@@ -116,9 +112,7 @@ class DaikinToggleSwitch(DaikinEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the zone on."""
await self.device.set({})
- await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the zone off."""
await self.device.set({DAIKIN_ATTR_MODE: "off"})
- await self.coordinator.async_refresh()
diff --git a/homeassistant/components/deako/manifest.json b/homeassistant/components/deako/manifest.json
index e3099439b9d..e8f6f235107 100644
--- a/homeassistant/components/deako/manifest.json
+++ b/homeassistant/components/deako/manifest.json
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/deako",
"iot_class": "local_polling",
"loggers": ["pydeako"],
- "requirements": ["pydeako==0.5.4"],
+ "requirements": ["pydeako==0.4.0"],
"single_config_entry": true,
"zeroconf": ["_deako._tcp.local."]
}
diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py
index 678e441a7a9..2f9bda6d5ed 100644
--- a/homeassistant/components/deconz/alarm_control_panel.py
+++ b/homeassistant/components/deconz/alarm_control_panel.py
@@ -13,10 +13,18 @@ from homeassistant.components.alarm_control_panel import (
DOMAIN as ALARM_CONTROl_PANEL_DOMAIN,
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -24,16 +32,16 @@ from .entity import DeconzDevice
from .hub import DeconzHub
DECONZ_TO_ALARM_STATE = {
- AncillaryControlPanel.ARMED_AWAY: AlarmControlPanelState.ARMED_AWAY,
- AncillaryControlPanel.ARMED_NIGHT: AlarmControlPanelState.ARMED_NIGHT,
- AncillaryControlPanel.ARMED_STAY: AlarmControlPanelState.ARMED_HOME,
- AncillaryControlPanel.ARMING_AWAY: AlarmControlPanelState.ARMING,
- AncillaryControlPanel.ARMING_NIGHT: AlarmControlPanelState.ARMING,
- AncillaryControlPanel.ARMING_STAY: AlarmControlPanelState.ARMING,
- AncillaryControlPanel.DISARMED: AlarmControlPanelState.DISARMED,
- AncillaryControlPanel.ENTRY_DELAY: AlarmControlPanelState.PENDING,
- AncillaryControlPanel.EXIT_DELAY: AlarmControlPanelState.PENDING,
- AncillaryControlPanel.IN_ALARM: AlarmControlPanelState.TRIGGERED,
+ AncillaryControlPanel.ARMED_AWAY: STATE_ALARM_ARMED_AWAY,
+ AncillaryControlPanel.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT,
+ AncillaryControlPanel.ARMED_STAY: STATE_ALARM_ARMED_HOME,
+ AncillaryControlPanel.ARMING_AWAY: STATE_ALARM_ARMING,
+ AncillaryControlPanel.ARMING_NIGHT: STATE_ALARM_ARMING,
+ AncillaryControlPanel.ARMING_STAY: STATE_ALARM_ARMING,
+ AncillaryControlPanel.DISARMED: STATE_ALARM_DISARMED,
+ AncillaryControlPanel.ENTRY_DELAY: STATE_ALARM_PENDING,
+ AncillaryControlPanel.EXIT_DELAY: STATE_ALARM_PENDING,
+ AncillaryControlPanel.IN_ALARM: STATE_ALARM_TRIGGERED,
}
@@ -97,7 +105,7 @@ class DeconzAlarmControlPanel(DeconzDevice[AncillaryControl], AlarmControlPanelE
super().async_update_callback()
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the control panel."""
if self._device.panel in DECONZ_TO_ALARM_STATE:
return DECONZ_TO_ALARM_STATE[self._device.panel]
diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py
index ed54701f656..d017e2c5c65 100644
--- a/homeassistant/components/deconz/config_flow.py
+++ b/homeassistant/components/deconz/config_flow.py
@@ -20,6 +20,7 @@ from pydeconz.utils import (
import voluptuous as vol
from homeassistant.components import ssdp
+from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.config_entries import (
SOURCE_HASSIO,
ConfigEntry,
@@ -30,7 +31,6 @@ from homeassistant.config_entries import (
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from .const import (
CONF_ALLOW_CLIP_SENSOR,
@@ -74,11 +74,9 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> DeconzOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
- return DeconzOptionsFlowHandler()
+ return DeconzOptionsFlowHandler(config_entry)
def __init__(self) -> None:
"""Initialize the deCONZ config flow."""
@@ -301,6 +299,11 @@ class DeconzOptionsFlowHandler(OptionsFlow):
gateway: DeconzHub
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize deCONZ options flow."""
+ self.config_entry = config_entry
+ self.options = dict(config_entry.options)
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -312,7 +315,8 @@ class DeconzOptionsFlowHandler(OptionsFlow):
) -> ConfigFlowResult:
"""Manage the deconz devices options."""
if user_input is not None:
- return self.async_create_entry(data=self.config_entry.options | user_input)
+ self.options.update(user_input)
+ return self.async_create_entry(title="", data=self.options)
schema_options = {}
for option, default in (
diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py
index 2aeeece3ac5..e31fdc66db2 100644
--- a/homeassistant/components/deconz/device_trigger.py
+++ b/homeassistant/components/deconz/device_trigger.py
@@ -169,30 +169,6 @@ FRIENDS_OF_HUE_SWITCH = {
(CONF_LONG_RELEASE, CONF_BOTTOM_BUTTONS): {CONF_EVENT: 6003},
}
-RODRET_REMOTE_MODEL = "RODRET Dimmer"
-RODRET_REMOTE = {
- (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002},
- (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001},
- (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003},
- (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 2002},
- (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 2001},
- (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 2003},
-}
-
-SOMRIG_REMOTE_MODEL = "SOMRIG shortcut button"
-SOMRIG_REMOTE = {
- (CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 1000},
- (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002},
- (CONF_LONG_PRESS, CONF_BUTTON_1): {CONF_EVENT: 1001},
- (CONF_LONG_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1003},
- (CONF_DOUBLE_PRESS, CONF_BUTTON_1): {CONF_EVENT: 1004},
- (CONF_SHORT_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2000},
- (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2002},
- (CONF_LONG_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2001},
- (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2003},
- (CONF_DOUBLE_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2004},
-}
-
STYRBAR_REMOTE_MODEL = "Remote Control N2"
STYRBAR_REMOTE = {
(CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 1002},
@@ -624,8 +600,6 @@ REMOTES = {
HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE,
HUE_WALL_REMOTE_MODEL: HUE_WALL_REMOTE,
FRIENDS_OF_HUE_SWITCH_MODEL: FRIENDS_OF_HUE_SWITCH,
- RODRET_REMOTE_MODEL: RODRET_REMOTE,
- SOMRIG_REMOTE_MODEL: SOMRIG_REMOTE,
STYRBAR_REMOTE_MODEL: STYRBAR_REMOTE,
SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER,
TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH,
diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py
index 95a97959d5b..a15aeb5a059 100644
--- a/homeassistant/components/deconz/light.py
+++ b/homeassistant/components/deconz/light.py
@@ -39,22 +39,7 @@ from .hub import DeconzHub
DECONZ_GROUP = "is_deconz_group"
EFFECT_TO_DECONZ = {
EFFECT_COLORLOOP: LightEffect.COLOR_LOOP,
- "none": LightEffect.NONE,
- # Specific to Philips Hue
- "candle": LightEffect.CANDLE,
- "cosmos": LightEffect.COSMOS,
- "enchant": LightEffect.ENCHANT,
- "fire": LightEffect.FIRE,
- "fireplace": LightEffect.FIREPLACE,
- "glisten": LightEffect.GLISTEN,
- "loop": LightEffect.LOOP,
- "opal": LightEffect.OPAL,
- "prism": LightEffect.PRISM,
- "sparkle": LightEffect.SPARKLE,
- "sunbeam": LightEffect.SUNBEAM,
- "sunrise": LightEffect.SUNRISE,
- "sunset": LightEffect.SUNSET,
- "underwater": LightEffect.UNDERWATER,
+ "None": LightEffect.NONE,
# Specific to Lidl christmas light
"carnival": LightEffect.CARNIVAL,
"collide": LightEffect.COLLIDE,
@@ -223,17 +208,8 @@ class DeconzBaseLight[_LightDeviceT: Group | Light](
if device.effect is not None:
self._attr_supported_features |= LightEntityFeature.EFFECT
self._attr_effect_list = [EFFECT_COLORLOOP]
-
- # For lights that report supported effects.
- if isinstance(device, Light):
- if device.supported_effects is not None:
- self._attr_effect_list = [
- EFFECT_TO_DECONZ[el]
- for el in device.supported_effects
- if el in EFFECT_TO_DECONZ
- ]
- if device.model_id in ("HG06467", "TS0601"):
- self._attr_effect_list = XMAS_LIGHT_EFFECTS
+ if device.model_id in ("HG06467", "TS0601"):
+ self._attr_effect_list = XMAS_LIGHT_EFFECTS
@property
def color_mode(self) -> str | None:
diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json
index 04aaa6bc324..2f58cacfa2c 100644
--- a/homeassistant/components/deconz/manifest.json
+++ b/homeassistant/components/deconz/manifest.json
@@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["pydeconz"],
"quality_scale": "platinum",
- "requirements": ["pydeconz==118"],
+ "requirements": ["pydeconz==116"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",
diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json
index 8299fe43f09..cbadb704a42 100644
--- a/homeassistant/components/default_config/manifest.json
+++ b/homeassistant/components/default_config/manifest.json
@@ -9,10 +9,10 @@
"conversation",
"dhcp",
"energy",
- "go2rtc",
"history",
"homeassistant_alerts",
"logbook",
+ "map",
"media_source",
"mobile_app",
"my",
diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py
index d34830042d7..f9b791668e8 100644
--- a/homeassistant/components/demo/alarm_control_panel.py
+++ b/homeassistant/components/demo/alarm_control_panel.py
@@ -4,10 +4,20 @@ from __future__ import annotations
import datetime
-from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.components.manual.alarm_control_panel import ManualAlarm
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_ARMING_TIME, CONF_DELAY_TIME, CONF_TRIGGER_TIME
+from homeassistant.const import (
+ CONF_ARMING_TIME,
+ CONF_DELAY_TIME,
+ CONF_TRIGGER_TIME,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -29,36 +39,36 @@ async def async_setup_entry(
True,
False,
{
- AlarmControlPanelState.ARMED_AWAY: {
+ STATE_ALARM_ARMED_AWAY: {
CONF_ARMING_TIME: datetime.timedelta(seconds=5),
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
- AlarmControlPanelState.ARMED_HOME: {
+ STATE_ALARM_ARMED_HOME: {
CONF_ARMING_TIME: datetime.timedelta(seconds=5),
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
- AlarmControlPanelState.ARMED_NIGHT: {
+ STATE_ALARM_ARMED_NIGHT: {
CONF_ARMING_TIME: datetime.timedelta(seconds=5),
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
- AlarmControlPanelState.ARMED_VACATION: {
+ STATE_ALARM_ARMED_VACATION: {
CONF_ARMING_TIME: datetime.timedelta(seconds=5),
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
- AlarmControlPanelState.DISARMED: {
+ STATE_ALARM_DISARMED: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS: {
+ STATE_ALARM_ARMED_CUSTOM_BYPASS: {
CONF_ARMING_TIME: datetime.timedelta(seconds=5),
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
- AlarmControlPanelState.TRIGGERED: {
+ STATE_ALARM_TRIGGERED: {
CONF_ARMING_TIME: datetime.timedelta(seconds=5)
},
},
diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py
index 53c1678aa81..c866873732c 100644
--- a/homeassistant/components/demo/config_flow.py
+++ b/homeassistant/components/demo/config_flow.py
@@ -39,6 +39,9 @@ class DemoConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Set the config entry up from yaml."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
return self.async_create_entry(title="Demo", data=import_data)
@@ -47,6 +50,7 @@ class OptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
+ self.config_entry = config_entry
self.options = dict(config_entry.options)
async def async_step_init(
diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json
index be3456b5619..887a82a0078 100644
--- a/homeassistant/components/demo/manifest.json
+++ b/homeassistant/components/demo/manifest.json
@@ -5,6 +5,5 @@
"dependencies": ["conversation", "group", "zone"],
"documentation": "https://www.home-assistant.io/integrations/demo",
"iot_class": "calculated",
- "quality_scale": "internal",
- "single_config_entry": true
+ "quality_scale": "internal"
}
diff --git a/homeassistant/components/demo/update.py b/homeassistant/components/demo/update.py
index 3fa037f6b02..7e53f5ce8ca 100644
--- a/homeassistant/components/demo/update.py
+++ b/homeassistant/components/demo/update.py
@@ -75,21 +75,6 @@ async def async_setup_entry(
support_release_notes=True,
release_url="https://www.example.com/release/1.93.3",
device_class=UpdateDeviceClass.FIRMWARE,
- update_steps=10,
- ),
- DemoUpdate(
- unique_id="update_support_decimal_progress",
- device_name="Demo Update with Decimal Progress",
- title="Philips Lamps Firmware",
- installed_version="1.93.3",
- latest_version="1.94.2",
- support_progress=True,
- release_summary="Added support for effects",
- support_release_notes=True,
- release_url="https://www.example.com/release/1.93.3",
- device_class=UpdateDeviceClass.FIRMWARE,
- display_precision=2,
- update_steps=1000,
),
]
)
@@ -121,13 +106,10 @@ class DemoUpdate(UpdateEntity):
support_install: bool = True,
support_release_notes: bool = False,
device_class: UpdateDeviceClass | None = None,
- display_precision: int = 0,
- update_steps: int = 100,
) -> None:
"""Initialize the Demo select entity."""
self._attr_installed_version = installed_version
self._attr_device_class = device_class
- self._attr_display_precision = display_precision
self._attr_latest_version = latest_version
self._attr_release_summary = release_summary
self._attr_release_url = release_url
@@ -137,7 +119,6 @@ class DemoUpdate(UpdateEntity):
identifiers={(DOMAIN, unique_id)},
name=device_name,
)
- self._update_steps = update_steps
if support_install:
self._attr_supported_features |= (
UpdateEntityFeature.INSTALL
@@ -155,14 +136,12 @@ class DemoUpdate(UpdateEntity):
) -> None:
"""Install an update."""
if self.supported_features & UpdateEntityFeature.PROGRESS:
- self._attr_in_progress = True
- for progress in range(0, self._update_steps, 1):
- self._attr_update_percentage = progress / (self._update_steps / 100)
+ for progress in range(0, 100, 10):
+ self._attr_in_progress = progress
self.async_write_ha_state()
await _fake_install()
self._attr_in_progress = False
- self._attr_update_percentage = None
self._attr_installed_version = (
version if version is not None else self.latest_version
)
diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py
index 9ff05411588..9a7d2a30438 100644
--- a/homeassistant/components/denonavr/config_flow.py
+++ b/homeassistant/components/denonavr/config_flow.py
@@ -52,6 +52,10 @@ CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str})
class OptionsFlowHandler(OptionsFlow):
"""Options for the component."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Init object."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -115,7 +119,7 @@ class DenonAvrFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
diff --git a/homeassistant/components/denonavr/receiver.py b/homeassistant/components/denonavr/receiver.py
index cbafe35cfc5..ebe09f518fb 100644
--- a/homeassistant/components/denonavr/receiver.py
+++ b/homeassistant/components/denonavr/receiver.py
@@ -3,11 +3,9 @@
from __future__ import annotations
from collections.abc import Callable
-import contextlib
import logging
from denonavr import DenonAVR
-from denonavr.exceptions import AvrProcessingError
import httpx
_LOGGER = logging.getLogger(__name__)
@@ -96,8 +94,7 @@ class ConnectDenonAVR:
# Do an initial update if telnet is used.
if self._use_telnet:
for zone in receiver.zones.values():
- with contextlib.suppress(AvrProcessingError):
- await zone.async_update()
+ await zone.async_update()
if self._update_audyssey:
await zone.async_update_audyssey()
await receiver.async_telnet_connect()
diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py
index 77ce5169d8d..be27201bda9 100644
--- a/homeassistant/components/derivative/sensor.py
+++ b/homeassistant/components/derivative/sensor.py
@@ -5,6 +5,7 @@ from __future__ import annotations
from datetime import datetime, timedelta
from decimal import Decimal, DecimalException
import logging
+from typing import TYPE_CHECKING
import voluptuous as vol
@@ -161,7 +162,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
self._attr_device_info = device_info
self._sensor_source_id = source_entity
self._round_digits = round_digits
- self._attr_native_value = round(Decimal(0), round_digits)
+ self._state: float | int | Decimal = 0
# List of tuples with (timestamp_start, timestamp_end, derivative)
self._state_list: list[tuple[datetime, datetime, Decimal]] = []
@@ -189,10 +190,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
restored_data.native_unit_of_measurement
)
try:
- self._attr_native_value = round(
- Decimal(restored_data.native_value), # type: ignore[arg-type]
- self._round_digits,
- )
+ self._state = Decimal(restored_data.native_value) # type: ignore[arg-type]
except SyntaxError as err:
_LOGGER.warning("Could not restore last state: %s", err)
@@ -272,11 +270,12 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
if elapsed_time > self._time_window:
derivative = new_derivative
else:
- derivative = Decimal(0.00)
+ derivative = Decimal(0)
for start, end, value in self._state_list:
weight = calculate_weight(start, end, new_state.last_updated)
derivative = derivative + (value * Decimal(weight))
- self._attr_native_value = round(derivative, self._round_digits)
+
+ self._state = derivative
self.async_write_ha_state()
self.async_on_remove(
@@ -284,3 +283,11 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
self.hass, self._sensor_source_id, calc_derivative
)
)
+
+ @property
+ def native_value(self) -> float | int | Decimal:
+ """Return the state of the sensor."""
+ value = round(self._state, self._round_digits)
+ if TYPE_CHECKING:
+ assert isinstance(value, (float, int, Decimal))
+ return value
diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py
index 70a94531431..f8a0f015543 100644
--- a/homeassistant/components/devolo_home_network/__init__.py
+++ b/homeassistant/components/devolo_home_network/__init__.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-from asyncio import Semaphore
from dataclasses import dataclass
import logging
from typing import Any
@@ -33,7 +32,7 @@ from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.httpx_client import get_async_client
-from homeassistant.helpers.update_coordinator import UpdateFailed
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
CONNECTED_PLC_DEVICES,
@@ -48,7 +47,6 @@ from .const import (
SWITCH_GUEST_WIFI,
SWITCH_LEDS,
)
-from .coordinator import DevoloDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -60,7 +58,7 @@ class DevoloHomeNetworkData:
"""The devolo Home Network data."""
device: Device
- coordinators: dict[str, DevoloDataUpdateCoordinator[Any]]
+ coordinators: dict[str, DataUpdateCoordinator[Any]]
async def async_setup_entry(
@@ -70,7 +68,6 @@ async def async_setup_entry(
zeroconf_instance = await zeroconf.async_get_async_instance(hass)
async_client = get_async_client(hass)
device_registry = dr.async_get(hass)
- semaphore = Semaphore(1)
try:
device = Device(
@@ -166,72 +163,58 @@ async def async_setup_entry(
"""Disconnect from device."""
await device.async_disconnect()
- coordinators: dict[str, DevoloDataUpdateCoordinator[Any]] = {}
+ coordinators: dict[str, DataUpdateCoordinator[Any]] = {}
if device.plcnet:
- coordinators[CONNECTED_PLC_DEVICES] = DevoloDataUpdateCoordinator(
+ coordinators[CONNECTED_PLC_DEVICES] = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=CONNECTED_PLC_DEVICES,
- semaphore=semaphore,
update_method=async_update_connected_plc_devices,
update_interval=LONG_UPDATE_INTERVAL,
)
if device.device and "led" in device.device.features:
- coordinators[SWITCH_LEDS] = DevoloDataUpdateCoordinator(
+ coordinators[SWITCH_LEDS] = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=SWITCH_LEDS,
- semaphore=semaphore,
update_method=async_update_led_status,
update_interval=SHORT_UPDATE_INTERVAL,
)
if device.device and "restart" in device.device.features:
- coordinators[LAST_RESTART] = DevoloDataUpdateCoordinator(
+ coordinators[LAST_RESTART] = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=LAST_RESTART,
- semaphore=semaphore,
update_method=async_update_last_restart,
update_interval=SHORT_UPDATE_INTERVAL,
)
if device.device and "update" in device.device.features:
- coordinators[REGULAR_FIRMWARE] = DevoloDataUpdateCoordinator(
+ coordinators[REGULAR_FIRMWARE] = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=REGULAR_FIRMWARE,
- semaphore=semaphore,
update_method=async_update_firmware_available,
update_interval=FIRMWARE_UPDATE_INTERVAL,
)
if device.device and "wifi1" in device.device.features:
- coordinators[CONNECTED_WIFI_CLIENTS] = DevoloDataUpdateCoordinator(
+ coordinators[CONNECTED_WIFI_CLIENTS] = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=CONNECTED_WIFI_CLIENTS,
- semaphore=semaphore,
update_method=async_update_wifi_connected_station,
update_interval=SHORT_UPDATE_INTERVAL,
)
- coordinators[NEIGHBORING_WIFI_NETWORKS] = DevoloDataUpdateCoordinator(
+ coordinators[NEIGHBORING_WIFI_NETWORKS] = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=NEIGHBORING_WIFI_NETWORKS,
- semaphore=semaphore,
update_method=async_update_wifi_neighbor_access_points,
update_interval=LONG_UPDATE_INTERVAL,
)
- coordinators[SWITCH_GUEST_WIFI] = DevoloDataUpdateCoordinator(
+ coordinators[SWITCH_GUEST_WIFI] = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=SWITCH_GUEST_WIFI,
- semaphore=semaphore,
update_method=async_update_guest_wifi_status,
update_interval=SHORT_UPDATE_INTERVAL,
)
diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py
index 5752956ffb5..c96d0273a50 100644
--- a/homeassistant/components/devolo_home_network/binary_sensor.py
+++ b/homeassistant/components/devolo_home_network/binary_sensor.py
@@ -15,13 +15,13 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import DevoloHomeNetworkConfigEntry
from .const import CONNECTED_PLC_DEVICES, CONNECTED_TO_ROUTER
-from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
-PARALLEL_UPDATES = 0
+PARALLEL_UPDATES = 1
def _is_connected_to_router(entity: DevoloBinarySensorEntity) -> bool:
@@ -78,7 +78,7 @@ class DevoloBinarySensorEntity(
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
- coordinator: DevoloDataUpdateCoordinator[LogicalNetwork],
+ coordinator: DataUpdateCoordinator[LogicalNetwork],
description: DevoloBinarySensorEntityDescription,
) -> None:
"""Initialize entity."""
diff --git a/homeassistant/components/devolo_home_network/button.py b/homeassistant/components/devolo_home_network/button.py
index 06822ff199e..ca17b572522 100644
--- a/homeassistant/components/devolo_home_network/button.py
+++ b/homeassistant/components/devolo_home_network/button.py
@@ -22,7 +22,7 @@ from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN, IDENTIFY, PAIRING, RESTART, START_WPS
from .entity import DevoloEntity
-PARALLEL_UPDATES = 0
+PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
diff --git a/homeassistant/components/devolo_home_network/coordinator.py b/homeassistant/components/devolo_home_network/coordinator.py
deleted file mode 100644
index c0af9668279..00000000000
--- a/homeassistant/components/devolo_home_network/coordinator.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""Base coordinator."""
-
-from asyncio import Semaphore
-from collections.abc import Awaitable, Callable
-from datetime import timedelta
-from logging import Logger
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
-
-
-class DevoloDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
- """Class to manage fetching data from devolo Home Network devices."""
-
- def __init__(
- self,
- hass: HomeAssistant,
- logger: Logger,
- *,
- config_entry: ConfigEntry,
- name: str,
- semaphore: Semaphore,
- update_interval: timedelta,
- update_method: Callable[[], Awaitable[_DataT]],
- ) -> None:
- """Initialize global data updater."""
- super().__init__(
- hass,
- logger,
- config_entry=config_entry,
- name=name,
- update_interval=update_interval,
- update_method=update_method,
- )
- self._semaphore = semaphore
-
- async def _async_update_data(self) -> _DataT:
- """Fetch the latest data from the source."""
- async with self._semaphore:
- return await super()._async_update_data()
diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py
index 583f022df84..d372ba3d468 100644
--- a/homeassistant/components/devolo_home_network/device_tracker.py
+++ b/homeassistant/components/devolo_home_network/device_tracker.py
@@ -13,13 +13,15 @@ from homeassistant.const import STATE_UNKNOWN, UnitOfFrequency
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
from . import DevoloHomeNetworkConfigEntry
from .const import CONNECTED_WIFI_CLIENTS, DOMAIN, WIFI_APTYPE, WIFI_BANDS
-from .coordinator import DevoloDataUpdateCoordinator
-PARALLEL_UPDATES = 0
+PARALLEL_UPDATES = 1
async def async_setup_entry(
@@ -29,7 +31,7 @@ async def async_setup_entry(
) -> None:
"""Get all devices and sensors and setup them via config entry."""
device = entry.runtime_data.device
- coordinators: dict[str, DevoloDataUpdateCoordinator[list[ConnectedStationInfo]]] = (
+ coordinators: dict[str, DataUpdateCoordinator[list[ConnectedStationInfo]]] = (
entry.runtime_data.coordinators
)
registry = er.async_get(hass)
@@ -49,7 +51,7 @@ async def async_setup_entry(
)
)
tracked.add(station.mac_address)
- async_add_entities(new_entities)
+ async_add_entities(new_entities)
@callback
def restore_entities() -> None:
@@ -81,16 +83,14 @@ async def async_setup_entry(
)
-# The pylint disable is needed because of https://github.com/pylint-dev/pylint/issues/9138
-class DevoloScannerEntity( # pylint: disable=hass-enforce-class-module
- CoordinatorEntity[DevoloDataUpdateCoordinator[list[ConnectedStationInfo]]],
- ScannerEntity,
+class DevoloScannerEntity(
+ CoordinatorEntity[DataUpdateCoordinator[list[ConnectedStationInfo]]], ScannerEntity
):
"""Representation of a devolo device tracker."""
def __init__(
self,
- coordinator: DevoloDataUpdateCoordinator[list[ConnectedStationInfo]],
+ coordinator: DataUpdateCoordinator[list[ConnectedStationInfo]],
device: Device,
mac: str,
) -> None:
diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py
index 93ec1b9a3a2..d381f48ca05 100644
--- a/homeassistant/components/devolo_home_network/entity.py
+++ b/homeassistant/components/devolo_home_network/entity.py
@@ -9,14 +9,15 @@ from devolo_plc_api.device_api import (
)
from devolo_plc_api.plcnet_api import DataRate, LogicalNetwork
-from homeassistant.const import ATTR_CONNECTIONS
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN
-from .coordinator import DevoloDataUpdateCoordinator
type _DataType = (
LogicalNetwork
@@ -44,6 +45,7 @@ class DevoloEntity(Entity):
self._attr_device_info = DeviceInfo(
configuration_url=f"http://{self.device.ip}",
+ connections={(CONNECTION_NETWORK_MAC, self.device.mac)},
identifiers={(DOMAIN, str(self.device.serial_number))},
manufacturer="devolo",
model=self.device.product,
@@ -51,10 +53,6 @@ class DevoloEntity(Entity):
serial_number=self.device.serial_number,
sw_version=self.device.firmware_version,
)
- if self.device.mac:
- self._attr_device_info[ATTR_CONNECTIONS] = {
- (CONNECTION_NETWORK_MAC, self.device.mac)
- }
self._attr_translation_key = self.entity_description.key
self._attr_unique_id = (
f"{self.device.serial_number}_{self.entity_description.key}"
@@ -62,14 +60,14 @@ class DevoloEntity(Entity):
class DevoloCoordinatorEntity[_DataT: _DataType](
- CoordinatorEntity[DevoloDataUpdateCoordinator[_DataT]], DevoloEntity
+ CoordinatorEntity[DataUpdateCoordinator[_DataT]], DevoloEntity
):
"""Representation of a coordinated devolo home network device."""
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
- coordinator: DevoloDataUpdateCoordinator[_DataT],
+ coordinator: DataUpdateCoordinator[_DataT],
) -> None:
"""Initialize a devolo home network device."""
super().__init__(coordinator)
diff --git a/homeassistant/components/devolo_home_network/image.py b/homeassistant/components/devolo_home_network/image.py
index 240686ed3bb..58052d3021e 100644
--- a/homeassistant/components/devolo_home_network/image.py
+++ b/homeassistant/components/devolo_home_network/image.py
@@ -13,14 +13,14 @@ from homeassistant.components.image import ImageEntity, ImageEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
import homeassistant.util.dt as dt_util
from . import DevoloHomeNetworkConfigEntry
from .const import IMAGE_GUEST_WIFI, SWITCH_GUEST_WIFI
-from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
-PARALLEL_UPDATES = 0
+PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
@@ -66,7 +66,7 @@ class DevoloImageEntity(DevoloCoordinatorEntity[WifiGuestAccessGet], ImageEntity
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
- coordinator: DevoloDataUpdateCoordinator[WifiGuestAccessGet],
+ coordinator: DataUpdateCoordinator[WifiGuestAccessGet],
description: DevoloImageEntityDescription,
) -> None:
"""Initialize entity."""
diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py
index 220ab66312a..667bbc2c557 100644
--- a/homeassistant/components/devolo_home_network/sensor.py
+++ b/homeassistant/components/devolo_home_network/sensor.py
@@ -6,7 +6,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import StrEnum
-from typing import Any
+from typing import Any, Generic, TypeVar
from devolo_plc_api.device_api import ConnectedStationInfo, NeighborAPInfo
from devolo_plc_api.plcnet_api import REMOTE, DataRate, LogicalNetwork
@@ -20,6 +20,7 @@ from homeassistant.components.sensor import (
from homeassistant.const import EntityCategory, UnitOfDataRate
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.dt import utcnow
from . import DevoloHomeNetworkConfigEntry
@@ -31,10 +32,9 @@ from .const import (
PLC_RX_RATE,
PLC_TX_RATE,
)
-from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
-PARALLEL_UPDATES = 0
+PARALLEL_UPDATES = 1
def _last_restart(runtime: int) -> datetime:
@@ -47,10 +47,26 @@ def _last_restart(runtime: int) -> datetime:
)
-type _CoordinatorDataType = (
- LogicalNetwork | DataRate | list[ConnectedStationInfo] | list[NeighborAPInfo] | int
+_CoordinatorDataT = TypeVar(
+ "_CoordinatorDataT",
+ bound=LogicalNetwork
+ | DataRate
+ | list[ConnectedStationInfo]
+ | list[NeighborAPInfo]
+ | int,
+)
+_ValueDataT = TypeVar(
+ "_ValueDataT",
+ bound=LogicalNetwork
+ | DataRate
+ | list[ConnectedStationInfo]
+ | list[NeighborAPInfo]
+ | int,
+)
+_SensorDataT = TypeVar(
+ "_SensorDataT",
+ bound=int | float | datetime,
)
-type _SensorDataType = int | float | datetime
class DataRateDirection(StrEnum):
@@ -61,10 +77,9 @@ class DataRateDirection(StrEnum):
@dataclass(frozen=True, kw_only=True)
-class DevoloSensorEntityDescription[
- _CoordinatorDataT: _CoordinatorDataType,
- _SensorDataT: _SensorDataType,
-](SensorEntityDescription):
+class DevoloSensorEntityDescription(
+ SensorEntityDescription, Generic[_CoordinatorDataT, _SensorDataT]
+):
"""Describes devolo sensor entity."""
value_func: Callable[[_CoordinatorDataT], _SensorDataT]
@@ -185,11 +200,8 @@ async def async_setup_entry(
async_add_entities(entities)
-class BaseDevoloSensorEntity[
- _CoordinatorDataT: _CoordinatorDataType,
- _ValueDataT: _CoordinatorDataType,
- _SensorDataT: _SensorDataType,
-](
+class BaseDevoloSensorEntity(
+ Generic[_CoordinatorDataT, _ValueDataT, _SensorDataT],
DevoloCoordinatorEntity[_CoordinatorDataT],
SensorEntity,
):
@@ -198,7 +210,7 @@ class BaseDevoloSensorEntity[
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
- coordinator: DevoloDataUpdateCoordinator[_CoordinatorDataT],
+ coordinator: DataUpdateCoordinator[_CoordinatorDataT],
description: DevoloSensorEntityDescription[_ValueDataT, _SensorDataT],
) -> None:
"""Initialize entity."""
@@ -206,11 +218,9 @@ class BaseDevoloSensorEntity[
super().__init__(entry, coordinator)
-class DevoloSensorEntity[
- _CoordinatorDataT: _CoordinatorDataType,
- _ValueDataT: _CoordinatorDataType,
- _SensorDataT: _SensorDataType,
-](BaseDevoloSensorEntity[_CoordinatorDataT, _ValueDataT, _SensorDataT]):
+class DevoloSensorEntity(
+ BaseDevoloSensorEntity[_CoordinatorDataT, _CoordinatorDataT, _SensorDataT]
+):
"""Representation of a generic devolo sensor."""
entity_description: DevoloSensorEntityDescription[_CoordinatorDataT, _SensorDataT]
@@ -231,7 +241,7 @@ class DevoloPlcDataRateSensorEntity(
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
- coordinator: DevoloDataUpdateCoordinator[LogicalNetwork],
+ coordinator: DataUpdateCoordinator[LogicalNetwork],
description: DevoloSensorEntityDescription[DataRate, float],
peer: str,
) -> None:
diff --git a/homeassistant/components/devolo_home_network/switch.py b/homeassistant/components/devolo_home_network/switch.py
index 8ff35dcc4b6..c3400916d78 100644
--- a/homeassistant/components/devolo_home_network/switch.py
+++ b/homeassistant/components/devolo_home_network/switch.py
@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
-from typing import Any
+from typing import Any, Generic, TypeVar
from devolo_plc_api.device import Device
from devolo_plc_api.device_api import WifiGuestAccessGet
@@ -15,19 +15,19 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN, SWITCH_GUEST_WIFI, SWITCH_LEDS
-from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
-PARALLEL_UPDATES = 0
+PARALLEL_UPDATES = 1
-type _DataType = WifiGuestAccessGet | bool
+_DataT = TypeVar("_DataT", bound=WifiGuestAccessGet | bool)
@dataclass(frozen=True, kw_only=True)
-class DevoloSwitchEntityDescription[_DataT: _DataType](SwitchEntityDescription):
+class DevoloSwitchEntityDescription(SwitchEntityDescription, Generic[_DataT]):
"""Describes devolo switch entity."""
is_on_func: Callable[[_DataT], bool]
@@ -81,9 +81,7 @@ async def async_setup_entry(
async_add_entities(entities)
-class DevoloSwitchEntity[_DataT: _DataType](
- DevoloCoordinatorEntity[_DataT], SwitchEntity
-):
+class DevoloSwitchEntity(DevoloCoordinatorEntity[_DataT], SwitchEntity):
"""Representation of a devolo switch."""
entity_description: DevoloSwitchEntityDescription[_DataT]
@@ -91,7 +89,7 @@ class DevoloSwitchEntity[_DataT: _DataType](
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
- coordinator: DevoloDataUpdateCoordinator[_DataT],
+ coordinator: DataUpdateCoordinator[_DataT],
description: DevoloSwitchEntityDescription[_DataT],
) -> None:
"""Initialize entity."""
diff --git a/homeassistant/components/devolo_home_network/update.py b/homeassistant/components/devolo_home_network/update.py
index 5091ce8e1e7..29c0c8762b9 100644
--- a/homeassistant/components/devolo_home_network/update.py
+++ b/homeassistant/components/devolo_home_network/update.py
@@ -20,13 +20,13 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN, REGULAR_FIRMWARE
-from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
-PARALLEL_UPDATES = 0
+PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
@@ -79,7 +79,7 @@ class DevoloUpdateEntity(DevoloCoordinatorEntity, UpdateEntity):
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
- coordinator: DevoloDataUpdateCoordinator,
+ coordinator: DataUpdateCoordinator,
description: DevoloUpdateEntityDescription,
) -> None:
"""Initialize entity."""
diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py
index b9a3bdba12d..5ff95fae47e 100644
--- a/homeassistant/components/dexcom/__init__.py
+++ b/homeassistant/components/dexcom/__init__.py
@@ -46,7 +46,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator[GlucoseReading](
hass,
_LOGGER,
- config_entry=entry,
name=DOMAIN,
update_method=async_update_data,
update_interval=SCAN_INTERVAL,
diff --git a/homeassistant/components/dexcom/config_flow.py b/homeassistant/components/dexcom/config_flow.py
index c5c830dedf6..c3ed43c8e9a 100644
--- a/homeassistant/components/dexcom/config_flow.py
+++ b/homeassistant/components/dexcom/config_flow.py
@@ -69,12 +69,16 @@ class DexcomConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> DexcomOptionsFlowHandler:
"""Get the options flow for this handler."""
- return DexcomOptionsFlowHandler()
+ return DexcomOptionsFlowHandler(config_entry)
class DexcomOptionsFlowHandler(OptionsFlow):
"""Handle a option flow for Dexcom."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py
index 75f50192500..06ac935e8d9 100644
--- a/homeassistant/components/dlna_dmr/config_flow.py
+++ b/homeassistant/components/dlna_dmr/config_flow.py
@@ -74,7 +74,7 @@ class DlnaDmrFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Define the config flow to handle options."""
- return DlnaDmrOptionsFlowHandler()
+ return DlnaDmrOptionsFlowHandler(config_entry)
async def async_step_user(self, user_input: FlowInput = None) -> ConfigFlowResult:
"""Handle a flow initialized by the user.
@@ -327,6 +327,10 @@ class DlnaDmrOptionsFlowHandler(OptionsFlow):
Configures the single instance and updates the existing config entry.
"""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py
index 8c2cfa5e556..6dda0c03910 100644
--- a/homeassistant/components/dnsip/config_flow.py
+++ b/homeassistant/components/dnsip/config_flow.py
@@ -14,7 +14,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_NAME, CONF_PORT
from homeassistant.core import callback
@@ -101,7 +101,7 @@ class DnsIPConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> DnsIPOptionsFlowHandler:
"""Return Option handler."""
- return DnsIPOptionsFlowHandler()
+ return DnsIPOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -165,7 +165,7 @@ class DnsIPConfigFlow(ConfigFlow, domain=DOMAIN):
)
-class DnsIPOptionsFlowHandler(OptionsFlow):
+class DnsIPOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle a option config flow for dnsip integration."""
async def async_step_init(
diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json
index 7c85ca63467..fabb2c30190 100644
--- a/homeassistant/components/doods/manifest.json
+++ b/homeassistant/components/doods/manifest.json
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/doods",
"iot_class": "local_polling",
"loggers": ["pydoods"],
- "requirements": ["pydoods==1.0.2", "Pillow==11.0.0"]
+ "requirements": ["pydoods==1.0.2", "Pillow==10.4.0"]
}
diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py
index ebb1d6fc126..650ddb8811d 100644
--- a/homeassistant/components/doorbird/config_flow.py
+++ b/homeassistant/components/doorbird/config_flow.py
@@ -213,12 +213,16 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Handle a option flow for doorbird."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json
index 8480a496762..153f552b698 100644
--- a/homeassistant/components/doorbird/manifest.json
+++ b/homeassistant/components/doorbird/manifest.json
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/doorbird",
"iot_class": "local_push",
"loggers": ["doorbirdpy"],
- "requirements": ["DoorBirdPy==3.0.8"],
+ "requirements": ["DoorBirdPy==3.0.4"],
"zeroconf": [
{
"type": "_axis-video._tcp.local.",
diff --git a/homeassistant/components/dormakaba_dkey/__init__.py b/homeassistant/components/dormakaba_dkey/__init__.py
index b4304e75aab..a8868e8563c 100644
--- a/homeassistant/components/dormakaba_dkey/__init__.py
+++ b/homeassistant/components/dormakaba_dkey/__init__.py
@@ -69,7 +69,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=lock.name,
update_method=_async_update,
update_interval=timedelta(seconds=UPDATE_SECONDS),
diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py
index 7d6a641b006..49e1818edcc 100644
--- a/homeassistant/components/dsmr/config_flow.py
+++ b/homeassistant/components/dsmr/config_flow.py
@@ -171,11 +171,9 @@ class DSMRFlowHandler(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> DSMROptionFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> DSMROptionFlowHandler:
"""Get the options flow for this handler."""
- return DSMROptionFlowHandler()
+ return DSMROptionFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -313,6 +311,10 @@ class DSMRFlowHandler(ConfigFlow, domain=DOMAIN):
class DSMROptionFlowHandler(OptionsFlow):
"""Handle options."""
+ def __init__(self, entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.entry = entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -326,7 +328,7 @@ class DSMROptionFlowHandler(OptionsFlow):
{
vol.Optional(
CONF_TIME_BETWEEN_UPDATE,
- default=self.config_entry.options.get(
+ default=self.entry.options.get(
CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE
),
): vol.All(vol.Coerce(int), vol.Range(min=0)),
diff --git a/homeassistant/components/duotecno/config_flow.py b/homeassistant/components/duotecno/config_flow.py
index 51b92d4673a..ca95726542f 100644
--- a/homeassistant/components/duotecno/config_flow.py
+++ b/homeassistant/components/duotecno/config_flow.py
@@ -34,6 +34,9 @@ class DuoTecnoConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
errors: dict[str, str] = {}
if user_input is not None:
try:
diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json
index 2a427e36e84..37ed4457184 100644
--- a/homeassistant/components/duotecno/manifest.json
+++ b/homeassistant/components/duotecno/manifest.json
@@ -7,6 +7,5 @@
"iot_class": "local_push",
"loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"],
"quality_scale": "silver",
- "requirements": ["pyDuotecno==2024.10.1"],
- "single_config_entry": true
+ "requirements": ["pyDuotecno==2024.10.0"]
}
diff --git a/homeassistant/components/duotecno/strings.json b/homeassistant/components/duotecno/strings.json
index 7f7c156768d..2342eeb8288 100644
--- a/homeassistant/components/duotecno/strings.json
+++ b/homeassistant/components/duotecno/strings.json
@@ -13,6 +13,9 @@
}
}
},
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ },
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
diff --git a/homeassistant/components/dwd_weather_warnings/coordinator.py b/homeassistant/components/dwd_weather_warnings/coordinator.py
index 8cf3813a85d..55705625685 100644
--- a/homeassistant/components/dwd_weather_warnings/coordinator.py
+++ b/homeassistant/components/dwd_weather_warnings/coordinator.py
@@ -37,8 +37,8 @@ class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]):
self._device_tracker = None
self._previous_position = None
- async def _async_setup(self) -> None:
- """Set up coordinator."""
+ async def async_config_entry_first_refresh(self) -> None:
+ """Perform first refresh."""
if region_identifier := self.config_entry.data.get(CONF_REGION_IDENTIFIER):
self.api = await self.hass.async_add_executor_job(
DwdWeatherWarningsAPI, region_identifier
@@ -48,6 +48,8 @@ class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]):
CONF_REGION_DEVICE_TRACKER
)
+ await super().async_config_entry_first_refresh()
+
async def _async_update_data(self) -> None:
"""Get the latest data from the DWD Weather Warnings API."""
if self._device_tracker:
diff --git a/homeassistant/components/eafm/__init__.py b/homeassistant/components/eafm/__init__.py
index dc618a983f3..1f95437484f 100644
--- a/homeassistant/components/eafm/__init__.py
+++ b/homeassistant/components/eafm/__init__.py
@@ -48,7 +48,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator[dict[str, dict[str, Any]]](
hass,
_LOGGER,
- config_entry=entry,
name="sensor",
update_method=_async_update_data,
update_interval=timedelta(seconds=15 * 60),
diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py
index 54af6c0f801..6f032fbaae9 100644
--- a/homeassistant/components/ecobee/__init__.py
+++ b/homeassistant/components/ecobee/__init__.py
@@ -6,14 +6,15 @@ from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, Ecobee, ExpiredTokenE
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
-from homeassistant.const import CONF_API_KEY
+from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
-from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import Throttle
from .const import (
_LOGGER,
+ ATTR_CONFIG_ENTRY_ID,
CONF_REFRESH_TOKEN,
DATA_ECOBEE_CONFIG,
DATA_HASS_CONFIG,
@@ -72,6 +73,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+ # The legacy Ecobee notify.notify service is deprecated
+ # was with HA Core 2024.5.0 and will be removed with HA core 2024.11.0
+ hass.async_create_task(
+ discovery.async_load_platform(
+ hass,
+ Platform.NOTIFY,
+ DOMAIN,
+ {CONF_NAME: entry.title, ATTR_CONFIG_ENTRY_ID: entry.entry_id},
+ hass.data[DATA_HASS_CONFIG],
+ )
+ )
+
return True
diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py
index 6a9ec0d5db9..e6801998e0d 100644
--- a/homeassistant/components/ecobee/climate.py
+++ b/homeassistant/components/ecobee/climate.py
@@ -32,8 +32,7 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, ServiceCall
-from homeassistant.exceptions import ServiceValidationError
-from homeassistant.helpers import device_registry as dr, entity_platform
+from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -42,8 +41,6 @@ from homeassistant.util.unit_conversion import TemperatureConverter
from . import EcobeeData
from .const import (
_LOGGER,
- ATTR_ACTIVE_SENSORS,
- ATTR_AVAILABLE_SENSORS,
DOMAIN,
ECOBEE_AUX_HEAT_ONLY,
ECOBEE_MODEL_TO_NAME,
@@ -65,8 +62,6 @@ ATTR_DST_ENABLED = "dst_enabled"
ATTR_MIC_ENABLED = "mic_enabled"
ATTR_AUTO_AWAY = "auto_away"
ATTR_FOLLOW_ME = "follow_me"
-ATTR_SENSOR_LIST = "device_ids"
-ATTR_PRESET_MODE = "preset_mode"
DEFAULT_RESUME_ALL = False
PRESET_AWAY_INDEFINITELY = "away_indefinitely"
@@ -134,7 +129,6 @@ SERVICE_SET_FAN_MIN_ON_TIME = "set_fan_min_on_time"
SERVICE_SET_DST_MODE = "set_dst_mode"
SERVICE_SET_MIC_MODE = "set_mic_mode"
SERVICE_SET_OCCUPANCY_MODES = "set_occupancy_modes"
-SERVICE_SET_SENSORS_USED_IN_CLIMATE = "set_sensors_used_in_climate"
DTGROUP_START_INCLUSIVE_MSG = (
f"{ATTR_START_DATE} and {ATTR_START_TIME} must be specified together"
@@ -223,7 +217,7 @@ async def async_setup_entry(
thermostat["name"],
thermostat["modelNumber"],
)
- entities.append(Thermostat(data, index, thermostat, hass))
+ entities.append(Thermostat(data, index, thermostat))
async_add_entities(entities, True)
@@ -333,15 +327,6 @@ async def async_setup_entry(
"set_occupancy_modes",
)
- platform.async_register_entity_service(
- SERVICE_SET_SENSORS_USED_IN_CLIMATE,
- {
- vol.Optional(ATTR_PRESET_MODE): cv.string,
- vol.Required(ATTR_SENSOR_LIST): cv.ensure_list,
- },
- "set_sensors_used_in_climate",
- )
-
class Thermostat(ClimateEntity):
"""A thermostat class for Ecobee."""
@@ -357,11 +342,7 @@ class Thermostat(ClimateEntity):
_attr_translation_key = "ecobee"
def __init__(
- self,
- data: EcobeeData,
- thermostat_index: int,
- thermostat: dict,
- hass: HomeAssistant,
+ self, data: EcobeeData, thermostat_index: int, thermostat: dict
) -> None:
"""Initialize the thermostat."""
self.data = data
@@ -371,7 +352,6 @@ class Thermostat(ClimateEntity):
self.vacation = None
self._last_active_hvac_mode = HVACMode.HEAT_COOL
self._last_hvac_mode_before_aux_heat = HVACMode.HEAT_COOL
- self._hass = hass
self._attr_hvac_modes = []
if self.settings["heatStages"] or self.settings["hasHeatPump"]:
@@ -381,11 +361,7 @@ class Thermostat(ClimateEntity):
if len(self._attr_hvac_modes) == 2:
self._attr_hvac_modes.insert(0, HVACMode.HEAT_COOL)
self._attr_hvac_modes.append(HVACMode.OFF)
- self._sensors = self.remote_sensors
- self._preset_modes = {
- comfort["climateRef"]: comfort["name"]
- for comfort in self.thermostat["program"]["climates"]
- }
+
self.update_without_throttle = False
async def async_update(self) -> None:
@@ -576,8 +552,6 @@ class Thermostat(ClimateEntity):
return HVACAction.IDLE
- _unrecorded_attributes = frozenset({ATTR_AVAILABLE_SENSORS, ATTR_ACTIVE_SENSORS})
-
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return device specific state attributes."""
@@ -589,62 +563,8 @@ class Thermostat(ClimateEntity):
),
"equipment_running": status,
"fan_min_on_time": self.settings["fanMinOnTime"],
- ATTR_AVAILABLE_SENSORS: self.remote_sensor_devices,
- ATTR_ACTIVE_SENSORS: self.active_sensor_devices_in_preset_mode,
}
- @property
- def remote_sensors(self) -> list:
- """Return the remote sensor names of the thermostat."""
- sensors_info = self.thermostat.get("remoteSensors", [])
- return [sensor["name"] for sensor in sensors_info if sensor.get("name")]
-
- @property
- def remote_sensor_devices(self) -> list:
- """Return the remote sensor device name_by_user or name for the thermostat."""
- return sorted(
- [
- f'{item["name_by_user"]} ({item["id"]})'
- for item in self.remote_sensor_ids_names
- ]
- )
-
- @property
- def remote_sensor_ids_names(self) -> list:
- """Return the remote sensor device id and name_by_user for the thermostat."""
- sensors_info = self.thermostat.get("remoteSensors", [])
- device_registry = dr.async_get(self._hass)
-
- return [
- {
- "id": device.id,
- "name_by_user": device.name_by_user
- if device.name_by_user
- else device.name,
- }
- for device in device_registry.devices.values()
- for sensor_info in sensors_info
- if device.name == sensor_info["name"]
- ]
-
- @property
- def active_sensors_in_preset_mode(self) -> list:
- """Return the currently active/participating sensors."""
- # https://support.ecobee.com/s/articles/SmartSensors-Sensor-Participation
- # During a manual hold, the ecobee will follow the Sensor Participation
- # rules for the Home Comfort Settings
- mode = self._preset_modes.get(self.preset_mode, "Home")
- return self._sensors_in_preset_mode(mode)
-
- @property
- def active_sensor_devices_in_preset_mode(self) -> list:
- """Return the currently active/participating sensor devices."""
- # https://support.ecobee.com/s/articles/SmartSensors-Sensor-Participation
- # During a manual hold, the ecobee will follow the Sensor Participation
- # rules for the Home Comfort Settings
- mode = self._preset_modes.get(self.preset_mode, "Home")
- return self._sensor_devices_in_preset_mode(mode)
-
def set_preset_mode(self, preset_mode: str) -> None:
"""Activate a preset."""
preset_mode = HASS_TO_ECOBEE_PRESET.get(preset_mode, preset_mode)
@@ -821,115 +741,6 @@ class Thermostat(ClimateEntity):
)
self.update_without_throttle = True
- def set_sensors_used_in_climate(
- self, device_ids: list[str], preset_mode: str | None = None
- ) -> None:
- """Set the sensors used on a climate for a thermostat."""
- if preset_mode is None:
- preset_mode = self.preset_mode
-
- # Check if climate is an available preset option.
- elif preset_mode not in self._preset_modes.values():
- if self.preset_modes:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="invalid_preset",
- translation_placeholders={
- "options": ", ".join(self._preset_modes.values())
- },
- )
-
- # Get device name from device id.
- device_registry = dr.async_get(self.hass)
- sensor_names: list[str] = []
- sensor_ids: list[str] = []
- for device_id in device_ids:
- device = device_registry.async_get(device_id)
- if device and device.name:
- r_sensors = self.thermostat.get("remoteSensors", [])
- ecobee_identifier = next(
- (
- identifier
- for identifier in device.identifiers
- if identifier[0] == "ecobee"
- ),
- None,
- )
- if ecobee_identifier:
- code = ecobee_identifier[1]
- for r_sensor in r_sensors:
- if ( # occurs if remote sensor
- len(code) == 4 and r_sensor.get("code") == code
- ) or ( # occurs if thermostat
- len(code) != 4 and r_sensor.get("type") == "thermostat"
- ):
- sensor_ids.append(r_sensor.get("id")) # noqa: PERF401
- sensor_names.append(device.name)
-
- # Ensure sensors provided are available for thermostat or not empty.
- if not set(sensor_names).issubset(set(self._sensors)) or not sensor_names:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="invalid_sensor",
- translation_placeholders={
- "options": ", ".join(
- [
- f'{item["name_by_user"]} ({item["id"]})'
- for item in self.remote_sensor_ids_names
- ]
- )
- },
- )
-
- # Check that an id was found for each sensor
- if len(device_ids) != len(sensor_ids):
- raise ServiceValidationError(
- translation_domain=DOMAIN, translation_key="sensor_lookup_failed"
- )
-
- # Check if sensors are currently used on the climate for the thermostat.
- current_sensors_in_climate = self._sensors_in_preset_mode(preset_mode)
- if set(sensor_names) == set(current_sensors_in_climate):
- _LOGGER.debug(
- "This action would not be an update, current sensors on climate (%s) are: %s",
- preset_mode,
- ", ".join(current_sensors_in_climate),
- )
- return
-
- _LOGGER.debug(
- "Setting sensors %s to be used on thermostat %s for program %s",
- sensor_names,
- self.device_info.get("name"),
- preset_mode,
- )
- self.data.ecobee.update_climate_sensors(
- self.thermostat_index, preset_mode, sensor_ids=sensor_ids
- )
- self.update_without_throttle = True
-
- def _sensors_in_preset_mode(self, preset_mode: str | None) -> list[str]:
- """Return current sensors used in climate."""
- climates = self.thermostat["program"]["climates"]
- for climate in climates:
- if climate.get("name") == preset_mode:
- return [sensor["name"] for sensor in climate["sensors"]]
-
- return []
-
- def _sensor_devices_in_preset_mode(self, preset_mode: str | None) -> list[str]:
- """Return current sensor device name_by_user or name used in climate."""
- device_registry = dr.async_get(self._hass)
- sensor_names = self._sensors_in_preset_mode(preset_mode)
- return sorted(
- [
- device.name_by_user if device.name_by_user else device.name
- for device in device_registry.devices.values()
- for sensor_name in sensor_names
- if device.name == sensor_name
- ]
- )
-
def hold_preference(self):
"""Return user preference setting for hold time."""
# Values returned from thermostat are:
diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py
index 687d9173a66..f7709c68d91 100644
--- a/homeassistant/components/ecobee/config_flow.py
+++ b/homeassistant/components/ecobee/config_flow.py
@@ -29,6 +29,10 @@ class EcobeeFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initiated by the user."""
+ if self._async_current_entries():
+ # Config entry already exists, only one allowed.
+ return self.async_abort(reason="single_instance_allowed")
+
errors = {}
stored_api_key = (
self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY)
diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py
index d0e9ba8e8e9..85a332f3c87 100644
--- a/homeassistant/components/ecobee/const.py
+++ b/homeassistant/components/ecobee/const.py
@@ -23,8 +23,6 @@ DOMAIN = "ecobee"
DATA_ECOBEE_CONFIG = "ecobee_config"
DATA_HASS_CONFIG = "ecobee_hass_config"
ATTR_CONFIG_ENTRY_ID = "entry_id"
-ATTR_AVAILABLE_SENSORS = "available_sensors"
-ATTR_ACTIVE_SENSORS = "active_sensors"
CONF_REFRESH_TOKEN = "refresh_token"
diff --git a/homeassistant/components/ecobee/icons.json b/homeassistant/components/ecobee/icons.json
index 647a14dc5d5..f24f1f7cfe5 100644
--- a/homeassistant/components/ecobee/icons.json
+++ b/homeassistant/components/ecobee/icons.json
@@ -20,9 +20,6 @@
},
"set_occupancy_modes": {
"service": "mdi:eye-settings"
- },
- "set_sensors_used_in_climate": {
- "service": "mdi:home-thermometer"
}
}
}
diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json
index 20b346b776b..83dd18fdaa2 100644
--- a/homeassistant/components/ecobee/manifest.json
+++ b/homeassistant/components/ecobee/manifest.json
@@ -10,7 +10,6 @@
"iot_class": "cloud_polling",
"loggers": ["pyecobee"],
"requirements": ["python-ecobee-api==0.2.20"],
- "single_config_entry": true,
"zeroconf": [
{
"type": "_ecobee._tcp.local."
diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py
index 28cfbebe506..167233e4071 100644
--- a/homeassistant/components/ecobee/notify.py
+++ b/homeassistant/components/ecobee/notify.py
@@ -2,16 +2,66 @@
from __future__ import annotations
-from homeassistant.components.notify import NotifyEntity
+from functools import partial
+from typing import Any
+
+from homeassistant.components.notify import (
+ ATTR_TARGET,
+ BaseNotificationService,
+ NotifyEntity,
+ migrate_notify_issue,
+)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
-from . import EcobeeData
+from . import Ecobee, EcobeeData
from .const import DOMAIN
from .entity import EcobeeBaseEntity
+def get_service(
+ hass: HomeAssistant,
+ config: ConfigType,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> EcobeeNotificationService | None:
+ """Get the Ecobee notification service."""
+ if discovery_info is None:
+ return None
+
+ data: EcobeeData = hass.data[DOMAIN]
+ return EcobeeNotificationService(data.ecobee)
+
+
+class EcobeeNotificationService(BaseNotificationService):
+ """Implement the notification service for the Ecobee thermostat."""
+
+ def __init__(self, ecobee: Ecobee) -> None:
+ """Initialize the service."""
+ self.ecobee = ecobee
+
+ async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
+ """Send a message and raise issue."""
+ migrate_notify_issue(
+ self.hass, DOMAIN, "Ecobee", "2024.11.0", service_name=self._service_name
+ )
+ await self.hass.async_add_executor_job(
+ partial(self.send_message, message, **kwargs)
+ )
+
+ def send_message(self, message: str = "", **kwargs: Any) -> None:
+ """Send a message."""
+ targets = kwargs.get(ATTR_TARGET)
+
+ if not targets:
+ raise ValueError("Missing required argument: target")
+
+ for target in targets:
+ thermostat_index = int(target)
+ self.ecobee.send_message(thermostat_index, message)
+
+
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
diff --git a/homeassistant/components/ecobee/number.py b/homeassistant/components/ecobee/number.py
index ed3744bf11e..ab09407903d 100644
--- a/homeassistant/components/ecobee/number.py
+++ b/homeassistant/components/ecobee/number.py
@@ -6,14 +6,9 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import logging
-from homeassistant.components.number import (
- NumberDeviceClass,
- NumberEntity,
- NumberEntityDescription,
- NumberMode,
-)
+from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import UnitOfTemperature, UnitOfTime
+from homeassistant.const import UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -59,30 +54,21 @@ async def async_setup_entry(
) -> None:
"""Set up the ecobee thermostat number entity."""
data: EcobeeData = hass.data[DOMAIN]
+ _LOGGER.debug("Adding min time ventilators numbers (if present)")
- assert data is not None
-
- entities: list[NumberEntity] = [
- EcobeeVentilatorMinTime(data, index, numbers)
- for index, thermostat in enumerate(data.ecobee.thermostats)
- if thermostat["settings"]["ventilatorType"] != "none"
- for numbers in VENTILATOR_NUMBERS
- ]
-
- _LOGGER.debug("Adding compressor min temp number (if present)")
- entities.extend(
+ async_add_entities(
(
- EcobeeCompressorMinTemp(data, index)
+ EcobeeVentilatorMinTime(data, index, numbers)
for index, thermostat in enumerate(data.ecobee.thermostats)
- if thermostat["settings"]["hasHeatPump"]
- )
+ if thermostat["settings"]["ventilatorType"] != "none"
+ for numbers in VENTILATOR_NUMBERS
+ ),
+ True,
)
- async_add_entities(entities, True)
-
class EcobeeVentilatorMinTime(EcobeeBaseEntity, NumberEntity):
- """A number class, representing min time for an ecobee thermostat with ventilator attached."""
+ """A number class, representing min time for an ecobee thermostat with ventilator attached."""
entity_description: EcobeeNumberEntityDescription
@@ -119,53 +105,3 @@ class EcobeeVentilatorMinTime(EcobeeBaseEntity, NumberEntity):
"""Set new ventilator Min On Time value."""
self.entity_description.set_fn(self.data, self.thermostat_index, int(value))
self.update_without_throttle = True
-
-
-class EcobeeCompressorMinTemp(EcobeeBaseEntity, NumberEntity):
- """Minimum outdoor temperature at which the compressor will operate.
-
- This applies more to air source heat pumps than geothermal. This serves as a safety
- feature (compressors have a minimum operating temperature) as well as
- providing the ability to choose fuel in a dual-fuel system (i.e. choose between
- electrical heat pump and fossil auxiliary heat depending on Time of Use, Solar,
- etc.).
- Note that python-ecobee-api refers to this as Aux Cutover Threshold, but Ecobee
- uses Compressor Protection Min Temp.
- """
-
- _attr_device_class = NumberDeviceClass.TEMPERATURE
- _attr_has_entity_name = True
- _attr_icon = "mdi:thermometer-off"
- _attr_mode = NumberMode.BOX
- _attr_native_min_value = -25
- _attr_native_max_value = 66
- _attr_native_step = 5
- _attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT
- _attr_translation_key = "compressor_protection_min_temp"
-
- def __init__(
- self,
- data: EcobeeData,
- thermostat_index: int,
- ) -> None:
- """Initialize ecobee compressor min temperature."""
- super().__init__(data, thermostat_index)
- self._attr_unique_id = f"{self.base_unique_id}_compressor_protection_min_temp"
- self.update_without_throttle = False
-
- async def async_update(self) -> None:
- """Get the latest state from the thermostat."""
- if self.update_without_throttle:
- await self.data.update(no_throttle=True)
- self.update_without_throttle = False
- else:
- await self.data.update()
-
- self._attr_native_value = (
- (self.thermostat["settings"]["compressorProtectionMinTemp"]) / 10
- )
-
- def set_native_value(self, value: float) -> None:
- """Set new compressor minimum temperature."""
- self.data.ecobee.set_aux_cutover_threshold(self.thermostat_index, value)
- self.update_without_throttle = True
diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml
index d58ae81d552..a184f422725 100644
--- a/homeassistant/components/ecobee/services.yaml
+++ b/homeassistant/components/ecobee/services.yaml
@@ -134,23 +134,3 @@ set_occupancy_modes:
follow_me:
selector:
boolean:
-
-set_sensors_used_in_climate:
- target:
- entity:
- integration: ecobee
- domain: climate
- fields:
- preset_mode:
- example: "Home"
- selector:
- text:
- device_ids:
- required: true
- selector:
- device:
- multiple: true
- integration: ecobee
- entity:
- - domain: climate
- - domain: sensor
diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json
index 8c636bd9b04..2af6e5a90f9 100644
--- a/homeassistant/components/ecobee/strings.json
+++ b/homeassistant/components/ecobee/strings.json
@@ -33,18 +33,15 @@
},
"number": {
"ventilator_min_type_home": {
- "name": "Ventilator minimum time home"
+ "name": "Ventilator min time home"
},
"ventilator_min_type_away": {
- "name": "Ventilator minimum time away"
- },
- "compressor_protection_min_temp": {
- "name": "Compressor minimum temperature"
+ "name": "Ventilator min time away"
}
},
"switch": {
"aux_heat_only": {
- "name": "Auxiliary heat only"
+ "name": "Aux heat only"
}
}
},
@@ -170,35 +167,6 @@
"description": "Enable Follow Me mode."
}
}
- },
- "set_sensors_used_in_climate": {
- "name": "Set Sensors Used in Climate",
- "description": "Sets the participating sensors for a climate.",
- "fields": {
- "entity_id": {
- "name": "Entity",
- "description": "Ecobee thermostat on which to set active sensors."
- },
- "preset_mode": {
- "name": "Climate Name",
- "description": "Name of the climate program to set the sensors active on.\nDefaults to currently active program."
- },
- "device_ids": {
- "name": "Sensors",
- "description": "Sensors to set as participating sensors."
- }
- }
- }
- },
- "exceptions": {
- "invalid_preset": {
- "message": "Invalid climate name, available options are: {options}"
- },
- "invalid_sensor": {
- "message": "Invalid sensor for thermostat, available options are: {options}"
- },
- "sensor_lookup_failed": {
- "message": "There was an error getting the sensor ids from sensor names. Try reloading the ecobee integration."
}
},
"issues": {
diff --git a/homeassistant/components/ecobee/switch.py b/homeassistant/components/ecobee/switch.py
index 89ee433c072..67be78fb21d 100644
--- a/homeassistant/components/ecobee/switch.py
+++ b/homeassistant/components/ecobee/switch.py
@@ -31,26 +31,25 @@ async def async_setup_entry(
"""Set up the ecobee thermostat switch entity."""
data: EcobeeData = hass.data[DOMAIN]
- entities: list[SwitchEntity] = [
- EcobeeVentilator20MinSwitch(
- data,
- index,
- (await dt_util.async_get_time_zone(thermostat["location"]["timeZone"]))
- or dt_util.get_default_time_zone(),
- )
- for index, thermostat in enumerate(data.ecobee.thermostats)
- if thermostat["settings"]["ventilatorType"] != "none"
- ]
-
- entities.extend(
- (
- EcobeeSwitchAuxHeatOnly(data, index)
+ async_add_entities(
+ [
+ EcobeeVentilator20MinSwitch(
+ data,
+ index,
+ (await dt_util.async_get_time_zone(thermostat["location"]["timeZone"]))
+ or dt_util.get_default_time_zone(),
+ )
for index, thermostat in enumerate(data.ecobee.thermostats)
- if thermostat["settings"]["hasHeatPump"]
- )
+ if thermostat["settings"]["ventilatorType"] != "none"
+ ],
+ update_before_add=True,
)
- async_add_entities(entities, update_before_add=True)
+ async_add_entities(
+ EcobeeSwitchAuxHeatOnly(data, index)
+ for index, thermostat in enumerate(data.ecobee.thermostats)
+ if thermostat["settings"]["hasHeatPump"]
+ )
class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity):
diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json
index 0ab9f9a4612..33977b3b0de 100644
--- a/homeassistant/components/ecovacs/manifest.json
+++ b/homeassistant/components/ecovacs/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
- "requirements": ["py-sucks==0.9.10", "deebot-client==8.4.1"]
+ "requirements": ["py-sucks==0.9.10", "deebot-client==8.4.0"]
}
diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py
index 5a18a23541a..706ba0db719 100644
--- a/homeassistant/components/egardia/alarm_control_panel.py
+++ b/homeassistant/components/egardia/alarm_control_panel.py
@@ -9,7 +9,13 @@ import requests
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
+)
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -27,13 +33,13 @@ from . import (
_LOGGER = logging.getLogger(__name__)
STATES = {
- "ARM": AlarmControlPanelState.ARMED_AWAY,
- "DAY HOME": AlarmControlPanelState.ARMED_HOME,
- "DISARM": AlarmControlPanelState.DISARMED,
- "ARMHOME": AlarmControlPanelState.ARMED_HOME,
- "HOME": AlarmControlPanelState.ARMED_HOME,
- "NIGHT HOME": AlarmControlPanelState.ARMED_NIGHT,
- "TRIGGERED": AlarmControlPanelState.TRIGGERED,
+ "ARM": STATE_ALARM_ARMED_AWAY,
+ "DAY HOME": STATE_ALARM_ARMED_HOME,
+ "DISARM": STATE_ALARM_DISARMED,
+ "ARMHOME": STATE_ALARM_ARMED_HOME,
+ "HOME": STATE_ALARM_ARMED_HOME,
+ "NIGHT HOME": STATE_ALARM_ARMED_NIGHT,
+ "TRIGGERED": STATE_ALARM_TRIGGERED,
}
@@ -60,6 +66,7 @@ def setup_platform(
class EgardiaAlarm(AlarmControlPanelEntity):
"""Representation of a Egardia alarm."""
+ _attr_state: str | None
_attr_code_arm_required = False
_attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME
@@ -116,7 +123,7 @@ class EgardiaAlarm(AlarmControlPanelEntity):
_LOGGER.debug("Not ignoring status %s", status)
newstatus = STATES.get(status.upper())
_LOGGER.debug("newstatus %s", newstatus)
- self._attr_alarm_state = newstatus
+ self._attr_state = newstatus
else:
_LOGGER.error("Ignoring status")
diff --git a/homeassistant/components/electric_kiwi/api.py b/homeassistant/components/electric_kiwi/api.py
index dead8a6a3c0..89109f01948 100644
--- a/homeassistant/components/electric_kiwi/api.py
+++ b/homeassistant/components/electric_kiwi/api.py
@@ -27,6 +27,7 @@ class AsyncConfigEntryAuth(AbstractAuth):
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
- await self._oauth_session.async_ensure_token_valid()
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
return cast(str, self._oauth_session.token["access_token"])
diff --git a/homeassistant/components/elevenlabs/config_flow.py b/homeassistant/components/elevenlabs/config_flow.py
index 227150a0f4e..b596ec05b00 100644
--- a/homeassistant/components/elevenlabs/config_flow.py
+++ b/homeassistant/components/elevenlabs/config_flow.py
@@ -14,6 +14,7 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
@@ -102,12 +103,13 @@ class ElevenLabsConfigFlow(ConfigFlow, domain=DOMAIN):
return ElevenLabsOptionsFlow(config_entry)
-class ElevenLabsOptionsFlow(OptionsFlow):
+class ElevenLabsOptionsFlow(OptionsFlowWithConfigEntry):
"""ElevenLabs options flow."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
- self.api_key: str = config_entry.data[CONF_API_KEY]
+ super().__init__(config_entry)
+ self.api_key: str = self.config_entry.data[CONF_API_KEY]
# id -> name
self.voices: dict[str, str] = {}
self.models: dict[str, str] = {}
@@ -168,7 +170,7 @@ class ElevenLabsOptionsFlow(OptionsFlow):
vol.Required(CONF_CONFIGURE_VOICE, default=False): bool,
}
),
- self.config_entry.options,
+ self.options,
)
async def async_step_voice_settings(
diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py
index f1ecf626263..f5437b6ed94 100644
--- a/homeassistant/components/elkm1/alarm_control_panel.py
+++ b/homeassistant/components/elkm1/alarm_control_panel.py
@@ -15,9 +15,17 @@ from homeassistant.components.alarm_control_panel import (
ATTR_CHANGED_BY,
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
@@ -117,7 +125,7 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity):
self._changed_by_time: str | None = None
self._changed_by_id: int | None = None
self._changed_by: str | None = None
- self._state: AlarmControlPanelState | None = None
+ self._state: str | None = None
async def async_added_to_hass(self) -> None:
"""Register callback for ElkM1 changes."""
@@ -169,7 +177,7 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity):
return CodeFormat.NUMBER
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the element."""
return self._state
@@ -199,25 +207,23 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity):
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
elk_state_to_hass_state = {
- ArmedStatus.DISARMED: AlarmControlPanelState.DISARMED,
- ArmedStatus.ARMED_AWAY: AlarmControlPanelState.ARMED_AWAY,
- ArmedStatus.ARMED_STAY: AlarmControlPanelState.ARMED_HOME,
- ArmedStatus.ARMED_STAY_INSTANT: AlarmControlPanelState.ARMED_HOME,
- ArmedStatus.ARMED_TO_NIGHT: AlarmControlPanelState.ARMED_NIGHT,
- ArmedStatus.ARMED_TO_NIGHT_INSTANT: AlarmControlPanelState.ARMED_NIGHT,
- ArmedStatus.ARMED_TO_VACATION: AlarmControlPanelState.ARMED_AWAY,
+ ArmedStatus.DISARMED: STATE_ALARM_DISARMED,
+ ArmedStatus.ARMED_AWAY: STATE_ALARM_ARMED_AWAY,
+ ArmedStatus.ARMED_STAY: STATE_ALARM_ARMED_HOME,
+ ArmedStatus.ARMED_STAY_INSTANT: STATE_ALARM_ARMED_HOME,
+ ArmedStatus.ARMED_TO_NIGHT: STATE_ALARM_ARMED_NIGHT,
+ ArmedStatus.ARMED_TO_NIGHT_INSTANT: STATE_ALARM_ARMED_NIGHT,
+ ArmedStatus.ARMED_TO_VACATION: STATE_ALARM_ARMED_AWAY,
}
if self._element.alarm_state is None:
self._state = None
elif self._element.in_alarm_state():
# Area is in alarm state
- self._state = AlarmControlPanelState.TRIGGERED
+ self._state = STATE_ALARM_TRIGGERED
elif self._entry_exit_timer_is_running():
self._state = (
- AlarmControlPanelState.ARMING
- if self._element.is_exit
- else AlarmControlPanelState.PENDING
+ STATE_ALARM_ARMING if self._element.is_exit else STATE_ALARM_PENDING
)
elif self._element.armed_status is not None:
self._state = elk_state_to_hass_state[self._element.armed_status]
diff --git a/homeassistant/components/elmax/alarm_control_panel.py b/homeassistant/components/elmax/alarm_control_panel.py
index 841b94a3d72..4162b177975 100644
--- a/homeassistant/components/elmax/alarm_control_panel.py
+++ b/homeassistant/components/elmax/alarm_control_panel.py
@@ -10,13 +10,20 @@ from elmax_api.model.panel import PanelStatus
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_DISARMING,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, InvalidStateError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import StateType
from .const import DOMAIN
from .coordinator import ElmaxCoordinator
@@ -67,16 +74,16 @@ class ElmaxArea(ElmaxEntity, AlarmControlPanelEntity):
_attr_code_arm_required = False
_attr_has_entity_name = True
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
- _pending_state: AlarmControlPanelState | None = None
+ _pending_state: str | None = None
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
- if self._attr_alarm_state == AlarmStatus.NOT_ARMED_NOT_ARMABLE:
+ if self._attr_state == AlarmStatus.NOT_ARMED_NOT_ARMABLE:
raise InvalidStateError(
f"Cannot arm {self.name}: please check for open windows/doors first"
)
- self._pending_state = AlarmControlPanelState.ARMING
+ self._pending_state = STATE_ALARM_ARMING
self.async_write_ha_state()
try:
@@ -100,7 +107,7 @@ class ElmaxArea(ElmaxEntity, AlarmControlPanelEntity):
if code is None or code == "":
raise ValueError("Please input the disarm code.")
- self._pending_state = AlarmControlPanelState.DISARMING
+ self._pending_state = STATE_ALARM_DISARMING
self.async_write_ha_state()
try:
@@ -123,7 +130,7 @@ class ElmaxArea(ElmaxEntity, AlarmControlPanelEntity):
await self.coordinator.async_refresh()
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> StateType:
"""Return the state of the entity."""
if self._pending_state is not None:
return self._pending_state
@@ -144,10 +151,10 @@ class ElmaxArea(ElmaxEntity, AlarmControlPanelEntity):
ALARM_STATE_TO_HA = {
- AlarmArmStatus.ARMED_TOTALLY: AlarmControlPanelState.ARMED_AWAY,
- AlarmArmStatus.ARMED_P1_P2: AlarmControlPanelState.ARMED_AWAY,
- AlarmArmStatus.ARMED_P2: AlarmControlPanelState.ARMED_AWAY,
- AlarmArmStatus.ARMED_P1: AlarmControlPanelState.ARMED_AWAY,
- AlarmArmStatus.NOT_ARMED: AlarmControlPanelState.DISARMED,
- AlarmStatus.TRIGGERED: AlarmControlPanelState.TRIGGERED,
+ AlarmArmStatus.ARMED_TOTALLY: STATE_ALARM_ARMED_AWAY,
+ AlarmArmStatus.ARMED_P1_P2: STATE_ALARM_ARMED_AWAY,
+ AlarmArmStatus.ARMED_P2: STATE_ALARM_ARMED_AWAY,
+ AlarmArmStatus.ARMED_P1: STATE_ALARM_ARMED_AWAY,
+ AlarmArmStatus.NOT_ARMED: STATE_ALARM_DISARMED,
+ AlarmStatus.TRIGGERED: STATE_ALARM_TRIGGERED,
}
diff --git a/homeassistant/components/emoncms/__init__.py b/homeassistant/components/emoncms/__init__.py
index 0cd686b5b56..98ed6328578 100644
--- a/homeassistant/components/emoncms/__init__.py
+++ b/homeassistant/components/emoncms/__init__.py
@@ -5,11 +5,8 @@ from pyemoncms import EmoncmsClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_URL, Platform
from homeassistant.core import HomeAssistant
-from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
-from .const import DOMAIN, EMONCMS_UUID_DOC_URL, LOGGER
from .coordinator import EmoncmsCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
@@ -17,49 +14,6 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
type EmonCMSConfigEntry = ConfigEntry[EmoncmsCoordinator]
-def _migrate_unique_id(
- hass: HomeAssistant, entry: EmonCMSConfigEntry, emoncms_unique_id: str
-) -> None:
- """Migrate to emoncms unique id if needed."""
- ent_reg = er.async_get(hass)
- entry_entities = ent_reg.entities.get_entries_for_config_entry_id(entry.entry_id)
- for entity in entry_entities:
- if entity.unique_id.split("-")[0] == entry.entry_id:
- feed_id = entity.unique_id.split("-")[-1]
- LOGGER.debug(f"moving feed {feed_id} to hardware uuid")
- ent_reg.async_update_entity(
- entity.entity_id, new_unique_id=f"{emoncms_unique_id}-{feed_id}"
- )
- hass.config_entries.async_update_entry(
- entry,
- unique_id=emoncms_unique_id,
- )
-
-
-async def _check_unique_id_migration(
- hass: HomeAssistant, entry: EmonCMSConfigEntry, emoncms_client: EmoncmsClient
-) -> None:
- """Check if we can migrate to the emoncms uuid."""
- emoncms_unique_id = await emoncms_client.async_get_uuid()
- if emoncms_unique_id:
- if entry.unique_id != emoncms_unique_id:
- _migrate_unique_id(hass, entry, emoncms_unique_id)
- else:
- async_create_issue(
- hass,
- DOMAIN,
- "migrate database",
- is_fixable=False,
- issue_domain=DOMAIN,
- severity=IssueSeverity.WARNING,
- translation_key="migrate_database",
- translation_placeholders={
- "url": entry.data[CONF_URL],
- "doc_url": EMONCMS_UUID_DOC_URL,
- },
- )
-
-
async def async_setup_entry(hass: HomeAssistant, entry: EmonCMSConfigEntry) -> bool:
"""Load a config entry."""
emoncms_client = EmoncmsClient(
@@ -67,7 +21,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: EmonCMSConfigEntry) -> b
entry.data[CONF_API_KEY],
session=async_get_clientsession(hass),
)
- await _check_unique_id_migration(hass, entry, emoncms_client)
coordinator = EmoncmsCoordinator(hass, emoncms_client)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
diff --git a/homeassistant/components/emoncms/config_flow.py b/homeassistant/components/emoncms/config_flow.py
index e0d4d0d03e9..fdd5d29788e 100644
--- a/homeassistant/components/emoncms/config_flow.py
+++ b/homeassistant/components/emoncms/config_flow.py
@@ -1,7 +1,5 @@
"""Configflow for the emoncms integration."""
-from __future__ import annotations
-
from typing import Any
from pyemoncms import EmoncmsClient
@@ -11,10 +9,10 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_API_KEY, CONF_URL
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import selector
from homeassistant.helpers.typing import ConfigType
@@ -48,10 +46,13 @@ def sensor_name(url: str) -> str:
return f"emoncms@{sensorip}"
-async def get_feed_list(
- emoncms_client: EmoncmsClient,
-) -> dict[str, Any]:
+async def get_feed_list(hass: HomeAssistant, url: str, api_key: str) -> dict[str, Any]:
"""Check connection to emoncms and return feed list if successful."""
+ emoncms_client = EmoncmsClient(
+ url,
+ api_key,
+ session=async_get_clientsession(hass),
+ )
return await emoncms_client.async_request("/feed/list.json")
@@ -67,7 +68,7 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
- ) -> EmoncmsOptionsFlow:
+ ) -> OptionsFlowWithConfigEntry:
"""Get the options flow for this handler."""
return EmoncmsOptionsFlow(config_entry)
@@ -76,28 +77,23 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Initiate a flow via the UI."""
errors: dict[str, str] = {}
- description_placeholders = {}
if user_input is not None:
- self.url = user_input[CONF_URL]
- self.api_key = user_input[CONF_API_KEY]
self._async_abort_entries_match(
{
- CONF_API_KEY: self.api_key,
- CONF_URL: self.url,
+ CONF_API_KEY: user_input[CONF_API_KEY],
+ CONF_URL: user_input[CONF_URL],
}
)
- emoncms_client = EmoncmsClient(
- self.url, self.api_key, session=async_get_clientsession(self.hass)
+ result = await get_feed_list(
+ self.hass, user_input[CONF_URL], user_input[CONF_API_KEY]
)
- result = await get_feed_list(emoncms_client)
if not result[CONF_SUCCESS]:
- errors["base"] = "api_error"
- description_placeholders = {"details": result[CONF_MESSAGE]}
+ errors["base"] = result[CONF_MESSAGE]
else:
self.include_only_feeds = user_input.get(CONF_ONLY_INCLUDE_FEEDID)
- await self.async_set_unique_id(await emoncms_client.async_get_uuid())
- self._abort_if_unique_id_configured()
+ self.url = user_input[CONF_URL]
+ self.api_key = user_input[CONF_API_KEY]
options = get_options(result[CONF_MESSAGE])
self.dropdown = {
"options": options,
@@ -117,7 +113,6 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
user_input,
),
errors=errors,
- description_placeholders=description_placeholders,
)
async def async_step_choose_feeds(
@@ -172,41 +167,32 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
return result
-class EmoncmsOptionsFlow(OptionsFlow):
+class EmoncmsOptionsFlow(OptionsFlowWithConfigEntry):
"""Emoncms Options flow handler."""
- def __init__(self, config_entry: ConfigEntry) -> None:
- """Initialize emoncms options flow."""
- self._url = config_entry.data[CONF_URL]
- self._api_key = config_entry.data[CONF_API_KEY]
-
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
errors: dict[str, str] = {}
- description_placeholders = {}
- include_only_feeds = self.config_entry.options.get(
- CONF_ONLY_INCLUDE_FEEDID,
- self.config_entry.data.get(CONF_ONLY_INCLUDE_FEEDID, []),
- )
+ data = self.options if self.options else self._config_entry.data
+ url = data[CONF_URL]
+ api_key = data[CONF_API_KEY]
+ include_only_feeds = data.get(CONF_ONLY_INCLUDE_FEEDID, [])
options: list = include_only_feeds
- emoncms_client = EmoncmsClient(
- self._url,
- self._api_key,
- session=async_get_clientsession(self.hass),
- )
- result = await get_feed_list(emoncms_client)
+ result = await get_feed_list(self.hass, url, api_key)
if not result[CONF_SUCCESS]:
- errors["base"] = "api_error"
- description_placeholders = {"details": result[CONF_MESSAGE]}
+ errors["base"] = result[CONF_MESSAGE]
else:
options = get_options(result[CONF_MESSAGE])
dropdown = {"options": options, "mode": "dropdown", "multiple": True}
if user_input:
include_only_feeds = user_input[CONF_ONLY_INCLUDE_FEEDID]
return self.async_create_entry(
+ title=sensor_name(url),
data={
+ CONF_URL: url,
+ CONF_API_KEY: api_key,
CONF_ONLY_INCLUDE_FEEDID: include_only_feeds,
},
)
@@ -221,5 +207,4 @@ class EmoncmsOptionsFlow(OptionsFlow):
}
),
errors=errors,
- description_placeholders=description_placeholders,
)
diff --git a/homeassistant/components/emoncms/const.py b/homeassistant/components/emoncms/const.py
index c53f7cc8a9f..256db5726bb 100644
--- a/homeassistant/components/emoncms/const.py
+++ b/homeassistant/components/emoncms/const.py
@@ -7,10 +7,6 @@ CONF_ONLY_INCLUDE_FEEDID = "include_only_feed_id"
CONF_MESSAGE = "message"
CONF_SUCCESS = "success"
DOMAIN = "emoncms"
-EMONCMS_UUID_DOC_URL = (
- "https://docs.openenergymonitor.org/emoncms/update.html"
- "#upgrading-to-a-version-producing-a-unique-identifier"
-)
FEED_ID = "id"
FEED_NAME = "name"
FEED_TAG = "tag"
diff --git a/homeassistant/components/emoncms/manifest.json b/homeassistant/components/emoncms/manifest.json
index c7f18cb205e..f8f0f2edb95 100644
--- a/homeassistant/components/emoncms/manifest.json
+++ b/homeassistant/components/emoncms/manifest.json
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/emoncms",
"iot_class": "local_polling",
- "requirements": ["pyemoncms==0.1.1"]
+ "requirements": ["pyemoncms==0.0.7"]
}
diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py
index c696a569135..4add7c9625d 100644
--- a/homeassistant/components/emoncms/sensor.py
+++ b/homeassistant/components/emoncms/sensor.py
@@ -138,30 +138,29 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the emoncms sensors."""
- name = sensor_name(entry.data[CONF_URL])
- exclude_feeds = entry.data.get(CONF_EXCLUDE_FEEDID)
- include_only_feeds = entry.options.get(
- CONF_ONLY_INCLUDE_FEEDID, entry.data.get(CONF_ONLY_INCLUDE_FEEDID)
- )
+ config = entry.options if entry.options else entry.data
+ name = sensor_name(config[CONF_URL])
+ exclude_feeds = config.get(CONF_EXCLUDE_FEEDID)
+ include_only_feeds = config.get(CONF_ONLY_INCLUDE_FEEDID)
if exclude_feeds is None and include_only_feeds is None:
return
coordinator = entry.runtime_data
- # uuid was added in emoncms database 11.5.7
- unique_id = entry.unique_id if entry.unique_id else entry.entry_id
elems = coordinator.data
if not elems:
return
+
sensors: list[EmonCmsSensor] = []
for idx, elem in enumerate(elems):
if include_only_feeds is not None and elem[FEED_ID] not in include_only_feeds:
continue
+
sensors.append(
EmonCmsSensor(
coordinator,
- unique_id,
+ entry.entry_id,
elem["unit"],
name,
idx,
@@ -176,7 +175,7 @@ class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
def __init__(
self,
coordinator: EmoncmsCoordinator,
- unique_id: str,
+ entry_id: str,
unit_of_measurement: str | None,
name: str,
idx: int,
@@ -189,7 +188,7 @@ class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
elem = self.coordinator.data[self.idx]
self._attr_name = f"{name} {elem[FEED_NAME]}"
self._attr_native_unit_of_measurement = unit_of_measurement
- self._attr_unique_id = f"{unique_id}-{elem[FEED_ID]}"
+ self._attr_unique_id = f"{entry_id}-{elem[FEED_ID]}"
if unit_of_measurement in ("kWh", "Wh"):
self._attr_device_class = SensorDeviceClass.ENERGY
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
diff --git a/homeassistant/components/emoncms/strings.json b/homeassistant/components/emoncms/strings.json
index 0d841f2efb4..4a700cc8981 100644
--- a/homeassistant/components/emoncms/strings.json
+++ b/homeassistant/components/emoncms/strings.json
@@ -1,8 +1,5 @@
{
"config": {
- "error": {
- "api_error": "An error occured in the pyemoncms API : {details}"
- },
"step": {
"user": {
"data": {
@@ -19,15 +16,9 @@
"include_only_feed_id": "Choose feeds to include"
}
}
- },
- "abort": {
- "already_configured": "This server is already configured"
}
},
"options": {
- "error": {
- "api_error": "[%key:component::emoncms::config::error::api_error%]"
- },
"step": {
"init": {
"data": {
@@ -44,10 +35,6 @@
"missing_include_only_feed_id": {
"title": "No feed synchronized with the {domain} sensor",
"description": "Configuring {domain} using YAML is being removed.\n\nPlease add manually the feeds you want to synchronize with the `configure` button of the integration."
- },
- "migrate_database": {
- "title": "Upgrade your emoncms version",
- "description": "Your [emoncms]({url}) does not ship a unique identifier.\n\n Please upgrade to at least version 11.5.7 and migrate your emoncms database.\n\n More info on [emoncms documentation]({doc_url})"
}
}
}
diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py
index 4316487352b..7506edae1d3 100644
--- a/homeassistant/components/emonitor/__init__.py
+++ b/homeassistant/components/emonitor/__init__.py
@@ -31,7 +31,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: EmonitorConfigEntry) ->
coordinator = DataUpdateCoordinator[EmonitorStatus](
hass,
_LOGGER,
- config_entry=entry,
name=entry.title,
update_method=emonitor.async_get_status,
update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE),
diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json
index d4889c0c5f5..640a2113d6f 100644
--- a/homeassistant/components/emulated_kasa/manifest.json
+++ b/homeassistant/components/emulated_kasa/manifest.json
@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
- "requirements": ["sense-energy==0.13.3"]
+ "requirements": ["sense-energy==0.12.4"]
}
diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py
index ff86177cf41..9c5a9fbacd1 100644
--- a/homeassistant/components/energy/data.py
+++ b/homeassistant/components/energy/data.py
@@ -331,7 +331,7 @@ class EnergyManager:
"device_consumption",
):
if key in update:
- data[key] = update[key]
+ data[key] = update[key] # type: ignore[literal-required]
self.data = data
self._store.async_delay_save(lambda: data, 60)
diff --git a/homeassistant/components/energy/strings.json b/homeassistant/components/energy/strings.json
index e9d72247319..4a9c1b4aacf 100644
--- a/homeassistant/components/energy/strings.json
+++ b/homeassistant/components/energy/strings.json
@@ -56,10 +56,6 @@
"entity_state_class_measurement_no_last_reset": {
"title": "Last reset missing",
"description": "The following entities have state class 'measurement' but 'last_reset' is missing:"
- },
- "statistics_not_defined": {
- "title": "Statistics not defined",
- "description": "Some entities currently have no statistics metadata. If these are newly created, it may take up to 5 minutes for this to be generated for the following entities:"
}
}
}
diff --git a/homeassistant/components/enocean/config_flow.py b/homeassistant/components/enocean/config_flow.py
index 2452d27b168..fef633d94c3 100644
--- a/homeassistant/components/enocean/config_flow.py
+++ b/homeassistant/components/enocean/config_flow.py
@@ -38,6 +38,9 @@ class EnOceanFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle an EnOcean config flow start."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
return await self.async_step_detect()
async def async_step_detect(
diff --git a/homeassistant/components/enocean/manifest.json b/homeassistant/components/enocean/manifest.json
index 2faba47e126..495ab6618e3 100644
--- a/homeassistant/components/enocean/manifest.json
+++ b/homeassistant/components/enocean/manifest.json
@@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/enocean",
"iot_class": "local_push",
"loggers": ["enocean"],
- "requirements": ["enocean==0.50"],
- "single_config_entry": true
+ "requirements": ["enocean==0.50"]
}
diff --git a/homeassistant/components/enocean/strings.json b/homeassistant/components/enocean/strings.json
index 9d9699481b1..97da526185f 100644
--- a/homeassistant/components/enocean/strings.json
+++ b/homeassistant/components/enocean/strings.json
@@ -18,7 +18,8 @@
"invalid_dongle_path": "No valid dongle found for this path"
},
"abort": {
- "invalid_dongle_path": "Invalid dongle path"
+ "invalid_dongle_path": "Invalid dongle path",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}
diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py
index 23c769293c8..8c1c0983417 100644
--- a/homeassistant/components/enphase_envoy/config_flow.py
+++ b/homeassistant/components/enphase_envoy/config_flow.py
@@ -16,7 +16,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
@@ -66,11 +66,9 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> EnvoyOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> EnvoyOptionsFlowHandler:
"""Options flow handler for Enphase_Envoy."""
- return EnvoyOptionsFlowHandler()
+ return EnvoyOptionsFlowHandler(config_entry)
@callback
def _async_generate_schema(self) -> vol.Schema:
@@ -236,6 +234,12 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Add reconfigure step to allow to manually reconfigure a config entry."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Add reconfigure step to allow to manually reconfigure a config entry."""
reconfigure_entry = self._get_reconfigure_entry()
@@ -281,7 +285,7 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
suggested_values: Mapping[str, Any] = user_input or reconfigure_entry.data
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=self.add_suggested_values_to_schema(
self._async_generate_schema(), suggested_values
),
@@ -290,7 +294,7 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
)
-class EnvoyOptionsFlowHandler(OptionsFlow):
+class EnvoyOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Envoy config flow options handler."""
async def async_step_init(
diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json
index 2d91b3b0960..d8511c58664 100644
--- a/homeassistant/components/enphase_envoy/strings.json
+++ b/homeassistant/components/enphase_envoy/strings.json
@@ -13,7 +13,7 @@
"host": "The hostname or IP address of your Enphase Envoy gateway."
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"description": "[%key:component::enphase_envoy::config::step::user::description%]",
"data": {
"host": "[%key:common::config_flow::data::host%]",
@@ -33,7 +33,6 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "The serial number of the device does not match the previous serial number"
}
},
diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py
index ce65178b8d8..4ad9a927d9c 100644
--- a/homeassistant/components/envisalink/alarm_control_panel.py
+++ b/homeassistant/components/envisalink/alarm_control_panel.py
@@ -9,10 +9,20 @@ import voluptuous as vol
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
-from homeassistant.const import ATTR_ENTITY_ID, CONF_CODE
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_CODE,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
+ STATE_UNKNOWN,
+)
from homeassistant.core import HomeAssistant, ServiceCall, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -134,24 +144,24 @@ class EnvisalinkAlarm(EnvisalinkEntity, AlarmControlPanelEntity):
self.async_write_ha_state()
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str:
"""Return the state of the device."""
- state = None
+ state = STATE_UNKNOWN
if self._info["status"]["alarm"]:
- state = AlarmControlPanelState.TRIGGERED
+ state = STATE_ALARM_TRIGGERED
elif self._info["status"]["armed_zero_entry_delay"]:
- state = AlarmControlPanelState.ARMED_NIGHT
+ state = STATE_ALARM_ARMED_NIGHT
elif self._info["status"]["armed_away"]:
- state = AlarmControlPanelState.ARMED_AWAY
+ state = STATE_ALARM_ARMED_AWAY
elif self._info["status"]["armed_stay"]:
- state = AlarmControlPanelState.ARMED_HOME
+ state = STATE_ALARM_ARMED_HOME
elif self._info["status"]["exit_delay"]:
- state = AlarmControlPanelState.ARMING
+ state = STATE_ALARM_ARMING
elif self._info["status"]["entry_delay"]:
- state = AlarmControlPanelState.PENDING
+ state = STATE_ALARM_PENDING
elif self._info["status"]["alpha"]:
- state = AlarmControlPanelState.DISARMED
+ state = STATE_ALARM_DISARMED
return state
async def async_alarm_disarm(self, code: str | None = None) -> None:
diff --git a/homeassistant/components/eq3btsmart/__init__.py b/homeassistant/components/eq3btsmart/__init__.py
index 84b27161edd..f63e627ea7d 100644
--- a/homeassistant/components/eq3btsmart/__init__.py
+++ b/homeassistant/components/eq3btsmart/__init__.py
@@ -15,23 +15,17 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_send
-from .const import SIGNAL_THERMOSTAT_CONNECTED, SIGNAL_THERMOSTAT_DISCONNECTED
+from .const import DOMAIN, SIGNAL_THERMOSTAT_CONNECTED, SIGNAL_THERMOSTAT_DISCONNECTED
from .models import Eq3Config, Eq3ConfigEntryData
PLATFORMS = [
- Platform.BINARY_SENSOR,
Platform.CLIMATE,
- Platform.NUMBER,
- Platform.SWITCH,
]
_LOGGER = logging.getLogger(__name__)
-type Eq3ConfigEntry = ConfigEntry[Eq3ConfigEntryData]
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: Eq3ConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle config entry setup."""
mac_address: str | None = entry.unique_id
@@ -59,11 +53,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: Eq3ConfigEntry) -> bool:
ble_device=device,
)
- entry.runtime_data = Eq3ConfigEntryData(
- eq3_config=eq3_config, thermostat=thermostat
- )
+ eq3_config_entry = Eq3ConfigEntryData(eq3_config=eq3_config, thermostat=thermostat)
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = eq3_config_entry
+
entry.async_on_unload(entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+
entry.async_create_background_task(
hass, _async_run_thermostat(hass, entry), entry.entry_id
)
@@ -71,27 +66,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: Eq3ConfigEntry) -> bool:
return True
-async def async_unload_entry(hass: HomeAssistant, entry: Eq3ConfigEntry) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle config entry unload."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
- await entry.runtime_data.thermostat.async_disconnect()
+ eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN].pop(entry.entry_id)
+ await eq3_config_entry.thermostat.async_disconnect()
return unload_ok
-async def update_listener(hass: HomeAssistant, entry: Eq3ConfigEntry) -> None:
+async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle config entry update."""
await hass.config_entries.async_reload(entry.entry_id)
-async def _async_run_thermostat(hass: HomeAssistant, entry: Eq3ConfigEntry) -> None:
+async def _async_run_thermostat(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Run the thermostat."""
- thermostat = entry.runtime_data.thermostat
- mac_address = entry.runtime_data.eq3_config.mac_address
- scan_interval = entry.runtime_data.eq3_config.scan_interval
+ eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][entry.entry_id]
+ thermostat = eq3_config_entry.thermostat
+ mac_address = eq3_config_entry.eq3_config.mac_address
+ scan_interval = eq3_config_entry.eq3_config.scan_interval
await _async_reconnect_thermostat(hass, entry)
@@ -120,14 +117,13 @@ async def _async_run_thermostat(hass: HomeAssistant, entry: Eq3ConfigEntry) -> N
await asyncio.sleep(scan_interval)
-async def _async_reconnect_thermostat(
- hass: HomeAssistant, entry: Eq3ConfigEntry
-) -> None:
+async def _async_reconnect_thermostat(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reconnect the thermostat."""
- thermostat = entry.runtime_data.thermostat
- mac_address = entry.runtime_data.eq3_config.mac_address
- scan_interval = entry.runtime_data.eq3_config.scan_interval
+ eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][entry.entry_id]
+ thermostat = eq3_config_entry.thermostat
+ mac_address = eq3_config_entry.eq3_config.mac_address
+ scan_interval = eq3_config_entry.eq3_config.scan_interval
while True:
try:
diff --git a/homeassistant/components/eq3btsmart/binary_sensor.py b/homeassistant/components/eq3btsmart/binary_sensor.py
deleted file mode 100644
index 27525d47972..00000000000
--- a/homeassistant/components/eq3btsmart/binary_sensor.py
+++ /dev/null
@@ -1,86 +0,0 @@
-"""Platform for eq3 binary sensor entities."""
-
-from collections.abc import Callable
-from dataclasses import dataclass
-from typing import TYPE_CHECKING
-
-from eq3btsmart.models import Status
-
-from homeassistant.components.binary_sensor import (
- BinarySensorDeviceClass,
- BinarySensorEntity,
- BinarySensorEntityDescription,
-)
-from homeassistant.const import EntityCategory
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import Eq3ConfigEntry
-from .const import ENTITY_KEY_BATTERY, ENTITY_KEY_DST, ENTITY_KEY_WINDOW
-from .entity import Eq3Entity
-
-
-@dataclass(frozen=True, kw_only=True)
-class Eq3BinarySensorEntityDescription(BinarySensorEntityDescription):
- """Entity description for eq3 binary sensors."""
-
- value_func: Callable[[Status], bool]
-
-
-BINARY_SENSOR_ENTITY_DESCRIPTIONS = [
- Eq3BinarySensorEntityDescription(
- value_func=lambda status: status.is_low_battery,
- key=ENTITY_KEY_BATTERY,
- device_class=BinarySensorDeviceClass.BATTERY,
- entity_category=EntityCategory.DIAGNOSTIC,
- ),
- Eq3BinarySensorEntityDescription(
- value_func=lambda status: status.is_window_open,
- key=ENTITY_KEY_WINDOW,
- device_class=BinarySensorDeviceClass.WINDOW,
- ),
- Eq3BinarySensorEntityDescription(
- value_func=lambda status: status.is_dst,
- key=ENTITY_KEY_DST,
- translation_key=ENTITY_KEY_DST,
- entity_category=EntityCategory.DIAGNOSTIC,
- ),
-]
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: Eq3ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the entry."""
-
- async_add_entities(
- Eq3BinarySensorEntity(entry, entity_description)
- for entity_description in BINARY_SENSOR_ENTITY_DESCRIPTIONS
- )
-
-
-class Eq3BinarySensorEntity(Eq3Entity, BinarySensorEntity):
- """Base class for eQ-3 binary sensor entities."""
-
- entity_description: Eq3BinarySensorEntityDescription
-
- def __init__(
- self,
- entry: Eq3ConfigEntry,
- entity_description: Eq3BinarySensorEntityDescription,
- ) -> None:
- """Initialize the entity."""
-
- super().__init__(entry, entity_description.key)
- self.entity_description = entity_description
-
- @property
- def is_on(self) -> bool:
- """Return the state of the binary sensor."""
-
- if TYPE_CHECKING:
- assert self._thermostat.status is not None
-
- return self.entity_description.value_func(self._thermostat.status)
diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py
index ae01d0fc9a7..7b8ccb6c990 100644
--- a/homeassistant/components/eq3btsmart/climate.py
+++ b/homeassistant/components/eq3btsmart/climate.py
@@ -3,6 +3,7 @@
import logging
from typing import Any
+from eq3btsmart import Thermostat
from eq3btsmart.const import EQ3BT_MAX_TEMP, EQ3BT_OFF_TEMP, Eq3Preset, OperationMode
from eq3btsmart.exceptions import Eq3Exception
@@ -14,35 +15,45 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import device_registry as dr
-from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH
+from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.util import slugify
-from . import Eq3ConfigEntry
from .const import (
+ DEVICE_MODEL,
+ DOMAIN,
EQ_TO_HA_HVAC,
HA_TO_EQ_HVAC,
+ MANUFACTURER,
+ SIGNAL_THERMOSTAT_CONNECTED,
+ SIGNAL_THERMOSTAT_DISCONNECTED,
CurrentTemperatureSelector,
Preset,
TargetTemperatureSelector,
)
from .entity import Eq3Entity
+from .models import Eq3Config, Eq3ConfigEntryData
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
- entry: Eq3ConfigEntry,
+ config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Handle config entry setup."""
+ eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][config_entry.entry_id]
+
async_add_entities(
- [Eq3Climate(entry)],
+ [Eq3Climate(eq3_config_entry.eq3_config, eq3_config_entry.thermostat)],
)
@@ -69,6 +80,53 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
_attr_preset_mode: str | None = None
_target_temperature: float | None = None
+ def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat) -> None:
+ """Initialize the climate entity."""
+
+ super().__init__(eq3_config, thermostat)
+ self._attr_unique_id = dr.format_mac(eq3_config.mac_address)
+ self._attr_device_info = DeviceInfo(
+ name=slugify(self._eq3_config.mac_address),
+ manufacturer=MANUFACTURER,
+ model=DEVICE_MODEL,
+ connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)},
+ )
+
+ async def async_added_to_hass(self) -> None:
+ """Run when entity about to be added to hass."""
+
+ self._thermostat.register_update_callback(self._async_on_updated)
+
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ f"{SIGNAL_THERMOSTAT_DISCONNECTED}_{self._eq3_config.mac_address}",
+ self._async_on_disconnected,
+ )
+ )
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ f"{SIGNAL_THERMOSTAT_CONNECTED}_{self._eq3_config.mac_address}",
+ self._async_on_connected,
+ )
+ )
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Run when entity will be removed from hass."""
+
+ self._thermostat.unregister_update_callback(self._async_on_updated)
+
+ @callback
+ def _async_on_disconnected(self) -> None:
+ self._attr_available = False
+ self.async_write_ha_state()
+
+ @callback
+ def _async_on_connected(self) -> None:
+ self._attr_available = True
+ self.async_write_ha_state()
+
@callback
def _async_on_updated(self) -> None:
"""Handle updated data from the thermostat."""
@@ -79,15 +137,12 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
if self._thermostat.device_data is not None:
self._async_on_device_updated()
- super()._async_on_updated()
+ self.async_write_ha_state()
@callback
def _async_on_status_updated(self) -> None:
"""Handle updated status from the thermostat."""
- if self._thermostat.status is None:
- return
-
self._target_temperature = self._thermostat.status.target_temperature.value
self._attr_hvac_mode = EQ_TO_HA_HVAC[self._thermostat.status.operation_mode]
self._attr_current_temperature = self._get_current_temperature()
@@ -99,16 +154,13 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
def _async_on_device_updated(self) -> None:
"""Handle updated device data from the thermostat."""
- if self._thermostat.device_data is None:
- return
-
device_registry = dr.async_get(self.hass)
if device := device_registry.async_get_device(
connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)},
):
device_registry.async_update_device(
device.id,
- sw_version=str(self._thermostat.device_data.firmware_version),
+ sw_version=self._thermostat.device_data.firmware_version,
serial_number=self._thermostat.device_data.device_serial.value,
)
@@ -213,7 +265,7 @@ class Eq3Climate(Eq3Entity, ClimateEntity):
self.async_write_ha_state()
try:
- await self._thermostat.async_set_temperature(temperature)
+ await self._thermostat.async_set_temperature(self._target_temperature)
except Eq3Exception:
_LOGGER.error(
"[%s] Failed setting temperature", self._eq3_config.mac_address
diff --git a/homeassistant/components/eq3btsmart/const.py b/homeassistant/components/eq3btsmart/const.py
index 78292940e60..111c4d0eba4 100644
--- a/homeassistant/components/eq3btsmart/const.py
+++ b/homeassistant/components/eq3btsmart/const.py
@@ -18,20 +18,9 @@ DOMAIN = "eq3btsmart"
MANUFACTURER = "eQ-3 AG"
DEVICE_MODEL = "CC-RT-BLE-EQ"
-ENTITY_KEY_DST = "dst"
-ENTITY_KEY_BATTERY = "battery"
-ENTITY_KEY_WINDOW = "window"
-ENTITY_KEY_LOCK = "lock"
-ENTITY_KEY_BOOST = "boost"
-ENTITY_KEY_AWAY = "away"
-ENTITY_KEY_COMFORT = "comfort"
-ENTITY_KEY_ECO = "eco"
-ENTITY_KEY_OFFSET = "offset"
-ENTITY_KEY_WINDOW_OPEN_TEMPERATURE = "window_open_temperature"
-ENTITY_KEY_WINDOW_OPEN_TIMEOUT = "window_open_timeout"
-
GET_DEVICE_TIMEOUT = 5 # seconds
+
EQ_TO_HA_HVAC: dict[OperationMode, HVACMode] = {
OperationMode.OFF: HVACMode.OFF,
OperationMode.ON: HVACMode.HEAT,
@@ -82,5 +71,3 @@ DEFAULT_SCAN_INTERVAL = 10 # seconds
SIGNAL_THERMOSTAT_DISCONNECTED = f"{DOMAIN}.thermostat_disconnected"
SIGNAL_THERMOSTAT_CONNECTED = f"{DOMAIN}.thermostat_connected"
-
-EQ3BT_STEP = 0.5
diff --git a/homeassistant/components/eq3btsmart/entity.py b/homeassistant/components/eq3btsmart/entity.py
index e68545c08c7..e8c00d4e3cf 100644
--- a/homeassistant/components/eq3btsmart/entity.py
+++ b/homeassistant/components/eq3btsmart/entity.py
@@ -1,22 +1,10 @@
"""Base class for all eQ-3 entities."""
-from homeassistant.core import callback
-from homeassistant.helpers.device_registry import (
- CONNECTION_BLUETOOTH,
- DeviceInfo,
- format_mac,
-)
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity
-from homeassistant.util import slugify
+from eq3btsmart.thermostat import Thermostat
-from . import Eq3ConfigEntry
-from .const import (
- DEVICE_MODEL,
- MANUFACTURER,
- SIGNAL_THERMOSTAT_CONNECTED,
- SIGNAL_THERMOSTAT_DISCONNECTED,
-)
+from homeassistant.helpers.entity import Entity
+
+from .models import Eq3Config
class Eq3Entity(Entity):
@@ -24,70 +12,8 @@ class Eq3Entity(Entity):
_attr_has_entity_name = True
- def __init__(
- self,
- entry: Eq3ConfigEntry,
- unique_id_key: str | None = None,
- ) -> None:
+ def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat) -> None:
"""Initialize the eq3 entity."""
- self._eq3_config = entry.runtime_data.eq3_config
- self._thermostat = entry.runtime_data.thermostat
- self._attr_device_info = DeviceInfo(
- name=slugify(self._eq3_config.mac_address),
- manufacturer=MANUFACTURER,
- model=DEVICE_MODEL,
- connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)},
- )
- suffix = f"_{unique_id_key}" if unique_id_key else ""
- self._attr_unique_id = f"{format_mac(self._eq3_config.mac_address)}{suffix}"
-
- async def async_added_to_hass(self) -> None:
- """Run when entity about to be added to hass."""
-
- self._thermostat.register_update_callback(self._async_on_updated)
-
- self.async_on_remove(
- async_dispatcher_connect(
- self.hass,
- f"{SIGNAL_THERMOSTAT_DISCONNECTED}_{self._eq3_config.mac_address}",
- self._async_on_disconnected,
- )
- )
- self.async_on_remove(
- async_dispatcher_connect(
- self.hass,
- f"{SIGNAL_THERMOSTAT_CONNECTED}_{self._eq3_config.mac_address}",
- self._async_on_connected,
- )
- )
-
- async def async_will_remove_from_hass(self) -> None:
- """Run when entity will be removed from hass."""
-
- self._thermostat.unregister_update_callback(self._async_on_updated)
-
- def _async_on_updated(self) -> None:
- """Handle updated data from the thermostat."""
-
- self.async_write_ha_state()
-
- @callback
- def _async_on_disconnected(self) -> None:
- """Handle disconnection from the thermostat."""
-
- self._attr_available = False
- self.async_write_ha_state()
-
- @callback
- def _async_on_connected(self) -> None:
- """Handle connection to the thermostat."""
-
- self._attr_available = True
- self.async_write_ha_state()
-
- @property
- def available(self) -> bool:
- """Whether the entity is available."""
-
- return self._thermostat.status is not None and self._attr_available
+ self._eq3_config = eq3_config
+ self._thermostat = thermostat
diff --git a/homeassistant/components/eq3btsmart/icons.json b/homeassistant/components/eq3btsmart/icons.json
deleted file mode 100644
index e6eb7532f37..00000000000
--- a/homeassistant/components/eq3btsmart/icons.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "entity": {
- "binary_sensor": {
- "dst": {
- "default": "mdi:sun-clock",
- "state": {
- "off": "mdi:sun-clock-outline"
- }
- }
- },
- "number": {
- "comfort": {
- "default": "mdi:sun-thermometer"
- },
- "eco": {
- "default": "mdi:snowflake-thermometer"
- },
- "offset": {
- "default": "mdi:thermometer-plus"
- },
- "window_open_temperature": {
- "default": "mdi:window-open-variant"
- },
- "window_open_timeout": {
- "default": "mdi:timer-refresh"
- }
- },
- "switch": {
- "away": {
- "default": "mdi:home-account",
- "state": {
- "on": "mdi:home-export"
- }
- },
- "lock": {
- "default": "mdi:lock",
- "state": {
- "off": "mdi:lock-off"
- }
- },
- "boost": {
- "default": "mdi:fire",
- "state": {
- "off": "mdi:fire-off"
- }
- }
- }
- }
-}
diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json
index b30f806bf63..8c56e5ec598 100644
--- a/homeassistant/components/eq3btsmart/manifest.json
+++ b/homeassistant/components/eq3btsmart/manifest.json
@@ -23,5 +23,5 @@
"iot_class": "local_polling",
"loggers": ["eq3btsmart"],
"quality_scale": "silver",
- "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==1.1.0"]
+ "requirements": ["eq3btsmart==1.1.9", "bleak-esphome==1.1.0"]
}
diff --git a/homeassistant/components/eq3btsmart/models.py b/homeassistant/components/eq3btsmart/models.py
index 858465effa8..8ea0955dbdd 100644
--- a/homeassistant/components/eq3btsmart/models.py
+++ b/homeassistant/components/eq3btsmart/models.py
@@ -2,6 +2,7 @@
from dataclasses import dataclass
+from eq3btsmart.const import DEFAULT_AWAY_HOURS, DEFAULT_AWAY_TEMP
from eq3btsmart.thermostat import Thermostat
from .const import (
@@ -22,6 +23,8 @@ class Eq3Config:
target_temp_selector: TargetTemperatureSelector = DEFAULT_TARGET_TEMP_SELECTOR
external_temp_sensor: str = ""
scan_interval: int = DEFAULT_SCAN_INTERVAL
+ default_away_hours: float = DEFAULT_AWAY_HOURS
+ default_away_temperature: float = DEFAULT_AWAY_TEMP
@dataclass(slots=True)
diff --git a/homeassistant/components/eq3btsmart/number.py b/homeassistant/components/eq3btsmart/number.py
deleted file mode 100644
index 2e069180fa3..00000000000
--- a/homeassistant/components/eq3btsmart/number.py
+++ /dev/null
@@ -1,158 +0,0 @@
-"""Platform for eq3 number entities."""
-
-from collections.abc import Awaitable, Callable
-from dataclasses import dataclass
-from typing import TYPE_CHECKING
-
-from eq3btsmart import Thermostat
-from eq3btsmart.const import (
- EQ3BT_MAX_OFFSET,
- EQ3BT_MAX_TEMP,
- EQ3BT_MIN_OFFSET,
- EQ3BT_MIN_TEMP,
-)
-from eq3btsmart.models import Presets
-
-from homeassistant.components.number import (
- NumberDeviceClass,
- NumberEntity,
- NumberEntityDescription,
- NumberMode,
-)
-from homeassistant.const import EntityCategory, UnitOfTemperature, UnitOfTime
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import Eq3ConfigEntry
-from .const import (
- ENTITY_KEY_COMFORT,
- ENTITY_KEY_ECO,
- ENTITY_KEY_OFFSET,
- ENTITY_KEY_WINDOW_OPEN_TEMPERATURE,
- ENTITY_KEY_WINDOW_OPEN_TIMEOUT,
- EQ3BT_STEP,
-)
-from .entity import Eq3Entity
-
-
-@dataclass(frozen=True, kw_only=True)
-class Eq3NumberEntityDescription(NumberEntityDescription):
- """Entity description for eq3 number entities."""
-
- value_func: Callable[[Presets], float]
- value_set_func: Callable[
- [Thermostat],
- Callable[[float], Awaitable[None]],
- ]
- mode: NumberMode = NumberMode.BOX
- entity_category: EntityCategory | None = EntityCategory.CONFIG
-
-
-NUMBER_ENTITY_DESCRIPTIONS = [
- Eq3NumberEntityDescription(
- key=ENTITY_KEY_COMFORT,
- value_func=lambda presets: presets.comfort_temperature.value,
- value_set_func=lambda thermostat: thermostat.async_configure_comfort_temperature,
- translation_key=ENTITY_KEY_COMFORT,
- native_min_value=EQ3BT_MIN_TEMP,
- native_max_value=EQ3BT_MAX_TEMP,
- native_step=EQ3BT_STEP,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- device_class=NumberDeviceClass.TEMPERATURE,
- ),
- Eq3NumberEntityDescription(
- key=ENTITY_KEY_ECO,
- value_func=lambda presets: presets.eco_temperature.value,
- value_set_func=lambda thermostat: thermostat.async_configure_eco_temperature,
- translation_key=ENTITY_KEY_ECO,
- native_min_value=EQ3BT_MIN_TEMP,
- native_max_value=EQ3BT_MAX_TEMP,
- native_step=EQ3BT_STEP,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- device_class=NumberDeviceClass.TEMPERATURE,
- ),
- Eq3NumberEntityDescription(
- key=ENTITY_KEY_WINDOW_OPEN_TEMPERATURE,
- value_func=lambda presets: presets.window_open_temperature.value,
- value_set_func=lambda thermostat: thermostat.async_configure_window_open_temperature,
- translation_key=ENTITY_KEY_WINDOW_OPEN_TEMPERATURE,
- native_min_value=EQ3BT_MIN_TEMP,
- native_max_value=EQ3BT_MAX_TEMP,
- native_step=EQ3BT_STEP,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- device_class=NumberDeviceClass.TEMPERATURE,
- ),
- Eq3NumberEntityDescription(
- key=ENTITY_KEY_OFFSET,
- value_func=lambda presets: presets.offset_temperature.value,
- value_set_func=lambda thermostat: thermostat.async_configure_temperature_offset,
- translation_key=ENTITY_KEY_OFFSET,
- native_min_value=EQ3BT_MIN_OFFSET,
- native_max_value=EQ3BT_MAX_OFFSET,
- native_step=EQ3BT_STEP,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- device_class=NumberDeviceClass.TEMPERATURE,
- ),
- Eq3NumberEntityDescription(
- key=ENTITY_KEY_WINDOW_OPEN_TIMEOUT,
- value_set_func=lambda thermostat: thermostat.async_configure_window_open_duration,
- value_func=lambda presets: presets.window_open_time.value.total_seconds() / 60,
- translation_key=ENTITY_KEY_WINDOW_OPEN_TIMEOUT,
- native_min_value=0,
- native_max_value=60,
- native_step=5,
- native_unit_of_measurement=UnitOfTime.MINUTES,
- ),
-]
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: Eq3ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the entry."""
-
- async_add_entities(
- Eq3NumberEntity(entry, entity_description)
- for entity_description in NUMBER_ENTITY_DESCRIPTIONS
- )
-
-
-class Eq3NumberEntity(Eq3Entity, NumberEntity):
- """Base class for all eq3 number entities."""
-
- entity_description: Eq3NumberEntityDescription
-
- def __init__(
- self, entry: Eq3ConfigEntry, entity_description: Eq3NumberEntityDescription
- ) -> None:
- """Initialize the entity."""
-
- super().__init__(entry, entity_description.key)
- self.entity_description = entity_description
-
- @property
- def native_value(self) -> float:
- """Return the state of the entity."""
-
- if TYPE_CHECKING:
- assert self._thermostat.status is not None
- assert self._thermostat.status.presets is not None
-
- return self.entity_description.value_func(self._thermostat.status.presets)
-
- async def async_set_native_value(self, value: float) -> None:
- """Set the state of the entity."""
-
- await self.entity_description.value_set_func(self._thermostat)(value)
-
- @property
- def available(self) -> bool:
- """Return whether the entity is available."""
-
- return (
- self._thermostat.status is not None
- and self._thermostat.status.presets is not None
- and self._attr_available
- )
diff --git a/homeassistant/components/eq3btsmart/strings.json b/homeassistant/components/eq3btsmart/strings.json
index acfd5082f45..7477aab4cfb 100644
--- a/homeassistant/components/eq3btsmart/strings.json
+++ b/homeassistant/components/eq3btsmart/strings.json
@@ -14,44 +14,6 @@
"init": {
"title": "Configure new eQ-3 device"
}
- },
- "error": {
- "invalid_mac_address": "Invalid MAC address"
- }
- },
- "entity": {
- "binary_sensor": {
- "dst": {
- "name": "Daylight saving time"
- }
- },
- "number": {
- "comfort": {
- "name": "Comfort temperature"
- },
- "eco": {
- "name": "Eco temperature"
- },
- "offset": {
- "name": "Offset temperature"
- },
- "window_open_temperature": {
- "name": "Window open temperature"
- },
- "window_open_timeout": {
- "name": "Window open timeout"
- }
- },
- "switch": {
- "lock": {
- "name": "Lock"
- },
- "boost": {
- "name": "Boost"
- },
- "away": {
- "name": "Away"
- }
}
}
}
diff --git a/homeassistant/components/eq3btsmart/switch.py b/homeassistant/components/eq3btsmart/switch.py
deleted file mode 100644
index 7525d8ca494..00000000000
--- a/homeassistant/components/eq3btsmart/switch.py
+++ /dev/null
@@ -1,94 +0,0 @@
-"""Platform for eq3 switch entities."""
-
-from collections.abc import Awaitable, Callable
-from dataclasses import dataclass
-from typing import TYPE_CHECKING, Any
-
-from eq3btsmart import Thermostat
-from eq3btsmart.models import Status
-
-from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import Eq3ConfigEntry
-from .const import ENTITY_KEY_AWAY, ENTITY_KEY_BOOST, ENTITY_KEY_LOCK
-from .entity import Eq3Entity
-
-
-@dataclass(frozen=True, kw_only=True)
-class Eq3SwitchEntityDescription(SwitchEntityDescription):
- """Entity description for eq3 switch entities."""
-
- toggle_func: Callable[[Thermostat], Callable[[bool], Awaitable[None]]]
- value_func: Callable[[Status], bool]
-
-
-SWITCH_ENTITY_DESCRIPTIONS = [
- Eq3SwitchEntityDescription(
- key=ENTITY_KEY_LOCK,
- translation_key=ENTITY_KEY_LOCK,
- toggle_func=lambda thermostat: thermostat.async_set_locked,
- value_func=lambda status: status.is_locked,
- ),
- Eq3SwitchEntityDescription(
- key=ENTITY_KEY_BOOST,
- translation_key=ENTITY_KEY_BOOST,
- toggle_func=lambda thermostat: thermostat.async_set_boost,
- value_func=lambda status: status.is_boost,
- ),
- Eq3SwitchEntityDescription(
- key=ENTITY_KEY_AWAY,
- translation_key=ENTITY_KEY_AWAY,
- toggle_func=lambda thermostat: thermostat.async_set_away,
- value_func=lambda status: status.is_away,
- ),
-]
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: Eq3ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the entry."""
-
- async_add_entities(
- Eq3SwitchEntity(entry, entity_description)
- for entity_description in SWITCH_ENTITY_DESCRIPTIONS
- )
-
-
-class Eq3SwitchEntity(Eq3Entity, SwitchEntity):
- """Base class for eq3 switch entities."""
-
- entity_description: Eq3SwitchEntityDescription
-
- def __init__(
- self,
- entry: Eq3ConfigEntry,
- entity_description: Eq3SwitchEntityDescription,
- ) -> None:
- """Initialize the entity."""
-
- super().__init__(entry, entity_description.key)
- self.entity_description = entity_description
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn on the switch."""
-
- await self.entity_description.toggle_func(self._thermostat)(True)
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn off the switch."""
-
- await self.entity_description.toggle_func(self._thermostat)(False)
-
- @property
- def is_on(self) -> bool:
- """Return the state of the switch."""
-
- if TYPE_CHECKING:
- assert self._thermostat.status is not None
-
- return self.entity_description.value_func(self._thermostat.status)
diff --git a/homeassistant/components/esphome/alarm_control_panel.py b/homeassistant/components/esphome/alarm_control_panel.py
index 8f1b5ae8b1a..64a0210f0f7 100644
--- a/homeassistant/components/esphome/alarm_control_panel.py
+++ b/homeassistant/components/esphome/alarm_control_panel.py
@@ -6,9 +6,9 @@ from functools import partial
from aioesphomeapi import (
AlarmControlPanelCommand,
- AlarmControlPanelEntityState as ESPHomeAlarmControlPanelEntityState,
+ AlarmControlPanelEntityState,
AlarmControlPanelInfo,
- AlarmControlPanelState as ESPHomeAlarmControlPanelState,
+ AlarmControlPanelState,
APIIntEnum,
EntityInfo,
)
@@ -16,9 +16,20 @@ from aioesphomeapi import (
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_DISARMING,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import callback
from .entity import (
@@ -29,21 +40,21 @@ from .entity import (
)
from .enum_mapper import EsphomeEnumMapper
-_ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[
- ESPHomeAlarmControlPanelState, AlarmControlPanelState
-] = EsphomeEnumMapper(
- {
- ESPHomeAlarmControlPanelState.DISARMED: AlarmControlPanelState.DISARMED,
- ESPHomeAlarmControlPanelState.ARMED_HOME: AlarmControlPanelState.ARMED_HOME,
- ESPHomeAlarmControlPanelState.ARMED_AWAY: AlarmControlPanelState.ARMED_AWAY,
- ESPHomeAlarmControlPanelState.ARMED_NIGHT: AlarmControlPanelState.ARMED_NIGHT,
- ESPHomeAlarmControlPanelState.ARMED_VACATION: AlarmControlPanelState.ARMED_VACATION,
- ESPHomeAlarmControlPanelState.ARMED_CUSTOM_BYPASS: AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
- ESPHomeAlarmControlPanelState.PENDING: AlarmControlPanelState.PENDING,
- ESPHomeAlarmControlPanelState.ARMING: AlarmControlPanelState.ARMING,
- ESPHomeAlarmControlPanelState.DISARMING: AlarmControlPanelState.DISARMING,
- ESPHomeAlarmControlPanelState.TRIGGERED: AlarmControlPanelState.TRIGGERED,
- }
+_ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[AlarmControlPanelState, str] = (
+ EsphomeEnumMapper(
+ {
+ AlarmControlPanelState.DISARMED: STATE_ALARM_DISARMED,
+ AlarmControlPanelState.ARMED_HOME: STATE_ALARM_ARMED_HOME,
+ AlarmControlPanelState.ARMED_AWAY: STATE_ALARM_ARMED_AWAY,
+ AlarmControlPanelState.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT,
+ AlarmControlPanelState.ARMED_VACATION: STATE_ALARM_ARMED_VACATION,
+ AlarmControlPanelState.ARMED_CUSTOM_BYPASS: STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ AlarmControlPanelState.PENDING: STATE_ALARM_PENDING,
+ AlarmControlPanelState.ARMING: STATE_ALARM_ARMING,
+ AlarmControlPanelState.DISARMING: STATE_ALARM_DISARMING,
+ AlarmControlPanelState.TRIGGERED: STATE_ALARM_TRIGGERED,
+ }
+ )
)
@@ -59,7 +70,7 @@ class EspHomeACPFeatures(APIIntEnum):
class EsphomeAlarmControlPanel(
- EsphomeEntity[AlarmControlPanelInfo, ESPHomeAlarmControlPanelEntityState],
+ EsphomeEntity[AlarmControlPanelInfo, AlarmControlPanelEntityState],
AlarmControlPanelEntity,
):
"""An Alarm Control Panel implementation for ESPHome."""
@@ -90,7 +101,7 @@ class EsphomeAlarmControlPanel(
@property
@esphome_state_property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the device."""
return _ESPHOME_ACP_STATE_TO_HASS_STATE.from_esphome(self._state.state)
@@ -148,5 +159,5 @@ async_setup_entry = partial(
platform_async_setup_entry,
info_type=AlarmControlPanelInfo,
entity_type=EsphomeAlarmControlPanel,
- state_type=ESPHomeAlarmControlPanelEntityState,
+ state_type=AlarmControlPanelEntityState,
)
diff --git a/homeassistant/components/esphome/assist_satellite.py b/homeassistant/components/esphome/assist_satellite.py
index dc513a03e02..b2794fe043f 100644
--- a/homeassistant/components/esphome/assist_satellite.py
+++ b/homeassistant/components/esphome/assist_satellite.py
@@ -36,7 +36,7 @@ from homeassistant.components.intent import (
)
from homeassistant.components.media_player import async_process_play_media_url
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import Platform
+from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -108,7 +108,9 @@ class EsphomeAssistSatellite(
"""Satellite running ESPHome."""
entity_description = assist_satellite.AssistSatelliteEntityDescription(
- key="assist_satellite", translation_key="assist_satellite"
+ key="assist_satellite",
+ translation_key="assist_satellite",
+ entity_category=EntityCategory.CONFIG,
)
def __init__(
@@ -245,15 +247,15 @@ class EsphomeAssistSatellite(
assist_satellite.AssistSatelliteEntityFeature.ANNOUNCE
)
- # Block until config is retrieved.
- # If the device supports announcements, it will return a config.
- _LOGGER.debug("Waiting for satellite configuration")
- await self._update_satellite_config()
-
if not (feature_flags & VoiceAssistantFeature.SPEAKER):
# Will use media player for TTS/announcements
self._update_tts_format()
+ # Fetch latest config in the background
+ self.config_entry.async_create_background_task(
+ self.hass, self._update_satellite_config(), "esphome_voice_assistant_config"
+ )
+
async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
await super().async_will_remove_from_hass()
diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py
index cb892b314cd..937cad040ea 100644
--- a/homeassistant/components/esphome/config_flow.py
+++ b/homeassistant/components/esphome/config_flow.py
@@ -21,6 +21,7 @@ import aiohttp
import voluptuous as vol
from homeassistant.components import dhcp, zeroconf
+from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.config_entries import (
SOURCE_REAUTH,
ConfigEntry,
@@ -31,7 +32,6 @@ from homeassistant.config_entries import (
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
from homeassistant.util.json import json_loads_object
@@ -257,9 +257,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
self, discovery_info: MqttServiceInfo
) -> ConfigFlowResult:
"""Handle MQTT discovery."""
- if not discovery_info.payload:
- return self.async_abort(reason="mqtt_missing_payload")
-
device_info = json_loads_object(discovery_info.payload)
if "mac" not in device_info:
return self.async_abort(reason="mqtt_missing_mac")
@@ -485,12 +482,16 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Handle a option flow for esphome."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/esphome/coordinator.py b/homeassistant/components/esphome/coordinator.py
index b31a74dcf3f..284e17fd183 100644
--- a/homeassistant/components/esphome/coordinator.py
+++ b/homeassistant/components/esphome/coordinator.py
@@ -31,7 +31,6 @@ class ESPHomeDashboardCoordinator(DataUpdateCoordinator[dict[str, ConfiguredDevi
super().__init__(
hass,
_LOGGER,
- config_entry=None,
name="ESPHome Dashboard",
update_interval=timedelta(minutes=5),
always_update=False,
diff --git a/homeassistant/components/esphome/ffmpeg_proxy.py b/homeassistant/components/esphome/ffmpeg_proxy.py
index cefe87f49ba..c2bf72c40e5 100644
--- a/homeassistant/components/esphome/ffmpeg_proxy.py
+++ b/homeassistant/components/esphome/ffmpeg_proxy.py
@@ -1,12 +1,10 @@
"""HTTP view that converts audio from a URL to a preferred format."""
import asyncio
-from collections import defaultdict
from dataclasses import dataclass, field
from http import HTTPStatus
import logging
import secrets
-from typing import Final
from aiohttp import web
from aiohttp.abc import AbstractStreamWriter, BaseRequest
@@ -19,8 +17,6 @@ from .const import DATA_FFMPEG_PROXY
_LOGGER = logging.getLogger(__name__)
-_MAX_CONVERSIONS_PER_DEVICE: Final[int] = 2
-
def async_create_proxy_url(
hass: HomeAssistant,
@@ -63,18 +59,13 @@ class FFmpegConversionInfo:
proc: asyncio.subprocess.Process | None = None
"""Subprocess doing ffmpeg conversion."""
- is_finished: bool = False
- """True if conversion has finished."""
-
@dataclass
class FFmpegProxyData:
"""Data for ffmpeg proxy conversion."""
- # device_id -> [info]
- conversions: dict[str, list[FFmpegConversionInfo]] = field(
- default_factory=lambda: defaultdict(list)
- )
+ # device_id -> info
+ conversions: dict[str, FFmpegConversionInfo] = field(default_factory=dict)
def async_create_proxy_url(
self,
@@ -86,15 +77,8 @@ class FFmpegProxyData:
width: int | None,
) -> str:
"""Create a one-time use proxy URL that automatically converts the media."""
-
- # Remove completed conversions
- device_conversions = [
- info for info in self.conversions[device_id] if not info.is_finished
- ]
-
- while len(device_conversions) >= _MAX_CONVERSIONS_PER_DEVICE:
- # Stop oldest conversion before adding a new one
- convert_info = device_conversions[0]
+ if (convert_info := self.conversions.pop(device_id, None)) is not None:
+ # Stop existing conversion before overwriting info
if (convert_info.proc is not None) and (
convert_info.proc.returncode is None
):
@@ -103,18 +87,12 @@ class FFmpegProxyData:
)
convert_info.proc.kill()
- device_conversions = device_conversions[1:]
-
convert_id = secrets.token_urlsafe(16)
- device_conversions.append(
- FFmpegConversionInfo(
- convert_id, media_url, media_format, rate, channels, width
- )
+ self.conversions[device_id] = FFmpegConversionInfo(
+ convert_id, media_url, media_format, rate, channels, width
)
_LOGGER.debug("Media URL allowed by proxy: %s", media_url)
- self.conversions[device_id] = device_conversions
-
return f"/api/esphome/ffmpeg_proxy/{device_id}/{convert_id}.{media_format}"
@@ -153,10 +131,11 @@ class FFmpegConvertResponse(web.StreamResponse):
self.proxy_data = proxy_data
self.chunk_size = chunk_size
- async def transcode(
- self, request: BaseRequest, writer: AbstractStreamWriter
- ) -> None:
+ async def prepare(self, request: BaseRequest) -> AbstractStreamWriter | None:
"""Stream url through ffmpeg conversion and out to HTTP client."""
+ writer = await super().prepare(request)
+ assert writer is not None
+
command_args = [
"-i",
self.convert_info.media_url,
@@ -176,9 +155,6 @@ class FFmpegConvertResponse(web.StreamResponse):
# 16-bit samples
command_args.extend(["-sample_fmt", "s16"])
- # Remove metadata and cover art
- command_args.extend(["-map_metadata", "-1", "-vn"])
-
# Output to stdout
command_args.append("pipe:")
@@ -188,24 +164,11 @@ class FFmpegConvertResponse(web.StreamResponse):
*command_args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
- close_fds=False, # use posix_spawn in CPython < 3.13
)
# Only one conversion process per device is allowed
self.convert_info.proc = proc
- # Create background task which will be cancelled when home assistant shuts down
- write_task = self.hass.async_create_background_task(
- self._write_ffmpeg_data(request, writer, proc), "ESPHome media proxy"
- )
- await write_task
-
- async def _write_ffmpeg_data(
- self,
- request: BaseRequest,
- writer: AbstractStreamWriter,
- proc: asyncio.subprocess.Process,
- ) -> None:
assert proc.stdout is not None
assert proc.stderr is not None
@@ -215,15 +178,12 @@ class FFmpegConvertResponse(web.StreamResponse):
self.hass.is_running
and (request.transport is not None)
and (not request.transport.is_closing())
+ and (proc.returncode is None)
and (chunk := await proc.stdout.read(self.chunk_size))
):
- await self.write(chunk)
+ await writer.write(chunk)
+ await writer.drain()
except asyncio.CancelledError:
- _LOGGER.debug("ffmpeg transcoding cancelled")
- # Abort the transport, we don't wait for ESPHome to drain the write buffer;
- # it may need a very long time or never finish if the player is paused.
- if request.transport:
- request.transport.abort()
raise # don't log error
except:
_LOGGER.exception("Unexpected error during ffmpeg conversion")
@@ -236,16 +196,14 @@ class FFmpegConvertResponse(web.StreamResponse):
raise
finally:
- # Allow conversion info to be removed
- self.convert_info.is_finished = True
-
# Terminate hangs, so kill is used
if proc.returncode is None:
proc.kill()
- # Close connection by writing EOF unless already closing
- if request.transport and not request.transport.is_closing():
- await writer.write_eof()
+ # Close connection
+ await writer.write_eof()
+
+ return writer
class FFmpegProxyView(HomeAssistantView):
@@ -264,8 +222,7 @@ class FFmpegProxyView(HomeAssistantView):
self, request: web.Request, device_id: str, filename: str
) -> web.StreamResponse:
"""Start a get request."""
- device_conversions = self.proxy_data.conversions[device_id]
- if not device_conversions:
+ if (convert_info := self.proxy_data.conversions.get(device_id)) is None:
return web.Response(
body="No proxy URL for device", status=HTTPStatus.NOT_FOUND
)
@@ -273,16 +230,9 @@ class FFmpegProxyView(HomeAssistantView):
# {id}.mp3 -> id, mp3
convert_id, media_format = filename.rsplit(".")
- # Look up conversion info
- convert_info: FFmpegConversionInfo | None = None
- for maybe_convert_info in device_conversions:
- if (maybe_convert_info.convert_id == convert_id) and (
- maybe_convert_info.media_format == media_format
- ):
- convert_info = maybe_convert_info
- break
-
- if convert_info is None:
+ if (convert_info.convert_id != convert_id) or (
+ convert_info.media_format != media_format
+ ):
return web.Response(body="Invalid proxy URL", status=HTTPStatus.BAD_REQUEST)
# Stop previous process if the URL is being reused.
@@ -293,10 +243,6 @@ class FFmpegProxyView(HomeAssistantView):
convert_info.proc = None
# Stream converted audio back to client
- resp = FFmpegConvertResponse(
+ return FFmpegConvertResponse(
self.manager, convert_info, device_id, self.proxy_data
)
- writer = await resp.prepare(request)
- assert writer is not None
- await resp.transcode(request, writer)
- return resp
diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py
index 007b4e791e1..c36a55d1f55 100644
--- a/homeassistant/components/esphome/manager.py
+++ b/homeassistant/components/esphome/manager.py
@@ -570,11 +570,7 @@ def _async_setup_device_registry(
configuration_url = None
if device_info.webserver_port > 0:
configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}"
- elif (
- (dashboard := async_get_dashboard(hass))
- and dashboard.data
- and dashboard.data.get(device_info.name)
- ):
+ elif dashboard := async_get_dashboard(hass):
configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}"
manufacturer = "espressif"
diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json
index b9b6a98dcd1..410c826c5a0 100644
--- a/homeassistant/components/esphome/manifest.json
+++ b/homeassistant/components/esphome/manifest.json
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
- "aioesphomeapi==27.0.1",
+ "aioesphomeapi==27.0.0",
"esphome-dashboard-api==1.2.3",
"bleak-esphome==1.1.0"
],
diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json
index 18a54772e30..ec7e6f674b3 100644
--- a/homeassistant/components/esphome/strings.json
+++ b/homeassistant/components/esphome/strings.json
@@ -8,8 +8,7 @@
"service_received": "Action received",
"mqtt_missing_mac": "Missing MAC address in MQTT properties.",
"mqtt_missing_api": "Missing API port in MQTT properties.",
- "mqtt_missing_ip": "Missing IP address in MQTT properties.",
- "mqtt_missing_payload": "Missing MQTT Payload."
+ "mqtt_missing_ip": "Missing IP address in MQTT properties."
},
"error": {
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address",
diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py
index 5e571399ecb..b7905fb4fdb 100644
--- a/homeassistant/components/esphome/update.py
+++ b/homeassistant/components/esphome/update.py
@@ -230,8 +230,10 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
@property
@esphome_state_property
- def in_progress(self) -> bool:
+ def in_progress(self) -> bool | int | None:
"""Return if the update is in progress."""
+ if self._state.has_progress:
+ return int(self._state.progress)
return self._state.in_progress
@property
@@ -258,14 +260,6 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
"""Return the title of the update."""
return self._state.title
- @property
- @esphome_state_property
- def update_percentage(self) -> int | None:
- """Return if the update is in progress."""
- if self._state.has_progress:
- return int(self._state.progress)
- return None
-
@convert_api_error_ha_error
async def async_update(self) -> None:
"""Command device to check for update."""
diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py
index 612131919d4..58e0e16e059 100644
--- a/homeassistant/components/evohome/__init__.py
+++ b/homeassistant/components/evohome/__init__.py
@@ -176,7 +176,7 @@ class EvoSession:
):
app_storage[ACCESS_TOKEN_EXPIRES] = dt_aware_to_naive(expires)
- user_data: dict[str, str] = app_storage.pop(USER_DATA, {}) or {}
+ user_data: dict[str, str] = app_storage.pop(USER_DATA, {})
self.session_id = user_data.get(SZ_SESSION_ID)
self._tokens = app_storage
@@ -223,7 +223,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
config[DOMAIN][CONF_PASSWORD],
)
- except (evo.AuthenticationFailed, evo.RequestFailed) as err:
+ except evo.AuthenticationFailed as err:
handle_evo_exception(err)
return False
@@ -240,7 +240,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=None,
name=f"{DOMAIN}_coordinator",
update_interval=config[DOMAIN][CONF_SCAN_INTERVAL],
update_method=broker.async_update,
diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py
index 1388585bc17..5aa99bca60e 100644
--- a/homeassistant/components/evohome/climate.py
+++ b/homeassistant/components/evohome/climate.py
@@ -66,6 +66,8 @@ _LOGGER = logging.getLogger(__name__)
PRESET_RESET = "Reset" # reset all child zones to EVO_FOLLOW
PRESET_CUSTOM = "Custom"
+HA_HVAC_TO_TCS = {HVACMode.OFF: EVO_HEATOFF, HVACMode.HEAT: EVO_AUTO}
+
TCS_PRESET_TO_HA = {
EVO_AWAY: PRESET_AWAY,
EVO_CUSTOM: PRESET_CUSTOM,
@@ -148,10 +150,14 @@ async def async_setup_platform(
class EvoClimateEntity(EvoDevice, ClimateEntity):
"""Base for any evohome-compatible climate entity (controller, zone)."""
- _attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT]
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_enable_turn_on_off_backwards_compatibility = False
+ @property
+ def hvac_modes(self) -> list[HVACMode]:
+ """Return a list of available hvac operation modes."""
+ return list(HA_HVAC_TO_TCS)
+
class EvoZone(EvoChild, EvoClimateEntity):
"""Base for any evohome-compatible heating zone."""
@@ -359,9 +365,9 @@ class EvoController(EvoClimateEntity):
self._attr_unique_id = evo_device.systemId
self._attr_name = evo_device.location.name
- self._evo_modes = [m[SZ_SYSTEM_MODE] for m in evo_device.allowedSystemModes]
+ modes = [m[SZ_SYSTEM_MODE] for m in evo_broker.tcs.allowedSystemModes]
self._attr_preset_modes = [
- TCS_PRESET_TO_HA[m] for m in self._evo_modes if m in list(TCS_PRESET_TO_HA)
+ TCS_PRESET_TO_HA[m] for m in modes if m in list(TCS_PRESET_TO_HA)
]
if self._attr_preset_modes:
self._attr_supported_features = ClimateEntityFeature.PRESET_MODE
@@ -395,14 +401,14 @@ class EvoController(EvoClimateEntity):
"""Set a Controller to any of its native EVO_* operating modes."""
until = dt_util.as_utc(until) if until else None
await self._evo_broker.call_client_api(
- self._evo_device.set_mode(mode, until=until) # type: ignore[arg-type]
+ self._evo_tcs.set_mode(mode, until=until) # type: ignore[arg-type]
)
@property
def hvac_mode(self) -> HVACMode:
"""Return the current operating mode of a Controller."""
- evo_mode = self._evo_device.system_mode
- return HVACMode.OFF if evo_mode in (EVO_HEATOFF, "Off") else HVACMode.HEAT
+ tcs_mode = self._evo_tcs.system_mode
+ return HVACMode.OFF if tcs_mode == EVO_HEATOFF else HVACMode.HEAT
@property
def current_temperature(self) -> float | None:
@@ -412,7 +418,7 @@ class EvoController(EvoClimateEntity):
"""
temps = [
z.temperature
- for z in self._evo_device.zones.values()
+ for z in self._evo_tcs.zones.values()
if z.temperature is not None
]
return round(sum(temps) / len(temps), 1) if temps else None
@@ -420,9 +426,9 @@ class EvoController(EvoClimateEntity):
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
- if not self._evo_device.system_mode:
+ if not self._evo_tcs.system_mode:
return None
- return TCS_PRESET_TO_HA.get(self._evo_device.system_mode)
+ return TCS_PRESET_TO_HA.get(self._evo_tcs.system_mode)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Raise exception as Controllers don't have a target temperature."""
@@ -430,13 +436,9 @@ class EvoController(EvoClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set an operating mode for a Controller."""
- if hvac_mode == HVACMode.HEAT:
- evo_mode = EVO_AUTO if EVO_AUTO in self._evo_modes else "Heat"
- elif hvac_mode == HVACMode.OFF:
- evo_mode = EVO_HEATOFF if EVO_HEATOFF in self._evo_modes else "Off"
- else:
+ if not (tcs_mode := HA_HVAC_TO_TCS.get(hvac_mode)):
raise HomeAssistantError(f"Invalid hvac_mode: {hvac_mode}")
- await self._set_tcs_mode(evo_mode)
+ await self._set_tcs_mode(tcs_mode)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode; if None, then revert to 'Auto' mode."""
@@ -449,6 +451,6 @@ class EvoController(EvoClimateEntity):
attrs = self._device_state_attrs
for attr in STATE_ATTRS_TCS:
if attr == SZ_ACTIVE_FAULTS:
- attrs["activeSystemFaults"] = getattr(self._evo_device, attr)
+ attrs["activeSystemFaults"] = getattr(self._evo_tcs, attr)
else:
- attrs[attr] = getattr(self._evo_device, attr)
+ attrs[attr] = getattr(self._evo_tcs, attr)
diff --git a/homeassistant/components/evohome/entity.py b/homeassistant/components/evohome/entity.py
index b5842c1073a..5da9df247cd 100644
--- a/homeassistant/components/evohome/entity.py
+++ b/homeassistant/components/evohome/entity.py
@@ -42,6 +42,7 @@ class EvoDevice(Entity):
"""Initialize an evohome-compatible entity (TCS, DHW, zone)."""
self._evo_device = evo_device
self._evo_broker = evo_broker
+ self._evo_tcs = evo_broker.tcs
self._device_state_attrs: dict[str, Any] = {}
@@ -100,8 +101,6 @@ class EvoChild(EvoDevice):
"""Initialize an evohome-compatible child entity (DHW, zone)."""
super().__init__(evo_broker, evo_device)
- self._evo_tcs = evo_device.tcs
-
self._schedule: dict[str, Any] = {}
self._setpoints: dict[str, Any] = {}
diff --git a/homeassistant/components/ezviz/alarm_control_panel.py b/homeassistant/components/ezviz/alarm_control_panel.py
index f30a7852b4e..21e9f2d0422 100644
--- a/homeassistant/components/ezviz/alarm_control_panel.py
+++ b/homeassistant/components/ezviz/alarm_control_panel.py
@@ -13,9 +13,13 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityDescription,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
@@ -41,9 +45,9 @@ ALARM_TYPE = EzvizAlarmControlPanelEntityDescription(
key="ezviz_alarm",
ezviz_alarm_states=[
None,
- AlarmControlPanelState.DISARMED,
- AlarmControlPanelState.ARMED_AWAY,
- AlarmControlPanelState.ARMED_HOME,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
],
)
@@ -92,7 +96,7 @@ class EzvizAlarm(AlarmControlPanelEntity):
self._attr_device_info = device_info
self.entity_description = entity_description
self.coordinator = coordinator
- self._attr_alarm_state = None
+ self._attr_state = None
async def async_added_to_hass(self) -> None:
"""Entity added to hass."""
@@ -104,7 +108,7 @@ class EzvizAlarm(AlarmControlPanelEntity):
if self.coordinator.ezviz_client.api_set_defence_mode(
DefenseModeType.HOME_MODE.value
):
- self._attr_alarm_state = AlarmControlPanelState.DISARMED
+ self._attr_state = STATE_ALARM_DISARMED
except PyEzvizError as err:
raise HomeAssistantError("Cannot disarm EZVIZ alarm") from err
@@ -115,7 +119,7 @@ class EzvizAlarm(AlarmControlPanelEntity):
if self.coordinator.ezviz_client.api_set_defence_mode(
DefenseModeType.AWAY_MODE.value
):
- self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
+ self._attr_state = STATE_ALARM_ARMED_AWAY
except PyEzvizError as err:
raise HomeAssistantError("Cannot arm EZVIZ alarm") from err
@@ -126,7 +130,7 @@ class EzvizAlarm(AlarmControlPanelEntity):
if self.coordinator.ezviz_client.api_set_defence_mode(
DefenseModeType.SLEEP_MODE.value
):
- self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
+ self._attr_state = STATE_ALARM_ARMED_HOME
except PyEzvizError as err:
raise HomeAssistantError("Cannot arm EZVIZ alarm") from err
@@ -141,7 +145,7 @@ class EzvizAlarm(AlarmControlPanelEntity):
_LOGGER.debug(
"Updating EZVIZ alarm with response %s", ezviz_alarm_state_number
)
- self._attr_alarm_state = self.entity_description.ezviz_alarm_states[
+ self._attr_state = self.entity_description.ezviz_alarm_states[
int(ezviz_alarm_state_number)
]
diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py
index a7551737c10..aa998cc6f60 100644
--- a/homeassistant/components/ezviz/config_flow.py
+++ b/homeassistant/components/ezviz/config_flow.py
@@ -150,7 +150,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> EzvizOptionsFlowHandler:
"""Get the options flow for this handler."""
- return EzvizOptionsFlowHandler()
+ return EzvizOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -391,6 +391,10 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
class EzvizOptionsFlowHandler(OptionsFlow):
"""Handle EZVIZ client options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/ezviz/update.py b/homeassistant/components/ezviz/update.py
index 25a506a0052..05735d152cf 100644
--- a/homeassistant/components/ezviz/update.py
+++ b/homeassistant/components/ezviz/update.py
@@ -73,9 +73,11 @@ class EzvizUpdateEntity(EzvizEntity, UpdateEntity):
return self.data["version"]
@property
- def in_progress(self) -> bool:
+ def in_progress(self) -> bool | int | None:
"""Update installation progress."""
- return bool(self.data["upgrade_in_progress"])
+ if self.data["upgrade_in_progress"]:
+ return self.data["upgrade_percent"]
+ return False
@property
def latest_version(self) -> str | None:
@@ -91,13 +93,6 @@ class EzvizUpdateEntity(EzvizEntity, UpdateEntity):
return self.data["latest_firmware_info"].get("desc")
return None
- @property
- def update_percentage(self) -> int | None:
- """Update installation progress."""
- if self.data["upgrade_in_progress"]:
- return self.data["upgrade_percent"]
- return None
-
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py
index 967e7ef8e35..b9593ec907f 100644
--- a/homeassistant/components/fastdotcom/__init__.py
+++ b/homeassistant/components/fastdotcom/__init__.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import logging
-from homeassistant.config_entries import ConfigEntry, ConfigEntryState
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.start import async_at_started
@@ -26,10 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _async_finish_startup(hass: HomeAssistant) -> None:
"""Run this only when HA has finished its startup."""
- if entry.state == ConfigEntryState.LOADED:
- await coordinator.async_refresh()
- else:
- await coordinator.async_config_entry_first_refresh()
+ await coordinator.async_config_entry_first_refresh()
# Don't start a speedtest during startup, this will slow down the overall startup dramatically
async_at_started(hass, _async_finish_startup)
diff --git a/homeassistant/components/feedreader/config_flow.py b/homeassistant/components/feedreader/config_flow.py
index b902d48a1c8..8c61a2f339f 100644
--- a/homeassistant/components/feedreader/config_flow.py
+++ b/homeassistant/components/feedreader/config_flow.py
@@ -15,6 +15,7 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant, callback
@@ -45,11 +46,9 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> OptionsFlow:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
- return FeedReaderOptionsFlowHandler()
+ return FeedReaderOptionsFlowHandler(config_entry)
def show_user_form(
self,
@@ -122,6 +121,12 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle a reconfiguration flow initialized by the user."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfiguration flow initialized by the user."""
reconfigure_entry = self._get_reconfigure_entry()
@@ -129,7 +134,7 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
return self.show_user_form(
user_input={**reconfigure_entry.data},
description_placeholders={"name": reconfigure_entry.title},
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
)
feed = await async_fetch_feed(self.hass, user_input[CONF_URL])
@@ -140,7 +145,7 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
return self.show_user_form(
user_input=user_input,
description_placeholders={"name": reconfigure_entry.title},
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
errors={"base": "url_error"},
)
@@ -148,7 +153,7 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="reconfigure_successful")
-class FeedReaderOptionsFlowHandler(OptionsFlow):
+class FeedReaderOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle an options flow."""
async def async_step_init(
@@ -163,9 +168,7 @@ class FeedReaderOptionsFlowHandler(OptionsFlow):
{
vol.Optional(
CONF_MAX_ENTRIES,
- default=self.config_entry.options.get(
- CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES
- ),
+ default=self.options.get(CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES),
): cv.positive_int,
}
)
diff --git a/homeassistant/components/feedreader/strings.json b/homeassistant/components/feedreader/strings.json
index 0f0492eb6c9..da66333fa5b 100644
--- a/homeassistant/components/feedreader/strings.json
+++ b/homeassistant/components/feedreader/strings.json
@@ -6,7 +6,7 @@
"url": "[%key:common::config_flow::data::url%]"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"description": "Update your configuration information for {name}.",
"data": {
"url": "[%key:common::config_flow::data::url%]"
diff --git a/homeassistant/components/ffmpeg/manifest.json b/homeassistant/components/ffmpeg/manifest.json
index 085db6791b3..ab9f3ed65c1 100644
--- a/homeassistant/components/ffmpeg/manifest.json
+++ b/homeassistant/components/ffmpeg/manifest.json
@@ -4,5 +4,5 @@
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/ffmpeg",
"integration_type": "system",
- "requirements": ["ha-ffmpeg==3.2.2"]
+ "requirements": ["ha-ffmpeg==3.2.0"]
}
diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py
index 0ffd9aaa48f..9003704348d 100644
--- a/homeassistant/components/fibaro/config_flow.py
+++ b/homeassistant/components/fibaro/config_flow.py
@@ -9,8 +9,8 @@ from typing import Any
from slugify import slugify
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
+from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from . import FibaroAuthFailed, FibaroConnectFailed, init_controller
@@ -63,6 +63,10 @@ class FibaroConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ def __init__(self) -> None:
+ """Initialize."""
+ self._reauth_entry: ConfigEntry | None = None
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -90,6 +94,9 @@ class FibaroConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthentication."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -98,10 +105,9 @@ class FibaroConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a flow initiated by reauthentication."""
errors = {}
- reauth_entry = self._get_reauth_entry()
-
+ assert self._reauth_entry
if user_input is not None:
- new_data = reauth_entry.data | user_input
+ new_data = self._reauth_entry.data | user_input
try:
await _validate_input(self.hass, new_data)
except FibaroConnectFailed:
@@ -109,16 +115,19 @@ class FibaroConfigFlow(ConfigFlow, domain=DOMAIN):
except FibaroAuthFailed:
errors["base"] = "invalid_auth"
else:
- return self.async_update_reload_and_abort(
- reauth_entry, data_updates=user_input
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry, data=new_data
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
errors=errors,
description_placeholders={
- CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
- CONF_NAME: reauth_entry.title,
+ CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME]
},
)
diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py
index c787ca70272..fc28e57af70 100644
--- a/homeassistant/components/fibaro/cover.py
+++ b/homeassistant/components/fibaro/cover.py
@@ -79,28 +79,6 @@ class FibaroCover(FibaroEntity, CoverEntity):
"""Return the current tilt position for venetian blinds."""
return self.bound(self.level2)
- @property
- def is_opening(self) -> bool | None:
- """Return if the cover is opening or not.
-
- Be aware that this property is only available for some modern devices.
- For example the Fibaro Roller Shutter 4 reports this correctly.
- """
- if self.fibaro_device.state.has_value:
- return self.fibaro_device.state.str_value().lower() == "opening"
- return None
-
- @property
- def is_closing(self) -> bool | None:
- """Return if the cover is closing or not.
-
- Be aware that this property is only available for some modern devices.
- For example the Fibaro Roller Shutter 4 reports this correctly.
- """
- if self.fibaro_device.state.has_value:
- return self.fibaro_device.state.str_value().lower() == "closing"
- return None
-
def set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
self.set_level(cast(int, kwargs.get(ATTR_POSITION)))
diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json
index d2a1186b05b..39850672d06 100644
--- a/homeassistant/components/fibaro/manifest.json
+++ b/homeassistant/components/fibaro/manifest.json
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["pyfibaro"],
- "requirements": ["pyfibaro==0.8.0"]
+ "requirements": ["pyfibaro==0.7.8"]
}
diff --git a/homeassistant/components/file/__init__.py b/homeassistant/components/file/__init__.py
index 7bc206057c8..0c9cfee5f4d 100644
--- a/homeassistant/components/file/__init__.py
+++ b/homeassistant/components/file/__init__.py
@@ -3,16 +3,88 @@
from copy import deepcopy
from typing import Any
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_FILE_PATH, CONF_NAME, CONF_PLATFORM, Platform
-from homeassistant.core import HomeAssistant
+from homeassistant.components.notify import migrate_notify_issue
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.const import (
+ CONF_FILE_PATH,
+ CONF_NAME,
+ CONF_PLATFORM,
+ CONF_SCAN_INTERVAL,
+ Platform,
+)
+from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import (
+ config_validation as cv,
+ discovery,
+ issue_registry as ir,
+)
+from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
+from .notify import PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA
+from .sensor import PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA
+
+IMPORT_SCHEMA = {
+ Platform.SENSOR: SENSOR_PLATFORM_SCHEMA,
+ Platform.NOTIFY: NOTIFY_PLATFORM_SCHEMA,
+}
+
+CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+ """Set up the file integration."""
+
+ hass.data[DOMAIN] = config
+ if hass.config_entries.async_entries(DOMAIN):
+ # We skip import in case we already have config entries
+ return True
+ # The use of the legacy notify service was deprecated with HA Core 2024.6.0
+ # and will be removed with HA Core 2024.12
+ migrate_notify_issue(hass, DOMAIN, "File", "2024.12.0")
+ # The YAML config was imported with HA Core 2024.6.0 and will be removed with
+ # HA Core 2024.12
+ ir.async_create_issue(
+ hass,
+ HOMEASSISTANT_DOMAIN,
+ f"deprecated_yaml_{DOMAIN}",
+ breaks_in_ha_version="2024.12.0",
+ is_fixable=False,
+ issue_domain=DOMAIN,
+ learn_more_url="https://www.home-assistant.io/integrations/file/",
+ severity=ir.IssueSeverity.WARNING,
+ translation_key="deprecated_yaml",
+ translation_placeholders={
+ "domain": DOMAIN,
+ "integration_title": "File",
+ },
+ )
+
+ # Import the YAML config into separate config entries
+ platforms_config: dict[Platform, list[ConfigType]] = {
+ domain: config[domain] for domain in PLATFORMS if domain in config
+ }
+ for domain, items in platforms_config.items():
+ for item in items:
+ if item[CONF_PLATFORM] == DOMAIN:
+ file_config_item = IMPORT_SCHEMA[domain](item)
+ file_config_item[CONF_PLATFORM] = domain
+ if CONF_SCAN_INTERVAL in file_config_item:
+ del file_config_item[CONF_SCAN_INTERVAL]
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=file_config_item,
+ )
+ )
+
+ return True
+
+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a file component entry."""
config = {**entry.data, **entry.options}
@@ -30,6 +102,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry, [Platform(entry.data[CONF_PLATFORM])]
)
entry.async_on_unload(entry.add_update_listener(update_listener))
+ if entry.data[CONF_PLATFORM] == Platform.NOTIFY and CONF_NAME in entry.data:
+ # New notify entities are being setup through the config entry,
+ # but during the deprecation period we want to keep the legacy notify platform,
+ # so we forward the setup config through discovery.
+ # Only the entities from yaml will still be available as legacy service.
+ hass.async_create_task(
+ discovery.async_load_platform(
+ hass,
+ Platform.NOTIFY,
+ DOMAIN,
+ config,
+ hass.data[DOMAIN],
+ )
+ )
return True
diff --git a/homeassistant/components/file/config_flow.py b/homeassistant/components/file/config_flow.py
index 992635d05fd..d74e36ce935 100644
--- a/homeassistant/components/file/config_flow.py
+++ b/homeassistant/components/file/config_flow.py
@@ -1,8 +1,7 @@
"""Config flow for file integration."""
-from __future__ import annotations
-
from copy import deepcopy
+import os
from typing import Any
import voluptuous as vol
@@ -12,9 +11,11 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import (
CONF_FILE_PATH,
+ CONF_FILENAME,
CONF_NAME,
CONF_PLATFORM,
CONF_UNIT_OF_MEASUREMENT,
@@ -73,11 +74,9 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> FileOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
- return FileOptionsFlowHandler()
+ return FileOptionsFlowHandler(config_entry)
async def validate_file_path(self, file_path: str) -> bool:
"""Ensure the file path is valid."""
@@ -130,8 +129,29 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle file sensor config flow."""
return await self._async_handle_step(Platform.SENSOR.value, user_input)
+ async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
+ """Import `file`` config from configuration.yaml."""
+ self._async_abort_entries_match(import_data)
+ platform = import_data[CONF_PLATFORM]
+ name: str = import_data.get(CONF_NAME, DEFAULT_NAME)
+ file_name: str
+ if platform == Platform.NOTIFY:
+ file_name = import_data.pop(CONF_FILENAME)
+ file_path: str = os.path.join(self.hass.config.config_dir, file_name)
+ import_data[CONF_FILE_PATH] = file_path
+ else:
+ file_path = import_data[CONF_FILE_PATH]
+ title = f"{name} [{file_path}]"
+ data = deepcopy(import_data)
+ options = {}
+ for key, value in import_data.items():
+ if key not in (CONF_FILE_PATH, CONF_PLATFORM, CONF_NAME):
+ data.pop(key)
+ options[key] = value
+ return self.async_create_entry(title=title, data=data, options=options)
-class FileOptionsFlowHandler(OptionsFlow):
+
+class FileOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle File options."""
async def async_step_init(
diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py
index 10e3d4a4ac6..9411b7cf1a8 100644
--- a/homeassistant/components/file/notify.py
+++ b/homeassistant/components/file/notify.py
@@ -2,23 +2,104 @@
from __future__ import annotations
+from functools import partial
+import logging
import os
from typing import Any, TextIO
+import voluptuous as vol
+
from homeassistant.components.notify import (
+ ATTR_TITLE,
ATTR_TITLE_DEFAULT,
+ PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA,
+ BaseNotificationService,
NotifyEntity,
NotifyEntityFeature,
+ migrate_notify_issue,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_FILE_PATH, CONF_NAME
+from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.dt as dt_util
from .const import CONF_TIMESTAMP, DEFAULT_NAME, DOMAIN, FILE_ICON
+_LOGGER = logging.getLogger(__name__)
+
+# The legacy platform schema uses a filename, after import
+# The full file path is stored in the config entry
+PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_FILENAME): cv.string,
+ vol.Optional(CONF_TIMESTAMP, default=False): cv.boolean,
+ }
+)
+
+
+async def async_get_service(
+ hass: HomeAssistant,
+ config: ConfigType,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> FileNotificationService | None:
+ """Get the file notification service."""
+ if discovery_info is None:
+ # We only set up through discovery
+ return None
+ file_path: str = discovery_info[CONF_FILE_PATH]
+ timestamp: bool = discovery_info[CONF_TIMESTAMP]
+
+ return FileNotificationService(file_path, timestamp)
+
+
+class FileNotificationService(BaseNotificationService):
+ """Implement the notification service for the File service."""
+
+ def __init__(self, file_path: str, add_timestamp: bool) -> None:
+ """Initialize the service."""
+ self._file_path = file_path
+ self.add_timestamp = add_timestamp
+
+ async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
+ """Send a message to a file."""
+ # The use of the legacy notify service was deprecated with HA Core 2024.6.0
+ # and will be removed with HA Core 2024.12
+ migrate_notify_issue(
+ self.hass, DOMAIN, "File", "2024.12.0", service_name=self._service_name
+ )
+ await self.hass.async_add_executor_job(
+ partial(self.send_message, message, **kwargs)
+ )
+
+ def send_message(self, message: str = "", **kwargs: Any) -> None:
+ """Send a message to a file."""
+ file: TextIO
+ filepath = self._file_path
+ try:
+ with open(filepath, "a", encoding="utf8") as file:
+ if os.stat(filepath).st_size == 0:
+ title = (
+ f"{kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)} notifications (Log"
+ f" started: {dt_util.utcnow().isoformat()})\n{'-' * 80}\n"
+ )
+ file.write(title)
+
+ if self.add_timestamp:
+ text = f"{dt_util.utcnow().isoformat()} {message}\n"
+ else:
+ text = f"{message}\n"
+ file.write(text)
+ except OSError as exc:
+ raise ServiceValidationError(
+ translation_domain=DOMAIN,
+ translation_key="write_access_failed",
+ translation_placeholders={"filename": filepath, "exc": f"{exc!r}"},
+ ) from exc
+
async def async_setup_entry(
hass: HomeAssistant,
diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py
index 879c06e29f3..e37a3df86a6 100644
--- a/homeassistant/components/file/sensor.py
+++ b/homeassistant/components/file/sensor.py
@@ -6,8 +6,12 @@ import logging
import os
from file_read_backwards import FileReadBackwards
+import voluptuous as vol
-from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.sensor import (
+ PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
+ SensorEntity,
+)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_FILE_PATH,
@@ -16,13 +20,38 @@ from homeassistant.const import (
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.template import Template
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DEFAULT_NAME, FILE_ICON
_LOGGER = logging.getLogger(__name__)
+PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_FILE_PATH): cv.isfile,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.string,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
+ }
+)
+
+
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
+ """Set up the file sensor from YAML.
+
+ The YAML platform config is automatically
+ imported to a config entry, this method can be removed
+ when YAML support is removed.
+ """
+
async def async_setup_entry(
hass: HomeAssistant,
diff --git a/homeassistant/components/file/strings.json b/homeassistant/components/file/strings.json
index 8806c67cd96..60ebf451f78 100644
--- a/homeassistant/components/file/strings.json
+++ b/homeassistant/components/file/strings.json
@@ -18,7 +18,7 @@
},
"data_description": {
"file_path": "The local file path to retrieve the sensor value from",
- "value_template": "A template to render the sensors value based on the file content",
+ "value_template": "A template to render the the sensors value based on the file content",
"unit_of_measurement": "Unit of measurement for the sensor"
}
},
diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py
index aa303a08795..9173a2b3392 100644
--- a/homeassistant/components/fireservicerota/__init__.py
+++ b/homeassistant/components/fireservicerota/__init__.py
@@ -46,7 +46,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name="duty binary sensor",
update_method=async_update_data,
update_interval=MIN_TIME_BETWEEN_UPDATES,
diff --git a/homeassistant/components/fitbit/api.py b/homeassistant/components/fitbit/api.py
index e5ae88c5420..1eed5acbcca 100644
--- a/homeassistant/components/fitbit/api.py
+++ b/homeassistant/components/fitbit/api.py
@@ -156,7 +156,8 @@ class OAuthFitbitApi(FitbitApi):
async def async_get_access_token(self) -> dict[str, Any]:
"""Return a valid access token for the Fitbit API."""
- await self._oauth_session.async_ensure_token_valid()
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token
diff --git a/homeassistant/components/fitbit/config_flow.py b/homeassistant/components/fitbit/config_flow.py
index cb4e3fb4ea3..eff4ba37773 100644
--- a/homeassistant/components/fitbit/config_flow.py
+++ b/homeassistant/components/fitbit/config_flow.py
@@ -4,7 +4,7 @@ from collections.abc import Mapping
import logging
from typing import Any
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.const import CONF_TOKEN
from homeassistant.helpers import config_entry_oauth2_flow
@@ -22,6 +22,8 @@ class OAuth2FlowHandler(
DOMAIN = DOMAIN
+ reauth_entry: ConfigEntry | None = None
+
@property
def logger(self) -> logging.Logger:
"""Return logger."""
@@ -32,13 +34,16 @@ class OAuth2FlowHandler(
"""Extra data that needs to be appended to the authorize url."""
return {
"scope": " ".join(OAUTH_SCOPES),
- "prompt": "consent" if self.source != SOURCE_REAUTH else "none",
+ "prompt": "consent" if not self.reauth_entry else "none",
}
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -77,13 +82,14 @@ class OAuth2FlowHandler(
_LOGGER.error("Failed to fetch user profile for Fitbit API: %s", err)
return self.async_abort(reason="cannot_connect")
- await self.async_set_unique_id(profile.encoded_id)
- 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=data
- )
+ if self.reauth_entry:
+ if self.reauth_entry.unique_id != profile.encoded_id:
+ return self.async_abort(reason="wrong_account")
+ self.hass.config_entries.async_update_entry(self.reauth_entry, data=data)
+ await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+ await self.async_set_unique_id(profile.encoded_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=profile.display_name, data=data)
diff --git a/homeassistant/components/flexit_bacnet/sensor.py b/homeassistant/components/flexit_bacnet/sensor.py
index be5f12e480e..2453acb90be 100644
--- a/homeassistant/components/flexit_bacnet/sensor.py
+++ b/homeassistant/components/flexit_bacnet/sensor.py
@@ -10,6 +10,7 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
+ StateType,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -21,7 +22,6 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import StateType
from . import FlexitCoordinator
from .const import DOMAIN
diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py
index 9a02120f33a..d78fc699579 100644
--- a/homeassistant/components/flux_led/config_flow.py
+++ b/homeassistant/components/flux_led/config_flow.py
@@ -71,11 +71,9 @@ class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> FluxLedOptionsFlow:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for the Flux LED component."""
- return FluxLedOptionsFlow()
+ return FluxLedOptionsFlow(config_entry)
async def async_step_dhcp(
self, discovery_info: dhcp.DhcpServiceInfo
@@ -322,6 +320,10 @@ class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
class FluxLedOptionsFlow(OptionsFlow):
"""Handle flux_led options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize the flux_led options flow."""
+ self._config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -330,7 +332,7 @@ class FluxLedOptionsFlow(OptionsFlow):
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
- options = self.config_entry.options
+ options = self._config_entry.options
options_schema = vol.Schema(
{
vol.Optional(
diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py
index 3aeaa6f7ef2..800a95509c2 100644
--- a/homeassistant/components/folder_watcher/__init__.py
+++ b/homeassistant/components/folder_watcher/__init__.py
@@ -4,8 +4,9 @@ from __future__ import annotations
import logging
import os
-from typing import cast
+from typing import Any, cast
+import voluptuous as vol
from watchdog.events import (
FileClosedEvent,
FileCreatedEvent,
@@ -18,17 +19,69 @@ from watchdog.events import (
)
from watchdog.observers import Observer
-from homeassistant.config_entries import ConfigEntry
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
+from homeassistant.helpers.typing import ConfigType
-from .const import CONF_FOLDER, CONF_PATTERNS, DOMAIN, PLATFORMS
+from .const import CONF_FOLDER, CONF_PATTERNS, DEFAULT_PATTERN, DOMAIN, PLATFORMS
_LOGGER = logging.getLogger(__name__)
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.All(
+ cv.ensure_list,
+ [
+ vol.Schema(
+ {
+ vol.Required(CONF_FOLDER): cv.isdir,
+ vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]): vol.All(
+ cv.ensure_list, [cv.string]
+ ),
+ }
+ )
+ ],
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+ """Set up the folder watcher."""
+ if DOMAIN in config:
+ conf: list[dict[str, Any]] = config[DOMAIN]
+ for watcher in conf:
+ path: str = watcher[CONF_FOLDER]
+ if not hass.config.is_allowed_path(path):
+ async_create_issue(
+ hass,
+ DOMAIN,
+ f"import_failed_not_allowed_path_{path}",
+ is_fixable=False,
+ is_persistent=False,
+ severity=IssueSeverity.ERROR,
+ translation_key="import_failed_not_allowed_path",
+ translation_placeholders={
+ "path": path,
+ "config_variable": "allowlist_external_dirs",
+ },
+ )
+ continue
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=watcher
+ )
+ )
+
+ return True
+
+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Folder watcher from a config entry."""
diff --git a/homeassistant/components/folder_watcher/config_flow.py b/homeassistant/components/folder_watcher/config_flow.py
index eb176cfaf24..fe43cd1c725 100644
--- a/homeassistant/components/folder_watcher/config_flow.py
+++ b/homeassistant/components/folder_watcher/config_flow.py
@@ -8,8 +8,10 @@ from typing import Any
import voluptuous as vol
+from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN
from homeassistant.config_entries import ConfigFlowResult
from homeassistant.core import callback
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
SchemaConfigFlowHandler,
@@ -44,6 +46,28 @@ async def validate_setup(
return user_input
+async def validate_import_setup(
+ handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
+) -> dict[str, Any]:
+ """Create issue on successful import."""
+ async_create_issue(
+ handler.parent_handler.hass,
+ HOMEASSISTANT_DOMAIN,
+ f"deprecated_yaml_{DOMAIN}",
+ breaks_in_ha_version="2024.11.0",
+ is_fixable=False,
+ is_persistent=False,
+ issue_domain=DOMAIN,
+ severity=IssueSeverity.WARNING,
+ translation_key="deprecated_yaml",
+ translation_placeholders={
+ "domain": DOMAIN,
+ "integration_title": "Folder Watcher",
+ },
+ )
+ return user_input
+
+
OPTIONS_SCHEMA = vol.Schema(
{
vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]): SelectSelector(
@@ -64,6 +88,9 @@ DATA_SCHEMA = vol.Schema(
CONFIG_FLOW = {
"user": SchemaFlowFormStep(schema=DATA_SCHEMA, validate_user_input=validate_setup),
+ "import": SchemaFlowFormStep(
+ schema=DATA_SCHEMA, validate_user_input=validate_import_setup
+ ),
}
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(schema=OPTIONS_SCHEMA),
diff --git a/homeassistant/components/forecast_solar/config_flow.py b/homeassistant/components/forecast_solar/config_flow.py
index 9a64ce6e1fb..982f32eb07b 100644
--- a/homeassistant/components/forecast_solar/config_flow.py
+++ b/homeassistant/components/forecast_solar/config_flow.py
@@ -41,7 +41,7 @@ class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> ForecastSolarOptionFlowHandler:
"""Get the options flow for this handler."""
- return ForecastSolarOptionFlowHandler()
+ return ForecastSolarOptionFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -91,6 +91,10 @@ class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN):
class ForecastSolarOptionFlowHandler(OptionsFlow):
"""Handle options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py
index 5fb9f08f1c0..5f061aa4be1 100644
--- a/homeassistant/components/forked_daapd/config_flow.py
+++ b/homeassistant/components/forked_daapd/config_flow.py
@@ -52,6 +52,10 @@ TEST_CONNECTION_ERROR_DICT = {
class ForkedDaapdOptionsFlowHandler(OptionsFlow):
"""Handle a forked-daapd options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -118,7 +122,7 @@ class ForkedDaapdFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> ForkedDaapdOptionsFlowHandler:
"""Return options flow handler."""
- return ForkedDaapdOptionsFlowHandler()
+ return ForkedDaapdOptionsFlowHandler(config_entry)
async def validate_input(self, user_input):
"""Validate the user input."""
diff --git a/homeassistant/components/freebox/alarm_control_panel.py b/homeassistant/components/freebox/alarm_control_panel.py
index 9d8e85a14ca..891180785b0 100644
--- a/homeassistant/components/freebox/alarm_control_panel.py
+++ b/homeassistant/components/freebox/alarm_control_panel.py
@@ -5,9 +5,15 @@ from typing import Any
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -16,14 +22,14 @@ from .entity import FreeboxHomeEntity
from .router import FreeboxRouter
FREEBOX_TO_STATUS = {
- "alarm1_arming": AlarmControlPanelState.ARMING,
- "alarm2_arming": AlarmControlPanelState.ARMING,
- "alarm1_armed": AlarmControlPanelState.ARMED_AWAY,
- "alarm2_armed": AlarmControlPanelState.ARMED_HOME,
- "alarm1_alert_timer": AlarmControlPanelState.TRIGGERED,
- "alarm2_alert_timer": AlarmControlPanelState.TRIGGERED,
- "alert": AlarmControlPanelState.TRIGGERED,
- "idle": AlarmControlPanelState.DISARMED,
+ "alarm1_arming": STATE_ALARM_ARMING,
+ "alarm2_arming": STATE_ALARM_ARMING,
+ "alarm1_armed": STATE_ALARM_ARMED_AWAY,
+ "alarm2_armed": STATE_ALARM_ARMED_HOME,
+ "alarm1_alert_timer": STATE_ALARM_TRIGGERED,
+ "alarm2_alert_timer": STATE_ALARM_TRIGGERED,
+ "alert": STATE_ALARM_TRIGGERED,
+ "idle": STATE_ALARM_DISARMED,
}
@@ -97,6 +103,6 @@ class FreeboxAlarm(FreeboxHomeEntity, AlarmControlPanelEntity):
"""Update state."""
state: str | None = await self.get_home_endpoint_value(self._command_state)
if state:
- self._attr_alarm_state = FREEBOX_TO_STATUS.get(state)
+ self._attr_state = FREEBOX_TO_STATUS.get(state)
else:
- self._attr_alarm_state = None
+ self._attr_state = None
diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py
index 920ecda1c52..547910b3cf0 100644
--- a/homeassistant/components/fritz/config_flow.py
+++ b/homeassistant/components/fritz/config_flow.py
@@ -23,6 +23,7 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import (
CONF_HOST,
@@ -57,18 +58,15 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
- _host: str
-
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> FritzBoxToolsOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
- return FritzBoxToolsOptionsFlowHandler()
+ return FritzBoxToolsOptionsFlowHandler(config_entry)
def __init__(self) -> None:
"""Initialize FRITZ!Box Tools flow."""
+ self._host: str | None = None
self._name: str = ""
self._password: str = ""
self._use_tls: bool = False
@@ -113,6 +111,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
async def async_check_configured_entry(self) -> ConfigEntry | None:
"""Check if entry is configured."""
+ assert self._host
current_host = await self.hass.async_add_executor_job(
socket.gethostbyname, self._host
)
@@ -154,17 +153,15 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle a flow initialized by discovery."""
ssdp_location: ParseResult = urlparse(discovery_info.ssdp_location or "")
- host = ssdp_location.hostname
- if not host or ipaddress.ip_address(host).is_link_local:
- return self.async_abort(reason="ignore_ip6_link_local")
-
- self._host = host
+ self._host = ssdp_location.hostname
self._name = (
discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)
or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME]
)
- uuid: str | None
+ if not self._host or ipaddress.ip_address(self._host).is_link_local:
+ return self.async_abort(reason="ignore_ip6_link_local")
+
if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
if uuid.startswith("uuid:"):
uuid = uuid[5:]
@@ -337,7 +334,20 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
},
)
- def _show_setup_form_reconfigure(
+ async def async_step_reconfigure(
+ self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle reconfigure flow ."""
+ entry_data = self._get_reconfigure_entry().data
+ self._host = entry_data[CONF_HOST]
+ self._port = entry_data[CONF_PORT]
+ self._username = entry_data[CONF_USERNAME]
+ self._password = entry_data[CONF_PASSWORD]
+ self._use_tls = entry_data.get(CONF_SSL, DEFAULT_SSL)
+
+ return await self.async_step_reconfigure_confirm()
+
+ def _show_setup_form_reconfigure_confirm(
self, user_input: dict[str, Any], errors: dict[str, str] | None = None
) -> ConfigFlowResult:
"""Show the reconfigure form to the user."""
@@ -348,7 +358,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
}
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST, default=user_input[CONF_HOST]): str,
@@ -356,21 +366,20 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
vol.Required(CONF_SSL, default=user_input[CONF_SSL]): bool,
}
),
- description_placeholders={"host": user_input[CONF_HOST]},
+ description_placeholders={"host": self._host},
errors=errors or {},
)
- async def async_step_reconfigure(
+ async def async_step_reconfigure_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfigure flow."""
if user_input is None:
- reconfigure_entry_data = self._get_reconfigure_entry().data
- return self._show_setup_form_reconfigure(
+ return self._show_setup_form_reconfigure_confirm(
{
- CONF_HOST: reconfigure_entry_data[CONF_HOST],
- CONF_PORT: reconfigure_entry_data[CONF_PORT],
- CONF_SSL: reconfigure_entry_data.get(CONF_SSL, DEFAULT_SSL),
+ CONF_HOST: self._host,
+ CONF_PORT: self._port,
+ CONF_SSL: self._use_tls,
}
)
@@ -378,25 +387,24 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
self._use_tls = user_input[CONF_SSL]
self._port = self._determine_port(user_input)
- reconfigure_entry = self._get_reconfigure_entry()
- self._username = reconfigure_entry.data[CONF_USERNAME]
- self._password = reconfigure_entry.data[CONF_PASSWORD]
if error := await self.async_fritz_tools_init():
- return self._show_setup_form_reconfigure(
+ return self._show_setup_form_reconfigure_confirm(
user_input={**user_input, CONF_PORT: self._port}, errors={"base": error}
)
return self.async_update_reload_and_abort(
- reconfigure_entry,
- data_updates={
+ self._get_reconfigure_entry(),
+ data={
CONF_HOST: self._host,
+ CONF_PASSWORD: self._password,
CONF_PORT: self._port,
+ CONF_USERNAME: self._username,
CONF_SSL: self._use_tls,
},
)
-class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
+class FritzBoxToolsOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle an options flow."""
async def async_step_init(
@@ -407,18 +415,19 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
- options = self.config_entry.options
data_schema = vol.Schema(
{
vol.Optional(
CONF_CONSIDER_HOME,
- default=options.get(
+ default=self.options.get(
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
),
): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)),
vol.Optional(
CONF_OLD_DISCOVERY,
- default=options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY),
+ default=self.options.get(
+ CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY
+ ),
): bool,
}
)
diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py
index 31d8ff81491..4134f0af026 100644
--- a/homeassistant/components/fritz/coordinator.py
+++ b/homeassistant/components/fritz/coordinator.py
@@ -606,9 +606,6 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
dev_info: Device = hosts[dev_mac]
for link in interf["node_links"]:
- if link.get("state") != "CONNECTED":
- continue # ignore orphan node links
-
intf = mesh_intf.get(link["node_interface_1_uid"])
if intf is not None:
if intf["op_mode"] == "AP_GUEST":
diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json
index 27aa42d9b2c..35250d9d34d 100644
--- a/homeassistant/components/fritz/manifest.json
+++ b/homeassistant/components/fritz/manifest.json
@@ -1,7 +1,7 @@
{
"domain": "fritz",
"name": "AVM FRITZ!Box Tools",
- "codeowners": ["@AaronDavidSchneider", "@chemelli74", "@mib1185"],
+ "codeowners": ["@mammuth", "@AaronDavidSchneider", "@chemelli74", "@mib1185"],
"config_flow": true,
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/fritz",
diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json
index 96eb6243529..54dc76e3c59 100644
--- a/homeassistant/components/fritz/strings.json
+++ b/homeassistant/components/fritz/strings.json
@@ -19,7 +19,7 @@
"password": "[%key:common::config_flow::data::password%]"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"title": "Updating FRITZ!Box Tools - configuration",
"description": "Update FRITZ!Box Tools configuration for: {host}.",
"data": {
diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py
index ffec4a9ea29..502336533c1 100644
--- a/homeassistant/components/fritzbox/config_flow.py
+++ b/homeassistant/components/fritzbox/config_flow.py
@@ -43,11 +43,10 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
- _name: str
-
def __init__(self) -> None:
"""Initialize flow."""
self._host: str | None = None
+ self._name: str | None = None
self._password: str | None = None
self._username: str | None = None
@@ -159,6 +158,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
result = await self.async_try_connect()
if result == RESULT_SUCCESS:
+ assert self._name is not None
return self._get_entry(self._name)
if result != RESULT_INVALID_AUTH:
return self.async_abort(reason=result)
@@ -220,6 +220,18 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle a reconfiguration flow initialized by the user."""
+ entry_data = self._get_reconfigure_entry().data
+ self._name = entry_data[CONF_HOST]
+ self._host = entry_data[CONF_HOST]
+ self._username = entry_data[CONF_USERNAME]
+ self._password = entry_data[CONF_PASSWORD]
+
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfiguration flow initialized by the user."""
errors = {}
@@ -227,27 +239,26 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
self._host = user_input[CONF_HOST]
- reconfigure_entry = self._get_reconfigure_entry()
- self._username = reconfigure_entry.data[CONF_USERNAME]
- self._password = reconfigure_entry.data[CONF_PASSWORD]
-
result = await self.async_try_connect()
if result == RESULT_SUCCESS:
return self.async_update_reload_and_abort(
- reconfigure_entry,
- data_updates={CONF_HOST: self._host},
+ self._get_reconfigure_entry(),
+ data={
+ CONF_HOST: self._host,
+ CONF_PASSWORD: self._password,
+ CONF_USERNAME: self._username,
+ },
)
errors["base"] = result
- host = self._get_reconfigure_entry().data[CONF_HOST]
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=vol.Schema(
{
- vol.Required(CONF_HOST, default=host): str,
+ vol.Required(CONF_HOST, default=self._host): str,
}
),
- description_placeholders={"name": host},
+ description_placeholders={"name": self._name},
errors=errors,
)
diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json
index c7c2439b566..2b7dbff0a20 100644
--- a/homeassistant/components/fritzbox/strings.json
+++ b/homeassistant/components/fritzbox/strings.json
@@ -27,7 +27,7 @@
"password": "[%key:common::config_flow::data::password%]"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"description": "Update your configuration information for {name}.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
diff --git a/homeassistant/components/fritzbox_callmonitor/config_flow.py b/homeassistant/components/fritzbox_callmonitor/config_flow.py
index 7bd0eacb66a..69efceae281 100644
--- a/homeassistant/components/fritzbox_callmonitor/config_flow.py
+++ b/homeassistant/components/fritzbox_callmonitor/config_flow.py
@@ -141,7 +141,7 @@ class FritzBoxCallMonitorConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> FritzBoxCallMonitorOptionsFlowHandler:
"""Get the options flow for this handler."""
- return FritzBoxCallMonitorOptionsFlowHandler()
+ return FritzBoxCallMonitorOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -278,6 +278,10 @@ class FritzBoxCallMonitorConfigFlow(ConfigFlow, domain=DOMAIN):
class FritzBoxCallMonitorOptionsFlowHandler(OptionsFlow):
"""Handle a fritzbox_callmonitor options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize."""
+ self.config_entry = config_entry
+
@classmethod
def _are_prefixes_valid(cls, prefixes: str | None) -> bool:
"""Check if prefixes are valid."""
diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py
index e30f8e85fa0..07271b91f28 100644
--- a/homeassistant/components/fronius/__init__.py
+++ b/homeassistant/components/fronius/__init__.py
@@ -199,10 +199,7 @@ class FroniusSolarNet:
name=_inverter_name,
inverter_info=_inverter_info,
)
- if self.config_entry.state == ConfigEntryState.LOADED:
- await _coordinator.async_refresh()
- else:
- await _coordinator.async_config_entry_first_refresh()
+ await _coordinator.async_config_entry_first_refresh()
self.inverter_coordinators.append(_coordinator)
# Only for re-scans. Initial setup adds entities through sensor.async_setup_entry
diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json
index 4dc5a2b0ae4..9f79dcf34f6 100644
--- a/homeassistant/components/frontend/manifest.json
+++ b/homeassistant/components/frontend/manifest.json
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
- "requirements": ["home-assistant-frontend==20241106.2"]
+ "requirements": ["home-assistant-frontend==20241002.2"]
}
diff --git a/homeassistant/components/frontier_silicon/config_flow.py b/homeassistant/components/frontier_silicon/config_flow.py
index 0612419fc33..06af041d8f2 100644
--- a/homeassistant/components/frontier_silicon/config_flow.py
+++ b/homeassistant/components/frontier_silicon/config_flow.py
@@ -16,7 +16,7 @@ from afsapi import (
import voluptuous as vol
from homeassistant.components import ssdp
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT
from .const import (
@@ -58,6 +58,7 @@ class FrontierSiliconConfigFlow(ConfigFlow, domain=DOMAIN):
_name: str
_webfsapi_url: str
+ _reauth_entry: ConfigEntry | None = None # Only used in reauth flows
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -177,6 +178,11 @@ class FrontierSiliconConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
self._webfsapi_url = entry_data[CONF_WEBFSAPI_URL]
+
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
+
return await self.async_step_device_config()
async def async_step_device_config(
@@ -207,11 +213,13 @@ class FrontierSiliconConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
- data_updates={CONF_PIN: user_input[CONF_PIN]},
+ if self._reauth_entry:
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry,
+ data={CONF_PIN: user_input[CONF_PIN]},
)
+ await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
try:
unique_id = await afsapi.get_radio_id()
diff --git a/homeassistant/components/fujitsu_fglair/config_flow.py b/homeassistant/components/fujitsu_fglair/config_flow.py
index c4b097ff0de..aef856631f6 100644
--- a/homeassistant/components/fujitsu_fglair/config_flow.py
+++ b/homeassistant/components/fujitsu_fglair/config_flow.py
@@ -8,7 +8,7 @@ from ayla_iot_unofficial import AylaAuthError, new_ayla_api
from ayla_iot_unofficial.fujitsu_consts import FGLAIR_APP_CREDENTIALS
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
@@ -41,6 +41,7 @@ class FGLairConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Fujitsu HVAC (based on Ayla IOT)."""
MINOR_VERSION = 2
+ _reauth_entry: ConfigEntry | None = None
async def _async_validate_credentials(
self, user_input: dict[str, Any]
@@ -92,6 +93,9 @@ class FGLairConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -99,23 +103,25 @@ class FGLairConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
+ assert self._reauth_entry
- reauth_entry = self._get_reauth_entry()
if user_input:
- errors = await self._async_validate_credentials(
- reauth_entry.data | user_input
- )
+ reauth_data = {
+ **self._reauth_entry.data,
+ CONF_PASSWORD: user_input[CONF_PASSWORD],
+ }
+ errors = await self._async_validate_credentials(reauth_data)
- if not errors:
+ if len(errors) == 0:
return self.async_update_reload_and_abort(
- reauth_entry, data_updates=user_input
+ self._reauth_entry, data=reauth_data
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_REAUTH_DATA_SCHEMA,
description_placeholders={
- CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
+ CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME],
**self.context["title_placeholders"],
},
errors=errors,
diff --git a/homeassistant/components/fujitsu_fglair/const.py b/homeassistant/components/fujitsu_fglair/const.py
index 73c811a1ed5..8aa911a8b30 100644
--- a/homeassistant/components/fujitsu_fglair/const.py
+++ b/homeassistant/components/fujitsu_fglair/const.py
@@ -9,5 +9,5 @@ DOMAIN = "fujitsu_fglair"
CONF_REGION = "region"
CONF_EUROPE = "is_europe"
-REGION_EU = "eu"
+REGION_EU = "EU"
REGION_DEFAULT = "default"
diff --git a/homeassistant/components/fujitsu_fglair/manifest.json b/homeassistant/components/fujitsu_fglair/manifest.json
index f7f3af8d037..76cf3966fbe 100644
--- a/homeassistant/components/fujitsu_fglair/manifest.json
+++ b/homeassistant/components/fujitsu_fglair/manifest.json
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/fujitsu_fglair",
"iot_class": "cloud_polling",
- "requirements": ["ayla-iot-unofficial==1.4.3"]
+ "requirements": ["ayla-iot-unofficial==1.4.1"]
}
diff --git a/homeassistant/components/fyta/config_flow.py b/homeassistant/components/fyta/config_flow.py
index 78cb7647785..f2b5163c9db 100644
--- a/homeassistant/components/fyta/config_flow.py
+++ b/homeassistant/components/fyta/config_flow.py
@@ -23,6 +23,7 @@ from homeassistant.helpers.selector import (
TextSelectorType,
)
+from . import FytaConfigEntry
from .const import CONF_EXPIRATION, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -50,6 +51,7 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Fyta."""
credentials: Credentials
+ _entry: FytaConfigEntry | None = None
VERSION = 1
MINOR_VERSION = 2
@@ -98,6 +100,7 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle flow upon an API authentication error."""
+ self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -105,21 +108,20 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle reauthorization flow."""
errors = {}
+ assert self._entry is not None
- reauth_entry = self._get_reauth_entry()
if user_input and not (errors := await self.async_auth(user_input)):
user_input |= {
CONF_ACCESS_TOKEN: self.credentials.access_token,
CONF_EXPIRATION: self.credentials.expiration.isoformat(),
}
return self.async_update_reload_and_abort(
- reauth_entry,
- data_updates=user_input,
+ self._entry, data={**self._entry.data, **user_input}
)
data_schema = self.add_suggested_values_to_schema(
DATA_SCHEMA,
- {CONF_USERNAME: reauth_entry.data[CONF_USERNAME], **(user_input or {})},
+ {CONF_USERNAME: self._entry.data[CONF_USERNAME], **(user_input or {})},
)
return self.async_show_form(
step_id="reauth_confirm",
diff --git a/homeassistant/components/fyta/coordinator.py b/homeassistant/components/fyta/coordinator.py
index c4aa9bfe589..df607de76b0 100644
--- a/homeassistant/components/fyta/coordinator.py
+++ b/homeassistant/components/fyta/coordinator.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-from collections.abc import Callable
from datetime import datetime, timedelta
import logging
from typing import TYPE_CHECKING
@@ -19,10 +18,9 @@ from fyta_cli.fyta_models import Plant
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
-import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-from .const import CONF_EXPIRATION, DOMAIN
+from .const import CONF_EXPIRATION
if TYPE_CHECKING:
from . import FytaConfigEntry
@@ -44,8 +42,6 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]):
update_interval=timedelta(minutes=4),
)
self.fyta = fyta
- self._plants_last_update: set[int] = set()
- self.new_device_callbacks: list[Callable[[int], None]] = []
async def _async_update_data(
self,
@@ -59,62 +55,9 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]):
await self.renew_authentication()
try:
- data = await self.fyta.update_all_plants()
+ return await self.fyta.update_all_plants()
except (FytaConnectionError, FytaPlantError) as err:
raise UpdateFailed(err) from err
- _LOGGER.debug("Data successfully updated")
-
- # data must be assigned before _async_add_remove_devices, as it is uses to set-up possible new devices
- self.data = data
- self._async_add_remove_devices()
-
- return data
-
- def _async_add_remove_devices(self) -> None:
- """Add new devices, remove non-existing devices."""
- if not self._plants_last_update:
- self._plants_last_update = set(self.fyta.plant_list.keys())
-
- if (
- current_plants := set(self.fyta.plant_list.keys())
- ) == self._plants_last_update:
- return
-
- _LOGGER.debug(
- "Check for new and removed plant(s): old plants: %s; new plants: %s",
- ", ".join(map(str, self._plants_last_update)),
- ", ".join(map(str, current_plants)),
- )
-
- # remove old plants
- if removed_plants := self._plants_last_update - current_plants:
- _LOGGER.debug("Removed plant(s): %s", ", ".join(map(str, removed_plants)))
-
- device_registry = dr.async_get(self.hass)
- for plant_id in removed_plants:
- if device := device_registry.async_get_device(
- identifiers={
- (
- DOMAIN,
- f"{self.config_entry.entry_id}-{plant_id}",
- )
- }
- ):
- device_registry.async_update_device(
- device_id=device.id,
- remove_config_entry_id=self.config_entry.entry_id,
- )
- _LOGGER.debug("Device removed from device registry: %s", device.id)
-
- # add new devices
- if new_plants := current_plants - self._plants_last_update:
- _LOGGER.debug("New plant(s) found: %s", ", ".join(map(str, new_plants)))
- for plant_id in new_plants:
- for callback in self.new_device_callbacks:
- callback(plant_id)
- _LOGGER.debug("Device added: %s", plant_id)
-
- self._plants_last_update = current_plants
async def renew_authentication(self) -> bool:
"""Renew access token for FYTA API."""
diff --git a/homeassistant/components/fyta/manifest.json b/homeassistant/components/fyta/manifest.json
index 17fe5199eee..73f6b42f53b 100644
--- a/homeassistant/components/fyta/manifest.json
+++ b/homeassistant/components/fyta/manifest.json
@@ -6,7 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/fyta",
"integration_type": "hub",
"iot_class": "cloud_polling",
- "loggers": ["fyta_cli"],
"quality_scale": "platinum",
- "requirements": ["fyta_cli==0.6.10"]
+ "requirements": ["fyta_cli==0.6.7"]
}
diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py
index 89ee22265cf..f324b9b3afe 100644
--- a/homeassistant/components/fyta/sensor.py
+++ b/homeassistant/components/fyta/sensor.py
@@ -150,15 +150,6 @@ async def async_setup_entry(
async_add_entities(plant_entities)
- def _async_add_new_device(plant_id: int) -> None:
- async_add_entities(
- FytaPlantSensor(coordinator, entry, sensor, plant_id)
- for sensor in SENSORS
- if sensor.key in dir(coordinator.data.get(plant_id))
- )
-
- coordinator.new_device_callbacks.append(_async_add_new_device)
-
class FytaPlantSensor(FytaPlantEntity, SensorEntity):
"""Represents a Fyta sensor."""
diff --git a/homeassistant/components/gardena_bluetooth/__init__.py b/homeassistant/components/gardena_bluetooth/__init__.py
index 7aae629974c..b6a26456168 100644
--- a/homeassistant/components/gardena_bluetooth/__init__.py
+++ b/homeassistant/components/gardena_bluetooth/__init__.py
@@ -32,8 +32,6 @@ LOGGER = logging.getLogger(__name__)
TIMEOUT = 20.0
DISCONNECT_DELAY = 5
-type GardenaBluetoothConfigEntry = ConfigEntry[GardenaBluetoothCoordinator]
-
def get_connection(hass: HomeAssistant, address: str) -> CachedConnection:
"""Set up a cached client that keeps connection after last use."""
@@ -49,9 +47,7 @@ def get_connection(hass: HomeAssistant, address: str) -> CachedConnection:
return CachedConnection(DISCONNECT_DELAY, _device_lookup)
-async def async_setup_entry(
- hass: HomeAssistant, entry: GardenaBluetoothConfigEntry
-) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Gardena Bluetooth from a config entry."""
address = entry.data[CONF_ADDRESS]
@@ -83,18 +79,17 @@ async def async_setup_entry(
hass, LOGGER, client, uuids, device, address
)
- entry.runtime_data = coordinator
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await coordinator.async_refresh()
return True
-async def async_unload_entry(
- hass: HomeAssistant, entry: GardenaBluetoothConfigEntry
-) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
- await entry.runtime_data.async_shutdown()
+ coordinator: GardenaBluetoothCoordinator = hass.data[DOMAIN].pop(entry.entry_id)
+ await coordinator.async_shutdown()
return unload_ok
diff --git a/homeassistant/components/gardena_bluetooth/binary_sensor.py b/homeassistant/components/gardena_bluetooth/binary_sensor.py
index d3ae096e291..be6d8bbeede 100644
--- a/homeassistant/components/gardena_bluetooth/binary_sensor.py
+++ b/homeassistant/components/gardena_bluetooth/binary_sensor.py
@@ -12,11 +12,13 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import GardenaBluetoothConfigEntry
+from .const import DOMAIN
+from .coordinator import GardenaBluetoothCoordinator
from .entity import GardenaBluetoothDescriptorEntity
@@ -51,12 +53,10 @@ DESCRIPTIONS = (
async def async_setup_entry(
- hass: HomeAssistant,
- entry: GardenaBluetoothConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up binary sensor based on a config entry."""
- coordinator = entry.runtime_data
+ coordinator: GardenaBluetoothCoordinator = hass.data[DOMAIN][entry.entry_id]
entities = [
GardenaBluetoothBinarySensor(coordinator, description, description.context)
for description in DESCRIPTIONS
diff --git a/homeassistant/components/gardena_bluetooth/button.py b/homeassistant/components/gardena_bluetooth/button.py
index 9d87cba2446..67377dc684e 100644
--- a/homeassistant/components/gardena_bluetooth/button.py
+++ b/homeassistant/components/gardena_bluetooth/button.py
@@ -8,11 +8,13 @@ from gardena_bluetooth.const import Reset
from gardena_bluetooth.parse import CharacteristicBool
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import GardenaBluetoothConfigEntry
+from .const import DOMAIN
+from .coordinator import GardenaBluetoothCoordinator
from .entity import GardenaBluetoothDescriptorEntity
@@ -40,12 +42,10 @@ DESCRIPTIONS = (
async def async_setup_entry(
- hass: HomeAssistant,
- entry: GardenaBluetoothConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up button based on a config entry."""
- coordinator = entry.runtime_data
+ coordinator: GardenaBluetoothCoordinator = hass.data[DOMAIN][entry.entry_id]
entities = [
GardenaBluetoothButton(coordinator, description, description.context)
for description in DESCRIPTIONS
diff --git a/homeassistant/components/gardena_bluetooth/manifest.json b/homeassistant/components/gardena_bluetooth/manifest.json
index da5c08c38c5..6d7566b3edf 100644
--- a/homeassistant/components/gardena_bluetooth/manifest.json
+++ b/homeassistant/components/gardena_bluetooth/manifest.json
@@ -14,5 +14,5 @@
"documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth",
"iot_class": "local_polling",
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
- "requirements": ["gardena-bluetooth==1.4.4"]
+ "requirements": ["gardena-bluetooth==1.4.3"]
}
diff --git a/homeassistant/components/gardena_bluetooth/number.py b/homeassistant/components/gardena_bluetooth/number.py
index b55630fa797..d3c178ee637 100644
--- a/homeassistant/components/gardena_bluetooth/number.py
+++ b/homeassistant/components/gardena_bluetooth/number.py
@@ -17,11 +17,12 @@ from homeassistant.components.number import (
NumberEntityDescription,
NumberMode,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import GardenaBluetoothConfigEntry
+from .const import DOMAIN
from .coordinator import GardenaBluetoothCoordinator
from .entity import GardenaBluetoothDescriptorEntity, GardenaBluetoothEntity
@@ -104,12 +105,10 @@ DESCRIPTIONS = (
async def async_setup_entry(
- hass: HomeAssistant,
- entry: GardenaBluetoothConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up entity based on a config entry."""
- coordinator = entry.runtime_data
+ coordinator: GardenaBluetoothCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[NumberEntity] = [
GardenaBluetoothNumber(coordinator, description, description.context)
for description in DESCRIPTIONS
diff --git a/homeassistant/components/gardena_bluetooth/sensor.py b/homeassistant/components/gardena_bluetooth/sensor.py
index ee8a2663218..19fefefa9aa 100644
--- a/homeassistant/components/gardena_bluetooth/sensor.py
+++ b/homeassistant/components/gardena_bluetooth/sensor.py
@@ -14,12 +14,13 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.dt as dt_util
-from . import GardenaBluetoothConfigEntry
+from .const import DOMAIN
from .coordinator import GardenaBluetoothCoordinator
from .entity import GardenaBluetoothDescriptorEntity, GardenaBluetoothEntity
@@ -94,12 +95,10 @@ DESCRIPTIONS = (
async def async_setup_entry(
- hass: HomeAssistant,
- entry: GardenaBluetoothConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Gardena Bluetooth sensor based on a config entry."""
- coordinator = entry.runtime_data
+ coordinator: GardenaBluetoothCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[GardenaBluetoothEntity] = [
GardenaBluetoothSensor(coordinator, description, description.context)
for description in DESCRIPTIONS
diff --git a/homeassistant/components/gardena_bluetooth/strings.json b/homeassistant/components/gardena_bluetooth/strings.json
index dd50bac0b2a..d0c1b878cef 100644
--- a/homeassistant/components/gardena_bluetooth/strings.json
+++ b/homeassistant/components/gardena_bluetooth/strings.json
@@ -16,8 +16,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
},
"entity": {
diff --git a/homeassistant/components/gardena_bluetooth/switch.py b/homeassistant/components/gardena_bluetooth/switch.py
index f82c39025a5..58b4b2e4e51 100644
--- a/homeassistant/components/gardena_bluetooth/switch.py
+++ b/homeassistant/components/gardena_bluetooth/switch.py
@@ -7,22 +7,21 @@ from typing import Any
from gardena_bluetooth.const import Valve
from homeassistant.components.switch import SwitchEntity
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import GardenaBluetoothConfigEntry
+from .const import DOMAIN
from .coordinator import GardenaBluetoothCoordinator
from .entity import GardenaBluetoothEntity
async def async_setup_entry(
- hass: HomeAssistant,
- entry: GardenaBluetoothConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up switch based on a config entry."""
- coordinator = entry.runtime_data
+ coordinator: GardenaBluetoothCoordinator = hass.data[DOMAIN][entry.entry_id]
entities = []
if GardenaBluetoothValveSwitch.characteristics.issubset(
coordinator.characteristics
diff --git a/homeassistant/components/gardena_bluetooth/valve.py b/homeassistant/components/gardena_bluetooth/valve.py
index ae6bf56a7ff..877cc5b505e 100644
--- a/homeassistant/components/gardena_bluetooth/valve.py
+++ b/homeassistant/components/gardena_bluetooth/valve.py
@@ -7,10 +7,11 @@ from typing import Any
from gardena_bluetooth.const import Valve
from homeassistant.components.valve import ValveEntity, ValveEntityFeature
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import GardenaBluetoothConfigEntry
+from .const import DOMAIN
from .coordinator import GardenaBluetoothCoordinator
from .entity import GardenaBluetoothEntity
@@ -18,12 +19,10 @@ FALLBACK_WATERING_TIME_IN_SECONDS = 60 * 60
async def async_setup_entry(
- hass: HomeAssistant,
- entry: GardenaBluetoothConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up switch based on a config entry."""
- coordinator = entry.runtime_data
+ coordinator: GardenaBluetoothCoordinator = hass.data[DOMAIN][entry.entry_id]
entities = []
if GardenaBluetoothValve.characteristics.issubset(coordinator.characteristics):
entities.append(GardenaBluetoothValve(coordinator))
diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py
index 8bd238fd0e6..7b10cdfb64b 100644
--- a/homeassistant/components/generic/config_flow.py
+++ b/homeassistant/components/generic/config_flow.py
@@ -324,7 +324,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> GenericOptionsFlowHandler:
"""Get the options flow for this handler."""
- return GenericOptionsFlowHandler()
+ return GenericOptionsFlowHandler(config_entry)
def check_for_existing(self, options: dict[str, Any]) -> bool:
"""Check whether an existing entry is using the same URLs."""
@@ -409,8 +409,9 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
class GenericOptionsFlowHandler(OptionsFlow):
"""Handle Generic IP Camera options."""
- def __init__(self) -> None:
+ def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize Generic IP Camera options flow."""
+ self.config_entry = config_entry
self.preview_cam: dict[str, Any] = {}
self.user_input: dict[str, Any] = {}
diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json
index c1fbc16d9be..b19d6d6293e 100644
--- a/homeassistant/components/generic/manifest.json
+++ b/homeassistant/components/generic/manifest.json
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/generic",
"integration_type": "device",
"iot_class": "local_push",
- "requirements": ["av==13.1.0", "Pillow==11.0.0"]
+ "requirements": ["ha-av==10.1.1", "Pillow==10.4.0"]
}
diff --git a/homeassistant/components/generic_hygrostat/strings.json b/homeassistant/components/generic_hygrostat/strings.json
index 2be3955eff1..a21ab68c628 100644
--- a/homeassistant/components/generic_hygrostat/strings.json
+++ b/homeassistant/components/generic_hygrostat/strings.json
@@ -4,7 +4,7 @@
"step": {
"user": {
"title": "Add generic hygrostat",
- "description": "Create a humidifier entity that control the humidity via a switch and sensor.",
+ "description": "Create a entity that control the humidity via a switch and sensor.",
"data": {
"device_class": "Device class",
"dry_tolerance": "Dry tolerance",
diff --git a/homeassistant/components/generic_thermostat/config_flow.py b/homeassistant/components/generic_thermostat/config_flow.py
index 5b0eae8ff66..e9079a9f41a 100644
--- a/homeassistant/components/generic_thermostat/config_flow.py
+++ b/homeassistant/components/generic_thermostat/config_flow.py
@@ -62,7 +62,7 @@ OPTIONS_SCHEMA = {
PRESETS_SCHEMA = {
vol.Optional(v): selector.NumberSelector(
selector.NumberSelectorConfig(
- mode=selector.NumberSelectorMode.BOX, unit_of_measurement=DEGREE, step=0.1
+ mode=selector.NumberSelectorMode.BOX, unit_of_measurement=DEGREE
)
)
for v in CONF_PRESETS.values()
diff --git a/homeassistant/components/generic_thermostat/strings.json b/homeassistant/components/generic_thermostat/strings.json
index 51549dc844e..1ddd41de734 100644
--- a/homeassistant/components/generic_thermostat/strings.json
+++ b/homeassistant/components/generic_thermostat/strings.json
@@ -3,7 +3,7 @@
"config": {
"step": {
"user": {
- "title": "Add generic thermostat",
+ "title": "Add generic thermostat helper",
"description": "Create a climate entity that controls the temperature via a switch and sensor.",
"data": {
"ac_mode": "Cooling mode",
@@ -17,8 +17,8 @@
"data_description": {
"ac_mode": "Set the actuator specified to be treated as a cooling device instead of a heating device.",
"heater": "Switch entity used to cool or heat depending on A/C mode.",
- "target_sensor": "Temperature sensor that reflects the current temperature.",
- "min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on.",
+ "target_sensor": "Temperature sensor that reflect the current temperature.",
+ "min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on. This option will be ignored if the keep alive option is set.",
"cold_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched on. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will start when the sensor equals or goes below 24.5.",
"hot_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched off. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will stop when the sensor equals or goes above 25.5."
}
diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py
index 9ca6ecfcfe0..18580f331d2 100644
--- a/homeassistant/components/geniushub/__init__.py
+++ b/homeassistant/components/geniushub/__init__.py
@@ -9,6 +9,7 @@ import aiohttp
from geniushubclient import GeniusHub
import voluptuous as vol
+from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -20,12 +21,20 @@ from homeassistant.const import (
CONF_USERNAME,
Platform,
)
-from homeassistant.core import HomeAssistant, ServiceCall, callback
+from homeassistant.core import (
+ DOMAIN as HOMEASSISTANT_DOMAIN,
+ HomeAssistant,
+ ServiceCall,
+ callback,
+)
+from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.service import verify_domain_control
+from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
@@ -36,6 +45,27 @@ SCAN_INTERVAL = timedelta(seconds=60)
MAC_ADDRESS_REGEXP = r"^([0-9A-F]{2}:){5}([0-9A-F]{2})$"
+CLOUD_API_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_TOKEN): cv.string,
+ vol.Required(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP),
+ }
+)
+
+
+LOCAL_API_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP),
+ }
+)
+
+CONFIG_SCHEMA = vol.Schema(
+ {DOMAIN: vol.Any(LOCAL_API_SCHEMA, CLOUD_API_SCHEMA)}, extra=vol.ALLOW_EXTRA
+)
+
ATTR_ZONE_MODE = "mode"
ATTR_DURATION = "duration"
@@ -70,6 +100,56 @@ PLATFORMS = [
]
+async def _async_import(hass: HomeAssistant, base_config: ConfigType) -> None:
+ """Import a config entry from configuration.yaml."""
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=base_config[DOMAIN],
+ )
+ if (
+ result["type"] is FlowResultType.CREATE_ENTRY
+ or result["reason"] == "already_configured"
+ ):
+ async_create_issue(
+ hass,
+ HOMEASSISTANT_DOMAIN,
+ f"deprecated_yaml_{DOMAIN}",
+ breaks_in_ha_version="2024.12.0",
+ is_fixable=False,
+ issue_domain=DOMAIN,
+ severity=IssueSeverity.WARNING,
+ translation_key="deprecated_yaml",
+ translation_placeholders={
+ "domain": DOMAIN,
+ "integration_title": "Genius Hub",
+ },
+ )
+ return
+ async_create_issue(
+ hass,
+ DOMAIN,
+ f"deprecated_yaml_import_issue_{result['reason']}",
+ breaks_in_ha_version="2024.12.0",
+ is_fixable=False,
+ issue_domain=DOMAIN,
+ severity=IssueSeverity.WARNING,
+ translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
+ translation_placeholders={
+ "domain": DOMAIN,
+ "integration_title": "Genius Hub",
+ },
+ )
+
+
+async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
+ """Set up a Genius Hub system."""
+ if DOMAIN in base_config:
+ hass.async_create_task(_async_import(hass, base_config))
+ return True
+
+
type GeniusHubConfigEntry = ConfigEntry[GeniusBroker]
@@ -90,6 +170,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GeniusHubConfigEntry) ->
)
session = async_get_clientsession(hass)
+ unique_id: str
if CONF_HOST in entry.data:
client = GeniusHub(
entry.data[CONF_HOST],
@@ -97,10 +178,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: GeniusHubConfigEntry) ->
password=entry.data[CONF_PASSWORD],
session=session,
)
+ unique_id = entry.data[CONF_MAC]
else:
client = GeniusHub(entry.data[CONF_TOKEN], session=session)
-
- unique_id = entry.unique_id or entry.entry_id
+ unique_id = entry.entry_id
broker = entry.runtime_data = GeniusBroker(hass, client, unique_id)
diff --git a/homeassistant/components/geniushub/config_flow.py b/homeassistant/components/geniushub/config_flow.py
index b106f9907bb..601eac6c2f2 100644
--- a/homeassistant/components/geniushub/config_flow.py
+++ b/homeassistant/components/geniushub/config_flow.py
@@ -13,6 +13,7 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
+from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
@@ -122,3 +123,14 @@ class GeniusHubConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="cloud_api", errors=errors, data_schema=CLOUD_API_SCHEMA
)
+
+ async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
+ """Import the yaml config."""
+ if CONF_HOST in import_data:
+ result = await self.async_step_local_api(import_data)
+ else:
+ result = await self.async_step_cloud_api(import_data)
+ if result["type"] is FlowResultType.FORM:
+ assert result["errors"]
+ return self.async_abort(reason=result["errors"]["base"])
+ return result
diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json
index b1eae512688..b509806d07f 100644
--- a/homeassistant/components/gios/manifest.json
+++ b/homeassistant/components/gios/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["dacite", "gios"],
"quality_scale": "platinum",
- "requirements": ["gios==5.0.0"]
+ "requirements": ["gios==4.0.0"]
}
diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py
index 9977f9d84cc..25d8782618f 100644
--- a/homeassistant/components/github/config_flow.py
+++ b/homeassistant/components/github/config_flow.py
@@ -211,12 +211,16 @@ class GitHubConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Handle a option flow for GitHub."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self,
user_input: dict[str, Any] | None = None,
diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py
index 1dbc939d532..9208a4b0ebd 100644
--- a/homeassistant/components/glances/config_flow.py
+++ b/homeassistant/components/glances/config_flow.py
@@ -11,7 +11,7 @@ from glances_api.exceptions import (
)
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -40,11 +40,15 @@ class GlancesFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Glances config flow."""
VERSION = 1
+ _reauth_entry: ConfigEntry | None
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -52,10 +56,9 @@ class GlancesFlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
errors = {}
-
- reauth_entry = self._get_reauth_entry()
+ assert self._reauth_entry
if user_input is not None:
- user_input = {**reauth_entry.data, **user_input}
+ user_input = {**self._reauth_entry.data, **user_input}
try:
await get_api(self.hass, user_input)
except GlancesApiAuthorizationError:
@@ -64,13 +67,15 @@ class GlancesFlowHandler(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
else:
self.hass.config_entries.async_update_entry(
- reauth_entry, data=user_input
+ self._reauth_entry, data=user_input
)
- await self.hass.config_entries.async_reload(reauth_entry.entry_id)
+ await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
- description_placeholders={CONF_USERNAME: reauth_entry.data[CONF_USERNAME]},
+ description_placeholders={
+ CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME]
+ },
step_id="reauth_confirm",
data_schema=vol.Schema(
{
diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py
index f1f6e44abc1..4ca1d72008f 100644
--- a/homeassistant/components/go2rtc/__init__.py
+++ b/homeassistant/components/go2rtc/__init__.py
@@ -1,59 +1,20 @@
"""The go2rtc component."""
-import logging
-import shutil
+from go2rtc_client import Go2RtcClient, WebRTCSdpOffer
-from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
-from awesomeversion import AwesomeVersion
-from go2rtc_client import Go2RtcRestClient
-from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError
-from go2rtc_client.ws import (
- Go2RtcWsClient,
- ReceiveMessages,
- WebRTCAnswer,
- WebRTCCandidate,
- WebRTCOffer,
- WsError,
-)
-import voluptuous as vol
-from webrtc_models import RTCIceCandidate
-
-from homeassistant.components.camera import (
- Camera,
+from homeassistant.components.camera import Camera
+from homeassistant.components.camera.webrtc import (
CameraWebRTCProvider,
- WebRTCAnswer as HAWebRTCAnswer,
- WebRTCCandidate as HAWebRTCCandidate,
- WebRTCError,
- WebRTCMessage,
- WebRTCSendMessage,
async_register_webrtc_provider,
)
-from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN
-from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
-from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
-from homeassistant.core import Event, HomeAssistant, callback
-from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers import (
- config_validation as cv,
- discovery_flow,
- issue_registry as ir,
-)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.typing import ConfigType
-from homeassistant.util.hass_dict import HassKey
-from homeassistant.util.package import is_docker_env
-from .const import (
- CONF_DEBUG_UI,
- DEBUG_UI_URL_MESSAGE,
- DOMAIN,
- HA_MANAGED_URL,
- RECOMMENDED_VERSION,
-)
+from .const import CONF_BINARY
from .server import Server
-_LOGGER = logging.getLogger(__name__)
-
_SUPPORTED_STREAMS = frozenset(
(
"bubble",
@@ -84,210 +45,49 @@ _SUPPORTED_STREAMS = frozenset(
)
)
-CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.Schema(
- {
- vol.Exclusive(CONF_URL, DOMAIN, DEBUG_UI_URL_MESSAGE): cv.url,
- vol.Exclusive(CONF_DEBUG_UI, DOMAIN, DEBUG_UI_URL_MESSAGE): cv.boolean,
- }
- )
- },
- extra=vol.ALLOW_EXTRA,
-)
-
-_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN)
-_RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError)
-
-
-async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
- """Set up WebRTC."""
- url: str | None = None
- if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config:
- await _remove_go2rtc_entries(hass)
- return True
-
- if not (configured_by_user := DOMAIN in config) or not (
- url := config[DOMAIN].get(CONF_URL)
- ):
- if not is_docker_env():
- if not configured_by_user:
- # Remove config entry if it exists
- await _remove_go2rtc_entries(hass)
- return True
- _LOGGER.warning("Go2rtc URL required in non-docker installs")
- return False
- if not (binary := await _get_binary(hass)):
- _LOGGER.error("Could not find go2rtc docker binary")
- return False
-
- # HA will manage the binary
- server = Server(
- hass, binary, enable_ui=config.get(DOMAIN, {}).get(CONF_DEBUG_UI, False)
- )
- try:
- await server.start()
- except Exception: # noqa: BLE001
- _LOGGER.warning("Could not start go2rtc server", exc_info=True)
- return False
-
- async def on_stop(event: Event) -> None:
- await server.stop()
-
- hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
-
- url = HA_MANAGED_URL
-
- hass.data[_DATA_GO2RTC] = url
- discovery_flow.async_create_flow(
- hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
- )
- return True
-
-
-async def _remove_go2rtc_entries(hass: HomeAssistant) -> None:
- """Remove go2rtc config entries, if any."""
- for entry in hass.config_entries.async_entries(DOMAIN):
- await hass.config_entries.async_remove(entry.entry_id)
-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
- """Set up go2rtc from a config entry."""
- url = hass.data[_DATA_GO2RTC]
+ """Set up WebRTC from a config entry."""
+ if binary := entry.data.get(CONF_BINARY):
+ # HA will manage the binary
+ server = Server(binary)
+ entry.async_on_unload(server.stop)
+ server.start()
- # Validate the server URL
- try:
- client = Go2RtcRestClient(async_get_clientsession(hass), url)
- version = await client.validate_server_version()
- if version < AwesomeVersion(RECOMMENDED_VERSION):
- ir.async_create_issue(
- hass,
- DOMAIN,
- "recommended_version",
- is_fixable=False,
- is_persistent=False,
- severity=ir.IssueSeverity.WARNING,
- translation_key="recommended_version",
- translation_placeholders={
- "recommended_version": RECOMMENDED_VERSION,
- "current_version": str(version),
- },
- )
- except Go2RtcClientError as err:
- if isinstance(err.__cause__, _RETRYABLE_ERRORS):
- raise ConfigEntryNotReady(
- f"Could not connect to go2rtc instance on {url}"
- ) from err
- _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
- return False
- except Go2RtcVersionError as err:
- raise ConfigEntryNotReady(
- f"The go2rtc server version is not supported, {err}"
- ) from err
- except Exception as err: # noqa: BLE001
- _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
- return False
+ client = Go2RtcClient(async_get_clientsession(hass), entry.data[CONF_HOST])
- provider = WebRTCProvider(hass, url)
- async_register_webrtc_provider(hass, provider)
+ provider = WebRTCProvider(client)
+ entry.async_on_unload(async_register_webrtc_provider(hass, provider))
return True
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
- """Unload a go2rtc config entry."""
- return True
-
-
-async def _get_binary(hass: HomeAssistant) -> str | None:
- """Return the binary path if found."""
- return await hass.async_add_executor_job(shutil.which, "go2rtc")
-
-
class WebRTCProvider(CameraWebRTCProvider):
"""WebRTC provider."""
- def __init__(self, hass: HomeAssistant, url: str) -> None:
+ def __init__(self, client: Go2RtcClient) -> None:
"""Initialize the WebRTC provider."""
- self._hass = hass
- self._url = url
- self._session = async_get_clientsession(hass)
- self._rest_client = Go2RtcRestClient(self._session, url)
- self._sessions: dict[str, Go2RtcWsClient] = {}
+ self._client = client
- @property
- def domain(self) -> str:
- """Return the integration domain of the provider."""
- return DOMAIN
-
- @callback
- def async_is_supported(self, stream_source: str) -> bool:
+ async def async_is_supported(self, stream_source: str) -> bool:
"""Return if this provider is supports the Camera as source."""
return stream_source.partition(":")[0] in _SUPPORTED_STREAMS
- async def async_handle_async_webrtc_offer(
- self,
- camera: Camera,
- offer_sdp: str,
- session_id: str,
- send_message: WebRTCSendMessage,
- ) -> None:
- """Handle the WebRTC offer and return the answer via the provided callback."""
- self._sessions[session_id] = ws_client = Go2RtcWsClient(
- self._session, self._url, source=camera.entity_id
+ async def async_handle_web_rtc_offer(
+ self, camera: Camera, offer_sdp: str
+ ) -> str | None:
+ """Handle the WebRTC offer and return an answer."""
+ streams = await self._client.streams.list()
+ if camera.entity_id not in streams:
+ if not (stream_source := await camera.stream_source()):
+ return None
+ await self._client.streams.add(camera.entity_id, stream_source)
+
+ answer = await self._client.webrtc.forward_whep_sdp_offer(
+ camera.entity_id, WebRTCSdpOffer(offer_sdp)
)
+ return answer.sdp
- if not (stream_source := await camera.stream_source()):
- send_message(
- WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source")
- )
- return
- streams = await self._rest_client.streams.list()
-
- if (stream := streams.get(camera.entity_id)) is None or not any(
- stream_source == producer.url for producer in stream.producers
- ):
- await self._rest_client.streams.add(
- camera.entity_id,
- [
- stream_source,
- # We are setting any ffmpeg rtsp related logs to debug
- # Connection problems to the camera will be logged by the first stream
- # Therefore setting it to debug will not hide any important logs
- f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
- ],
- )
-
- @callback
- def on_messages(message: ReceiveMessages) -> None:
- """Handle messages."""
- value: WebRTCMessage
- match message:
- case WebRTCCandidate():
- value = HAWebRTCCandidate(RTCIceCandidate(message.candidate))
- case WebRTCAnswer():
- value = HAWebRTCAnswer(message.sdp)
- case WsError():
- value = WebRTCError("go2rtc_webrtc_offer_failed", message.error)
-
- send_message(value)
-
- ws_client.subscribe(on_messages)
- config = camera.async_get_webrtc_client_configuration()
- await ws_client.send(WebRTCOffer(offer_sdp, config.configuration.ice_servers))
-
- async def async_on_webrtc_candidate(
- self, session_id: str, candidate: RTCIceCandidate
- ) -> None:
- """Handle the WebRTC candidate."""
-
- if ws_client := self._sessions.get(session_id):
- await ws_client.send(WebRTCCandidate(candidate.candidate))
- else:
- _LOGGER.debug("Unknown session %s. Ignoring candidate", session_id)
-
- @callback
- def async_close_session(self, session_id: str) -> None:
- """Close the session."""
- ws_client = self._sessions.pop(session_id)
- self._hass.async_create_task(ws_client.close())
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ return True
diff --git a/homeassistant/components/go2rtc/config_flow.py b/homeassistant/components/go2rtc/config_flow.py
index 02fdfb656a6..51628504614 100644
--- a/homeassistant/components/go2rtc/config_flow.py
+++ b/homeassistant/components/go2rtc/config_flow.py
@@ -1,21 +1,90 @@
-"""Config flow for the go2rtc integration."""
+"""Config flow for WebRTC."""
from __future__ import annotations
+import shutil
from typing import Any
+from urllib.parse import urlparse
+
+from go2rtc_client import Go2RtcClient
+import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.const import CONF_HOST
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import selector
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.util.package import is_docker_env
-from .const import DOMAIN
+from .const import CONF_BINARY, DOMAIN
+
+_VALID_URL_SCHEMA = {"http", "https"}
-class CloudConfigFlow(ConfigFlow, domain=DOMAIN):
- """Handle a config flow for the go2rtc integration."""
+async def _validate_url(
+ hass: HomeAssistant,
+ value: str,
+) -> str | None:
+ """Validate the URL and return error or None if it's valid."""
+ if urlparse(value).scheme not in _VALID_URL_SCHEMA:
+ return "invalid_url_schema"
+ try:
+ vol.Schema(vol.Url())(value)
+ except vol.Invalid:
+ return "invalid_url"
- VERSION = 1
+ try:
+ client = Go2RtcClient(async_get_clientsession(hass), value)
+ await client.streams.list()
+ except Exception: # noqa: BLE001
+ return "cannot_connect"
+ return None
- async def async_step_system(
+
+class Go2RTCConfigFlow(ConfigFlow, domain=DOMAIN):
+ """go2rtc config flow."""
+
+ def _get_binary(self) -> str | None:
+ """Return the binary path if found."""
+ return shutil.which(DOMAIN)
+
+ async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
- """Handle the system step."""
- return self.async_create_entry(title="go2rtc", data={})
+ """Init step."""
+ if is_docker_env() and (binary := self._get_binary()):
+ return self.async_create_entry(
+ title=DOMAIN,
+ data={CONF_BINARY: binary, CONF_HOST: "http://localhost:1984/"},
+ )
+
+ return await self.async_step_host()
+
+ async def async_step_host(
+ self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Step to use selfhosted go2rtc server."""
+ errors = {}
+ if user_input is not None:
+ if error := await _validate_url(self.hass, user_input[CONF_HOST]):
+ errors[CONF_HOST] = error
+ else:
+ return self.async_create_entry(title=DOMAIN, data=user_input)
+
+ return self.async_show_form(
+ step_id="host",
+ data_schema=self.add_suggested_values_to_schema(
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_HOST): selector.TextSelector(
+ selector.TextSelectorConfig(
+ type=selector.TextSelectorType.URL
+ )
+ ),
+ }
+ ),
+ suggested_values=user_input,
+ ),
+ errors=errors,
+ last_step=True,
+ )
diff --git a/homeassistant/components/go2rtc/const.py b/homeassistant/components/go2rtc/const.py
index 3c1c84c42b5..af8266e0d72 100644
--- a/homeassistant/components/go2rtc/const.py
+++ b/homeassistant/components/go2rtc/const.py
@@ -2,8 +2,4 @@
DOMAIN = "go2rtc"
-CONF_DEBUG_UI = "debug_ui"
-DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
-HA_MANAGED_API_PORT = 11984
-HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
-RECOMMENDED_VERSION = "1.9.7"
+CONF_BINARY = "binary"
diff --git a/homeassistant/components/go2rtc/manifest.json b/homeassistant/components/go2rtc/manifest.json
index 201b7168847..faf6c991ac1 100644
--- a/homeassistant/components/go2rtc/manifest.json
+++ b/homeassistant/components/go2rtc/manifest.json
@@ -2,11 +2,10 @@
"domain": "go2rtc",
"name": "go2rtc",
"codeowners": ["@home-assistant/core"],
- "config_flow": false,
+ "config_flow": true,
"dependencies": ["camera"],
"documentation": "https://www.home-assistant.io/integrations/go2rtc",
- "integration_type": "system",
"iot_class": "local_polling",
- "requirements": ["go2rtc-client==0.1.1"],
+ "requirements": ["go2rtc-client==0.0.1b0"],
"single_config_entry": true
}
diff --git a/homeassistant/components/go2rtc/server.py b/homeassistant/components/go2rtc/server.py
index 6699ee4d8a2..fc9c2b17f60 100644
--- a/homeassistant/components/go2rtc/server.py
+++ b/homeassistant/components/go2rtc/server.py
@@ -1,252 +1,56 @@
"""Go2rtc server."""
-import asyncio
-from collections import deque
-from contextlib import suppress
+from __future__ import annotations
+
import logging
+import subprocess
from tempfile import NamedTemporaryFile
+from threading import Thread
-from go2rtc_client import Go2RtcRestClient
-
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-
-from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
-_TERMINATE_TIMEOUT = 5
-_SETUP_TIMEOUT = 30
-_SUCCESSFUL_BOOT_MESSAGE = "INF [api] listen addr="
-_LOCALHOST_IP = "127.0.0.1"
-_LOG_BUFFER_SIZE = 512
-_RESPAWN_COOLDOWN = 1
-
-# Default configuration for HA
-# - Api is listening only on localhost
-# - Enable rtsp for localhost only as ffmpeg needs it
-# - Clear default ice servers
-_GO2RTC_CONFIG_FORMAT = r"""# This file is managed by Home Assistant
-# Do not edit it manually
-
-api:
- listen: "{api_ip}:{api_port}"
-
-rtsp:
- listen: "127.0.0.1:18554"
-
-webrtc:
- listen: ":18555/tcp"
- ice_servers: []
-"""
-
-_LOG_LEVEL_MAP = {
- "TRC": logging.DEBUG,
- "DBG": logging.DEBUG,
- "INF": logging.DEBUG,
- "WRN": logging.WARNING,
- "ERR": logging.WARNING,
- "FTL": logging.ERROR,
- "PNC": logging.ERROR,
-}
-class Go2RTCServerStartError(HomeAssistantError):
- """Raised when server does not start."""
+class Server(Thread):
+ """Server thread."""
- _message = "Go2rtc server didn't start correctly"
-
-
-class Go2RTCWatchdogError(HomeAssistantError):
- """Raised on watchdog error."""
-
-
-def _create_temp_file(api_ip: str) -> str:
- """Create temporary config file."""
- # Set delete=False to prevent the file from being deleted when the file is closed
- # Linux is clearing tmp folder on reboot, so no need to delete it manually
- with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
- file.write(
- _GO2RTC_CONFIG_FORMAT.format(
- api_ip=api_ip, api_port=HA_MANAGED_API_PORT
- ).encode()
- )
- return file.name
-
-
-class Server:
- """Go2rtc server."""
-
- def __init__(
- self, hass: HomeAssistant, binary: str, *, enable_ui: bool = False
- ) -> None:
+ def __init__(self, binary: str) -> None:
"""Initialize the server."""
- self._hass = hass
+ super().__init__(name=DOMAIN, daemon=True)
self._binary = binary
- self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
- self._process: asyncio.subprocess.Process | None = None
- self._startup_complete = asyncio.Event()
- self._api_ip = _LOCALHOST_IP
- if enable_ui:
- # Listen on all interfaces for allowing access from all ips
- self._api_ip = ""
- self._watchdog_task: asyncio.Task | None = None
- self._watchdog_tasks: list[asyncio.Task] = []
+ self._stop_requested = False
- async def start(self) -> None:
- """Start the server."""
- await self._start()
- self._watchdog_task = asyncio.create_task(
- self._watchdog(), name="Go2rtc respawn"
- )
-
- async def _start(self) -> None:
- """Start the server."""
+ def run(self) -> None:
+ """Run the server."""
_LOGGER.debug("Starting go2rtc server")
- config_file = await self._hass.async_add_executor_job(
- _create_temp_file, self._api_ip
- )
+ self._stop_requested = False
+ with (
+ NamedTemporaryFile(prefix="go2rtc", suffix=".yaml") as file,
+ subprocess.Popen(
+ [self._binary, "-c", "webrtc.ice_servers=[]", "-c", file.name],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ ) as process,
+ ):
+ while not self._stop_requested and process.poll() is None:
+ assert process.stdout
+ line = process.stdout.readline()
+ if line == b"":
+ break
+ _LOGGER.debug(line[:-1].decode())
- self._startup_complete.clear()
-
- self._process = await asyncio.create_subprocess_exec(
- self._binary,
- "-c",
- config_file,
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.STDOUT,
- close_fds=False, # required for posix_spawn on CPython < 3.13
- )
-
- self._hass.async_create_background_task(
- self._log_output(self._process), "Go2rtc log output"
- )
-
- try:
- async with asyncio.timeout(_SETUP_TIMEOUT):
- await self._startup_complete.wait()
- except TimeoutError as err:
- msg = "Go2rtc server didn't start correctly"
- _LOGGER.exception(msg)
- self._log_server_output(logging.WARNING)
- await self._stop()
- raise Go2RTCServerStartError from err
-
- # Check the server version
- client = Go2RtcRestClient(async_get_clientsession(self._hass), HA_MANAGED_URL)
- await client.validate_server_version()
-
- async def _log_output(self, process: asyncio.subprocess.Process) -> None:
- """Log the output of the process."""
- assert process.stdout is not None
-
- async for line in process.stdout:
- msg = line[:-1].decode().strip()
- self._log_buffer.append(msg)
- loglevel = logging.WARNING
- if len(split_msg := msg.split(" ", 2)) == 3:
- loglevel = _LOG_LEVEL_MAP.get(split_msg[1], loglevel)
- _LOGGER.log(loglevel, msg)
- if not self._startup_complete.is_set() and _SUCCESSFUL_BOOT_MESSAGE in msg:
- self._startup_complete.set()
-
- def _log_server_output(self, loglevel: int) -> None:
- """Log captured process output, then clear the log buffer."""
- for line in list(self._log_buffer): # Copy the deque to avoid mutation error
- _LOGGER.log(loglevel, line)
- self._log_buffer.clear()
-
- async def _watchdog(self) -> None:
- """Keep respawning go2rtc servers.
-
- A new go2rtc server is spawned if the process terminates or the API
- stops responding.
- """
- while True:
+ _LOGGER.debug("Terminating go2rtc server")
+ process.terminate()
try:
- monitor_process_task = asyncio.create_task(self._monitor_process())
- self._watchdog_tasks.append(monitor_process_task)
- monitor_process_task.add_done_callback(self._watchdog_tasks.remove)
- monitor_api_task = asyncio.create_task(self._monitor_api())
- self._watchdog_tasks.append(monitor_api_task)
- monitor_api_task.add_done_callback(self._watchdog_tasks.remove)
- try:
- await asyncio.gather(monitor_process_task, monitor_api_task)
- except Go2RTCWatchdogError:
- _LOGGER.debug("Caught Go2RTCWatchdogError")
- for task in self._watchdog_tasks:
- if task.done():
- if not task.cancelled():
- task.exception()
- continue
- task.cancel()
- await asyncio.sleep(_RESPAWN_COOLDOWN)
- try:
- await self._stop()
- _LOGGER.warning("Go2rtc unexpectedly stopped, server log:")
- self._log_server_output(logging.WARNING)
- _LOGGER.debug("Spawning new go2rtc server")
- with suppress(Go2RTCServerStartError):
- await self._start()
- except Exception:
- _LOGGER.exception(
- "Unexpected error when restarting go2rtc server"
- )
- except Exception:
- _LOGGER.exception("Unexpected error in go2rtc server watchdog")
+ process.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ _LOGGER.warning("Go2rtc server didn't terminate gracefully.Killing it")
+ process.kill()
+ _LOGGER.debug("Go2rtc server has been stopped")
- async def _monitor_process(self) -> None:
- """Raise if the go2rtc process terminates."""
- _LOGGER.debug("Monitoring go2rtc server process")
- if self._process:
- await self._process.wait()
- _LOGGER.debug("go2rtc server terminated")
- raise Go2RTCWatchdogError("Process ended")
-
- async def _monitor_api(self) -> None:
- """Raise if the go2rtc process terminates."""
- client = Go2RtcRestClient(async_get_clientsession(self._hass), HA_MANAGED_URL)
-
- _LOGGER.debug("Monitoring go2rtc API")
- try:
- while True:
- await client.validate_server_version()
- await asyncio.sleep(10)
- except Exception as err:
- _LOGGER.debug("go2rtc API did not reply", exc_info=True)
- raise Go2RTCWatchdogError("API error") from err
-
- async def _stop_watchdog(self) -> None:
- """Handle watchdog stop request."""
- tasks: list[asyncio.Task] = []
- if watchdog_task := self._watchdog_task:
- self._watchdog_task = None
- tasks.append(watchdog_task)
- watchdog_task.cancel()
- for task in self._watchdog_tasks:
- tasks.append(task)
- task.cancel()
- await asyncio.gather(*tasks, return_exceptions=True)
-
- async def stop(self) -> None:
- """Stop the server and abort the watchdog task."""
- _LOGGER.debug("Server stop requested")
- await self._stop_watchdog()
- await self._stop()
-
- async def _stop(self) -> None:
+ def stop(self) -> None:
"""Stop the server."""
- if self._process:
- _LOGGER.debug("Stopping go2rtc server")
- process = self._process
- self._process = None
- with suppress(ProcessLookupError):
- process.terminate()
- try:
- await asyncio.wait_for(process.wait(), timeout=_TERMINATE_TIMEOUT)
- except TimeoutError:
- _LOGGER.warning("Go2rtc server didn't terminate gracefully. Killing it")
- with suppress(ProcessLookupError):
- process.kill()
- else:
- _LOGGER.debug("Go2rtc server has been stopped")
+ self._stop_requested = True
+ if self.is_alive():
+ self.join()
diff --git a/homeassistant/components/go2rtc/strings.json b/homeassistant/components/go2rtc/strings.json
index e350c19af96..44e28d712c1 100644
--- a/homeassistant/components/go2rtc/strings.json
+++ b/homeassistant/components/go2rtc/strings.json
@@ -1,8 +1,19 @@
{
- "issues": {
- "recommended_version": {
- "title": "Outdated go2rtc server detected",
- "description": "We detected that you are using an outdated go2rtc server version. For the best experience, we recommend updating the go2rtc server to version `{recommended_version}`.\nCurrently you are using version `{current_version}`."
+ "config": {
+ "step": {
+ "host": {
+ "data": {
+ "host": "[%key:common::config_flow::data::url%]"
+ },
+ "data_description": {
+ "host": "The URL of your go2rtc instance."
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_url": "Invalid URL",
+ "invalid_url_schema": "Invalid URL scheme.\nThe URL should start with `http://` or `https://`."
}
}
}
diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py
index 2ad400aabab..9bb6dbd059f 100644
--- a/homeassistant/components/google/__init__.py
+++ b/homeassistant/components/google/__init__.py
@@ -175,7 +175,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
- if not async_entry_has_scopes(entry):
+ if not async_entry_has_scopes(hass, entry):
raise ConfigEntryAuthFailed(
"Required scopes are not available, reauth required"
)
@@ -198,7 +198,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.config_entries.async_update_entry(entry, unique_id=primary_calendar.id)
# Only expose the add event service if we have the correct permissions
- if get_feature_access(entry) is FeatureAccess.read_write:
+ if get_feature_access(hass, entry) is FeatureAccess.read_write:
await async_setup_add_event_service(hass, calendar_service)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -208,9 +208,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
-def async_entry_has_scopes(entry: ConfigEntry) -> bool:
+def async_entry_has_scopes(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Verify that the config entry desired scope is present in the oauth token."""
- access = get_feature_access(entry)
+ access = get_feature_access(hass, entry)
token_scopes = entry.data.get("token", {}).get("scope", [])
return access.scope in token_scopes
@@ -224,7 +224,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry if the access options change."""
- if not async_entry_has_scopes(entry):
+ if not async_entry_has_scopes(hass, entry):
await hass.config_entries.async_reload(entry.entry_id)
diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py
index 194c2a0b4a5..8ed18cca41c 100644
--- a/homeassistant/components/google/api.py
+++ b/homeassistant/components/google/api.py
@@ -26,7 +26,13 @@ from homeassistant.helpers.event import (
)
from homeassistant.util import dt as dt_util
-from .const import CONF_CALENDAR_ACCESS, DEFAULT_FEATURE_ACCESS, FeatureAccess
+from .const import (
+ CONF_CALENDAR_ACCESS,
+ DATA_CONFIG,
+ DEFAULT_FEATURE_ACCESS,
+ DOMAIN,
+ FeatureAccess,
+)
_LOGGER = logging.getLogger(__name__)
@@ -155,11 +161,27 @@ class DeviceFlow:
self._listener()
-def get_feature_access(config_entry: ConfigEntry) -> FeatureAccess:
+def get_feature_access(
+ hass: HomeAssistant, config_entry: ConfigEntry | None = None
+) -> FeatureAccess:
"""Return the desired calendar feature access."""
- if config_entry.options and CONF_CALENDAR_ACCESS in config_entry.options:
+ if (
+ config_entry
+ and config_entry.options
+ and CONF_CALENDAR_ACCESS in config_entry.options
+ ):
return FeatureAccess[config_entry.options[CONF_CALENDAR_ACCESS]]
- return DEFAULT_FEATURE_ACCESS
+
+ # This may be called during config entry setup without integration setup running when there
+ # is no google entry in configuration.yaml
+ return cast(
+ FeatureAccess,
+ (
+ hass.data.get(DOMAIN, {})
+ .get(DATA_CONFIG, {})
+ .get(CONF_CALENDAR_ACCESS, DEFAULT_FEATURE_ACCESS)
+ ),
+ )
async def async_create_device_flow(
diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py
index 5ac5dae616c..7fb55f3cfb7 100644
--- a/homeassistant/components/google/calendar.py
+++ b/homeassistant/components/google/calendar.py
@@ -10,14 +10,7 @@ from typing import Any, cast
from gcal_sync.api import Range, SyncEventsRequest
from gcal_sync.exceptions import ApiException
-from gcal_sync.model import (
- AccessRole,
- Calendar,
- DateOrDatetime,
- Event,
- EventTypeEnum,
- ResponseStatus,
-)
+from gcal_sync.model import AccessRole, Calendar, DateOrDatetime, Event, EventTypeEnum
from gcal_sync.store import ScopedCalendarStore
from gcal_sync.sync import CalendarEventSyncManager
@@ -139,7 +132,7 @@ def _get_entity_descriptions(
)
read_only = not (
calendar_item.access_role.is_writer
- and get_feature_access(config_entry) is FeatureAccess.read_write
+ and get_feature_access(hass, config_entry) is FeatureAccess.read_write
)
# Prefer calendar sync down of resources when possible. However,
# sync does not work for search. Also free-busy calendars denormalize
@@ -311,7 +304,7 @@ async def async_setup_entry(
platform = entity_platform.async_get_current_platform()
if (
any(calendar_item.access_role.is_writer for calendar_item in result.items)
- and get_feature_access(config_entry) is FeatureAccess.read_write
+ and get_feature_access(hass, config_entry) is FeatureAccess.read_write
):
platform.async_register_entity_service(
SERVICE_CREATE_EVENT,
@@ -374,14 +367,7 @@ class GoogleCalendarEntity(
return event
def _event_filter(self, event: Event) -> bool:
- """Return True if the event is visible and not declined."""
-
- if any(
- attendee.is_self and attendee.response_status == ResponseStatus.DECLINED
- for attendee in event.attendees
- ):
- return False
-
+ """Return True if the event is visible."""
if event.event_type == EventTypeEnum.WORKING_LOCATION:
return self.entity_description.working_location
if self._ignore_availability:
diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py
index 8ae09b58957..98424ef24f5 100644
--- a/homeassistant/components/google/config_flow.py
+++ b/homeassistant/components/google/config_flow.py
@@ -11,12 +11,7 @@ from gcal_sync.api import GoogleCalendarService
from gcal_sync.exceptions import ApiException, ApiForbiddenException
import voluptuous as vol
-from homeassistant.config_entries import (
- SOURCE_REAUTH,
- ConfigEntry,
- ConfigFlowResult,
- OptionsFlow,
-)
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult, OptionsFlow
from homeassistant.core import callback
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -29,6 +24,7 @@ from .api import (
InvalidCredential,
OAuthError,
async_create_device_flow,
+ get_feature_access,
)
from .const import (
CONF_CALENDAR_ACCESS,
@@ -78,6 +74,7 @@ class OAuth2FlowHandler(
def __init__(self) -> None:
"""Set up instance."""
super().__init__()
+ self._reauth_config_entry: ConfigEntry | None = None
self._device_flow: DeviceFlow | None = None
# First attempt is device auth, then fallback to web auth
self._web_auth = False
@@ -120,11 +117,11 @@ class OAuth2FlowHandler(
self.flow_impl,
)
return self.async_abort(reason="oauth_error")
- calendar_access = DEFAULT_FEATURE_ACCESS
- if self.source == SOURCE_REAUTH and (
- reauth_options := self._get_reauth_entry().options
- ):
- calendar_access = FeatureAccess[reauth_options[CONF_CALENDAR_ACCESS]]
+ calendar_access = get_feature_access(self.hass)
+ if self._reauth_config_entry and self._reauth_config_entry.options:
+ calendar_access = FeatureAccess[
+ self._reauth_config_entry.options[CONF_CALENDAR_ACCESS]
+ ]
try:
device_flow = await async_create_device_flow(
self.hass,
@@ -181,10 +178,14 @@ class OAuth2FlowHandler(
data[CONF_CREDENTIAL_TYPE] = (
CredentialType.WEB_AUTH if self._web_auth else CredentialType.DEVICE_AUTH
)
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=data
+ if self._reauth_config_entry:
+ self.hass.config_entries.async_update_entry(
+ self._reauth_config_entry, data=data
)
+ await self.hass.config_entries.async_reload(
+ self._reauth_config_entry.entry_id
+ )
+ return self.async_abort(reason="reauth_successful")
calendar_service = GoogleCalendarService(
AccessTokenAuthImpl(
async_get_clientsession(self.hass), data["token"]["access_token"]
@@ -213,7 +214,7 @@ class OAuth2FlowHandler(
title=primary_calendar.id,
data=data,
options={
- CONF_CALENDAR_ACCESS: DEFAULT_FEATURE_ACCESS.name,
+ CONF_CALENDAR_ACCESS: get_feature_access(self.hass).name,
},
)
@@ -221,6 +222,9 @@ class OAuth2FlowHandler(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self._reauth_config_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
self._web_auth = entry_data.get(CONF_CREDENTIAL_TYPE) == CredentialType.WEB_AUTH
return await self.async_step_reauth_confirm()
@@ -238,12 +242,16 @@ class OAuth2FlowHandler(
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Create an options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Google Calendar options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json
index 85c4714432b..288ccbd6899 100644
--- a/homeassistant/components/google/manifest.json
+++ b/homeassistant/components/google/manifest.json
@@ -4,8 +4,8 @@
"codeowners": ["@allenporter"],
"config_flow": true,
"dependencies": ["application_credentials"],
- "documentation": "https://www.home-assistant.io/integrations/google",
+ "documentation": "https://www.home-assistant.io/integrations/calendar.google",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
- "requirements": ["gcal-sync==6.2.0", "oauth2client==4.1.3", "ical==8.2.0"]
+ "requirements": ["gcal-sync==6.1.5", "oauth2client==4.1.3", "ical==8.2.0"]
}
diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json
index 2ea45239a53..05c7b8ab190 100644
--- a/homeassistant/components/google/strings.json
+++ b/homeassistant/components/google/strings.json
@@ -19,7 +19,6 @@
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
- "missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]",
"code_expired": "Authentication code expired or credential setup is invalid, please try again.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
@@ -87,8 +86,8 @@
}
},
"create_event": {
- "name": "Create event",
- "description": "Adds a new calendar event.",
+ "name": "Creates event",
+ "description": "Add a new calendar event.",
"fields": {
"summary": {
"name": "Summary",
diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py
index 8132ecaae2c..04c85639e07 100644
--- a/homeassistant/components/google_assistant/const.py
+++ b/homeassistant/components/google_assistant/const.py
@@ -78,7 +78,6 @@ TYPE_AWNING = f"{PREFIX_TYPES}AWNING"
TYPE_BLINDS = f"{PREFIX_TYPES}BLINDS"
TYPE_CAMERA = f"{PREFIX_TYPES}CAMERA"
TYPE_CURTAIN = f"{PREFIX_TYPES}CURTAIN"
-TYPE_CARBON_MONOXIDE_DETECTOR = f"{PREFIX_TYPES}CARBON_MONOXIDE_DETECTOR"
TYPE_DEHUMIDIFIER = f"{PREFIX_TYPES}DEHUMIDIFIER"
TYPE_DOOR = f"{PREFIX_TYPES}DOOR"
TYPE_DOORBELL = f"{PREFIX_TYPES}DOORBELL"
@@ -94,7 +93,6 @@ TYPE_SCENE = f"{PREFIX_TYPES}SCENE"
TYPE_SENSOR = f"{PREFIX_TYPES}SENSOR"
TYPE_SETTOP = f"{PREFIX_TYPES}SETTOP"
TYPE_SHUTTER = f"{PREFIX_TYPES}SHUTTER"
-TYPE_SMOKE_DETECTOR = f"{PREFIX_TYPES}SMOKE_DETECTOR"
TYPE_SPEAKER = f"{PREFIX_TYPES}SPEAKER"
TYPE_SWITCH = f"{PREFIX_TYPES}SWITCH"
TYPE_THERMOSTAT = f"{PREFIX_TYPES}THERMOSTAT"
@@ -138,7 +136,6 @@ EVENT_SYNC_RECEIVED = "google_assistant_sync"
DOMAIN_TO_GOOGLE_TYPES = {
alarm_control_panel.DOMAIN: TYPE_ALARM,
- binary_sensor.DOMAIN: TYPE_SENSOR,
button.DOMAIN: TYPE_SCENE,
camera.DOMAIN: TYPE_CAMERA,
climate.DOMAIN: TYPE_THERMOSTAT,
@@ -171,14 +168,6 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = {
binary_sensor.DOMAIN,
binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR,
): TYPE_GARAGE,
- (
- binary_sensor.DOMAIN,
- binary_sensor.BinarySensorDeviceClass.SMOKE,
- ): TYPE_SMOKE_DETECTOR,
- (
- binary_sensor.DOMAIN,
- binary_sensor.BinarySensorDeviceClass.CO,
- ): TYPE_CARBON_MONOXIDE_DETECTOR,
(cover.DOMAIN, cover.CoverDeviceClass.AWNING): TYPE_AWNING,
(cover.DOMAIN, cover.CoverDeviceClass.CURTAIN): TYPE_CURTAIN,
(cover.DOMAIN, cover.CoverDeviceClass.DOOR): TYPE_DOOR,
diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py
index f99f1574038..95faf7c3321 100644
--- a/homeassistant/components/google_assistant/trait.py
+++ b/homeassistant/components/google_assistant/trait.py
@@ -33,10 +33,7 @@ from homeassistant.components import (
valve,
water_heater,
)
-from homeassistant.components.alarm_control_panel import (
- AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
-)
+from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature
from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.climate import ClimateEntityFeature
from homeassistant.components.cover import CoverEntityFeature
@@ -66,6 +63,13 @@ from homeassistant.const import (
SERVICE_ALARM_TRIGGER,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
STATE_IDLE,
STATE_OFF,
STATE_ON,
@@ -105,42 +109,61 @@ from .error import ChallengeNeeded, SmartHomeError
_LOGGER = logging.getLogger(__name__)
PREFIX_TRAITS = "action.devices.traits."
-TRAIT_ARM_DISARM = f"{PREFIX_TRAITS}ArmDisarm"
-TRAIT_BRIGHTNESS = f"{PREFIX_TRAITS}Brightness"
TRAIT_CAMERA_STREAM = f"{PREFIX_TRAITS}CameraStream"
-TRAIT_CHANNEL = f"{PREFIX_TRAITS}Channel"
-TRAIT_COLOR_SETTING = f"{PREFIX_TRAITS}ColorSetting"
+TRAIT_ONOFF = f"{PREFIX_TRAITS}OnOff"
TRAIT_DOCK = f"{PREFIX_TRAITS}Dock"
-TRAIT_ENERGY_STORAGE = f"{PREFIX_TRAITS}EnergyStorage"
-TRAIT_FAN_SPEED = f"{PREFIX_TRAITS}FanSpeed"
-TRAIT_HUMIDITY_SETTING = f"{PREFIX_TRAITS}HumiditySetting"
-TRAIT_INPUT_SELECTOR = f"{PREFIX_TRAITS}InputSelector"
-TRAIT_LOCATOR = f"{PREFIX_TRAITS}Locator"
-TRAIT_LOCK_UNLOCK = f"{PREFIX_TRAITS}LockUnlock"
-TRAIT_MEDIA_STATE = f"{PREFIX_TRAITS}MediaState"
-TRAIT_MODES = f"{PREFIX_TRAITS}Modes"
-TRAIT_OBJECT_DETECTION = f"{PREFIX_TRAITS}ObjectDetection"
-TRAIT_ON_OFF = f"{PREFIX_TRAITS}OnOff"
-TRAIT_OPEN_CLOSE = f"{PREFIX_TRAITS}OpenClose"
+TRAIT_STARTSTOP = f"{PREFIX_TRAITS}StartStop"
+TRAIT_BRIGHTNESS = f"{PREFIX_TRAITS}Brightness"
+TRAIT_COLOR_SETTING = f"{PREFIX_TRAITS}ColorSetting"
TRAIT_SCENE = f"{PREFIX_TRAITS}Scene"
-TRAIT_SENSOR_STATE = f"{PREFIX_TRAITS}SensorState"
-TRAIT_START_STOP = f"{PREFIX_TRAITS}StartStop"
-TRAIT_TEMPERATURE_CONTROL = f"{PREFIX_TRAITS}TemperatureControl"
TRAIT_TEMPERATURE_SETTING = f"{PREFIX_TRAITS}TemperatureSetting"
-TRAIT_TRANSPORT_CONTROL = f"{PREFIX_TRAITS}TransportControl"
+TRAIT_TEMPERATURE_CONTROL = f"{PREFIX_TRAITS}TemperatureControl"
+TRAIT_LOCKUNLOCK = f"{PREFIX_TRAITS}LockUnlock"
+TRAIT_FANSPEED = f"{PREFIX_TRAITS}FanSpeed"
+TRAIT_MODES = f"{PREFIX_TRAITS}Modes"
+TRAIT_INPUTSELECTOR = f"{PREFIX_TRAITS}InputSelector"
+TRAIT_OBJECTDETECTION = f"{PREFIX_TRAITS}ObjectDetection"
+TRAIT_OPENCLOSE = f"{PREFIX_TRAITS}OpenClose"
TRAIT_VOLUME = f"{PREFIX_TRAITS}Volume"
+TRAIT_ARMDISARM = f"{PREFIX_TRAITS}ArmDisarm"
+TRAIT_HUMIDITY_SETTING = f"{PREFIX_TRAITS}HumiditySetting"
+TRAIT_TRANSPORT_CONTROL = f"{PREFIX_TRAITS}TransportControl"
+TRAIT_MEDIA_STATE = f"{PREFIX_TRAITS}MediaState"
+TRAIT_CHANNEL = f"{PREFIX_TRAITS}Channel"
+TRAIT_LOCATOR = f"{PREFIX_TRAITS}Locator"
+TRAIT_ENERGYSTORAGE = f"{PREFIX_TRAITS}EnergyStorage"
+TRAIT_SENSOR_STATE = f"{PREFIX_TRAITS}SensorState"
PREFIX_COMMANDS = "action.devices.commands."
-COMMAND_ACTIVATE_SCENE = f"{PREFIX_COMMANDS}ActivateScene"
-COMMAND_ARM_DISARM = f"{PREFIX_COMMANDS}ArmDisarm"
-COMMAND_BRIGHTNESS_ABSOLUTE = f"{PREFIX_COMMANDS}BrightnessAbsolute"
-COMMAND_CHARGE = f"{PREFIX_COMMANDS}Charge"
-COMMAND_COLOR_ABSOLUTE = f"{PREFIX_COMMANDS}ColorAbsolute"
-COMMAND_DOCK = f"{PREFIX_COMMANDS}Dock"
+COMMAND_ONOFF = f"{PREFIX_COMMANDS}OnOff"
COMMAND_GET_CAMERA_STREAM = f"{PREFIX_COMMANDS}GetCameraStream"
-COMMAND_LOCK_UNLOCK = f"{PREFIX_COMMANDS}LockUnlock"
-COMMAND_LOCATE = f"{PREFIX_COMMANDS}Locate"
+COMMAND_DOCK = f"{PREFIX_COMMANDS}Dock"
+COMMAND_STARTSTOP = f"{PREFIX_COMMANDS}StartStop"
+COMMAND_PAUSEUNPAUSE = f"{PREFIX_COMMANDS}PauseUnpause"
+COMMAND_BRIGHTNESS_ABSOLUTE = f"{PREFIX_COMMANDS}BrightnessAbsolute"
+COMMAND_COLOR_ABSOLUTE = f"{PREFIX_COMMANDS}ColorAbsolute"
+COMMAND_ACTIVATE_SCENE = f"{PREFIX_COMMANDS}ActivateScene"
+COMMAND_SET_TEMPERATURE = f"{PREFIX_COMMANDS}SetTemperature"
+COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT = (
+ f"{PREFIX_COMMANDS}ThermostatTemperatureSetpoint"
+)
+COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
+ f"{PREFIX_COMMANDS}ThermostatTemperatureSetRange"
+)
+COMMAND_THERMOSTAT_SET_MODE = f"{PREFIX_COMMANDS}ThermostatSetMode"
+COMMAND_LOCKUNLOCK = f"{PREFIX_COMMANDS}LockUnlock"
+COMMAND_FANSPEED = f"{PREFIX_COMMANDS}SetFanSpeed"
+COMMAND_FANSPEEDRELATIVE = f"{PREFIX_COMMANDS}SetFanSpeedRelative"
+COMMAND_MODES = f"{PREFIX_COMMANDS}SetModes"
+COMMAND_INPUT = f"{PREFIX_COMMANDS}SetInput"
COMMAND_NEXT_INPUT = f"{PREFIX_COMMANDS}NextInput"
+COMMAND_PREVIOUS_INPUT = f"{PREFIX_COMMANDS}PreviousInput"
+COMMAND_OPENCLOSE = f"{PREFIX_COMMANDS}OpenClose"
+COMMAND_OPENCLOSE_RELATIVE = f"{PREFIX_COMMANDS}OpenCloseRelative"
+COMMAND_SET_VOLUME = f"{PREFIX_COMMANDS}setVolume"
+COMMAND_VOLUME_RELATIVE = f"{PREFIX_COMMANDS}volumeRelative"
+COMMAND_MUTE = f"{PREFIX_COMMANDS}mute"
+COMMAND_ARMDISARM = f"{PREFIX_COMMANDS}ArmDisarm"
COMMAND_MEDIA_NEXT = f"{PREFIX_COMMANDS}mediaNext"
COMMAND_MEDIA_PAUSE = f"{PREFIX_COMMANDS}mediaPause"
COMMAND_MEDIA_PREVIOUS = f"{PREFIX_COMMANDS}mediaPrevious"
@@ -149,30 +172,11 @@ COMMAND_MEDIA_SEEK_RELATIVE = f"{PREFIX_COMMANDS}mediaSeekRelative"
COMMAND_MEDIA_SEEK_TO_POSITION = f"{PREFIX_COMMANDS}mediaSeekToPosition"
COMMAND_MEDIA_SHUFFLE = f"{PREFIX_COMMANDS}mediaShuffle"
COMMAND_MEDIA_STOP = f"{PREFIX_COMMANDS}mediaStop"
-COMMAND_MUTE = f"{PREFIX_COMMANDS}mute"
-COMMAND_OPEN_CLOSE = f"{PREFIX_COMMANDS}OpenClose"
-COMMAND_ON_OFF = f"{PREFIX_COMMANDS}OnOff"
-COMMAND_OPEN_CLOSE_RELATIVE = f"{PREFIX_COMMANDS}OpenCloseRelative"
-COMMAND_PAUSE_UNPAUSE = f"{PREFIX_COMMANDS}PauseUnpause"
COMMAND_REVERSE = f"{PREFIX_COMMANDS}Reverse"
-COMMAND_PREVIOUS_INPUT = f"{PREFIX_COMMANDS}PreviousInput"
-COMMAND_SELECT_CHANNEL = f"{PREFIX_COMMANDS}selectChannel"
-COMMAND_SET_TEMPERATURE = f"{PREFIX_COMMANDS}SetTemperature"
-COMMAND_SET_FAN_SPEED = f"{PREFIX_COMMANDS}SetFanSpeed"
-COMMAND_SET_FAN_SPEED_RELATIVE = f"{PREFIX_COMMANDS}SetFanSpeedRelative"
COMMAND_SET_HUMIDITY = f"{PREFIX_COMMANDS}SetHumidity"
-COMMAND_SET_INPUT = f"{PREFIX_COMMANDS}SetInput"
-COMMAND_SET_MODES = f"{PREFIX_COMMANDS}SetModes"
-COMMAND_SET_VOLUME = f"{PREFIX_COMMANDS}setVolume"
-COMMAND_START_STOP = f"{PREFIX_COMMANDS}StartStop"
-COMMAND_THERMOSTAT_SET_MODE = f"{PREFIX_COMMANDS}ThermostatSetMode"
-COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT = (
- f"{PREFIX_COMMANDS}ThermostatTemperatureSetpoint"
-)
-COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
- f"{PREFIX_COMMANDS}ThermostatTemperatureSetRange"
-)
-COMMAND_VOLUME_RELATIVE = f"{PREFIX_COMMANDS}volumeRelative"
+COMMAND_SELECT_CHANNEL = f"{PREFIX_COMMANDS}selectChannel"
+COMMAND_LOCATE = f"{PREFIX_COMMANDS}Locate"
+COMMAND_CHARGE = f"{PREFIX_COMMANDS}Charge"
TRAITS: list[type[_Trait]] = []
@@ -411,7 +415,7 @@ class ObjectDetection(_Trait):
https://developers.google.com/actions/smarthome/traits/objectdetection
"""
- name = TRAIT_OBJECT_DETECTION
+ name = TRAIT_OBJECTDETECTION
commands = []
@staticmethod
@@ -469,8 +473,8 @@ class OnOffTrait(_Trait):
https://developers.google.com/actions/smarthome/traits/onoff
"""
- name = TRAIT_ON_OFF
- commands = [COMMAND_ON_OFF]
+ name = TRAIT_ONOFF
+ commands = [COMMAND_ONOFF]
@staticmethod
def supported(domain, features, device_class, _):
@@ -789,7 +793,7 @@ class EnergyStorageTrait(_Trait):
https://developers.google.com/actions/smarthome/traits/energystorage
"""
- name = TRAIT_ENERGY_STORAGE
+ name = TRAIT_ENERGYSTORAGE
commands = [COMMAND_CHARGE]
@staticmethod
@@ -844,8 +848,8 @@ class StartStopTrait(_Trait):
https://developers.google.com/actions/smarthome/traits/startstop
"""
- name = TRAIT_START_STOP
- commands = [COMMAND_START_STOP, COMMAND_PAUSE_UNPAUSE]
+ name = TRAIT_STARTSTOP
+ commands = [COMMAND_STARTSTOP, COMMAND_PAUSEUNPAUSE]
@staticmethod
def supported(domain, features, device_class, _):
@@ -909,7 +913,7 @@ class StartStopTrait(_Trait):
async def _execute_vacuum(self, command, data, params, challenge):
"""Execute a StartStop command."""
- if command == COMMAND_START_STOP:
+ if command == COMMAND_STARTSTOP:
if params["start"]:
await self.hass.services.async_call(
self.state.domain,
@@ -926,7 +930,7 @@ class StartStopTrait(_Trait):
blocking=not self.config.should_report_state,
context=data.context,
)
- elif command == COMMAND_PAUSE_UNPAUSE:
+ elif command == COMMAND_PAUSEUNPAUSE:
if params["pause"]:
await self.hass.services.async_call(
self.state.domain,
@@ -947,7 +951,7 @@ class StartStopTrait(_Trait):
async def _execute_cover_or_valve(self, command, data, params, challenge):
"""Execute a StartStop command."""
domain = self.state.domain
- if command == COMMAND_START_STOP:
+ if command == COMMAND_STARTSTOP:
if params["start"] is False:
if self.state.state in (
COVER_VALVE_STATES[domain]["closing"],
@@ -1500,8 +1504,8 @@ class LockUnlockTrait(_Trait):
https://developers.google.com/actions/smarthome/traits/lockunlock
"""
- name = TRAIT_LOCK_UNLOCK
- commands = [COMMAND_LOCK_UNLOCK]
+ name = TRAIT_LOCKUNLOCK
+ commands = [COMMAND_LOCKUNLOCK]
@staticmethod
def supported(domain, features, device_class, _):
@@ -1549,23 +1553,23 @@ class ArmDisArmTrait(_Trait):
https://developers.google.com/actions/smarthome/traits/armdisarm
"""
- name = TRAIT_ARM_DISARM
- commands = [COMMAND_ARM_DISARM]
+ name = TRAIT_ARMDISARM
+ commands = [COMMAND_ARMDISARM]
state_to_service = {
- AlarmControlPanelState.ARMED_HOME: SERVICE_ALARM_ARM_HOME,
- AlarmControlPanelState.ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT,
- AlarmControlPanelState.ARMED_AWAY: SERVICE_ALARM_ARM_AWAY,
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS: SERVICE_ALARM_ARM_CUSTOM_BYPASS,
- AlarmControlPanelState.TRIGGERED: SERVICE_ALARM_TRIGGER,
+ STATE_ALARM_ARMED_HOME: SERVICE_ALARM_ARM_HOME,
+ STATE_ALARM_ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT,
+ STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS: SERVICE_ALARM_ARM_CUSTOM_BYPASS,
+ STATE_ALARM_TRIGGERED: SERVICE_ALARM_TRIGGER,
}
state_to_support = {
- AlarmControlPanelState.ARMED_HOME: AlarmControlPanelEntityFeature.ARM_HOME,
- AlarmControlPanelState.ARMED_NIGHT: AlarmControlPanelEntityFeature.ARM_NIGHT,
- AlarmControlPanelState.ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY,
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
- AlarmControlPanelState.TRIGGERED: AlarmControlPanelEntityFeature.TRIGGER,
+ STATE_ALARM_ARMED_HOME: AlarmControlPanelEntityFeature.ARM_HOME,
+ STATE_ALARM_ARMED_NIGHT: AlarmControlPanelEntityFeature.ARM_NIGHT,
+ STATE_ALARM_ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
+ STATE_ALARM_TRIGGERED: AlarmControlPanelEntityFeature.TRIGGER,
}
"""The list of states to support in increasing security state."""
@@ -1591,8 +1595,8 @@ class ArmDisArmTrait(_Trait):
def _default_arm_state(self):
states = self._supported_states()
- if AlarmControlPanelState.TRIGGERED in states:
- states.remove(AlarmControlPanelState.TRIGGERED)
+ if STATE_ALARM_TRIGGERED in states:
+ states.remove(STATE_ALARM_TRIGGERED)
if not states:
raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing")
@@ -1607,7 +1611,7 @@ class ArmDisArmTrait(_Trait):
# level synonyms are generated from state names
# 'armed_away' becomes 'armed away' or 'away'
level_synonym = [state.replace("_", " ")]
- if state != AlarmControlPanelState.TRIGGERED:
+ if state != STATE_ALARM_TRIGGERED:
level_synonym.append(state.split("_")[1])
level = {
@@ -1648,11 +1652,11 @@ class ArmDisArmTrait(_Trait):
elif (
params["arm"]
and params.get("cancel")
- and self.state.state == AlarmControlPanelState.PENDING
+ and self.state.state == STATE_ALARM_PENDING
):
service = SERVICE_ALARM_DISARM
else:
- if self.state.state == AlarmControlPanelState.DISARMED:
+ if self.state.state == STATE_ALARM_DISARMED:
raise SmartHomeError(ERR_ALREADY_DISARMED, "System is already disarmed")
_verify_pin_challenge(data, self.state, challenge)
service = SERVICE_ALARM_DISARM
@@ -1690,8 +1694,8 @@ class FanSpeedTrait(_Trait):
https://developers.google.com/actions/smarthome/traits/fanspeed
"""
- name = TRAIT_FAN_SPEED
- commands = [COMMAND_SET_FAN_SPEED, COMMAND_REVERSE]
+ name = TRAIT_FANSPEED
+ commands = [COMMAND_FANSPEED, COMMAND_REVERSE]
def __init__(self, hass, state, config):
"""Initialize a trait for a state."""
@@ -1836,7 +1840,7 @@ class FanSpeedTrait(_Trait):
async def execute(self, command, data, params, challenge):
"""Execute a smart home command."""
- if command == COMMAND_SET_FAN_SPEED:
+ if command == COMMAND_FANSPEED:
await self.execute_fanspeed(data, params)
elif command == COMMAND_REVERSE:
await self.execute_reverse(data, params)
@@ -1850,7 +1854,7 @@ class ModesTrait(_Trait):
"""
name = TRAIT_MODES
- commands = [COMMAND_SET_MODES]
+ commands = [COMMAND_MODES]
SYNONYMS = {
"preset mode": ["preset mode", "mode", "preset"],
@@ -2084,8 +2088,8 @@ class InputSelectorTrait(_Trait):
https://developers.google.com/assistant/smarthome/traits/inputselector
"""
- name = TRAIT_INPUT_SELECTOR
- commands = [COMMAND_SET_INPUT, COMMAND_NEXT_INPUT, COMMAND_PREVIOUS_INPUT]
+ name = TRAIT_INPUTSELECTOR
+ commands = [COMMAND_INPUT, COMMAND_NEXT_INPUT, COMMAND_PREVIOUS_INPUT]
SYNONYMS: dict[str, list[str]] = {}
@@ -2120,7 +2124,7 @@ class InputSelectorTrait(_Trait):
sources = self.state.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST) or []
source = self.state.attributes.get(media_player.ATTR_INPUT_SOURCE)
- if command == COMMAND_SET_INPUT:
+ if command == COMMAND_INPUT:
requested_source = params.get("newInput")
elif command == COMMAND_NEXT_INPUT:
requested_source = _next_selected(sources, source)
@@ -2158,8 +2162,8 @@ class OpenCloseTrait(_Trait):
cover.CoverDeviceClass.GATE,
)
- name = TRAIT_OPEN_CLOSE
- commands = [COMMAND_OPEN_CLOSE, COMMAND_OPEN_CLOSE_RELATIVE]
+ name = TRAIT_OPENCLOSE
+ commands = [COMMAND_OPENCLOSE, COMMAND_OPENCLOSE_RELATIVE]
@staticmethod
def supported(domain, features, device_class, _):
@@ -2259,7 +2263,7 @@ class OpenCloseTrait(_Trait):
if domain in COVER_VALVE_DOMAINS:
svc_params = {ATTR_ENTITY_ID: self.state.entity_id}
should_verify = False
- if command == COMMAND_OPEN_CLOSE_RELATIVE:
+ if command == COMMAND_OPENCLOSE_RELATIVE:
position = self.state.attributes.get(
COVER_VALVE_CURRENT_POSITION[domain]
)
@@ -2706,21 +2710,6 @@ class SensorStateTrait(_Trait):
),
}
- binary_sensor_types = {
- binary_sensor.BinarySensorDeviceClass.CO: (
- "CarbonMonoxideLevel",
- ["carbon monoxide detected", "no carbon monoxide detected", "unknown"],
- ),
- binary_sensor.BinarySensorDeviceClass.SMOKE: (
- "SmokeLevel",
- ["smoke detected", "no smoke detected", "unknown"],
- ),
- binary_sensor.BinarySensorDeviceClass.MOISTURE: (
- "WaterLeak",
- ["leak", "no leak", "unknown"],
- ),
- }
-
name = TRAIT_SENSOR_STATE
commands: list[str] = []
@@ -2743,37 +2732,24 @@ class SensorStateTrait(_Trait):
@classmethod
def supported(cls, domain, features, device_class, _):
"""Test if state is supported."""
- return (domain == sensor.DOMAIN and device_class in cls.sensor_types) or (
- domain == binary_sensor.DOMAIN and device_class in cls.binary_sensor_types
- )
+ return domain == sensor.DOMAIN and device_class in cls.sensor_types
def sync_attributes(self) -> dict[str, Any]:
"""Return attributes for a sync request."""
device_class = self.state.attributes.get(ATTR_DEVICE_CLASS)
+ data = self.sensor_types.get(device_class)
- def create_sensor_state(
- name: str,
- raw_value_unit: str | None = None,
- available_states: list[str] | None = None,
- ) -> dict[str, Any]:
- sensor_state: dict[str, Any] = {
- "name": name,
- }
- if raw_value_unit:
- sensor_state["numericCapabilities"] = {"rawValueUnit": raw_value_unit}
- if available_states:
- sensor_state["descriptiveCapabilities"] = {
- "availableStates": available_states
- }
- return {"sensorStatesSupported": [sensor_state]}
+ if device_class is None or data is None:
+ return {}
- if self.state.domain == sensor.DOMAIN:
- sensor_data = self.sensor_types.get(device_class)
- if device_class is None or sensor_data is None:
- return {}
- available_states: list[str] | None = None
- if device_class == sensor.SensorDeviceClass.AQI:
- available_states = [
+ sensor_state = {
+ "name": data[0],
+ "numericCapabilities": {"rawValueUnit": data[1]},
+ }
+
+ if device_class == sensor.SensorDeviceClass.AQI:
+ sensor_state["descriptiveCapabilities"] = {
+ "availableStates": [
"healthy",
"moderate",
"unhealthy for sensitive groups",
@@ -2781,53 +2757,30 @@ class SensorStateTrait(_Trait):
"very unhealthy",
"hazardous",
"unknown",
- ]
- return create_sensor_state(sensor_data[0], sensor_data[1], available_states)
- binary_sensor_data = self.binary_sensor_types.get(device_class)
- if device_class is None or binary_sensor_data is None:
- return {}
- return create_sensor_state(
- binary_sensor_data[0], available_states=binary_sensor_data[1]
- )
+ ],
+ }
+
+ return {"sensorStatesSupported": [sensor_state]}
def query_attributes(self) -> dict[str, Any]:
"""Return the attributes of this trait for this entity."""
device_class = self.state.attributes.get(ATTR_DEVICE_CLASS)
+ data = self.sensor_types.get(device_class)
- def create_sensor_state(
- name: str, raw_value: float | None = None, current_state: str | None = None
- ) -> dict[str, Any]:
- sensor_state: dict[str, Any] = {
- "name": name,
- "rawValue": raw_value,
- }
- if current_state:
- sensor_state["currentSensorState"] = current_state
- return {"currentSensorStateData": [sensor_state]}
-
- if self.state.domain == sensor.DOMAIN:
- sensor_data = self.sensor_types.get(device_class)
- if device_class is None or sensor_data is None:
- return {}
- try:
- value = float(self.state.state)
- except ValueError:
- value = None
- if self.state.state == STATE_UNKNOWN:
- value = None
- current_state: str | None = None
- if device_class == sensor.SensorDeviceClass.AQI:
- current_state = self._air_quality_description_for_aqi(value)
- return create_sensor_state(sensor_data[0], value, current_state)
-
- binary_sensor_data = self.binary_sensor_types.get(device_class)
- if device_class is None or binary_sensor_data is None:
+ if device_class is None or data is None:
return {}
- value = {
- STATE_ON: 0,
- STATE_OFF: 1,
- STATE_UNKNOWN: 2,
- }[self.state.state]
- return create_sensor_state(
- binary_sensor_data[0], current_state=binary_sensor_data[1][value]
- )
+
+ try:
+ value = float(self.state.state)
+ except ValueError:
+ value = None
+ if self.state.state == STATE_UNKNOWN:
+ value = None
+ sensor_data = {"name": data[0], "rawValue": value}
+
+ if device_class == sensor.SensorDeviceClass.AQI:
+ sensor_data["currentSensorState"] = self._air_quality_description_for_aqi(
+ value
+ )
+
+ return {"currentSensorStateData": [sensor_data]}
diff --git a/homeassistant/components/google_assistant_sdk/config_flow.py b/homeassistant/components/google_assistant_sdk/config_flow.py
index cd78c90e297..85dfd974b22 100644
--- a/homeassistant/components/google_assistant_sdk/config_flow.py
+++ b/homeassistant/components/google_assistant_sdk/config_flow.py
@@ -8,12 +8,7 @@ from typing import Any
import voluptuous as vol
-from homeassistant.config_entries import (
- SOURCE_REAUTH,
- ConfigEntry,
- ConfigFlowResult,
- OptionsFlow,
-)
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult, OptionsFlow
from homeassistant.core import callback
from homeassistant.helpers import config_entry_oauth2_flow
@@ -30,6 +25,8 @@ class OAuth2FlowHandler(
DOMAIN = DOMAIN
+ reauth_entry: ConfigEntry | None = None
+
@property
def logger(self) -> logging.Logger:
"""Return logger."""
@@ -49,6 +46,9 @@ class OAuth2FlowHandler(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -61,10 +61,10 @@ class OAuth2FlowHandler(
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create an entry for the flow, or update existing entry."""
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=data
- )
+ if self.reauth_entry:
+ self.hass.config_entries.async_update_entry(self.reauth_entry, data=data)
+ await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
if self._async_current_entries():
# Config entry already exists, only one allowed.
@@ -84,12 +84,16 @@ class OAuth2FlowHandler(
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Create the options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Google Assistant SDK options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/google_cloud/config_flow.py b/homeassistant/components/google_cloud/config_flow.py
index fa6c952022b..dec849de4e6 100644
--- a/homeassistant/components/google_cloud/config_flow.py
+++ b/homeassistant/components/google_cloud/config_flow.py
@@ -15,7 +15,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.core import callback
from homeassistant.helpers.selector import (
@@ -135,10 +135,10 @@ class GoogleCloudConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> GoogleCloudOptionsFlowHandler:
"""Create the options flow."""
- return GoogleCloudOptionsFlowHandler()
+ return GoogleCloudOptionsFlowHandler(config_entry)
-class GoogleCloudOptionsFlowHandler(OptionsFlow):
+class GoogleCloudOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Google Cloud options flow."""
async def async_step_init(
@@ -169,7 +169,7 @@ class GoogleCloudOptionsFlowHandler(OptionsFlow):
)
),
**tts_options_schema(
- self.config_entry.options, voices, from_config_flow=True
+ self.options, voices, from_config_flow=True
).schema,
vol.Optional(
CONF_STT_MODEL,
@@ -182,6 +182,6 @@ class GoogleCloudOptionsFlowHandler(OptionsFlow):
),
}
),
- self.config_entry.options,
+ self.options,
),
)
diff --git a/homeassistant/components/google_cloud/helpers.py b/homeassistant/components/google_cloud/helpers.py
index f6e89fae7fa..3c614156132 100644
--- a/homeassistant/components/google_cloud/helpers.py
+++ b/homeassistant/components/google_cloud/helpers.py
@@ -52,7 +52,7 @@ async def async_tts_voices(
def tts_options_schema(
- config_options: Mapping[str, Any],
+ config_options: dict[str, Any],
voices: dict[str, list[str]],
from_config_flow: bool = False,
) -> vol.Schema:
diff --git a/homeassistant/components/google_generative_ai_conversation/config_flow.py b/homeassistant/components/google_generative_ai_conversation/config_flow.py
index 83eec25ed15..ab23ac25f26 100644
--- a/homeassistant/components/google_generative_ai_conversation/config_flow.py
+++ b/homeassistant/components/google_generative_ai_conversation/config_flow.py
@@ -15,7 +15,6 @@ import google.generativeai as genai
import voluptuous as vol
from homeassistant.config_entries import (
- SOURCE_REAUTH,
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
@@ -86,6 +85,10 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ def __init__(self) -> None:
+ """Initialize a new GoogleGenerativeAIConfigFlow."""
+ self.reauth_entry: ConfigEntry | None = None
+
async def async_step_api(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -103,9 +106,9 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
- if self.source == SOURCE_REAUTH:
+ if self.reauth_entry:
return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
+ self.reauth_entry,
data=user_input,
)
return self.async_create_entry(
@@ -132,6 +135,9 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -140,13 +146,12 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
"""Dialog that informs the user that reauth is required."""
if user_input is not None:
return await self.async_step_api()
-
- reauth_entry = self._get_reauth_entry()
+ assert self.reauth_entry
return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={
- CONF_NAME: reauth_entry.title,
- CONF_API_KEY: reauth_entry.data.get(CONF_API_KEY, ""),
+ CONF_NAME: self.reauth_entry.title,
+ CONF_API_KEY: self.reauth_entry.data.get(CONF_API_KEY, ""),
},
)
@@ -163,6 +168,7 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
+ self.config_entry = config_entry
self.last_rendered_recommended = config_entry.options.get(
CONF_RECOMMENDED, False
)
diff --git a/homeassistant/components/google_mail/config_flow.py b/homeassistant/components/google_mail/config_flow.py
index b3a9a0e5d56..5c81f7d49f5 100644
--- a/homeassistant/components/google_mail/config_flow.py
+++ b/homeassistant/components/google_mail/config_flow.py
@@ -9,10 +9,11 @@ from typing import Any, cast
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers import config_entry_oauth2_flow
+from . import GoogleMailConfigEntry
from .const import DEFAULT_ACCESS, DOMAIN
@@ -23,6 +24,8 @@ class OAuth2FlowHandler(
DOMAIN = DOMAIN
+ reauth_entry: GoogleMailConfigEntry | None = None
+
@property
def logger(self) -> logging.Logger:
"""Return logger."""
@@ -42,6 +45,9 @@ class OAuth2FlowHandler(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -63,15 +69,18 @@ class OAuth2FlowHandler(
credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
email = await self.hass.async_add_executor_job(_get_profile)
- await self.async_set_unique_id(email)
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
+ await self.async_set_unique_id(email)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=email, data=data)
- reauth_entry = self._get_reauth_entry()
- self._abort_if_unique_id_mismatch(
+ if self.reauth_entry.unique_id == email:
+ self.hass.config_entries.async_update_entry(self.reauth_entry, data=data)
+ await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+
+ return self.async_abort(
reason="wrong_account",
- description_placeholders={"email": cast(str, reauth_entry.unique_id)},
+ description_placeholders={"email": cast(str, self.reauth_entry.unique_id)},
)
- return self.async_update_reload_and_abort(reauth_entry, data=data)
diff --git a/homeassistant/components/google_photos/config_flow.py b/homeassistant/components/google_photos/config_flow.py
index a336455c9b4..6b025cac6be 100644
--- a/homeassistant/components/google_photos/config_flow.py
+++ b/homeassistant/components/google_photos/config_flow.py
@@ -7,11 +7,11 @@ from typing import Any
from google_photos_library_api.api import GooglePhotosLibraryApi
from google_photos_library_api.exceptions import GooglePhotosApiError
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
-from . import api
+from . import GooglePhotosConfigEntry, api
from .const import DOMAIN, OAUTH2_SCOPES
@@ -22,6 +22,8 @@ class OAuth2FlowHandler(
DOMAIN = DOMAIN
+ reauth_entry: GooglePhotosConfigEntry | None = None
+
@property
def logger(self) -> logging.Logger:
"""Return logger."""
@@ -56,13 +58,14 @@ class OAuth2FlowHandler(
return self.async_abort(reason="unknown")
user_id = user_resource_info.id
- await self.async_set_unique_id(user_id)
- 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=data
- )
+ if self.reauth_entry:
+ if self.reauth_entry.unique_id == user_id:
+ return self.async_update_reload_and_abort(
+ self.reauth_entry, unique_id=user_id, data=data
+ )
+ return self.async_abort(reason="wrong_account")
+ await self.async_set_unique_id(user_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=user_resource_info.name, data=data)
@@ -70,6 +73,9 @@ class OAuth2FlowHandler(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
diff --git a/homeassistant/components/google_photos/strings.json b/homeassistant/components/google_photos/strings.json
index bd565a6122d..21942ce71a7 100644
--- a/homeassistant/components/google_photos/strings.json
+++ b/homeassistant/components/google_photos/strings.json
@@ -21,8 +21,7 @@
"wrong_account": "Wrong account: Please authenticate with the right account.",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
- "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
diff --git a/homeassistant/components/google_sheets/config_flow.py b/homeassistant/components/google_sheets/config_flow.py
index 81c82bf1bc4..4008d42f52d 100644
--- a/homeassistant/components/google_sheets/config_flow.py
+++ b/homeassistant/components/google_sheets/config_flow.py
@@ -9,10 +9,11 @@ from typing import Any
from google.oauth2.credentials import Credentials
from gspread import Client, GSpreadException
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers import config_entry_oauth2_flow
+from . import GoogleSheetsConfigEntry
from .const import DEFAULT_ACCESS, DEFAULT_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -25,6 +26,8 @@ class OAuth2FlowHandler(
DOMAIN = DOMAIN
+ reauth_entry: GoogleSheetsConfigEntry | None = None
+
@property
def logger(self) -> logging.Logger:
"""Return logger."""
@@ -44,6 +47,9 @@ class OAuth2FlowHandler(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -60,23 +66,24 @@ class OAuth2FlowHandler(
Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
)
- if self.source == SOURCE_REAUTH:
- reauth_entry = self._get_reauth_entry()
+ if self.reauth_entry:
_LOGGER.debug("service.open_by_key")
try:
await self.hass.async_add_executor_job(
service.open_by_key,
- reauth_entry.unique_id,
+ self.reauth_entry.unique_id,
)
except GSpreadException as err:
_LOGGER.error(
"Could not find spreadsheet '%s': %s",
- reauth_entry.unique_id,
+ self.reauth_entry.unique_id,
str(err),
)
return self.async_abort(reason="open_spreadsheet_failure")
- return self.async_update_reload_and_abort(reauth_entry, data=data)
+ self.hass.config_entries.async_update_entry(self.reauth_entry, data=data)
+ await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
try:
doc = await self.hass.async_add_executor_job(
diff --git a/homeassistant/components/google_tasks/api.py b/homeassistant/components/google_tasks/api.py
index 2a294b84654..c8b30c173eb 100644
--- a/homeassistant/components/google_tasks/api.py
+++ b/homeassistant/components/google_tasks/api.py
@@ -46,7 +46,8 @@ class AsyncConfigEntryAuth:
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
- await self._oauth_session.async_ensure_token_valid()
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token[CONF_ACCESS_TOKEN]
async def _get_service(self) -> Resource:
diff --git a/homeassistant/components/google_tasks/config_flow.py b/homeassistant/components/google_tasks/config_flow.py
index 795b6e6eff5..965c215ee4d 100644
--- a/homeassistant/components/google_tasks/config_flow.py
+++ b/homeassistant/components/google_tasks/config_flow.py
@@ -9,7 +9,7 @@ from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from googleapiclient.http import HttpRequest
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers import config_entry_oauth2_flow
@@ -23,6 +23,8 @@ class OAuth2FlowHandler(
DOMAIN = DOMAIN
+ reauth_entry: ConfigEntry | None = None
+
@property
def logger(self) -> logging.Logger:
"""Return logger."""
@@ -68,24 +70,25 @@ class OAuth2FlowHandler(
self.logger.exception("Unknown error occurred")
return self.async_abort(reason="unknown")
user_id = user_resource_info["id"]
- await self.async_set_unique_id(user_id)
-
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
+ await self.async_set_unique_id(user_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=user_resource_info["name"], data=data)
- reauth_entry = self._get_reauth_entry()
- if reauth_entry.unique_id:
- self._abort_if_unique_id_mismatch(reason="wrong_account")
+ if self.reauth_entry.unique_id == user_id or not self.reauth_entry.unique_id:
+ return self.async_update_reload_and_abort(
+ self.reauth_entry, unique_id=user_id, data=data
+ )
- return self.async_update_reload_and_abort(
- reauth_entry, unique_id=user_id, data=data
- )
+ return self.async_abort(reason="wrong_account")
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
diff --git a/homeassistant/components/google_tasks/strings.json b/homeassistant/components/google_tasks/strings.json
index a26cf8c58ec..447da5e24c2 100644
--- a/homeassistant/components/google_tasks/strings.json
+++ b/homeassistant/components/google_tasks/strings.json
@@ -21,8 +21,7 @@
"wrong_account": "Wrong account: Please authenticate with the right account.",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
- "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
diff --git a/homeassistant/components/google_tasks/todo.py b/homeassistant/components/google_tasks/todo.py
index 5196f89728d..95c5f1c3a16 100644
--- a/homeassistant/components/google_tasks/todo.py
+++ b/homeassistant/components/google_tasks/todo.py
@@ -106,7 +106,7 @@ class GoogleTaskTodoListEntity(
config_entry_id: str,
task_list_id: str,
) -> None:
- """Initialize GoogleTaskTodoListEntity."""
+ """Initialize LocalTodoListEntity."""
super().__init__(coordinator)
self._attr_name = name.capitalize()
self._attr_unique_id = f"{config_entry_id}-{task_list_id}"
@@ -153,9 +153,9 @@ class GoogleTaskTodoListEntity(
def _order_tasks(tasks: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Order the task items response.
- All tasks have an order amongst their siblings based on position.
+ All tasks have an order amongst their sibblings based on position.
- Home Assistant To-do items do not support the Google Task parent/sibling
+ Home Assistant To-do items do not support the Google Task parent/sibbling
relationships and the desired behavior is for them to be filtered.
"""
parents = [task for task in tasks if task.get("parent") is None]
diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py
index 08de293bc7d..b7a26d3a4eb 100644
--- a/homeassistant/components/google_travel_time/config_flow.py
+++ b/homeassistant/components/google_travel_time/config_flow.py
@@ -148,6 +148,10 @@ def default_options(hass: HomeAssistant) -> dict[str, str]:
class GoogleOptionsFlow(OptionsFlow):
"""Handle an options flow for Google Travel Time."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize google options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(self, user_input=None) -> ConfigFlowResult:
"""Handle the initial step."""
if user_input is not None:
@@ -209,7 +213,7 @@ class GoogleTravelTimeConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> GoogleOptionsFlow:
"""Get the options flow for this handler."""
- return GoogleOptionsFlow()
+ return GoogleOptionsFlow(config_entry)
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
"""Handle the initial step."""
@@ -232,6 +236,12 @@ class GoogleTravelTimeConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle reconfiguration."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration."""
errors: dict[str, str] | None = None
@@ -243,7 +253,7 @@ class GoogleTravelTimeConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=self.add_suggested_values_to_schema(
RECONFIGURE_SCHEMA, self._get_reconfigure_entry().data
),
diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json
index 765cfc9c4b6..6397336d9ac 100644
--- a/homeassistant/components/google_travel_time/strings.json
+++ b/homeassistant/components/google_travel_time/strings.json
@@ -11,7 +11,7 @@
"destination": "Destination"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"description": "[%key:component::google_travel_time::config::step::user::description%]",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
diff --git a/homeassistant/components/govee_light_local/__init__.py b/homeassistant/components/govee_light_local/__init__.py
index 44dbc825665..088f9bae22b 100644
--- a/homeassistant/components/govee_light_local/__init__.py
+++ b/homeassistant/components/govee_light_local/__init__.py
@@ -9,21 +9,23 @@ import logging
from govee_local_api.controller import LISTENING_PORT
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
-from .const import DISCOVERY_TIMEOUT
-from .coordinator import GoveeLocalApiCoordinator, GoveeLocalConfigEntry
+from .const import DISCOVERY_TIMEOUT, DOMAIN
+from .coordinator import GoveeLocalApiCoordinator
PLATFORMS: list[Platform] = [Platform.LIGHT]
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Govee light local from a config entry."""
- coordinator = GoveeLocalApiCoordinator(hass=hass)
+
+ coordinator: GoveeLocalApiCoordinator = GoveeLocalApiCoordinator(hass=hass)
async def await_cleanup():
cleanup_complete: asyncio.Event = coordinator.cleanup()
@@ -50,11 +52,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -
except TimeoutError as ex:
raise ConfigEntryNotReady from ex
- entry.runtime_data = coordinator
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
-async def async_unload_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+
+ if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
+ hass.data[DOMAIN].pop(entry.entry_id)
+ return unload_ok
diff --git a/homeassistant/components/govee_light_local/coordinator.py b/homeassistant/components/govee_light_local/coordinator.py
index 240313a34b8..64119f1871c 100644
--- a/homeassistant/components/govee_light_local/coordinator.py
+++ b/homeassistant/components/govee_light_local/coordinator.py
@@ -6,7 +6,6 @@ import logging
from govee_local_api import GoveeController, GoveeDevice
-from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@@ -20,8 +19,6 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
-type GoveeLocalConfigEntry = ConfigEntry[GoveeLocalApiCoordinator]
-
class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]):
"""Govee light local coordinator."""
diff --git a/homeassistant/components/govee_light_local/light.py b/homeassistant/components/govee_light_local/light.py
index cb2e24fa8a6..fb52c233436 100644
--- a/homeassistant/components/govee_light_local/light.py
+++ b/homeassistant/components/govee_light_local/light.py
@@ -15,25 +15,26 @@ from homeassistant.components.light import (
LightEntity,
filter_supported_color_modes,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER
-from .coordinator import GoveeLocalApiCoordinator, GoveeLocalConfigEntry
+from .coordinator import GoveeLocalApiCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
- config_entry: GoveeLocalConfigEntry,
+ config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Govee light setup."""
- coordinator = config_entry.runtime_data
+ coordinator: GoveeLocalApiCoordinator = hass.data[DOMAIN][config_entry.entry_id]
def discovery_callback(device: GoveeDevice, is_new: bool) -> bool:
if is_new:
diff --git a/homeassistant/components/govee_light_local/manifest.json b/homeassistant/components/govee_light_local/manifest.json
index a94d4e58e9a..b6b25f5aa09 100644
--- a/homeassistant/components/govee_light_local/manifest.json
+++ b/homeassistant/components/govee_light_local/manifest.json
@@ -6,5 +6,5 @@
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
"iot_class": "local_push",
- "requirements": ["govee-local-api==1.5.3"]
+ "requirements": ["govee-local-api==1.5.2"]
}
diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py
index 7ac5770f171..e0a74d32f44 100644
--- a/homeassistant/components/group/registry.py
+++ b/homeassistant/components/group/registry.py
@@ -8,7 +8,6 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol
-from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.components.climate import HVACMode
from homeassistant.components.lock import LockState
from homeassistant.components.vacuum import STATE_CLEANING, STATE_ERROR, STATE_RETURNING
@@ -21,6 +20,12 @@ from homeassistant.components.water_heater import (
STATE_PERFORMANCE,
)
from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_TRIGGERED,
STATE_CLOSED,
STATE_HOME,
STATE_IDLE,
@@ -55,12 +60,12 @@ ON_OFF_STATES: dict[Platform | str, tuple[set[str], str, str]] = {
Platform.ALARM_CONTROL_PANEL: (
{
STATE_ON,
- AlarmControlPanelState.ARMED_AWAY,
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
- AlarmControlPanelState.ARMED_HOME,
- AlarmControlPanelState.ARMED_NIGHT,
- AlarmControlPanelState.ARMED_VACATION,
- AlarmControlPanelState.TRIGGERED,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_TRIGGERED,
},
STATE_ON,
STATE_OFF,
diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py
index 4a3e191e511..32744bebc33 100644
--- a/homeassistant/components/group/sensor.py
+++ b/homeassistant/components/group/sensor.py
@@ -36,7 +36,14 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
-from homeassistant.core import HomeAssistant, State, callback
+from homeassistant.core import (
+ CALLBACK_TYPE,
+ Event,
+ EventStateChangedData,
+ HomeAssistant,
+ State,
+ callback,
+)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity import (
@@ -45,6 +52,7 @@ from homeassistant.helpers.entity import (
get_unit_of_measurement,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
@@ -172,17 +180,6 @@ def async_create_preview_sensor(
)
-def _has_numeric_state(hass: HomeAssistant, entity_id: str) -> bool:
- """Test if state is numeric."""
- if not (state := hass.states.get(entity_id)):
- return False
- try:
- float(state.state)
- except ValueError:
- return False
- return True
-
-
def calc_min(
sensor_values: list[tuple[str, float, State]],
) -> tuple[dict[str, str | None], float | None]:
@@ -335,11 +332,12 @@ class SensorGroup(GroupEntity, SensorEntity):
self.hass = hass
self._entity_ids = entity_ids
self._sensor_type = sensor_type
- self._configured_state_class = state_class
- self._configured_device_class = device_class
- self._configured_unit_of_measurement = unit_of_measurement
+ self._state_class = state_class
+ self._device_class = device_class
+ self._native_unit_of_measurement = unit_of_measurement
self._valid_units: set[str | None] = set()
self._can_convert: bool = False
+ self.calculate_attributes_later: CALLBACK_TYPE | None = None
self._attr_name = name
if name == DEFAULT_NAME:
self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize()
@@ -354,25 +352,39 @@ class SensorGroup(GroupEntity, SensorEntity):
self._state_incorrect: set[str] = set()
self._extra_state_attribute: dict[str, Any] = {}
- def calculate_state_attributes(self, valid_state_entities: list[str]) -> None:
+ async def async_added_to_hass(self) -> None:
+ """When added to hass."""
+ for entity_id in self._entity_ids:
+ if self.hass.states.get(entity_id) is None:
+ self.calculate_attributes_later = async_track_state_change_event(
+ self.hass, self._entity_ids, self.calculate_state_attributes
+ )
+ break
+ if not self.calculate_attributes_later:
+ await self.calculate_state_attributes()
+ await super().async_added_to_hass()
+
+ async def calculate_state_attributes(
+ self, event: Event[EventStateChangedData] | None = None
+ ) -> None:
"""Calculate state attributes."""
- self._attr_state_class = self._calculate_state_class(
- self._configured_state_class, valid_state_entities
- )
- self._attr_device_class = self._calculate_device_class(
- self._configured_device_class, valid_state_entities
- )
+ for entity_id in self._entity_ids:
+ if self.hass.states.get(entity_id) is None:
+ return
+ if self.calculate_attributes_later:
+ self.calculate_attributes_later()
+ self.calculate_attributes_later = None
+ self._attr_state_class = self._calculate_state_class(self._state_class)
+ self._attr_device_class = self._calculate_device_class(self._device_class)
self._attr_native_unit_of_measurement = self._calculate_unit_of_measurement(
- self._configured_unit_of_measurement, valid_state_entities
+ self._native_unit_of_measurement
)
self._valid_units = self._get_valid_units()
@callback
def async_update_group_state(self) -> None:
"""Query all members and determine the sensor group state."""
- self.calculate_state_attributes(self._get_valid_entities())
states: list[StateType] = []
- valid_units = self._valid_units
valid_states: list[bool] = []
sensor_values: list[tuple[str, float, State]] = []
for entity_id in self._entity_ids:
@@ -380,18 +392,20 @@ class SensorGroup(GroupEntity, SensorEntity):
states.append(state.state)
try:
numeric_state = float(state.state)
- uom = state.attributes.get("unit_of_measurement")
-
- # Convert the state to the native unit of measurement when we have valid units
- # and a correct device class
- if valid_units and uom in valid_units and self._can_convert is True:
+ if (
+ self._valid_units
+ and (uom := state.attributes["unit_of_measurement"])
+ in self._valid_units
+ and self._can_convert is True
+ ):
numeric_state = UNIT_CONVERTERS[self.device_class].convert(
numeric_state, uom, self.native_unit_of_measurement
)
-
- # If we have valid units and the entity's unit does not match
- # we raise which skips the state and log a warning once
- if valid_units and uom not in valid_units:
+ if (
+ self._valid_units
+ and (uom := state.attributes["unit_of_measurement"])
+ not in self._valid_units
+ ):
raise HomeAssistantError("Not a valid unit") # noqa: TRY301
sensor_values.append((entity_id, numeric_state, state))
@@ -466,9 +480,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return None
def _calculate_state_class(
- self,
- state_class: SensorStateClass | None,
- valid_state_entities: list[str],
+ self, state_class: SensorStateClass | None
) -> SensorStateClass | None:
"""Calculate state class.
@@ -479,18 +491,8 @@ class SensorGroup(GroupEntity, SensorEntity):
"""
if state_class:
return state_class
-
- if not valid_state_entities:
- return None
-
- if not self._ignore_non_numeric and len(valid_state_entities) < len(
- self._entity_ids
- ):
- # Only return state class if all states are valid when not ignoring non numeric
- return None
-
state_classes: list[SensorStateClass] = []
- for entity_id in valid_state_entities:
+ for entity_id in self._entity_ids:
try:
_state_class = get_capability(self.hass, entity_id, "state_class")
except HomeAssistantError:
@@ -521,9 +523,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return None
def _calculate_device_class(
- self,
- device_class: SensorDeviceClass | None,
- valid_state_entities: list[str],
+ self, device_class: SensorDeviceClass | None
) -> SensorDeviceClass | None:
"""Calculate device class.
@@ -534,18 +534,8 @@ class SensorGroup(GroupEntity, SensorEntity):
"""
if device_class:
return device_class
-
- if not valid_state_entities:
- return None
-
- if not self._ignore_non_numeric and len(valid_state_entities) < len(
- self._entity_ids
- ):
- # Only return device class if all states are valid when not ignoring non numeric
- return None
-
device_classes: list[SensorDeviceClass] = []
- for entity_id in valid_state_entities:
+ for entity_id in self._entity_ids:
try:
_device_class = get_device_class(self.hass, entity_id)
except HomeAssistantError:
@@ -578,9 +568,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return None
def _calculate_unit_of_measurement(
- self,
- unit_of_measurement: str | None,
- valid_state_entities: list[str],
+ self, unit_of_measurement: str | None
) -> str | None:
"""Calculate the unit of measurement.
@@ -591,17 +579,8 @@ class SensorGroup(GroupEntity, SensorEntity):
if unit_of_measurement:
return unit_of_measurement
- if not valid_state_entities:
- return None
-
- if not self._ignore_non_numeric and len(valid_state_entities) < len(
- self._entity_ids
- ):
- # Only return device class if all states are valid when not ignoring non numeric
- return None
-
unit_of_measurements: list[str] = []
- for entity_id in valid_state_entities:
+ for entity_id in self._entity_ids:
try:
_unit_of_measurement = get_unit_of_measurement(self.hass, entity_id)
except HomeAssistantError:
@@ -686,31 +665,19 @@ class SensorGroup(GroupEntity, SensorEntity):
If device class is set and compatible unit of measurements.
If device class is not set, use one unit of measurement.
- Only calculate valid units if there are no valid units set.
"""
- if (valid_units := self._valid_units) and not self._ignore_non_numeric:
- # If we have valid units already and not using ignore_non_numeric
- # we should not recalculate.
- return valid_units
-
- native_uom = self.native_unit_of_measurement
- if (device_class := self.device_class) in UNIT_CONVERTERS and native_uom:
+ if (
+ device_class := self.device_class
+ ) in UNIT_CONVERTERS and self.native_unit_of_measurement:
self._can_convert = True
return UNIT_CONVERTERS[device_class].VALID_UNITS
- if device_class and (device_class) in DEVICE_CLASS_UNITS and native_uom:
+ if (
+ device_class
+ and (device_class) in DEVICE_CLASS_UNITS
+ and self.native_unit_of_measurement
+ ):
valid_uoms: set = DEVICE_CLASS_UNITS[device_class]
return valid_uoms
- if device_class is None and native_uom:
- return {native_uom}
+ if device_class is None and self.native_unit_of_measurement:
+ return {self.native_unit_of_measurement}
return set()
-
- def _get_valid_entities(
- self,
- ) -> list[str]:
- """Return list of valid entities."""
-
- return [
- entity_id
- for entity_id in self._entity_ids
- if _has_numeric_state(self.hass, entity_id)
- ]
diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json
index b1b72b71002..e8622fe9d03 100644
--- a/homeassistant/components/guardian/strings.json
+++ b/homeassistant/components/guardian/strings.json
@@ -12,9 +12,6 @@
"description": "Do you want to set up this Guardian device?"
}
},
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
- },
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py
index 5843e14d63e..0f5b9bd2b50 100644
--- a/homeassistant/components/habitica/__init__.py
+++ b/homeassistant/components/habitica/__init__.py
@@ -1,48 +1,155 @@
"""The habitica integration."""
from http import HTTPStatus
+import logging
+from typing import Any
from aiohttp import ClientResponseError
from habitipy.aio import HabitipyAsync
+import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
- APPLICATION_NAME,
+ ATTR_NAME,
CONF_API_KEY,
CONF_NAME,
CONF_URL,
CONF_VERIFY_SSL,
Platform,
- __version__,
)
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.core import (
+ HomeAssistant,
+ ServiceCall,
+ ServiceResponse,
+ SupportsResponse,
+)
+from homeassistant.exceptions import (
+ ConfigEntryNotReady,
+ HomeAssistantError,
+ ServiceValidationError,
+)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.selector import ConfigEntrySelector
from homeassistant.helpers.typing import ConfigType
-from .const import CONF_API_USER, DEVELOPER_ID, DOMAIN
+from .const import (
+ ATTR_ARGS,
+ ATTR_CONFIG_ENTRY,
+ ATTR_DATA,
+ ATTR_PATH,
+ ATTR_SKILL,
+ ATTR_TASK,
+ CONF_API_USER,
+ DOMAIN,
+ EVENT_API_CALL_SUCCESS,
+ SERVICE_API_CALL,
+ SERVICE_CAST_SKILL,
+)
from .coordinator import HabiticaDataUpdateCoordinator
-from .services import async_setup_services
-from .types import HabiticaConfigEntry
+_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
+type HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator]
-PLATFORMS = [
- Platform.BINARY_SENSOR,
- Platform.BUTTON,
- Platform.CALENDAR,
- Platform.SENSOR,
- Platform.SWITCH,
- Platform.TODO,
-]
+
+PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH, Platform.TODO]
+
+
+SERVICE_API_CALL_SCHEMA = vol.Schema(
+ {
+ vol.Required(ATTR_NAME): str,
+ vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]),
+ vol.Optional(ATTR_ARGS): dict,
+ }
+)
+SERVICE_CAST_SKILL_SCHEMA = vol.Schema(
+ {
+ vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
+ vol.Required(ATTR_SKILL): cv.string,
+ vol.Optional(ATTR_TASK): cv.string,
+ }
+)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Habitica service."""
- async_setup_services(hass)
+ async def cast_skill(call: ServiceCall) -> ServiceResponse:
+ """Skill action."""
+ entry: HabiticaConfigEntry | None
+ if not (
+ entry := hass.config_entries.async_get_entry(call.data[ATTR_CONFIG_ENTRY])
+ ):
+ raise ServiceValidationError(
+ translation_domain=DOMAIN,
+ translation_key="entry_not_found",
+ )
+ coordinator = entry.runtime_data
+ skill = {
+ "pickpocket": {"spellId": "pickPocket", "cost": "10 MP"},
+ "backstab": {"spellId": "backStab", "cost": "15 MP"},
+ "smash": {"spellId": "smash", "cost": "10 MP"},
+ "fireball": {"spellId": "fireball", "cost": "10 MP"},
+ }
+ try:
+ task_id = next(
+ task["id"]
+ for task in coordinator.data.tasks
+ if call.data[ATTR_TASK] in (task["id"], task.get("alias"))
+ or call.data[ATTR_TASK] == task["text"]
+ )
+ except StopIteration as e:
+ raise ServiceValidationError(
+ translation_domain=DOMAIN,
+ translation_key="task_not_found",
+ translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
+ ) from e
+
+ try:
+ response: dict[str, Any] = await coordinator.api.user.class_.cast[
+ skill[call.data[ATTR_SKILL]]["spellId"]
+ ].post(targetId=task_id)
+ except ClientResponseError as e:
+ if e.status == HTTPStatus.TOO_MANY_REQUESTS:
+ raise ServiceValidationError(
+ translation_domain=DOMAIN,
+ translation_key="setup_rate_limit_exception",
+ ) from e
+ if e.status == HTTPStatus.UNAUTHORIZED:
+ raise ServiceValidationError(
+ translation_domain=DOMAIN,
+ translation_key="not_enough_mana",
+ translation_placeholders={
+ "cost": skill[call.data[ATTR_SKILL]]["cost"],
+ "mana": f"{int(coordinator.data.user.get("stats", {}).get("mp", 0))} MP",
+ },
+ ) from e
+ if e.status == HTTPStatus.NOT_FOUND:
+ # could also be task not found, but the task is looked up
+ # before the request, so most likely wrong skill selected
+ # or the skill hasn't been unlocked yet.
+ raise ServiceValidationError(
+ translation_domain=DOMAIN,
+ translation_key="skill_not_found",
+ translation_placeholders={"skill": call.data[ATTR_SKILL]},
+ ) from e
+ raise HomeAssistantError(
+ translation_domain=DOMAIN,
+ translation_key="service_call_exception",
+ ) from e
+ else:
+ await coordinator.async_request_refresh()
+ return response
+
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_CAST_SKILL,
+ cast_skill,
+ schema=SERVICE_CAST_SKILL_SCHEMA,
+ supports_response=SupportsResponse.ONLY,
+ )
return True
@@ -57,12 +164,32 @@ async def async_setup_entry(
def __call__(self, **kwargs):
return super().__call__(websession, **kwargs)
- def _make_headers(self) -> dict[str, str]:
- headers = super()._make_headers()
- headers.update(
- {"x-client": f"{DEVELOPER_ID} - {APPLICATION_NAME} {__version__}"}
+ async def handle_api_call(call: ServiceCall) -> None:
+ name = call.data[ATTR_NAME]
+ path = call.data[ATTR_PATH]
+ entries = hass.config_entries.async_entries(DOMAIN)
+
+ api = None
+ for entry in entries:
+ if entry.data[CONF_NAME] == name:
+ api = entry.runtime_data.api
+ break
+ if api is None:
+ _LOGGER.error("API_CALL: User '%s' not configured", name)
+ return
+ try:
+ for element in path:
+ api = api[element]
+ except KeyError:
+ _LOGGER.error(
+ "API_CALL: Path %s is invalid for API on '{%s}' element", path, element
)
- return headers
+ return
+ kwargs = call.data.get(ATTR_ARGS, {})
+ data = await api(**kwargs)
+ hass.bus.async_fire(
+ EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data}
+ )
websession = async_get_clientsession(
hass, verify_ssl=config_entry.data.get(CONF_VERIFY_SSL, True)
@@ -99,9 +226,16 @@ async def async_setup_entry(
config_entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
+ if not hass.services.has_service(DOMAIN, SERVICE_API_CALL):
+ hass.services.async_register(
+ DOMAIN, SERVICE_API_CALL, handle_api_call, schema=SERVICE_API_CALL_SCHEMA
+ )
+
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
+ if len(hass.config_entries.async_entries(DOMAIN)) == 1:
+ hass.services.async_remove(DOMAIN, SERVICE_API_CALL)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/habitica/binary_sensor.py b/homeassistant/components/habitica/binary_sensor.py
deleted file mode 100644
index bc79370ea63..00000000000
--- a/homeassistant/components/habitica/binary_sensor.py
+++ /dev/null
@@ -1,85 +0,0 @@
-"""Binary sensor platform for Habitica integration."""
-
-from __future__ import annotations
-
-from collections.abc import Callable
-from dataclasses import dataclass
-from enum import StrEnum
-from typing import Any
-
-from homeassistant.components.binary_sensor import (
- BinarySensorEntity,
- BinarySensorEntityDescription,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from .const import ASSETS_URL
-from .entity import HabiticaBase
-from .types import HabiticaConfigEntry
-
-
-@dataclass(kw_only=True, frozen=True)
-class HabiticaBinarySensorEntityDescription(BinarySensorEntityDescription):
- """Habitica Binary Sensor Description."""
-
- value_fn: Callable[[dict[str, Any]], bool | None]
- entity_picture: Callable[[dict[str, Any]], str | None]
-
-
-class HabiticaBinarySensor(StrEnum):
- """Habitica Entities."""
-
- PENDING_QUEST = "pending_quest"
-
-
-def get_scroll_image_for_pending_quest_invitation(user: dict[str, Any]) -> str | None:
- """Entity picture for pending quest invitation."""
- if user["party"]["quest"].get("key") and user["party"]["quest"]["RSVPNeeded"]:
- return f"inventory_quest_scroll_{user["party"]["quest"]["key"]}.png"
- return None
-
-
-BINARY_SENSOR_DESCRIPTIONS: tuple[HabiticaBinarySensorEntityDescription, ...] = (
- HabiticaBinarySensorEntityDescription(
- key=HabiticaBinarySensor.PENDING_QUEST,
- translation_key=HabiticaBinarySensor.PENDING_QUEST,
- value_fn=lambda user: user["party"]["quest"]["RSVPNeeded"],
- entity_picture=get_scroll_image_for_pending_quest_invitation,
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: HabiticaConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the habitica binary sensors."""
-
- coordinator = config_entry.runtime_data
-
- async_add_entities(
- HabiticaBinarySensorEntity(coordinator, description)
- for description in BINARY_SENSOR_DESCRIPTIONS
- )
-
-
-class HabiticaBinarySensorEntity(HabiticaBase, BinarySensorEntity):
- """Representation of a Habitica binary sensor."""
-
- entity_description: HabiticaBinarySensorEntityDescription
-
- @property
- def is_on(self) -> bool | None:
- """If the binary sensor is on."""
- return self.entity_description.value_fn(self.coordinator.data.user)
-
- @property
- def entity_picture(self) -> str | None:
- """Return the entity picture to use in the frontend, if any."""
- if entity_picture := self.entity_description.entity_picture(
- self.coordinator.data.user
- ):
- return f"{ASSETS_URL}{entity_picture}"
- return None
diff --git a/homeassistant/components/habitica/button.py b/homeassistant/components/habitica/button.py
index 8b41fb8c987..276aa4e7fc0 100644
--- a/homeassistant/components/habitica/button.py
+++ b/homeassistant/components/habitica/button.py
@@ -10,20 +10,15 @@ from typing import Any
from aiohttp import ClientResponseError
-from homeassistant.components.button import (
- DOMAIN as BUTTON_DOMAIN,
- ButtonEntity,
- ButtonEntityDescription,
-)
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
-from homeassistant.helpers import entity_registry as er
+from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from .const import ASSETS_URL, DOMAIN, HEALER, MAGE, ROGUE, WARRIOR
+from . import HabiticaConfigEntry
+from .const import DOMAIN
from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator
from .entity import HabiticaBase
-from .types import HabiticaConfigEntry
@dataclass(kw_only=True, frozen=True)
@@ -32,8 +27,6 @@ class HabiticaButtonEntityDescription(ButtonEntityDescription):
press_fn: Callable[[HabiticaDataUpdateCoordinator], Any]
available_fn: Callable[[HabiticaData], bool] | None = None
- class_needed: str | None = None
- entity_picture: str | None = None
class HabitipyButtonEntity(StrEnum):
@@ -43,18 +36,6 @@ class HabitipyButtonEntity(StrEnum):
BUY_HEALTH_POTION = "buy_health_potion"
ALLOCATE_ALL_STAT_POINTS = "allocate_all_stat_points"
REVIVE = "revive"
- MPHEAL = "mpheal"
- EARTH = "earth"
- FROST = "frost"
- DEFENSIVE_STANCE = "defensive_stance"
- VALOROUS_PRESENCE = "valorous_presence"
- INTIMIDATE = "intimidate"
- TOOLS_OF_TRADE = "tools_of_trade"
- STEALTH = "stealth"
- HEAL = "heal"
- PROTECT_AURA = "protect_aura"
- BRIGHTNESS = "brightness"
- HEAL_ALL = "heal_all"
BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = (
@@ -74,7 +55,6 @@ BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = (
lambda data: data.user["stats"]["gp"] >= 25
and data.user["stats"]["hp"] < 50
),
- entity_picture="shop_potion.png",
),
HabiticaButtonEntityDescription(
key=HabitipyButtonEntity.ALLOCATE_ALL_STAT_POINTS,
@@ -94,175 +74,6 @@ BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = (
)
-CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
- HabiticaButtonEntityDescription(
- key=HabitipyButtonEntity.MPHEAL,
- translation_key=HabitipyButtonEntity.MPHEAL,
- press_fn=lambda coordinator: coordinator.api.user.class_.cast["mpheal"].post(),
- available_fn=(
- lambda data: data.user["stats"]["lvl"] >= 12
- and data.user["stats"]["mp"] >= 30
- ),
- class_needed=MAGE,
- entity_picture="shop_mpheal.png",
- ),
- HabiticaButtonEntityDescription(
- key=HabitipyButtonEntity.EARTH,
- translation_key=HabitipyButtonEntity.EARTH,
- press_fn=lambda coordinator: coordinator.api.user.class_.cast["earth"].post(),
- available_fn=(
- lambda data: data.user["stats"]["lvl"] >= 13
- and data.user["stats"]["mp"] >= 35
- ),
- class_needed=MAGE,
- entity_picture="shop_earth.png",
- ),
- HabiticaButtonEntityDescription(
- key=HabitipyButtonEntity.FROST,
- translation_key=HabitipyButtonEntity.FROST,
- press_fn=lambda coordinator: coordinator.api.user.class_.cast["frost"].post(),
- # chilling frost can only be cast once per day (streaks buff is false)
- available_fn=(
- lambda data: data.user["stats"]["lvl"] >= 14
- and data.user["stats"]["mp"] >= 40
- and not data.user["stats"]["buffs"]["streaks"]
- ),
- class_needed=MAGE,
- entity_picture="shop_frost.png",
- ),
- HabiticaButtonEntityDescription(
- key=HabitipyButtonEntity.DEFENSIVE_STANCE,
- translation_key=HabitipyButtonEntity.DEFENSIVE_STANCE,
- press_fn=(
- lambda coordinator: coordinator.api.user.class_.cast[
- "defensiveStance"
- ].post()
- ),
- available_fn=(
- lambda data: data.user["stats"]["lvl"] >= 12
- and data.user["stats"]["mp"] >= 25
- ),
- class_needed=WARRIOR,
- entity_picture="shop_defensiveStance.png",
- ),
- HabiticaButtonEntityDescription(
- key=HabitipyButtonEntity.VALOROUS_PRESENCE,
- translation_key=HabitipyButtonEntity.VALOROUS_PRESENCE,
- press_fn=(
- lambda coordinator: coordinator.api.user.class_.cast[
- "valorousPresence"
- ].post()
- ),
- available_fn=(
- lambda data: data.user["stats"]["lvl"] >= 13
- and data.user["stats"]["mp"] >= 20
- ),
- class_needed=WARRIOR,
- entity_picture="shop_valorousPresence.png",
- ),
- HabiticaButtonEntityDescription(
- key=HabitipyButtonEntity.INTIMIDATE,
- translation_key=HabitipyButtonEntity.INTIMIDATE,
- press_fn=(
- lambda coordinator: coordinator.api.user.class_.cast["intimidate"].post()
- ),
- available_fn=(
- lambda data: data.user["stats"]["lvl"] >= 14
- and data.user["stats"]["mp"] >= 15
- ),
- class_needed=WARRIOR,
- entity_picture="shop_intimidate.png",
- ),
- HabiticaButtonEntityDescription(
- key=HabitipyButtonEntity.TOOLS_OF_TRADE,
- translation_key=HabitipyButtonEntity.TOOLS_OF_TRADE,
- press_fn=(
- lambda coordinator: coordinator.api.user.class_.cast["toolsOfTrade"].post()
- ),
- available_fn=(
- lambda data: data.user["stats"]["lvl"] >= 13
- and data.user["stats"]["mp"] >= 25
- ),
- class_needed=ROGUE,
- entity_picture="shop_toolsOfTrade.png",
- ),
- HabiticaButtonEntityDescription(
- key=HabitipyButtonEntity.STEALTH,
- translation_key=HabitipyButtonEntity.STEALTH,
- press_fn=(
- lambda coordinator: coordinator.api.user.class_.cast["stealth"].post()
- ),
- # Stealth buffs stack and it can only be cast if the amount of
- # unfinished dailies is smaller than the amount of buffs
- available_fn=(
- lambda data: data.user["stats"]["lvl"] >= 14
- and data.user["stats"]["mp"] >= 45
- and data.user["stats"]["buffs"]["stealth"]
- < len(
- [
- r
- for r in data.tasks
- if r.get("type") == "daily"
- and r.get("isDue") is True
- and r.get("completed") is False
- ]
- )
- ),
- class_needed=ROGUE,
- entity_picture="shop_stealth.png",
- ),
- HabiticaButtonEntityDescription(
- key=HabitipyButtonEntity.HEAL,
- translation_key=HabitipyButtonEntity.HEAL,
- press_fn=lambda coordinator: coordinator.api.user.class_.cast["heal"].post(),
- available_fn=(
- lambda data: data.user["stats"]["lvl"] >= 11
- and data.user["stats"]["mp"] >= 15
- and data.user["stats"]["hp"] < 50
- ),
- class_needed=HEALER,
- entity_picture="shop_heal.png",
- ),
- HabiticaButtonEntityDescription(
- key=HabitipyButtonEntity.BRIGHTNESS,
- translation_key=HabitipyButtonEntity.BRIGHTNESS,
- press_fn=(
- lambda coordinator: coordinator.api.user.class_.cast["brightness"].post()
- ),
- available_fn=(
- lambda data: data.user["stats"]["lvl"] >= 12
- and data.user["stats"]["mp"] >= 15
- ),
- class_needed=HEALER,
- entity_picture="shop_brightness.png",
- ),
- HabiticaButtonEntityDescription(
- key=HabitipyButtonEntity.PROTECT_AURA,
- translation_key=HabitipyButtonEntity.PROTECT_AURA,
- press_fn=(
- lambda coordinator: coordinator.api.user.class_.cast["protectAura"].post()
- ),
- available_fn=(
- lambda data: data.user["stats"]["lvl"] >= 13
- and data.user["stats"]["mp"] >= 30
- ),
- class_needed=HEALER,
- entity_picture="shop_protectAura.png",
- ),
- HabiticaButtonEntityDescription(
- key=HabitipyButtonEntity.HEAL_ALL,
- translation_key=HabitipyButtonEntity.HEAL_ALL,
- press_fn=lambda coordinator: coordinator.api.user.class_.cast["healAll"].post(),
- available_fn=(
- lambda data: data.user["stats"]["lvl"] >= 14
- and data.user["stats"]["mp"] >= 25
- ),
- class_needed=HEALER,
- entity_picture="shop_healAll.png",
- ),
-)
-
-
async def async_setup_entry(
hass: HomeAssistant,
entry: HabiticaConfigEntry,
@@ -271,40 +82,6 @@ async def async_setup_entry(
"""Set up buttons from a config entry."""
coordinator = entry.runtime_data
- skills_added: set[str] = set()
-
- @callback
- def add_entities() -> None:
- """Add or remove a skillset based on the player's class."""
-
- nonlocal skills_added
- buttons = []
- entity_registry = er.async_get(hass)
-
- for description in CLASS_SKILLS:
- if (
- coordinator.data.user["stats"]["lvl"] >= 10
- and coordinator.data.user["flags"]["classSelected"]
- and not coordinator.data.user["preferences"]["disableClasses"]
- and description.class_needed == coordinator.data.user["stats"]["class"]
- ):
- if description.key not in skills_added:
- buttons.append(HabiticaButton(coordinator, description))
- skills_added.add(description.key)
- elif description.key in skills_added:
- if entity_id := entity_registry.async_get_entity_id(
- BUTTON_DOMAIN,
- DOMAIN,
- f"{coordinator.config_entry.unique_id}_{description.key}",
- ):
- entity_registry.async_remove(entity_id)
- skills_added.remove(description.key)
-
- if buttons:
- async_add_entities(buttons)
-
- coordinator.async_add_listener(add_entities)
- add_entities()
async_add_entities(
HabiticaButton(coordinator, description) for description in BUTTON_DESCRIPTIONS
@@ -331,7 +108,7 @@ class HabiticaButton(HabiticaBase, ButtonEntity):
translation_domain=DOMAIN,
translation_key="service_call_unallowed",
) from e
- raise HomeAssistantError(
+ raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="service_call_exception",
) from e
@@ -346,10 +123,3 @@ class HabiticaButton(HabiticaBase, ButtonEntity):
if self.entity_description.available_fn:
return self.entity_description.available_fn(self.coordinator.data)
return True
-
- @property
- def entity_picture(self) -> str | None:
- """Return the entity picture to use in the frontend, if any."""
- if entity_picture := self.entity_description.entity_picture:
- return f"{ASSETS_URL}{entity_picture}"
- return None
diff --git a/homeassistant/components/habitica/calendar.py b/homeassistant/components/habitica/calendar.py
deleted file mode 100644
index 5a0470c3440..00000000000
--- a/homeassistant/components/habitica/calendar.py
+++ /dev/null
@@ -1,227 +0,0 @@
-"""Calendar platform for Habitica integration."""
-
-from __future__ import annotations
-
-from datetime import date, datetime, timedelta
-from enum import StrEnum
-
-from dateutil.rrule import rrule
-
-from homeassistant.components.calendar import (
- CalendarEntity,
- CalendarEntityDescription,
- CalendarEvent,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.util import dt as dt_util
-
-from . import HabiticaConfigEntry
-from .coordinator import HabiticaDataUpdateCoordinator
-from .entity import HabiticaBase
-from .types import HabiticaTaskType
-from .util import build_rrule, get_recurrence_rule
-
-
-class HabiticaCalendar(StrEnum):
- """Habitica calendars."""
-
- DAILIES = "dailys"
- TODOS = "todos"
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: HabiticaConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the calendar platform."""
- coordinator = config_entry.runtime_data
-
- async_add_entities(
- [
- HabiticaTodosCalendarEntity(coordinator),
- HabiticaDailiesCalendarEntity(coordinator),
- ]
- )
-
-
-class HabiticaCalendarEntity(HabiticaBase, CalendarEntity):
- """Base Habitica calendar entity."""
-
- def __init__(
- self,
- coordinator: HabiticaDataUpdateCoordinator,
- ) -> None:
- """Initialize calendar entity."""
- super().__init__(coordinator, self.entity_description)
-
-
-class HabiticaTodosCalendarEntity(HabiticaCalendarEntity):
- """Habitica todos calendar entity."""
-
- entity_description = CalendarEntityDescription(
- key=HabiticaCalendar.TODOS,
- translation_key=HabiticaCalendar.TODOS,
- )
-
- def dated_todos(
- self, start_date: datetime, end_date: datetime | None = None
- ) -> list[CalendarEvent]:
- """Get all dated todos."""
-
- events = []
- for task in self.coordinator.data.tasks:
- if not (
- task["type"] == HabiticaTaskType.TODO
- and not task["completed"]
- and task.get("date") # only if has due date
- ):
- continue
-
- start = dt_util.start_of_local_day(datetime.fromisoformat(task["date"]))
- end = start + timedelta(days=1)
- # return current and upcoming events or events within the requested range
-
- if end < start_date:
- # Event ends before date range
- continue
-
- if end_date and start > end_date:
- # Event starts after date range
- continue
-
- events.append(
- CalendarEvent(
- start=start.date(),
- end=end.date(),
- summary=task["text"],
- description=task["notes"],
- uid=task["id"],
- )
- )
- return sorted(
- events,
- key=lambda event: (
- event.start,
- self.coordinator.data.user["tasksOrder"]["todos"].index(event.uid),
- ),
- )
-
- @property
- def event(self) -> CalendarEvent | None:
- """Return the current or next upcoming event."""
-
- return next(iter(self.dated_todos(dt_util.now())), None)
-
- async def async_get_events(
- self, hass: HomeAssistant, start_date: datetime, end_date: datetime
- ) -> list[CalendarEvent]:
- """Return calendar events within a datetime range."""
- return self.dated_todos(start_date, end_date)
-
-
-class HabiticaDailiesCalendarEntity(HabiticaCalendarEntity):
- """Habitica dailies calendar entity."""
-
- entity_description = CalendarEntityDescription(
- key=HabiticaCalendar.DAILIES,
- translation_key=HabiticaCalendar.DAILIES,
- )
-
- @property
- def today(self) -> datetime:
- """Habitica daystart."""
- return dt_util.start_of_local_day(
- datetime.fromisoformat(self.coordinator.data.user["lastCron"])
- )
-
- def end_date(self, recurrence: datetime, end: datetime | None = None) -> date:
- """Calculate the end date for a yesterdaily.
-
- The enddates of events from yesterday move forward to the end
- of the current day (until the cron resets the dailies) to show them
- as still active events on the calendar state entity (state: on).
-
- Events in the calendar view will show all-day events on their due day
- """
- if end:
- return recurrence.date() + timedelta(days=1)
- return (
- dt_util.start_of_local_day() if recurrence == self.today else recurrence
- ).date() + timedelta(days=1)
-
- def get_recurrence_dates(
- self, recurrences: rrule, start_date: datetime, end_date: datetime | None = None
- ) -> list[datetime]:
- """Calculate recurrence dates based on start_date and end_date."""
- if end_date:
- return recurrences.between(
- start_date, end_date - timedelta(days=1), inc=True
- )
- # if no end_date is given, return only the next recurrence
- return [recurrences.after(self.today, inc=True)]
-
- def due_dailies(
- self, start_date: datetime, end_date: datetime | None = None
- ) -> list[CalendarEvent]:
- """Get dailies and recurrences for a given period or the next upcoming."""
-
- # we only have dailies for today and future recurrences
- if end_date and end_date < self.today:
- return []
- start_date = max(start_date, self.today)
-
- events = []
- for task in self.coordinator.data.tasks:
- # only dailies that that are not 'grey dailies'
- if not (task["type"] == HabiticaTaskType.DAILY and task["everyX"]):
- continue
-
- recurrences = build_rrule(task)
- recurrence_dates = self.get_recurrence_dates(
- recurrences, start_date, end_date
- )
- for recurrence in recurrence_dates:
- is_future_event = recurrence > self.today
- is_current_event = recurrence <= self.today and not task["completed"]
-
- if not (is_future_event or is_current_event):
- continue
-
- events.append(
- CalendarEvent(
- start=recurrence.date(),
- end=self.end_date(recurrence, end_date),
- summary=task["text"],
- description=task["notes"],
- uid=task["id"],
- rrule=get_recurrence_rule(recurrences),
- )
- )
- return sorted(
- events,
- key=lambda event: (
- event.start,
- self.coordinator.data.user["tasksOrder"]["dailys"].index(event.uid),
- ),
- )
-
- @property
- def event(self) -> CalendarEvent | None:
- """Return the next upcoming event."""
- return next(iter(self.due_dailies(self.today)), None)
-
- async def async_get_events(
- self, hass: HomeAssistant, start_date: datetime, end_date: datetime
- ) -> list[CalendarEvent]:
- """Return calendar events within a datetime range."""
-
- return self.due_dailies(start_date, end_date)
-
- @property
- def extra_state_attributes(self) -> dict[str, bool | None] | None:
- """Return entity specific state attributes."""
- return {
- "yesterdaily": self.event.start < self.today.date() if self.event else None
- }
diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py
index ae98cb13dcb..f089be1b736 100644
--- a/homeassistant/components/habitica/const.py
+++ b/homeassistant/components/habitica/const.py
@@ -25,20 +25,4 @@ UNIT_TASKS = "tasks"
ATTR_CONFIG_ENTRY = "config_entry"
ATTR_SKILL = "skill"
ATTR_TASK = "task"
-ATTR_DIRECTION = "direction"
SERVICE_CAST_SKILL = "cast_skill"
-SERVICE_START_QUEST = "start_quest"
-SERVICE_ACCEPT_QUEST = "accept_quest"
-SERVICE_CANCEL_QUEST = "cancel_quest"
-SERVICE_ABORT_QUEST = "abort_quest"
-SERVICE_REJECT_QUEST = "reject_quest"
-SERVICE_LEAVE_QUEST = "leave_quest"
-SERVICE_SCORE_HABIT = "score_habit"
-SERVICE_SCORE_REWARD = "score_reward"
-
-WARRIOR = "warrior"
-ROGUE = "rogue"
-HEALER = "healer"
-MAGE = "wizard"
-
-DEVELOPER_ID = "4c4ca53f-c059-4ffa-966e-9d29dd405daf"
diff --git a/homeassistant/components/habitica/coordinator.py b/homeassistant/components/habitica/coordinator.py
index f9ffb1b53bd..4e949b703fb 100644
--- a/homeassistant/components/habitica/coordinator.py
+++ b/homeassistant/components/habitica/coordinator.py
@@ -51,22 +51,17 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
),
)
self.api = habitipy
- self.content: dict[str, Any] = {}
async def _async_update_data(self) -> HabiticaData:
try:
user_response = await self.api.user.get()
tasks_response = await self.api.tasks.user.get()
tasks_response.extend(await self.api.tasks.user.get(type="completedTodos"))
- if not self.content:
- self.content = await self.api.content.get(
- language=user_response["preferences"]["language"]
- )
except ClientResponseError as error:
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
- _LOGGER.debug("Rate limit exceeded, will try again later")
+ _LOGGER.debug("Currently rate limited, skipping update")
return self.data
- raise UpdateFailed(f"Unable to connect to Habitica: {error}") from error
+ raise UpdateFailed(f"Error communicating with API: {error}") from error
return HabiticaData(user=user_response, tasks=tasks_response)
diff --git a/homeassistant/components/habitica/icons.json b/homeassistant/components/habitica/icons.json
index d33b9c60c96..544c28e4b9d 100644
--- a/homeassistant/components/habitica/icons.json
+++ b/homeassistant/components/habitica/icons.json
@@ -20,50 +20,6 @@
},
"revive": {
"default": "mdi:grave-stone"
- },
- "mpheal": {
- "default": "mdi:broadcast"
- },
- "earth": {
- "default": "mdi:landslide"
- },
- "frost": {
- "default": "mdi:snowflake"
- },
- "defensive_stance": {
- "default": "mdi:shield-sword"
- },
- "valorous_presence": {
- "default": "mdi:shield-sun"
- },
- "intimidate": {
- "default": "mdi:emoticon-angry"
- },
- "tools_of_trade": {
- "default": "mdi:domino-mask"
- },
- "stealth": {
- "default": "mdi:ninja"
- },
- "heal": {
- "default": "mdi:aurora"
- },
- "brightness": {
- "default": "mdi:flare"
- },
- "protect_aura": {
- "default": "mdi:shimmer"
- },
- "heal_all": {
- "default": "mdi:hand-heart-outline"
- }
- },
- "calendar": {
- "todos": {
- "default": "mdi:calendar-check"
- },
- "dailys": {
- "default": "mdi:calendar-multiple"
}
},
"sensor": {
@@ -126,18 +82,6 @@
},
"rewards": {
"default": "mdi:treasure-chest"
- },
- "strength": {
- "default": "mdi:arm-flex-outline"
- },
- "intelligence": {
- "default": "mdi:head-snowflake-outline"
- },
- "perception": {
- "default": "mdi:eye-outline"
- },
- "constitution": {
- "default": "mdi:run-fast"
}
},
"switch": {
@@ -147,14 +91,6 @@
"on": "mdi:sleep"
}
}
- },
- "binary_sensor": {
- "pending_quest": {
- "default": "mdi:script-outline",
- "state": {
- "on": "mdi:script-text-outline"
- }
- }
}
},
"services": {
@@ -163,30 +99,6 @@
},
"cast_skill": {
"service": "mdi:creation-outline"
- },
- "accept_quest": {
- "service": "mdi:script-text"
- },
- "reject_quest": {
- "service": "mdi:script-text"
- },
- "leave_quest": {
- "service": "mdi:script-text"
- },
- "abort_quest": {
- "service": "mdi:script-text-key"
- },
- "cancel_quest": {
- "service": "mdi:script-text-key"
- },
- "start_quest": {
- "service": "mdi:script-text-key"
- },
- "score_habit": {
- "service": "mdi:counter"
- },
- "score_reward": {
- "service": "mdi:sack"
}
}
}
diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json
index 8e3396d32cf..16a4ef959a8 100644
--- a/homeassistant/components/habitica/manifest.json
+++ b/homeassistant/components/habitica/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/habitica",
"iot_class": "cloud_polling",
"loggers": ["habitipy", "plumbum"],
- "requirements": ["habitipy==0.3.3"]
+ "requirements": ["habitipy==0.3.1"]
}
diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py
index 3b2395ecc52..ccf1e998049 100644
--- a/homeassistant/components/habitica/sensor.py
+++ b/homeassistant/components/habitica/sensor.py
@@ -24,10 +24,10 @@ from homeassistant.helpers.issue_registry import (
)
from homeassistant.helpers.typing import StateType
+from . import HabiticaConfigEntry
from .const import DOMAIN, UNIT_TASKS
from .entity import HabiticaBase
-from .types import HabiticaConfigEntry
-from .util import entity_used_in, get_attribute_points, get_attributes_total
+from .util import entity_used_in
_LOGGER = logging.getLogger(__name__)
@@ -36,10 +36,7 @@ _LOGGER = logging.getLogger(__name__)
class HabitipySensorEntityDescription(SensorEntityDescription):
"""Habitipy Sensor Description."""
- value_fn: Callable[[dict[str, Any], dict[str, Any]], StateType]
- attributes_fn: (
- Callable[[dict[str, Any], dict[str, Any]], dict[str, Any] | None] | None
- ) = None
+ value_fn: Callable[[dict[str, Any]], StateType]
@dataclass(kw_only=True, frozen=True)
@@ -68,80 +65,76 @@ class HabitipySensorEntity(StrEnum):
REWARDS = "rewards"
GEMS = "gems"
TRINKETS = "trinkets"
- STRENGTH = "strength"
- INTELLIGENCE = "intelligence"
- CONSTITUTION = "constitution"
- PERCEPTION = "perception"
SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = (
HabitipySensorEntityDescription(
key=HabitipySensorEntity.DISPLAY_NAME,
translation_key=HabitipySensorEntity.DISPLAY_NAME,
- value_fn=lambda user, _: user.get("profile", {}).get("name"),
+ value_fn=lambda user: user.get("profile", {}).get("name"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.HEALTH,
translation_key=HabitipySensorEntity.HEALTH,
native_unit_of_measurement="HP",
suggested_display_precision=0,
- value_fn=lambda user, _: user.get("stats", {}).get("hp"),
+ value_fn=lambda user: user.get("stats", {}).get("hp"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.HEALTH_MAX,
translation_key=HabitipySensorEntity.HEALTH_MAX,
native_unit_of_measurement="HP",
entity_registry_enabled_default=False,
- value_fn=lambda user, _: user.get("stats", {}).get("maxHealth"),
+ value_fn=lambda user: user.get("stats", {}).get("maxHealth"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.MANA,
translation_key=HabitipySensorEntity.MANA,
native_unit_of_measurement="MP",
suggested_display_precision=0,
- value_fn=lambda user, _: user.get("stats", {}).get("mp"),
+ value_fn=lambda user: user.get("stats", {}).get("mp"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.MANA_MAX,
translation_key=HabitipySensorEntity.MANA_MAX,
native_unit_of_measurement="MP",
- value_fn=lambda user, _: user.get("stats", {}).get("maxMP"),
+ value_fn=lambda user: user.get("stats", {}).get("maxMP"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.EXPERIENCE,
translation_key=HabitipySensorEntity.EXPERIENCE,
native_unit_of_measurement="XP",
- value_fn=lambda user, _: user.get("stats", {}).get("exp"),
+ value_fn=lambda user: user.get("stats", {}).get("exp"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.EXPERIENCE_MAX,
translation_key=HabitipySensorEntity.EXPERIENCE_MAX,
native_unit_of_measurement="XP",
- value_fn=lambda user, _: user.get("stats", {}).get("toNextLevel"),
+ value_fn=lambda user: user.get("stats", {}).get("toNextLevel"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.LEVEL,
translation_key=HabitipySensorEntity.LEVEL,
- value_fn=lambda user, _: user.get("stats", {}).get("lvl"),
+ value_fn=lambda user: user.get("stats", {}).get("lvl"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.GOLD,
translation_key=HabitipySensorEntity.GOLD,
native_unit_of_measurement="GP",
suggested_display_precision=2,
- value_fn=lambda user, _: user.get("stats", {}).get("gp"),
+ value_fn=lambda user: user.get("stats", {}).get("gp"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.CLASS,
translation_key=HabitipySensorEntity.CLASS,
- value_fn=lambda user, _: user.get("stats", {}).get("class"),
+ value_fn=lambda user: user.get("stats", {}).get("class"),
device_class=SensorDeviceClass.ENUM,
options=["warrior", "healer", "wizard", "rogue"],
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.GEMS,
translation_key=HabitipySensorEntity.GEMS,
- value_fn=lambda user, _: user.get("balance", 0) * 4,
+ value_fn=lambda user: user.get("balance", 0) * 4,
suggested_display_precision=0,
native_unit_of_measurement="gems",
),
@@ -149,7 +142,7 @@ SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = (
key=HabitipySensorEntity.TRINKETS,
translation_key=HabitipySensorEntity.TRINKETS,
value_fn=(
- lambda user, _: user.get("purchased", {})
+ lambda user: user.get("purchased", {})
.get("plan", {})
.get("consecutive", {})
.get("trinkets", 0)
@@ -157,38 +150,6 @@ SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = (
suggested_display_precision=0,
native_unit_of_measurement="⧖",
),
- HabitipySensorEntityDescription(
- key=HabitipySensorEntity.STRENGTH,
- translation_key=HabitipySensorEntity.STRENGTH,
- value_fn=lambda user, content: get_attributes_total(user, content, "str"),
- attributes_fn=lambda user, content: get_attribute_points(user, content, "str"),
- suggested_display_precision=0,
- native_unit_of_measurement="STR",
- ),
- HabitipySensorEntityDescription(
- key=HabitipySensorEntity.INTELLIGENCE,
- translation_key=HabitipySensorEntity.INTELLIGENCE,
- value_fn=lambda user, content: get_attributes_total(user, content, "int"),
- attributes_fn=lambda user, content: get_attribute_points(user, content, "int"),
- suggested_display_precision=0,
- native_unit_of_measurement="INT",
- ),
- HabitipySensorEntityDescription(
- key=HabitipySensorEntity.PERCEPTION,
- translation_key=HabitipySensorEntity.PERCEPTION,
- value_fn=lambda user, content: get_attributes_total(user, content, "per"),
- attributes_fn=lambda user, content: get_attribute_points(user, content, "per"),
- suggested_display_precision=0,
- native_unit_of_measurement="PER",
- ),
- HabitipySensorEntityDescription(
- key=HabitipySensorEntity.CONSTITUTION,
- translation_key=HabitipySensorEntity.CONSTITUTION,
- value_fn=lambda user, content: get_attributes_total(user, content, "con"),
- attributes_fn=lambda user, content: get_attribute_points(user, content, "con"),
- suggested_display_precision=0,
- native_unit_of_measurement="CON",
- ),
)
@@ -282,16 +243,7 @@ class HabitipySensor(HabiticaBase, SensorEntity):
def native_value(self) -> StateType:
"""Return the state of the device."""
- return self.entity_description.value_fn(
- self.coordinator.data.user, self.coordinator.content
- )
-
- @property
- def extra_state_attributes(self) -> dict[str, float | None] | None:
- """Return entity specific state attributes."""
- if func := self.entity_description.attributes_fn:
- return func(self.coordinator.data.user, self.coordinator.content)
- return None
+ return self.entity_description.value_fn(self.coordinator.data.user)
class HabitipyTaskSensor(HabiticaBase, SensorEntity):
diff --git a/homeassistant/components/habitica/services.py b/homeassistant/components/habitica/services.py
deleted file mode 100644
index a50e5f1e6e3..00000000000
--- a/homeassistant/components/habitica/services.py
+++ /dev/null
@@ -1,325 +0,0 @@
-"""Actions for the Habitica integration."""
-
-from __future__ import annotations
-
-from http import HTTPStatus
-import logging
-from typing import Any
-
-from aiohttp import ClientResponseError
-import voluptuous as vol
-
-from homeassistant.config_entries import ConfigEntryState
-from homeassistant.const import ATTR_NAME, CONF_NAME
-from homeassistant.core import (
- HomeAssistant,
- ServiceCall,
- ServiceResponse,
- SupportsResponse,
-)
-from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
-from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
-from homeassistant.helpers.selector import ConfigEntrySelector
-
-from .const import (
- ATTR_ARGS,
- ATTR_CONFIG_ENTRY,
- ATTR_DATA,
- ATTR_DIRECTION,
- ATTR_PATH,
- ATTR_SKILL,
- ATTR_TASK,
- DOMAIN,
- EVENT_API_CALL_SUCCESS,
- SERVICE_ABORT_QUEST,
- SERVICE_ACCEPT_QUEST,
- SERVICE_API_CALL,
- SERVICE_CANCEL_QUEST,
- SERVICE_CAST_SKILL,
- SERVICE_LEAVE_QUEST,
- SERVICE_REJECT_QUEST,
- SERVICE_SCORE_HABIT,
- SERVICE_SCORE_REWARD,
- SERVICE_START_QUEST,
-)
-from .types import HabiticaConfigEntry
-
-_LOGGER = logging.getLogger(__name__)
-
-
-SERVICE_API_CALL_SCHEMA = vol.Schema(
- {
- vol.Required(ATTR_NAME): str,
- vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]),
- vol.Optional(ATTR_ARGS): dict,
- }
-)
-
-SERVICE_CAST_SKILL_SCHEMA = vol.Schema(
- {
- vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
- vol.Required(ATTR_SKILL): cv.string,
- vol.Optional(ATTR_TASK): cv.string,
- }
-)
-
-SERVICE_MANAGE_QUEST_SCHEMA = vol.Schema(
- {
- vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
- }
-)
-SERVICE_SCORE_TASK_SCHEMA = vol.Schema(
- {
- vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
- vol.Required(ATTR_TASK): cv.string,
- vol.Optional(ATTR_DIRECTION): cv.string,
- }
-)
-
-
-def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
- """Return config entry or raise if not found or not loaded."""
- if not (entry := hass.config_entries.async_get_entry(entry_id)):
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="entry_not_found",
- )
- if entry.state is not ConfigEntryState.LOADED:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="entry_not_loaded",
- )
- return entry
-
-
-def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
- """Set up services for Habitica integration."""
-
- async def handle_api_call(call: ServiceCall) -> None:
- async_create_issue(
- hass,
- DOMAIN,
- "deprecated_api_call",
- breaks_in_ha_version="2025.6.0",
- is_fixable=False,
- severity=IssueSeverity.WARNING,
- translation_key="deprecated_api_call",
- )
- _LOGGER.warning(
- "Deprecated action called: 'habitica.api_call' is deprecated and will be removed in Home Assistant version 2025.6.0"
- )
-
- name = call.data[ATTR_NAME]
- path = call.data[ATTR_PATH]
- entries = hass.config_entries.async_entries(DOMAIN)
-
- api = None
- for entry in entries:
- if entry.data[CONF_NAME] == name:
- api = entry.runtime_data.api
- break
- if api is None:
- _LOGGER.error("API_CALL: User '%s' not configured", name)
- return
- try:
- for element in path:
- api = api[element]
- except KeyError:
- _LOGGER.error(
- "API_CALL: Path %s is invalid for API on '{%s}' element", path, element
- )
- return
- kwargs = call.data.get(ATTR_ARGS, {})
- data = await api(**kwargs)
- hass.bus.async_fire(
- EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data}
- )
-
- async def cast_skill(call: ServiceCall) -> ServiceResponse:
- """Skill action."""
- entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
- coordinator = entry.runtime_data
- skill = {
- "pickpocket": {"spellId": "pickPocket", "cost": "10 MP"},
- "backstab": {"spellId": "backStab", "cost": "15 MP"},
- "smash": {"spellId": "smash", "cost": "10 MP"},
- "fireball": {"spellId": "fireball", "cost": "10 MP"},
- }
- try:
- task_id = next(
- task["id"]
- for task in coordinator.data.tasks
- if call.data[ATTR_TASK] in (task["id"], task.get("alias"))
- or call.data[ATTR_TASK] == task["text"]
- )
- except StopIteration as e:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="task_not_found",
- translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
- ) from e
-
- try:
- response: dict[str, Any] = await coordinator.api.user.class_.cast[
- skill[call.data[ATTR_SKILL]]["spellId"]
- ].post(targetId=task_id)
- except ClientResponseError as e:
- if e.status == HTTPStatus.TOO_MANY_REQUESTS:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="setup_rate_limit_exception",
- ) from e
- if e.status == HTTPStatus.UNAUTHORIZED:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="not_enough_mana",
- translation_placeholders={
- "cost": skill[call.data[ATTR_SKILL]]["cost"],
- "mana": f"{int(coordinator.data.user.get("stats", {}).get("mp", 0))} MP",
- },
- ) from e
- if e.status == HTTPStatus.NOT_FOUND:
- # could also be task not found, but the task is looked up
- # before the request, so most likely wrong skill selected
- # or the skill hasn't been unlocked yet.
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="skill_not_found",
- translation_placeholders={"skill": call.data[ATTR_SKILL]},
- ) from e
- raise HomeAssistantError(
- translation_domain=DOMAIN,
- translation_key="service_call_exception",
- ) from e
- else:
- await coordinator.async_request_refresh()
- return response
-
- async def manage_quests(call: ServiceCall) -> ServiceResponse:
- """Accept, reject, start, leave or cancel quests."""
- entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
- coordinator = entry.runtime_data
-
- COMMAND_MAP = {
- SERVICE_ABORT_QUEST: "abort",
- SERVICE_ACCEPT_QUEST: "accept",
- SERVICE_CANCEL_QUEST: "cancel",
- SERVICE_LEAVE_QUEST: "leave",
- SERVICE_REJECT_QUEST: "reject",
- SERVICE_START_QUEST: "force-start",
- }
- try:
- return await coordinator.api.groups.party.quests[
- COMMAND_MAP[call.service]
- ].post()
- except ClientResponseError as e:
- if e.status == HTTPStatus.TOO_MANY_REQUESTS:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="setup_rate_limit_exception",
- ) from e
- if e.status == HTTPStatus.UNAUTHORIZED:
- raise ServiceValidationError(
- translation_domain=DOMAIN, translation_key="quest_action_unallowed"
- ) from e
- if e.status == HTTPStatus.NOT_FOUND:
- raise ServiceValidationError(
- translation_domain=DOMAIN, translation_key="quest_not_found"
- ) from e
- raise HomeAssistantError(
- translation_domain=DOMAIN, translation_key="service_call_exception"
- ) from e
-
- for service in (
- SERVICE_ABORT_QUEST,
- SERVICE_ACCEPT_QUEST,
- SERVICE_CANCEL_QUEST,
- SERVICE_LEAVE_QUEST,
- SERVICE_REJECT_QUEST,
- SERVICE_START_QUEST,
- ):
- hass.services.async_register(
- DOMAIN,
- service,
- manage_quests,
- schema=SERVICE_MANAGE_QUEST_SCHEMA,
- supports_response=SupportsResponse.ONLY,
- )
-
- async def score_task(call: ServiceCall) -> ServiceResponse:
- """Score a task action."""
- entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
- coordinator = entry.runtime_data
- try:
- task_id, task_value = next(
- (task["id"], task.get("value"))
- for task in coordinator.data.tasks
- if call.data[ATTR_TASK] in (task["id"], task.get("alias"))
- or call.data[ATTR_TASK] == task["text"]
- )
- except StopIteration as e:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="task_not_found",
- translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
- ) from e
-
- try:
- response: dict[str, Any] = (
- await coordinator.api.tasks[task_id]
- .score[call.data.get(ATTR_DIRECTION, "up")]
- .post()
- )
- except ClientResponseError as e:
- if e.status == HTTPStatus.TOO_MANY_REQUESTS:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="setup_rate_limit_exception",
- ) from e
- if e.status == HTTPStatus.UNAUTHORIZED and task_value is not None:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="not_enough_gold",
- translation_placeholders={
- "gold": f"{coordinator.data.user["stats"]["gp"]:.2f} GP",
- "cost": f"{task_value} GP",
- },
- ) from e
- raise HomeAssistantError(
- translation_domain=DOMAIN,
- translation_key="service_call_exception",
- ) from e
- else:
- await coordinator.async_request_refresh()
- return response
-
- hass.services.async_register(
- DOMAIN,
- SERVICE_API_CALL,
- handle_api_call,
- schema=SERVICE_API_CALL_SCHEMA,
- )
-
- hass.services.async_register(
- DOMAIN,
- SERVICE_CAST_SKILL,
- cast_skill,
- schema=SERVICE_CAST_SKILL_SCHEMA,
- supports_response=SupportsResponse.ONLY,
- )
-
- hass.services.async_register(
- DOMAIN,
- SERVICE_SCORE_HABIT,
- score_task,
- schema=SERVICE_SCORE_TASK_SCHEMA,
- supports_response=SupportsResponse.ONLY,
- )
- hass.services.async_register(
- DOMAIN,
- SERVICE_SCORE_REWARD,
- score_task,
- schema=SERVICE_SCORE_TASK_SCHEMA,
- supports_response=SupportsResponse.ONLY,
- )
diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml
index b539f6c65bf..546ac8c1c34 100644
--- a/homeassistant/components/habitica/services.yaml
+++ b/homeassistant/components/habitica/services.yaml
@@ -17,7 +17,7 @@ api_call:
object:
cast_skill:
fields:
- config_entry: &config_entry
+ config_entry:
required: true
selector:
config_entry:
@@ -33,42 +33,7 @@ cast_skill:
- "fireball"
mode: dropdown
translation_key: "skill_select"
- task: &task
+ task:
required: true
selector:
text:
-accept_quest:
- fields:
- config_entry: *config_entry
-reject_quest:
- fields:
- config_entry: *config_entry
-start_quest:
- fields:
- config_entry: *config_entry
-cancel_quest:
- fields:
- config_entry: *config_entry
-abort_quest:
- fields:
- config_entry: *config_entry
-leave_quest:
- fields:
- config_entry: *config_entry
-score_habit:
- fields:
- config_entry: *config_entry
- task: *task
- direction:
- required: true
- selector:
- select:
- options:
- - value: up
- label: "➕"
- - value: down
- label: "➖"
-score_reward:
- fields:
- config_entry: *config_entry
- task: *task
diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json
index ac1faf5fcef..824b3ab3457 100644
--- a/homeassistant/components/habitica/strings.json
+++ b/homeassistant/components/habitica/strings.json
@@ -1,9 +1,4 @@
{
- "common": {
- "todos": "To-Do's",
- "dailies": "Dailies",
- "config_entry_name": "Select character"
- },
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
@@ -39,11 +34,6 @@
}
},
"entity": {
- "binary_sensor": {
- "pending_quest": {
- "name": "Pending quest invitation"
- }
- },
"button": {
"run_cron": {
"name": "Start my day"
@@ -56,59 +46,6 @@
},
"revive": {
"name": "Revive from death"
- },
- "mpheal": {
- "name": "Ethereal surge"
- },
- "earth": {
- "name": "Earthquake"
- },
- "frost": {
- "name": "Chilling frost"
- },
- "defensive_stance": {
- "name": "Defensive stance"
- },
- "valorous_presence": {
- "name": "Valorous presence"
- },
- "intimidate": {
- "name": "Intimidating gaze"
- },
- "tools_of_trade": {
- "name": "Tools of the trade"
- },
- "stealth": {
- "name": "Stealth"
- },
- "heal": {
- "name": "Healing light"
- },
- "brightness": {
- "name": "Searing brightness"
- },
- "protect_aura": {
- "name": "Protective aura"
- },
- "heal_all": {
- "name": "Blessing"
- }
- },
- "calendar": {
- "todos": {
- "name": "[%key:component::habitica::common::todos%]"
- },
- "dailys": {
- "name": "[%key:component::habitica::common::dailies%]",
- "state_attributes": {
- "yesterdaily": {
- "name": "Yester-Daily",
- "state": {
- "true": "[%key:common::state::yes%]",
- "false": "[%key:common::state::no%]"
- }
- }
- }
}
},
"sensor": {
@@ -155,96 +92,16 @@
}
},
"todos": {
- "name": "[%key:component::habitica::common::todos%]"
+ "name": "To-Do's"
},
"dailys": {
- "name": "[%key:component::habitica::common::dailies%]"
+ "name": "Dailies"
},
"habits": {
"name": "Habits"
},
"rewards": {
"name": "Rewards"
- },
- "strength": {
- "name": "Strength",
- "state_attributes": {
- "level": {
- "name": "[%key:component::habitica::entity::sensor::level::name%]"
- },
- "equipment": {
- "name": "Battle gear"
- },
- "class": {
- "name": "Class equip bonus"
- },
- "allocated": {
- "name": "Allocated attribute points"
- },
- "buffs": {
- "name": "Buffs"
- }
- }
- },
- "intelligence": {
- "name": "Intelligence",
- "state_attributes": {
- "level": {
- "name": "[%key:component::habitica::entity::sensor::level::name%]"
- },
- "equipment": {
- "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::equipment::name%]"
- },
- "class": {
- "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::class::name%]"
- },
- "allocated": {
- "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::allocated::name%]"
- },
- "buffs": {
- "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::buffs::name%]"
- }
- }
- },
- "perception": {
- "name": "Perception",
- "state_attributes": {
- "level": {
- "name": "[%key:component::habitica::entity::sensor::level::name%]"
- },
- "equipment": {
- "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::equipment::name%]"
- },
- "class": {
- "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::class::name%]"
- },
- "allocated": {
- "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::allocated::name%]"
- },
- "buffs": {
- "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::buffs::name%]"
- }
- }
- },
- "constitution": {
- "name": "Constitution",
- "state_attributes": {
- "level": {
- "name": "[%key:component::habitica::entity::sensor::level::name%]"
- },
- "equipment": {
- "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::equipment::name%]"
- },
- "class": {
- "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::class::name%]"
- },
- "allocated": {
- "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::allocated::name%]"
- },
- "buffs": {
- "name": "[%key:component::habitica::entity::sensor::strength::state_attributes::buffs::name%]"
- }
- }
}
},
"switch": {
@@ -254,10 +111,10 @@
},
"todo": {
"todos": {
- "name": "[%key:component::habitica::common::todos%]"
+ "name": "To-Do's"
},
"dailys": {
- "name": "[%key:component::habitica::common::dailies%]"
+ "name": "Dailies"
}
}
},
@@ -290,10 +147,10 @@
"message": "Unable to create new to-do `{name}` for Habitica, please try again"
},
"setup_rate_limit_exception": {
- "message": "Rate limit exceeded, try again later"
+ "message": "Currently rate limited, try again later"
},
"service_call_unallowed": {
- "message": "Unable to complete action, the required conditions are not met"
+ "message": "Unable to carry out this action, because the required conditions are not met"
},
"service_call_exception": {
"message": "Unable to connect to Habitica, try again later"
@@ -301,36 +158,20 @@
"not_enough_mana": {
"message": "Unable to cast skill, not enough mana. Your character has {mana}, but the skill costs {cost}."
},
- "not_enough_gold": {
- "message": "Unable to buy reward, not enough gold. Your character has {gold}, but the reward costs {cost}."
- },
"skill_not_found": {
"message": "Unable to cast skill, your character does not have the skill or spell {skill}."
},
"entry_not_found": {
- "message": "The selected character is not configured in Home Assistant."
- },
- "entry_not_loaded": {
- "message": "The selected character is currently not loaded or disabled in Home Assistant."
+ "message": "The selected character is currently not configured or loaded in Home Assistant."
},
"task_not_found": {
- "message": "Unable to complete action, could not find the task {task}"
- },
- "quest_action_unallowed": {
- "message": "Action not allowed, only quest leader or group leader can perform this action"
- },
- "quest_not_found": {
- "message": "Unable to complete action, quest or group not found"
+ "message": "Unable to cast skill, could not find the task {task}"
}
},
"issues": {
"deprecated_task_entity": {
"title": "The Habitica {task_name} sensor is deprecated",
"description": "The Habitica entity `{entity}` is deprecated and will be removed in a future release.\nPlease update your automations and scripts to replace the sensor entity with the newly added todo entity.\nWhen you are done migrating you can disable `{entity}`."
- },
- "deprecated_api_call": {
- "title": "The Habitica action habitica.api_call is deprecated",
- "description": "The Habitica action `habitica.api_call` is deprecated and will be removed in Home Assistant 2025.5.0.\n\nPlease update your automations and scripts to use other Habitica actions and entities."
}
},
"services": {
@@ -357,7 +198,7 @@
"description": "Use a skill or spell from your Habitica character on a specific task to affect its progress or status.",
"fields": {
"config_entry": {
- "name": "[%key:component::habitica::common::config_entry_name%]",
+ "name": "Select character",
"description": "Choose the Habitica character to cast the skill."
},
"skill": {
@@ -369,98 +210,6 @@
"description": "The name (or task ID) of the task you want to target with the skill or spell."
}
}
- },
- "accept_quest": {
- "name": "Accept a quest invitation",
- "description": "Accept a pending invitation to a quest.",
- "fields": {
- "config_entry": {
- "name": "[%key:component::habitica::common::config_entry_name%]",
- "description": "Choose the Habitica character for which to perform the action."
- }
- }
- },
- "reject_quest": {
- "name": "Reject a quest invitation",
- "description": "Reject a pending invitation to a quest.",
- "fields": {
- "config_entry": {
- "name": "[%key:component::habitica::common::config_entry_name%]",
- "description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]"
- }
- }
- },
- "leave_quest": {
- "name": "Leave a quest",
- "description": "Leave the current quest you are participating in.",
- "fields": {
- "config_entry": {
- "name": "[%key:component::habitica::common::config_entry_name%]",
- "description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]"
- }
- }
- },
- "abort_quest": {
- "name": "Abort an active quest",
- "description": "Terminate your party's ongoing quest. All progress will be lost and the quest roll returned to the owner's inventory. Only quest leader or group leader can perform this action.",
- "fields": {
- "config_entry": {
- "name": "[%key:component::habitica::common::config_entry_name%]",
- "description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]"
- }
- }
- },
- "cancel_quest": {
- "name": "Cancel a pending quest",
- "description": "Cancel a quest that has not yet startet. All accepted and pending invitations will be canceled and the quest roll returned to the owner's inventory. Only quest leader or group leader can perform this action.",
- "fields": {
- "config_entry": {
- "name": "[%key:component::habitica::common::config_entry_name%]",
- "description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]"
- }
- }
- },
- "start_quest": {
- "name": "Force-start a pending quest",
- "description": "Begin the quest immediately, bypassing any pending invitations that haven't been accepted or rejected. Only quest leader or group leader can perform this action.",
- "fields": {
- "config_entry": {
- "name": "[%key:component::habitica::common::config_entry_name%]",
- "description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]"
- }
- }
- },
- "score_habit": {
- "name": "Track a habit",
- "description": "Increase the positive or negative streak of a habit to track its progress.",
- "fields": {
- "config_entry": {
- "name": "[%key:component::habitica::common::config_entry_name%]",
- "description": "Select the Habitica character tracking your habit."
- },
- "task": {
- "name": "Habit name",
- "description": "The name (or task ID) of the Habitica habit."
- },
- "direction": {
- "name": "Reward or loss",
- "description": "Is it positive or negative progress you want to track for your habit."
- }
- }
- },
- "score_reward": {
- "name": "Buy a reward",
- "description": "Reward yourself and buy one of your custom rewards with gold earned by fulfilling tasks.",
- "fields": {
- "config_entry": {
- "name": "[%key:component::habitica::common::config_entry_name%]",
- "description": "Select the Habitica character buying the reward."
- },
- "task": {
- "name": "Reward name",
- "description": "The name (or task ID) of the custom reward."
- }
- }
}
},
"selector": {
diff --git a/homeassistant/components/habitica/switch.py b/homeassistant/components/habitica/switch.py
index 6682911e892..c83d2332030 100644
--- a/homeassistant/components/habitica/switch.py
+++ b/homeassistant/components/habitica/switch.py
@@ -15,9 +15,9 @@ from homeassistant.components.switch import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from . import HabiticaConfigEntry
from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator
from .entity import HabiticaBase
-from .types import HabiticaConfigEntry
@dataclass(kw_only=True, frozen=True)
diff --git a/homeassistant/components/habitica/todo.py b/homeassistant/components/habitica/todo.py
index 0fff7b66605..ae739d47262 100644
--- a/homeassistant/components/habitica/todo.py
+++ b/homeassistant/components/habitica/todo.py
@@ -21,10 +21,10 @@ from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
+from . import HabiticaConfigEntry
from .const import ASSETS_URL, DOMAIN
from .coordinator import HabiticaDataUpdateCoordinator
from .entity import HabiticaBase
-from .types import HabiticaConfigEntry, HabiticaTaskType
from .util import next_due_date
@@ -37,6 +37,15 @@ class HabiticaTodoList(StrEnum):
REWARDS = "rewards"
+class HabiticaTaskType(StrEnum):
+ """Habitica Entities."""
+
+ HABIT = "habit"
+ DAILY = "daily"
+ TODO = "todo"
+ REWARD = "reward"
+
+
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HabiticaConfigEntry,
diff --git a/homeassistant/components/habitica/types.py b/homeassistant/components/habitica/types.py
deleted file mode 100644
index 9789a65dc40..00000000000
--- a/homeassistant/components/habitica/types.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""Types for Habitica integration."""
-
-from enum import StrEnum
-
-from homeassistant.config_entries import ConfigEntry
-
-from .coordinator import HabiticaDataUpdateCoordinator
-
-type HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator]
-
-
-class HabiticaTaskType(StrEnum):
- """Habitica Entities."""
-
- HABIT = "habit"
- DAILY = "daily"
- TODO = "todo"
- REWARD = "reward"
diff --git a/homeassistant/components/habitica/util.py b/homeassistant/components/habitica/util.py
index 03acb08baf9..26549e29cb0 100644
--- a/homeassistant/components/habitica/util.py
+++ b/homeassistant/components/habitica/util.py
@@ -3,24 +3,8 @@
from __future__ import annotations
import datetime
-from math import floor
from typing import TYPE_CHECKING, Any
-from dateutil.rrule import (
- DAILY,
- FR,
- MO,
- MONTHLY,
- SA,
- SU,
- TH,
- TU,
- WE,
- WEEKLY,
- YEARLY,
- rrule,
-)
-
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.core import HomeAssistant
@@ -78,114 +62,3 @@ def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]:
used_in = automations_with_entity(hass, entity_id)
used_in += scripts_with_entity(hass, entity_id)
return used_in
-
-
-FREQUENCY_MAP = {"daily": DAILY, "weekly": WEEKLY, "monthly": MONTHLY, "yearly": YEARLY}
-WEEKDAY_MAP = {"m": MO, "t": TU, "w": WE, "th": TH, "f": FR, "s": SA, "su": SU}
-
-
-def build_rrule(task: dict[str, Any]) -> rrule:
- """Build rrule string."""
-
- rrule_frequency = FREQUENCY_MAP.get(task["frequency"], DAILY)
- weekdays = [
- WEEKDAY_MAP[day] for day, is_active in task["repeat"].items() if is_active
- ]
- bymonthday = (
- task["daysOfMonth"]
- if rrule_frequency == MONTHLY and task["daysOfMonth"]
- else None
- )
-
- bysetpos = None
- if rrule_frequency == MONTHLY and task["weeksOfMonth"]:
- bysetpos = task["weeksOfMonth"]
- weekdays = weekdays if weekdays else [MO]
-
- return rrule(
- freq=rrule_frequency,
- interval=task["everyX"],
- dtstart=dt_util.start_of_local_day(
- datetime.datetime.fromisoformat(task["startDate"])
- ),
- byweekday=weekdays if rrule_frequency in [WEEKLY, MONTHLY] else None,
- bymonthday=bymonthday,
- bysetpos=bysetpos,
- )
-
-
-def get_recurrence_rule(recurrence: rrule) -> str:
- r"""Extract and return the recurrence rule portion of an RRULE.
-
- This function takes an RRULE representing a task's recurrence pattern,
- builds the RRULE string, and extracts the recurrence rule part.
-
- 'DTSTART:YYYYMMDDTHHMMSS\nRRULE:FREQ=YEARLY;INTERVAL=2'
-
- Parameters
- ----------
- recurrence : rrule
- An RRULE object.
-
- Returns
- -------
- str
- The recurrence rule portion of the RRULE string, starting with 'FREQ='.
-
- Example
- -------
- >>> rule = get_recurrence_rule(task)
- >>> print(rule)
- 'FREQ=YEARLY;INTERVAL=2'
-
- """
- return str(recurrence).split("RRULE:")[1]
-
-
-def get_attribute_points(
- user: dict[str, Any], content: dict[str, Any], attribute: str
-) -> dict[str, float]:
- """Get modifiers contributing to strength attribute."""
-
- gear_set = {
- "weapon",
- "armor",
- "head",
- "shield",
- "back",
- "headAccessory",
- "eyewear",
- "body",
- }
-
- equipment = sum(
- stats[attribute]
- for gear in gear_set
- if (equipped := user["items"]["gear"]["equipped"].get(gear))
- and (stats := content["gear"]["flat"].get(equipped))
- )
-
- class_bonus = sum(
- stats[attribute] / 2
- for gear in gear_set
- if (equipped := user["items"]["gear"]["equipped"].get(gear))
- and (stats := content["gear"]["flat"].get(equipped))
- and stats["klass"] == user["stats"]["class"]
- )
-
- return {
- "level": min(round(user["stats"]["lvl"] / 2), 50),
- "equipment": equipment,
- "class": class_bonus,
- "allocated": user["stats"][attribute],
- "buffs": user["stats"]["buffs"][attribute],
- }
-
-
-def get_attributes_total(
- user: dict[str, Any], content: dict[str, Any], attribute: str
-) -> int:
- """Get total attribute points."""
- return floor(
- sum(value for value in get_attribute_points(user, content, attribute).values())
- )
diff --git a/homeassistant/components/hardkernel/__init__.py b/homeassistant/components/hardkernel/__init__.py
index 66d2fa9d154..5d70f6cbfe0 100644
--- a/homeassistant/components/hardkernel/__init__.py
+++ b/homeassistant/components/hardkernel/__init__.py
@@ -2,11 +2,10 @@
from __future__ import annotations
-from homeassistant.components.hassio import get_os_info
+from homeassistant.components.hassio import get_os_info, is_hassio
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers.hassio import is_hassio
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
diff --git a/homeassistant/components/hardkernel/config_flow.py b/homeassistant/components/hardkernel/config_flow.py
index 5fa3611aa86..cf70adae55a 100644
--- a/homeassistant/components/hardkernel/config_flow.py
+++ b/homeassistant/components/hardkernel/config_flow.py
@@ -18,4 +18,7 @@ class HardkernelConfigFlow(ConfigFlow, domain=DOMAIN):
self, data: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
return self.async_create_entry(title="Hardkernel", data={})
diff --git a/homeassistant/components/hardkernel/manifest.json b/homeassistant/components/hardkernel/manifest.json
index aca1b207f4f..2a528a5173e 100644
--- a/homeassistant/components/hardkernel/manifest.json
+++ b/homeassistant/components/hardkernel/manifest.json
@@ -6,6 +6,5 @@
"config_flow": false,
"dependencies": ["hardware"],
"documentation": "https://www.home-assistant.io/integrations/hardkernel",
- "integration_type": "hardware",
- "single_config_entry": true
+ "integration_type": "hardware"
}
diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py
index b75ad617b39..87eb657a0a9 100644
--- a/homeassistant/components/harmony/config_flow.py
+++ b/homeassistant/components/harmony/config_flow.py
@@ -28,6 +28,7 @@ from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID
+from .data import HarmonyConfigEntry
from .util import (
find_best_name_for_remote,
find_unique_id_for_remote,
@@ -155,7 +156,7 @@ class HarmonyConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def _async_create_entry_from_valid_input(
self, validated: dict[str, Any], user_input: dict[str, Any]
@@ -185,6 +186,10 @@ def _options_from_user_input(user_input: dict[str, Any]) -> dict[str, Any]:
class OptionsFlowHandler(OptionsFlow):
"""Handle a option flow for Harmony."""
+ def __init__(self, config_entry: HarmonyConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py
index 306c9d43d72..2f962b2e5db 100644
--- a/homeassistant/components/hassio/__init__.py
+++ b/homeassistant/components/hassio/__init__.py
@@ -5,13 +5,11 @@ from __future__ import annotations
import asyncio
from contextlib import suppress
from datetime import datetime
-from functools import partial
import logging
import os
import re
from typing import Any, NamedTuple
-from aiohasupervisor import SupervisorError
import voluptuous as vol
from homeassistant.auth.const import GROUP_ID_ADMIN
@@ -39,22 +37,8 @@ from homeassistant.helpers import (
discovery_flow,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.deprecation import (
- DeprecatedConstant,
- all_with_deprecated_constants,
- check_if_deprecated_constant,
- deprecated_function,
- dir_with_deprecated_constants,
-)
from homeassistant.helpers.event import async_call_later
-from homeassistant.helpers.hassio import (
- get_supervisor_ip as _get_supervisor_ip,
- is_hassio as _is_hassio,
-)
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
-from homeassistant.helpers.service_info.hassio import (
- HassioServiceInfo as _HassioServiceInfo,
-)
from homeassistant.helpers.storage import Store
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
@@ -109,20 +93,29 @@ from .coordinator import (
get_info, # noqa: F401
get_issues_info, # noqa: F401
get_os_info,
+ get_store, # noqa: F401
get_supervisor_info, # noqa: F401
get_supervisor_stats, # noqa: F401
)
-from .discovery import async_setup_discovery_view # noqa: F401
+from .discovery import HassioServiceInfo, async_setup_discovery_view # noqa: F401
from .handler import ( # noqa: F401
HassIO,
HassioAPIError,
async_create_backup,
+ async_get_addon_discovery_info,
+ async_get_addon_store_info,
async_get_green_settings,
async_get_yellow_settings,
+ async_install_addon,
async_reboot_host,
+ async_set_addon_options,
async_set_green_settings,
async_set_yellow_settings,
+ async_update_addon,
+ async_update_core,
async_update_diagnostics,
+ async_update_os,
+ async_update_supervisor,
get_supervisor_client,
)
from .http import HassIOView
@@ -132,14 +125,6 @@ from .websocket_api import async_load_websocket_api
_LOGGER = logging.getLogger(__name__)
-get_supervisor_ip = deprecated_function(
- "homeassistant.helpers.hassio.get_supervisor_ip", breaks_in_ha_version="2025.11"
-)(_get_supervisor_ip)
-_DEPRECATED_HassioServiceInfo = DeprecatedConstant(
- _HassioServiceInfo,
- "homeassistant.helpers.service_info.hassio.HassioServiceInfo",
- "2025.11",
-)
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
@@ -295,16 +280,21 @@ def hostname_from_addon_slug(addon_slug: str) -> str:
@callback
-@deprecated_function(
- "homeassistant.helpers.hassio.is_hassio", breaks_in_ha_version="2025.11"
-)
@bind_hass
def is_hassio(hass: HomeAssistant) -> bool:
"""Return true if Hass.io is loaded.
Async friendly.
"""
- return _is_hassio(hass)
+ return DOMAIN in hass.config.components
+
+
+@callback
+def get_supervisor_ip() -> str | None:
+ """Return the supervisor ip address."""
+ if "SUPERVISOR" not in os.environ:
+ return None
+ return os.environ["SUPERVISOR"].partition(":")[0]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
@@ -325,11 +315,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
host = os.environ["SUPERVISOR"]
websession = async_get_clientsession(hass)
hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host)
- supervisor_client = get_supervisor_client(hass)
- try:
- await supervisor_client.supervisor.ping()
- except SupervisorError:
+ if not await hassio.is_connected():
_LOGGER.warning("Not connected with the supervisor / system too busy!")
store = Store[dict[str, str]](hass, STORAGE_VERSION, STORAGE_KEY)
@@ -448,13 +435,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
async def update_info_data(_: datetime | None = None) -> None:
"""Update last available supervisor information."""
- supervisor_client = get_supervisor_client(hass)
try:
(
hass.data[DATA_INFO],
hass.data[DATA_HOST_INFO],
- store_info,
+ hass.data[DATA_STORE],
hass.data[DATA_CORE_INFO],
hass.data[DATA_SUPERVISOR_INFO],
hass.data[DATA_OS_INFO],
@@ -462,7 +448,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
) = await asyncio.gather(
create_eager_task(hassio.get_info()),
create_eager_task(hassio.get_host_info()),
- create_eager_task(supervisor_client.store.info()),
+ create_eager_task(hassio.get_store()),
create_eager_task(hassio.get_core_info()),
create_eager_task(hassio.get_supervisor_info()),
create_eager_task(hassio.get_os_info()),
@@ -471,8 +457,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
except HassioAPIError as err:
_LOGGER.warning("Can't read Supervisor data: %s", err)
- else:
- hass.data[DATA_STORE] = store_info.to_dict()
async_call_later(
hass,
@@ -486,9 +470,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
async def _async_stop(hass: HomeAssistant, restart: bool) -> None:
"""Stop or restart home assistant."""
if restart:
- await supervisor_client.homeassistant.restart()
+ await hassio.restart_homeassistant()
else:
- await supervisor_client.homeassistant.stop()
+ await hassio.stop_homeassistant()
# Set a custom handler for the homeassistant.restart and homeassistant.stop services
async_set_stop_handler(hass, _async_stop)
@@ -569,11 +553,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.pop(ADDONS_COORDINATOR, None)
return unload_ok
-
-
-# These can be removed if no deprecated constant are in this module anymore
-__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
-__dir__ = partial(
- dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
-)
-__all__ = all_with_deprecated_constants(globals())
diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py
index db81e17e48d..1d51ef30e0f 100644
--- a/homeassistant/components/hassio/addon_manager.py
+++ b/homeassistant/components/hassio/addon_manager.py
@@ -12,16 +12,23 @@ from typing import Any, Concatenate
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import (
- AddonsOptions,
AddonState as SupervisorAddonState,
InstalledAddonComplete,
- StoreAddonUpdate,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
-from .handler import HassioAPIError, async_create_backup, get_supervisor_client
+from .handler import (
+ HassioAPIError,
+ async_create_backup,
+ async_get_addon_discovery_info,
+ async_get_addon_store_info,
+ async_install_addon,
+ async_set_addon_options,
+ async_update_addon,
+ get_supervisor_client,
+)
type _FuncType[_T, **_P, _R] = Callable[Concatenate[_T, _P], Awaitable[_R]]
type _ReturnFuncType[_T, **_P, _R] = Callable[
@@ -31,13 +38,10 @@ type _ReturnFuncType[_T, **_P, _R] = Callable[
def api_error[_AddonManagerT: AddonManager, **_P, _R](
error_message: str,
- *,
- expected_error_type: type[HassioAPIError | SupervisorError] | None = None,
) -> Callable[
[_FuncType[_AddonManagerT, _P, _R]], _ReturnFuncType[_AddonManagerT, _P, _R]
]:
"""Handle HassioAPIError and raise a specific AddonError."""
- error_type = expected_error_type or (HassioAPIError, SupervisorError)
def handle_hassio_api_error(
func: _FuncType[_AddonManagerT, _P, _R],
@@ -51,7 +55,7 @@ def api_error[_AddonManagerT: AddonManager, **_P, _R](
"""Wrap an add-on manager method."""
try:
return_value = await func(self, *args, **kwargs)
- except error_type as err:
+ except (HassioAPIError, SupervisorError) as err:
raise AddonError(
f"{error_message.format(addon_name=self.addon_name)}: {err}"
) from err
@@ -109,7 +113,6 @@ class AddonManager:
self._restart_task: asyncio.Task | None = None
self._start_task: asyncio.Task | None = None
self._update_task: asyncio.Task | None = None
- self._supervisor_client = get_supervisor_client(hass)
def task_in_progress(self) -> bool:
"""Return True if any of the add-on tasks are in progress."""
@@ -123,39 +126,28 @@ class AddonManager:
)
)
- @api_error(
- "Failed to get the {addon_name} add-on discovery info",
- expected_error_type=SupervisorError,
- )
+ @api_error("Failed to get the {addon_name} add-on discovery info")
async def async_get_addon_discovery_info(self) -> dict:
"""Return add-on discovery info."""
- discovery_info = next(
- (
- msg
- for msg in await self._supervisor_client.discovery.list()
- if msg.addon == self.addon_slug
- ),
- None,
+ discovery_info = await async_get_addon_discovery_info(
+ self._hass, self.addon_slug
)
if not discovery_info:
raise AddonError(f"Failed to get {self.addon_name} add-on discovery info")
- return discovery_info.config
+ discovery_info_config: dict = discovery_info["config"]
+ return discovery_info_config
- @api_error(
- "Failed to get the {addon_name} add-on info",
- expected_error_type=SupervisorError,
- )
+ @api_error("Failed to get the {addon_name} add-on info")
async def async_get_addon_info(self) -> AddonInfo:
"""Return and cache manager add-on info."""
- addon_store_info = await self._supervisor_client.store.addon_info(
- self.addon_slug
- )
- self._logger.debug("Add-on store info: %s", addon_store_info.to_dict())
- if not addon_store_info.installed:
+ supervisor_client = get_supervisor_client(self._hass)
+ addon_store_info = await async_get_addon_store_info(self._hass, self.addon_slug)
+ self._logger.debug("Add-on store info: %s", addon_store_info)
+ if not addon_store_info["installed"]:
return AddonInfo(
- available=addon_store_info.available,
+ available=addon_store_info["available"],
hostname=None,
options={},
state=AddonState.NOT_INSTALLED,
@@ -163,7 +155,7 @@ class AddonManager:
version=None,
)
- addon_info = await self._supervisor_client.addons.addon_info(self.addon_slug)
+ addon_info = await supervisor_client.addons.addon_info(self.addon_slug)
addon_state = self.async_get_addon_state(addon_info)
return AddonInfo(
available=addon_info.available,
@@ -188,39 +180,31 @@ class AddonManager:
return addon_state
- @api_error(
- "Failed to set the {addon_name} add-on options",
- expected_error_type=SupervisorError,
- )
+ @api_error("Failed to set the {addon_name} add-on options")
async def async_set_addon_options(self, config: dict) -> None:
"""Set manager add-on options."""
- await self._supervisor_client.addons.set_addon_options(
- self.addon_slug, AddonsOptions(config=config)
- )
+ options = {"options": config}
+ await async_set_addon_options(self._hass, self.addon_slug, options)
def _check_addon_available(self, addon_info: AddonInfo) -> None:
"""Check if the managed add-on is available."""
+
if not addon_info.available:
raise AddonError(f"{self.addon_name} add-on is not available")
- @api_error(
- "Failed to install the {addon_name} add-on", expected_error_type=SupervisorError
- )
+ @api_error("Failed to install the {addon_name} add-on")
async def async_install_addon(self) -> None:
"""Install the managed add-on."""
addon_info = await self.async_get_addon_info()
self._check_addon_available(addon_info)
- await self._supervisor_client.store.install_addon(self.addon_slug)
+ await async_install_addon(self._hass, self.addon_slug)
- @api_error(
- "Failed to uninstall the {addon_name} add-on",
- expected_error_type=SupervisorError,
- )
+ @api_error("Failed to uninstall the {addon_name} add-on")
async def async_uninstall_addon(self) -> None:
"""Uninstall the managed add-on."""
- await self._supervisor_client.addons.uninstall_addon(self.addon_slug)
+ await get_supervisor_client(self._hass).addons.uninstall_addon(self.addon_slug)
@api_error("Failed to update the {addon_name} add-on")
async def async_update_addon(self) -> None:
@@ -236,30 +220,22 @@ class AddonManager:
return
await self.async_create_backup()
- await self._supervisor_client.store.update_addon(
- self.addon_slug, StoreAddonUpdate(backup=False)
- )
+ await async_update_addon(self._hass, self.addon_slug)
- @api_error(
- "Failed to start the {addon_name} add-on", expected_error_type=SupervisorError
- )
+ @api_error("Failed to start the {addon_name} add-on")
async def async_start_addon(self) -> None:
"""Start the managed add-on."""
- await self._supervisor_client.addons.start_addon(self.addon_slug)
+ await get_supervisor_client(self._hass).addons.start_addon(self.addon_slug)
- @api_error(
- "Failed to restart the {addon_name} add-on", expected_error_type=SupervisorError
- )
+ @api_error("Failed to restart the {addon_name} add-on")
async def async_restart_addon(self) -> None:
"""Restart the managed add-on."""
- await self._supervisor_client.addons.restart_addon(self.addon_slug)
+ await get_supervisor_client(self._hass).addons.restart_addon(self.addon_slug)
- @api_error(
- "Failed to stop the {addon_name} add-on", expected_error_type=SupervisorError
- )
+ @api_error("Failed to stop the {addon_name} add-on")
async def async_stop_addon(self) -> None:
"""Stop the managed add-on."""
- await self._supervisor_client.addons.stop_addon(self.addon_slug)
+ await get_supervisor_client(self._hass).addons.stop_addon(self.addon_slug)
@api_error("Failed to create a backup of the {addon_name} add-on")
async def async_create_backup(self) -> None:
diff --git a/homeassistant/components/hassio/config_flow.py b/homeassistant/components/hassio/config_flow.py
index e8bed912fd7..57be400acc7 100644
--- a/homeassistant/components/hassio/config_flow.py
+++ b/homeassistant/components/hassio/config_flow.py
@@ -18,4 +18,7 @@ class HassIoConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
+ # We only need one Hass.io config entry
+ await self.async_set_unique_id(DOMAIN)
+ self._abort_if_unique_id_configured()
return self.async_create_entry(title="Supervisor", data={})
diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py
index 82ce74832c2..6e6c9006fca 100644
--- a/homeassistant/components/hassio/const.py
+++ b/homeassistant/components/hassio/const.py
@@ -103,7 +103,6 @@ PLACEHOLDER_KEY_ADDON_URL = "addon_url"
PLACEHOLDER_KEY_REFERENCE = "reference"
PLACEHOLDER_KEY_COMPONENTS = "components"
-ISSUE_KEY_ADDON_BOOT_FAIL = "issue_addon_boot_fail"
ISSUE_KEY_SYSTEM_DOCKER_CONFIG = "issue_system_docker_config"
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING = "issue_addon_detached_addon_missing"
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED = "issue_addon_detached_addon_removed"
@@ -137,3 +136,17 @@ class SupervisorEntityModel(StrEnum):
CORE = "Home Assistant Core"
SUPERVIOSR = "Home Assistant Supervisor"
HOST = "Home Assistant Host"
+
+
+class SupervisorIssueContext(StrEnum):
+ """Context for supervisor issues."""
+
+ ADDON = "addon"
+ CORE = "core"
+ DNS_SERVER = "dns_server"
+ MOUNT = "mount"
+ OS = "os"
+ PLUGIN = "plugin"
+ SUPERVISOR = "supervisor"
+ STORE = "store"
+ SYSTEM = "system"
diff --git a/homeassistant/components/hassio/coordinator.py b/homeassistant/components/hassio/coordinator.py
index cb1dda8aeed..dc62f41abb5 100644
--- a/homeassistant/components/hassio/coordinator.py
+++ b/homeassistant/components/hassio/coordinator.py
@@ -8,7 +8,6 @@ import logging
from typing import TYPE_CHECKING, Any
from aiohasupervisor import SupervisorError
-from aiohasupervisor.models import StoreInfo
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_MANUFACTURER, ATTR_NAME
@@ -56,7 +55,7 @@ from .const import (
SUPERVISOR_CONTAINER,
SupervisorEntityModel,
)
-from .handler import HassIO, HassioAPIError, get_supervisor_client
+from .handler import HassIO, HassioAPIError
if TYPE_CHECKING:
from .issues import SupervisorIssues
@@ -318,7 +317,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
self._container_updates: defaultdict[str, dict[str, set[str]]] = defaultdict(
lambda: defaultdict(set)
)
- self.supervisor_client = get_supervisor_client(hass)
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
@@ -334,15 +332,12 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
addons_info = get_addons_info(self.hass) or {}
addons_stats = get_addons_stats(self.hass)
addons_changelogs = get_addons_changelogs(self.hass)
- store_data = get_store(self.hass)
+ store_data = get_store(self.hass) or {}
- if store_data:
- repositories = {
- repo.slug: repo.name
- for repo in StoreInfo.from_dict(store_data).repositories
- }
- else:
- repositories = {}
+ repositories = {
+ repo[ATTR_SLUG]: repo[ATTR_NAME]
+ for repo in store_data.get("repositories", [])
+ }
new_data[DATA_KEY_ADDONS] = {
addon[ATTR_SLUG]: {
@@ -503,17 +498,17 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
async def _update_addon_stats(self, slug: str) -> tuple[str, dict[str, Any] | None]:
"""Update single addon stats."""
try:
- stats = await self.supervisor_client.addons.addon_stats(slug)
- except SupervisorError as err:
+ stats = await self.hassio.get_addon_stats(slug)
+ except HassioAPIError as err:
_LOGGER.warning("Could not fetch stats for %s: %s", slug, err)
return (slug, None)
- return (slug, stats.to_dict())
+ return (slug, stats)
async def _update_addon_changelog(self, slug: str) -> tuple[str, str | None]:
"""Return the changelog for an add-on."""
try:
- changelog = await self.supervisor_client.store.addon_changelog(slug)
- except SupervisorError as err:
+ changelog = await self.hassio.get_addon_changelog(slug)
+ except HassioAPIError as err:
_LOGGER.warning("Could not fetch changelog for %s: %s", slug, err)
return (slug, None)
return (slug, changelog)
@@ -521,7 +516,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]:
"""Return the info for an add-on."""
try:
- info = await self.supervisor_client.addons.addon_info(slug)
+ info = await self.hassio.client.addons.addon_info(slug)
except SupervisorError as err:
_LOGGER.warning("Could not fetch info for %s: %s", slug, err)
return (slug, None)
@@ -563,8 +558,8 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
# updates if this is not a scheduled refresh and
# we are not doing the first refresh.
try:
- await self.supervisor_client.refresh_updates()
- except SupervisorError as err:
+ await self.hassio.refresh_updates()
+ except HassioAPIError as err:
_LOGGER.warning("Error on Supervisor API: %s", err)
await super()._async_refresh(
diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py
index b51b8e5a8f2..5eaac1405ac 100644
--- a/homeassistant/components/hassio/discovery.py
+++ b/homeassistant/components/hassio/discovery.py
@@ -3,12 +3,10 @@
from __future__ import annotations
import asyncio
+from dataclasses import dataclass
import logging
from typing import Any
-from uuid import UUID
-from aiohasupervisor import SupervisorError
-from aiohasupervisor.models import Discovery
from aiohttp import web
from aiohttp.web_exceptions import HTTPServiceUnavailable
@@ -16,35 +14,44 @@ from homeassistant import config_entries
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import ATTR_SERVICE, EVENT_HOMEASSISTANT_START
from homeassistant.core import Event, HomeAssistant, callback
+from homeassistant.data_entry_flow import BaseServiceInfo
from homeassistant.helpers import discovery_flow
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
-from .const import ATTR_ADDON, ATTR_UUID, DOMAIN
-from .handler import HassIO, get_supervisor_client
+from .const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_UUID, DOMAIN
+from .handler import HassIO, HassioAPIError
_LOGGER = logging.getLogger(__name__)
+@dataclass(slots=True)
+class HassioServiceInfo(BaseServiceInfo):
+ """Prepared info from hassio entries."""
+
+ config: dict[str, Any]
+ name: str
+ slug: str
+ uuid: str
+
+
@callback
def async_setup_discovery_view(hass: HomeAssistant, hassio: HassIO) -> None:
"""Discovery setup."""
hassio_discovery = HassIODiscovery(hass, hassio)
- supervisor_client = get_supervisor_client(hass)
hass.http.register_view(hassio_discovery)
# Handle exists discovery messages
async def _async_discovery_start_handler(event: Event) -> None:
"""Process all exists discovery on startup."""
try:
- data = await supervisor_client.discovery.list()
- except SupervisorError as err:
+ data = await hassio.retrieve_discovery_messages()
+ except HassioAPIError as err:
_LOGGER.error("Can't read discover info: %s", err)
return
jobs = [
asyncio.create_task(hassio_discovery.async_process_new(discovery))
- for discovery in data
+ for discovery in data[ATTR_DISCOVERY]
]
if jobs:
await asyncio.wait(jobs)
@@ -81,14 +88,13 @@ class HassIODiscovery(HomeAssistantView):
"""Initialize WebView."""
self.hass = hass
self.hassio = hassio
- self._supervisor_client = get_supervisor_client(hass)
async def post(self, request: web.Request, uuid: str) -> web.Response:
"""Handle new discovery requests."""
# Fetch discovery data and prevent injections
try:
- data = await self._supervisor_client.discovery.get(UUID(uuid))
- except SupervisorError as err:
+ data = await self.hassio.get_discovery_message(uuid)
+ except HassioAPIError as err:
_LOGGER.error("Can't read discovery data: %s", err)
raise HTTPServiceUnavailable from None
@@ -105,50 +111,52 @@ class HassIODiscovery(HomeAssistantView):
async def async_rediscover(self, uuid: str) -> None:
"""Rediscover add-on when config entry is removed."""
try:
- data = await self._supervisor_client.discovery.get(UUID(uuid))
- except SupervisorError as err:
+ data = await self.hassio.get_discovery_message(uuid)
+ except HassioAPIError as err:
_LOGGER.debug("Can't read discovery data: %s", err)
else:
await self.async_process_new(data)
- async def async_process_new(self, data: Discovery) -> None:
+ async def async_process_new(self, data: dict[str, Any]) -> None:
"""Process add discovery entry."""
+ service: str = data[ATTR_SERVICE]
+ config_data: dict[str, Any] = data[ATTR_CONFIG]
+ slug: str = data[ATTR_ADDON]
+ uuid: str = data[ATTR_UUID]
+
# Read additional Add-on info
try:
- addon_info = await self._supervisor_client.addons.addon_info(data.addon)
- except SupervisorError as err:
+ addon_info = await self.hassio.client.addons.addon_info(slug)
+ except HassioAPIError as err:
_LOGGER.error("Can't read add-on info: %s", err)
return
- data.config[ATTR_ADDON] = addon_info.name
+ config_data[ATTR_ADDON] = addon_info.name
# Use config flow
discovery_flow.async_create_flow(
self.hass,
- data.service,
+ service,
context={"source": config_entries.SOURCE_HASSIO},
data=HassioServiceInfo(
- config=data.config,
- name=addon_info.name,
- slug=data.addon,
- uuid=data.uuid.hex,
+ config=config_data, name=addon_info.name, slug=slug, uuid=uuid
),
discovery_key=discovery_flow.DiscoveryKey(
domain=DOMAIN,
- key=data.uuid.hex,
+ key=data[ATTR_UUID],
version=1,
),
)
async def async_process_del(self, data: dict[str, Any]) -> None:
"""Process remove discovery entry."""
- service: str = data[ATTR_SERVICE]
- uuid: str = data[ATTR_UUID]
+ service = data[ATTR_SERVICE]
+ uuid = data[ATTR_UUID]
# Check if really deletet / prevent injections
try:
- await self._supervisor_client.discovery.get(UUID(uuid))
- except SupervisorError:
+ data = await self.hassio.get_discovery_message(uuid)
+ except HassioAPIError:
pass
else:
_LOGGER.warning("Retrieve wrong unload for %s", service)
diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py
index 58f2aa8c144..afa5cb31aba 100644
--- a/homeassistant/components/hassio/handler.py
+++ b/homeassistant/components/hassio/handler.py
@@ -21,15 +21,12 @@ from homeassistant.components.http import (
)
from homeassistant.const import SERVER_PORT
from homeassistant.core import HomeAssistant
-from homeassistant.helpers.singleton import singleton
from homeassistant.loader import bind_hass
-from .const import ATTR_MESSAGE, ATTR_RESULT, DOMAIN, X_HASS_SOURCE
+from .const import ATTR_DISCOVERY, ATTR_MESSAGE, ATTR_RESULT, DOMAIN, X_HASS_SOURCE
_LOGGER = logging.getLogger(__name__)
-KEY_SUPERVISOR_CLIENT = "supervisor_client"
-
class HassioAPIError(RuntimeError):
"""Return if a API trow a error."""
@@ -66,6 +63,17 @@ def api_data[**_P](
return _wrapper
+@api_data
+async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict:
+ """Return add-on store info.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio: HassIO = hass.data[DOMAIN]
+ command = f"/store/addons/{slug}"
+ return await hassio.send_command(command, method="get")
+
+
@bind_hass
async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> bool:
"""Update Supervisor diagnostics toggle.
@@ -76,6 +84,61 @@ async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> bo
return await hassio.update_diagnostics(diagnostics)
+@bind_hass
+@api_data
+async def async_install_addon(hass: HomeAssistant, slug: str) -> dict:
+ """Install add-on.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio: HassIO = hass.data[DOMAIN]
+ command = f"/addons/{slug}/install"
+ return await hassio.send_command(command, timeout=None)
+
+
+@bind_hass
+@api_data
+async def async_update_addon(
+ hass: HomeAssistant,
+ slug: str,
+ backup: bool = False,
+) -> dict:
+ """Update add-on.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio: HassIO = hass.data[DOMAIN]
+ command = f"/addons/{slug}/update"
+ return await hassio.send_command(
+ command,
+ payload={"backup": backup},
+ timeout=None,
+ )
+
+
+@bind_hass
+@api_data
+async def async_set_addon_options(
+ hass: HomeAssistant, slug: str, options: dict
+) -> dict:
+ """Set add-on options.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio: HassIO = hass.data[DOMAIN]
+ command = f"/addons/{slug}/options"
+ return await hassio.send_command(command, payload=options)
+
+
+@bind_hass
+async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict | None:
+ """Return discovery data for an add-on."""
+ hassio: HassIO = hass.data[DOMAIN]
+ data = await hassio.retrieve_discovery_messages()
+ discovered_addons = data[ATTR_DISCOVERY]
+ return next((addon for addon in discovered_addons if addon["addon"] == slug), None)
+
+
@bind_hass
@api_data
async def async_create_backup(
@@ -91,6 +154,61 @@ async def async_create_backup(
return await hassio.send_command(command, payload=payload, timeout=None)
+@bind_hass
+@api_data
+async def async_update_os(hass: HomeAssistant, version: str | None = None) -> dict:
+ """Update Home Assistant Operating System.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio: HassIO = hass.data[DOMAIN]
+ command = "/os/update"
+ return await hassio.send_command(
+ command,
+ payload={"version": version},
+ timeout=None,
+ )
+
+
+@bind_hass
+@api_data
+async def async_update_supervisor(hass: HomeAssistant) -> dict:
+ """Update Home Assistant Supervisor.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio: HassIO = hass.data[DOMAIN]
+ command = "/supervisor/update"
+ return await hassio.send_command(command, timeout=None)
+
+
+@bind_hass
+@api_data
+async def async_update_core(
+ hass: HomeAssistant, version: str | None = None, backup: bool = False
+) -> dict:
+ """Update Home Assistant Core.
+
+ The caller of the function should handle HassioAPIError.
+ """
+ hassio: HassIO = hass.data[DOMAIN]
+ command = "/core/update"
+ return await hassio.send_command(
+ command,
+ payload={"version": version, "backup": backup},
+ timeout=None,
+ )
+
+
+@bind_hass
+@_api_bool
+async def async_apply_suggestion(hass: HomeAssistant, suggestion_uuid: str) -> dict:
+ """Apply a suggestion from supervisor's resolution center."""
+ hassio: HassIO = hass.data[DOMAIN]
+ command = f"/resolution/suggestion/{suggestion_uuid}"
+ return await hassio.send_command(command, timeout=None)
+
+
@api_data
async def async_get_green_settings(hass: HomeAssistant) -> dict[str, bool]:
"""Return settings specific to Home Assistant Green."""
@@ -158,11 +276,22 @@ class HassIO:
self._ip = ip
base_url = f"http://{ip}"
self._base_url = URL(base_url)
+ self._client = SupervisorClient(
+ base_url, os.environ.get("SUPERVISOR_TOKEN", ""), session=websession
+ )
@property
- def base_url(self) -> URL:
- """Return base url for Supervisor."""
- return self._base_url
+ def client(self) -> SupervisorClient:
+ """Return aiohasupervisor client."""
+ return self._client
+
+ @_api_bool
+ def is_connected(self) -> Coroutine:
+ """Return true if it connected to Hass.io supervisor.
+
+ This method returns a coroutine.
+ """
+ return self.send_command("/supervisor/ping", method="get", timeout=15)
@api_data
def get_info(self) -> Coroutine:
@@ -220,6 +349,14 @@ class HassIO:
"""
return self.send_command("/core/stats", method="get")
+ @api_data
+ def get_addon_stats(self, addon: str) -> Coroutine:
+ """Return stats for an Add-on.
+
+ This method returns a coroutine.
+ """
+ return self.send_command(f"/addons/{addon}/stats", method="get")
+
@api_data
def get_supervisor_stats(self) -> Coroutine:
"""Return stats for the supervisor.
@@ -228,6 +365,23 @@ class HassIO:
"""
return self.send_command("/supervisor/stats", method="get")
+ def get_addon_changelog(self, addon: str) -> Coroutine:
+ """Return changelog for an Add-on.
+
+ This method returns a coroutine.
+ """
+ return self.send_command(
+ f"/addons/{addon}/changelog", method="get", return_text=True
+ )
+
+ @api_data
+ def get_store(self) -> Coroutine:
+ """Return data from the store.
+
+ This method returns a coroutine.
+ """
+ return self.send_command("/store", method="get")
+
@api_data
def get_ingress_panels(self) -> Coroutine:
"""Return data for Add-on ingress panels.
@@ -236,6 +390,66 @@ class HassIO:
"""
return self.send_command("/ingress/panels", method="get")
+ @_api_bool
+ def restart_homeassistant(self) -> Coroutine:
+ """Restart Home-Assistant container.
+
+ This method returns a coroutine.
+ """
+ return self.send_command("/homeassistant/restart")
+
+ @_api_bool
+ def stop_homeassistant(self) -> Coroutine:
+ """Stop Home-Assistant container.
+
+ This method returns a coroutine.
+ """
+ return self.send_command("/homeassistant/stop")
+
+ @_api_bool
+ def refresh_updates(self) -> Coroutine:
+ """Refresh available updates.
+
+ This method returns a coroutine.
+ """
+ return self.send_command("/refresh_updates", timeout=300)
+
+ @api_data
+ def retrieve_discovery_messages(self) -> Coroutine:
+ """Return all discovery data from Hass.io API.
+
+ This method returns a coroutine.
+ """
+ return self.send_command("/discovery", method="get", timeout=60)
+
+ @api_data
+ def get_discovery_message(self, uuid: str) -> Coroutine:
+ """Return a single discovery data message.
+
+ This method returns a coroutine.
+ """
+ return self.send_command(f"/discovery/{uuid}", method="get")
+
+ @api_data
+ def get_resolution_info(self) -> Coroutine:
+ """Return data for Supervisor resolution center.
+
+ This method returns a coroutine.
+ """
+ return self.send_command("/resolution/info", method="get")
+
+ @api_data
+ def get_suggestions_for_issue(
+ self, issue_id: str
+ ) -> Coroutine[Any, Any, dict[str, Any]]:
+ """Return suggestions for issue from Supervisor resolution center.
+
+ This method returns a coroutine.
+ """
+ return self.send_command(
+ f"/resolution/issue/{issue_id}/suggestions", method="get"
+ )
+
@_api_bool
async def update_hass_api(
self, http_config: dict[str, Any], refresh_token: RefreshToken
@@ -275,6 +489,14 @@ class HassIO:
"/supervisor/options", payload={"diagnostics": diagnostics}
)
+ @_api_bool
+ def apply_suggestion(self, suggestion_uuid: str) -> Coroutine:
+ """Apply a suggestion from supervisor's resolution center.
+
+ This method returns a coroutine.
+ """
+ return self.send_command(f"/resolution/suggestion/{suggestion_uuid}")
+
async def send_command(
self,
command: str,
@@ -340,12 +562,7 @@ class HassIO:
raise HassioAPIError
-@singleton(KEY_SUPERVISOR_CLIENT)
def get_supervisor_client(hass: HomeAssistant) -> SupervisorClient:
"""Return supervisor client."""
hassio: HassIO = hass.data[DOMAIN]
- return SupervisorClient(
- str(hassio.base_url),
- os.environ.get("SUPERVISOR_TOKEN", ""),
- session=hassio.websession,
- )
+ return hassio.client
diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py
index 2b34a48149b..8c1fb11973e 100644
--- a/homeassistant/components/hassio/http.py
+++ b/homeassistant/components/hassio/http.py
@@ -18,7 +18,6 @@ from aiohttp.hdrs import (
CONTENT_ENCODING,
CONTENT_LENGTH,
CONTENT_TYPE,
- RANGE,
TRANSFER_ENCODING,
)
from aiohttp.web_exceptions import HTTPBadGateway
@@ -42,15 +41,6 @@ NO_TIMEOUT = re.compile(
r"|backups/.+/full"
r"|backups/.+/partial"
r"|backups/[^/]+/(?:upload|download)"
- r"|audio/logs/(follow|boots/-?\d+(/follow)?)"
- r"|cli/logs/(follow|boots/-?\d+(/follow)?)"
- r"|core/logs/(follow|boots/-?\d+(/follow)?)"
- r"|dns/logs/(follow|boots/-?\d+(/follow)?)"
- r"|host/logs/(follow|boots/-?\d+(/follow)?)"
- r"|multicast/logs/(follow|boots/-?\d+(/follow)?)"
- r"|observer/logs/(follow|boots/-?\d+(/follow)?)"
- r"|supervisor/logs/(follow|boots/-?\d+(/follow)?)"
- r"|addons/[^/]+/logs/(follow|boots/-?\d+(/follow)?)"
r")$"
)
@@ -68,16 +58,15 @@ PATHS_ADMIN = re.compile(
r"^(?:"
r"|backups/[a-f0-9]{8}(/info|/download|/restore/full|/restore/partial)?"
r"|backups/new/upload"
- r"|audio/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|cli/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|core/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|dns/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|host/logs(/follow|/boots(/-?\d+(/follow)?)?)?"
- r"|multicast/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|observer/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|supervisor/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|addons/[^/]+/(changelog|documentation)"
- r"|addons/[^/]+/logs(/follow|/boots/-?\d+(/follow)?)?"
+ r"|audio/logs"
+ r"|cli/logs"
+ r"|core/logs"
+ r"|dns/logs"
+ r"|host/logs"
+ r"|multicast/logs"
+ r"|observer/logs"
+ r"|supervisor/logs"
+ r"|addons/[^/]+/(changelog|documentation|logs)"
r")$"
)
@@ -94,38 +83,8 @@ NO_STORE = re.compile(
r"|app/entrypoint.js"
r")$"
)
-
-# Follow logs should not be compressed, to be able to get streamed by frontend
-NO_COMPRESS = re.compile(
- r"^(?:"
- r"|audio/logs/(follow|boots/-?\d+(/follow)?)"
- r"|cli/logs/(follow|boots/-?\d+(/follow)?)"
- r"|core/logs/(follow|boots/-?\d+(/follow)?)"
- r"|dns/logs/(follow|boots/-?\d+(/follow)?)"
- r"|host/logs/(follow|boots/-?\d+(/follow)?)"
- r"|multicast/logs/(follow|boots/-?\d+(/follow)?)"
- r"|observer/logs/(follow|boots/-?\d+(/follow)?)"
- r"|supervisor/logs/(follow|boots/-?\d+(/follow)?)"
- r"|addons/[^/]+/logs/(follow|boots/-?\d+(/follow)?)"
- r")$"
-)
-
-PATHS_LOGS = re.compile(
- r"^(?:"
- r"|audio/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|cli/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|core/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|dns/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|host/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|multicast/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|observer/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|supervisor/logs(/follow|/boots/-?\d+(/follow)?)?"
- r"|addons/[^/]+/logs(/follow|/boots/-?\d+(/follow)?)?"
- r")$"
-)
# fmt: on
-
RESPONSE_HEADERS_FILTER = {
TRANSFER_ENCODING,
CONTENT_LENGTH,
@@ -202,10 +161,6 @@ class HassIOView(HomeAssistantView):
assert isinstance(request._stored_content_type, str) # noqa: SLF001
headers[CONTENT_TYPE] = request._stored_content_type # noqa: SLF001
- # forward range headers for logs
- if PATHS_LOGS.match(path) and request.headers.get(RANGE):
- headers[RANGE] = request.headers[RANGE]
-
try:
client = await self._websession.request(
method=request.method,
@@ -222,7 +177,7 @@ class HassIOView(HomeAssistantView):
)
response.content_type = client.content_type
- if should_compress(response.content_type, path):
+ if should_compress(response.content_type):
response.enable_compression()
await response.prepare(request)
# In testing iter_chunked, iter_any, and iter_chunks:
@@ -262,10 +217,8 @@ def _get_timeout(path: str) -> ClientTimeout:
return ClientTimeout(connect=10, total=300)
-def should_compress(content_type: str, path: str | None = None) -> bool:
+def should_compress(content_type: str) -> bool:
"""Return if we should compress a response."""
- if path is not None and NO_COMPRESS.match(path):
- return False
if content_type.startswith("image/"):
return "svg" in content_type
if content_type.startswith("application/"):
diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py
index 16697659077..9c2152489d6 100644
--- a/homeassistant/components/hassio/issues.py
+++ b/homeassistant/components/hassio/issues.py
@@ -7,10 +7,6 @@ from dataclasses import dataclass, field
from datetime import datetime
import logging
from typing import Any, NotRequired, TypedDict
-from uuid import UUID
-
-from aiohasupervisor import SupervisorError
-from aiohasupervisor.models import ContextType, Issue as SupervisorIssue
from homeassistant.core import HassJob, HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -24,8 +20,12 @@ from homeassistant.helpers.issue_registry import (
from .const import (
ATTR_DATA,
ATTR_HEALTHY,
+ ATTR_ISSUES,
+ ATTR_SUGGESTIONS,
ATTR_SUPPORTED,
+ ATTR_UNHEALTHY,
ATTR_UNHEALTHY_REASONS,
+ ATTR_UNSUPPORTED,
ATTR_UNSUPPORTED_REASONS,
ATTR_UPDATE_KEY,
ATTR_WS_EVENT,
@@ -36,7 +36,6 @@ from .const import (
EVENT_SUPERVISOR_EVENT,
EVENT_SUPERVISOR_UPDATE,
EVENT_SUPPORTED_CHANGED,
- ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
@@ -45,9 +44,10 @@ from .const import (
PLACEHOLDER_KEY_REFERENCE,
REQUEST_REFRESH_DELAY,
UPDATE_KEY_SUPERVISOR,
+ SupervisorIssueContext,
)
from .coordinator import get_addons_info
-from .handler import HassIO, get_supervisor_client
+from .handler import HassIO, HassioAPIError
ISSUE_KEY_UNHEALTHY = "unhealthy"
ISSUE_KEY_UNSUPPORTED = "unsupported"
@@ -94,7 +94,6 @@ UNHEALTHY_REASONS = {
# Keys (type + context) of issues that when found should be made into a repair
ISSUE_KEYS_FOR_REPAIRS = {
- ISSUE_KEY_ADDON_BOOT_FAIL,
"issue_mount_mount_failed",
"issue_system_multiple_data_disks",
"issue_system_reboot_required",
@@ -119,9 +118,9 @@ class SuggestionDataType(TypedDict):
class Suggestion:
"""Suggestion from Supervisor which resolves an issue."""
- uuid: UUID
+ uuid: str
type: str
- context: ContextType
+ context: SupervisorIssueContext
reference: str | None = None
@property
@@ -133,9 +132,9 @@ class Suggestion:
def from_dict(cls, data: SuggestionDataType) -> Suggestion:
"""Convert from dictionary representation."""
return cls(
- uuid=UUID(data["uuid"]),
+ uuid=data["uuid"],
type=data["type"],
- context=ContextType(data["context"]),
+ context=SupervisorIssueContext(data["context"]),
reference=data["reference"],
)
@@ -154,9 +153,9 @@ class IssueDataType(TypedDict):
class Issue:
"""Issue from Supervisor."""
- uuid: UUID
+ uuid: str
type: str
- context: ContextType
+ context: SupervisorIssueContext
reference: str | None = None
suggestions: list[Suggestion] = field(default_factory=list, compare=False)
@@ -170,9 +169,9 @@ class Issue:
"""Convert from dictionary representation."""
suggestions: list[SuggestionDataType] = data.get("suggestions", [])
return cls(
- uuid=UUID(data["uuid"]),
+ uuid=data["uuid"],
type=data["type"],
- context=ContextType(data["context"]),
+ context=SupervisorIssueContext(data["context"]),
reference=data["reference"],
suggestions=[
Suggestion.from_dict(suggestion) for suggestion in suggestions
@@ -189,8 +188,7 @@ class SupervisorIssues:
self._client = client
self._unsupported_reasons: set[str] = set()
self._unhealthy_reasons: set[str] = set()
- self._issues: dict[UUID, Issue] = {}
- self._supervisor_client = get_supervisor_client(hass)
+ self._issues: dict[str, Issue] = {}
@property
def unhealthy_reasons(self) -> set[str]:
@@ -283,7 +281,7 @@ class SupervisorIssues:
async_create_issue(
self._hass,
DOMAIN,
- issue.uuid.hex,
+ issue.uuid,
is_fixable=bool(issue.suggestions),
severity=IssueSeverity.WARNING,
translation_key=issue.key,
@@ -292,37 +290,19 @@ class SupervisorIssues:
self._issues[issue.uuid] = issue
- async def add_issue_from_data(self, data: SupervisorIssue) -> None:
+ async def add_issue_from_data(self, data: IssueDataType) -> None:
"""Add issue from data to list after getting latest suggestions."""
try:
- suggestions = (
- await self._supervisor_client.resolution.suggestions_for_issue(
- data.uuid
- )
- )
- except SupervisorError:
+ data["suggestions"] = (
+ await self._client.get_suggestions_for_issue(data["uuid"])
+ )[ATTR_SUGGESTIONS]
+ except HassioAPIError:
_LOGGER.error(
"Could not get suggestions for supervisor issue %s, skipping it",
- data.uuid.hex,
+ data["uuid"],
)
return
- self.add_issue(
- Issue(
- uuid=data.uuid,
- type=str(data.type),
- context=data.context,
- reference=data.reference,
- suggestions=[
- Suggestion(
- uuid=suggestion.uuid,
- type=str(suggestion.type),
- context=suggestion.context,
- reference=suggestion.reference,
- )
- for suggestion in suggestions
- ],
- )
- )
+ self.add_issue(Issue.from_dict(data))
def remove_issue(self, issue: Issue) -> None:
"""Remove an issue from the list. Delete a repair if necessary."""
@@ -330,13 +310,13 @@ class SupervisorIssues:
return
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
- async_delete_issue(self._hass, DOMAIN, issue.uuid.hex)
+ async_delete_issue(self._hass, DOMAIN, issue.uuid)
del self._issues[issue.uuid]
def get_issue(self, issue_id: str) -> Issue | None:
"""Get issue from key."""
- return self._issues.get(UUID(issue_id))
+ return self._issues.get(issue_id)
async def setup(self) -> None:
"""Create supervisor events listener."""
@@ -349,8 +329,8 @@ class SupervisorIssues:
async def _update(self, _: datetime | None = None) -> None:
"""Update issues from Supervisor resolution center."""
try:
- data = await self._supervisor_client.resolution.info()
- except SupervisorError as err:
+ data = await self._client.get_resolution_info()
+ except HassioAPIError as err:
_LOGGER.error("Failed to update supervisor issues: %r", err)
async_call_later(
self._hass,
@@ -358,16 +338,18 @@ class SupervisorIssues:
HassJob(self._update, cancel_on_shutdown=True),
)
return
- self.unhealthy_reasons = set(data.unhealthy)
- self.unsupported_reasons = set(data.unsupported)
+ self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
+ self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])
# Remove any cached issues that weren't returned
- for issue_id in set(self._issues) - {issue.uuid for issue in data.issues}:
+ for issue_id in set(self._issues.keys()) - {
+ issue["uuid"] for issue in data[ATTR_ISSUES]
+ }:
self.remove_issue(self._issues[issue_id])
# Add/update any issues that came back
await asyncio.gather(
- *[self.add_issue_from_data(issue) for issue in data.issues]
+ *[self.add_issue_from_data(issue) for issue in data[ATTR_ISSUES]]
)
@callback
diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json
index 31fa27a92c4..14e3f3598f1 100644
--- a/homeassistant/components/hassio/manifest.json
+++ b/homeassistant/components/hassio/manifest.json
@@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hassio",
"iot_class": "local_polling",
"quality_scale": "internal",
- "requirements": ["aiohasupervisor==0.2.1"],
- "single_config_entry": true
+ "requirements": ["aiohasupervisor==0.1.0"]
}
diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py
index 0e8122c08b9..082dbe38bee 100644
--- a/homeassistant/components/hassio/repairs.py
+++ b/homeassistant/components/hassio/repairs.py
@@ -6,8 +6,6 @@ from collections.abc import Callable, Coroutine
from types import MethodType
from typing import Any
-from aiohasupervisor import SupervisorError
-from aiohasupervisor.models import ContextType
import voluptuous as vol
from homeassistant.components.repairs import RepairsFlow
@@ -16,14 +14,14 @@ from homeassistant.data_entry_flow import FlowResult
from . import get_addons_info, get_issues_info
from .const import (
- ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
PLACEHOLDER_KEY_ADDON,
PLACEHOLDER_KEY_COMPONENTS,
PLACEHOLDER_KEY_REFERENCE,
+ SupervisorIssueContext,
)
-from .handler import get_supervisor_client
+from .handler import async_apply_suggestion
from .issues import Issue, Suggestion
HELP_URLS = {
@@ -52,10 +50,9 @@ class SupervisorIssueRepairFlow(RepairsFlow):
_data: dict[str, Any] | None = None
_issue: Issue | None = None
- def __init__(self, hass: HomeAssistant, issue_id: str) -> None:
+ def __init__(self, issue_id: str) -> None:
"""Initialize repair flow."""
self._issue_id = issue_id
- self._supervisor_client = get_supervisor_client(hass)
super().__init__()
@property
@@ -126,12 +123,9 @@ class SupervisorIssueRepairFlow(RepairsFlow):
if not confirmed and suggestion.key in SUGGESTION_CONFIRMATION_REQUIRED:
return self._async_form_for_suggestion(suggestion)
- try:
- await self._supervisor_client.resolution.apply_suggestion(suggestion.uuid)
- except SupervisorError:
- return self.async_abort(reason="apply_suggestion_fail")
-
- return self.async_create_entry(data={})
+ if await async_apply_suggestion(self.hass, suggestion.uuid):
+ return self.async_create_entry(data={})
+ return self.async_abort(reason="apply_suggestion_fail")
@staticmethod
def _async_step(
@@ -168,9 +162,9 @@ class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow):
if issue.key == self.issue.key or issue.type != self.issue.type:
continue
- if issue.context == ContextType.CORE:
+ if issue.context == SupervisorIssueContext.CORE:
components.insert(0, "Home Assistant")
- elif issue.context == ContextType.ADDON:
+ elif issue.context == SupervisorIssueContext.ADDON:
components.append(
next(
(
@@ -187,8 +181,8 @@ class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow):
return placeholders
-class AddonIssueRepairFlow(SupervisorIssueRepairFlow):
- """Handler for addon issue fixing flows."""
+class DetachedAddonIssueRepairFlow(SupervisorIssueRepairFlow):
+ """Handler for detached addon issue fixing flows."""
@property
def description_placeholders(self) -> dict[str, str] | None:
@@ -215,11 +209,8 @@ async def async_create_fix_flow(
supervisor_issues = get_issues_info(hass)
issue = supervisor_issues and supervisor_issues.get_issue(issue_id)
if issue and issue.key == ISSUE_KEY_SYSTEM_DOCKER_CONFIG:
- return DockerConfigIssueRepairFlow(hass, issue_id)
- if issue and issue.key in {
- ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
- ISSUE_KEY_ADDON_BOOT_FAIL,
- }:
- return AddonIssueRepairFlow(hass, issue_id)
+ return DockerConfigIssueRepairFlow(issue_id)
+ if issue and issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED:
+ return DetachedAddonIssueRepairFlow(issue_id)
- return SupervisorIssueRepairFlow(hass, issue_id)
+ return SupervisorIssueRepairFlow(issue_id)
diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json
index 09ed45bd5bc..8688934ee3d 100644
--- a/homeassistant/components/hassio/strings.json
+++ b/homeassistant/components/hassio/strings.json
@@ -17,23 +17,6 @@
}
},
"issues": {
- "issue_addon_boot_fail": {
- "title": "Add-on failed to start at boot",
- "fix_flow": {
- "step": {
- "fix_menu": {
- "description": "Add-on {addon} is set to start at boot but failed to start. Usually this occurs when the configuration is incorrect or the same port is used in multiple add-ons. Check the configuration as well as logs for {addon} and Supervisor.\n\nUse Start to try again or Disable to turn off the start at boot option.",
- "menu_options": {
- "addon_execute_start": "Start",
- "addon_disable_boot": "Disable"
- }
- }
- },
- "abort": {
- "apply_suggestion_fail": "Could not apply the fix. Check the Supervisor logs for more details."
- }
- }
- },
"issue_addon_detached_addon_missing": {
"title": "Missing repository for an installed add-on",
"description": "Repository for add-on {addon} is missing. This means it will not get updates, and backups may not be restored correctly as the supervisor may not be able to build/download the resources required.\n\nPlease check the [add-on's documentation]({addon_url}) for installation instructions and add the repository to the store."
diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py
index fbb3e191f81..a7974850e19 100644
--- a/homeassistant/components/hassio/update.py
+++ b/homeassistant/components/hassio/update.py
@@ -4,12 +4,6 @@ from __future__ import annotations
from typing import Any
-from aiohasupervisor import SupervisorError
-from aiohasupervisor.models import (
- HomeAssistantUpdateOptions,
- OSUpdate,
- StoreAddonUpdate,
-)
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
from homeassistant.components.update import (
@@ -40,6 +34,13 @@ from .entity import (
HassioOSEntity,
HassioSupervisorEntity,
)
+from .handler import (
+ HassioAPIError,
+ async_update_addon,
+ async_update_core,
+ async_update_os,
+ async_update_supervisor,
+)
ENTITY_DESCRIPTION = UpdateEntityDescription(
name="Update",
@@ -164,10 +165,8 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
) -> None:
"""Install an update."""
try:
- await self.coordinator.supervisor_client.store.update_addon(
- self._addon_slug, StoreAddonUpdate(backup=backup)
- )
- except SupervisorError as err:
+ await async_update_addon(self.hass, slug=self._addon_slug, backup=backup)
+ except HassioAPIError as err:
raise HomeAssistantError(f"Error updating {self.title}: {err}") from err
await self.coordinator.force_info_update_supervisor()
@@ -211,10 +210,8 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
) -> None:
"""Install an update."""
try:
- await self.coordinator.supervisor_client.os.update(
- OSUpdate(version=version)
- )
- except SupervisorError as err:
+ await async_update_os(self.hass, version)
+ except HassioAPIError as err:
raise HomeAssistantError(
f"Error updating Home Assistant Operating System: {err}"
) from err
@@ -259,8 +256,8 @@ class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):
) -> None:
"""Install an update."""
try:
- await self.coordinator.supervisor_client.supervisor.update()
- except SupervisorError as err:
+ await async_update_supervisor(self.hass)
+ except HassioAPIError as err:
raise HomeAssistantError(
f"Error updating Home Assistant Supervisor: {err}"
) from err
@@ -304,10 +301,8 @@ class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity):
) -> None:
"""Install an update."""
try:
- await self.coordinator.supervisor_client.homeassistant.update(
- HomeAssistantUpdateOptions(version=version, backup=backup)
- )
- except SupervisorError as err:
+ await async_update_core(self.hass, version=version, backup=backup)
+ except HassioAPIError as err:
raise HomeAssistantError(
f"Error updating Home Assistant Core: {err}"
) from err
diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py
index 1102dbc0c74..f9f0cfacf60 100644
--- a/homeassistant/components/heatmiser/climate.py
+++ b/homeassistant/components/heatmiser/climate.py
@@ -1,11 +1,11 @@
-"""Support for the PRT Heatmiser thermostats using the V3 protocol."""
+"""Support for the PRT Heatmiser themostats using the V3 protocol."""
from __future__ import annotations
import logging
from typing import Any
-from heatmiserv3 import connection, heatmiser
+from heatmiserV3 import connection, heatmiser
import voluptuous as vol
from homeassistant.components.climate import (
diff --git a/homeassistant/components/heatmiser/manifest.json b/homeassistant/components/heatmiser/manifest.json
index f3f33f79b04..7ae9cac1297 100644
--- a/homeassistant/components/heatmiser/manifest.json
+++ b/homeassistant/components/heatmiser/manifest.json
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/heatmiser",
"iot_class": "local_polling",
"loggers": ["heatmiserV3"],
- "requirements": ["heatmiserV3==2.0.3"]
+ "requirements": ["heatmiserV3==1.1.18"]
}
diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py
index c2b70de148c..4376ae793c0 100644
--- a/homeassistant/components/here_travel_time/config_flow.py
+++ b/homeassistant/components/here_travel_time/config_flow.py
@@ -113,7 +113,7 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> HERETravelTimeOptionsFlow:
"""Get the options flow."""
- return HERETravelTimeOptionsFlow()
+ return HERETravelTimeOptionsFlow(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -297,8 +297,9 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN):
class HERETravelTimeOptionsFlow(OptionsFlow):
"""Handle HERE Travel Time options."""
- def __init__(self) -> None:
+ def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize HERE Travel Time options flow."""
+ self.config_entry = config_entry
self._config: dict[str, Any] = {}
async def async_step_init(
diff --git a/homeassistant/components/history_stats/__init__.py b/homeassistant/components/history_stats/__init__.py
index 63f32138dba..dcca10d73e9 100644
--- a/homeassistant/components/history_stats/__init__.py
+++ b/homeassistant/components/history_stats/__init__.py
@@ -41,7 +41,7 @@ async def async_setup_entry(
Template(end, hass) if end else None,
duration,
)
- coordinator = HistoryStatsUpdateCoordinator(hass, history_stats, entry, entry.title)
+ coordinator = HistoryStatsUpdateCoordinator(hass, history_stats, entry.title)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
diff --git a/homeassistant/components/history_stats/coordinator.py b/homeassistant/components/history_stats/coordinator.py
index fafbb5d3ce0..0d613d2bbc0 100644
--- a/homeassistant/components/history_stats/coordinator.py
+++ b/homeassistant/components/history_stats/coordinator.py
@@ -6,7 +6,6 @@ from datetime import timedelta
import logging
from typing import Any
-from homeassistant.config_entries import ConfigEntry
from homeassistant.core import (
CALLBACK_TYPE,
Event,
@@ -34,7 +33,6 @@ class HistoryStatsUpdateCoordinator(DataUpdateCoordinator[HistoryStatsState]):
self,
hass: HomeAssistant,
history_stats: HistoryStats,
- config_entry: ConfigEntry | None,
name: str,
) -> None:
"""Initialize DataUpdateCoordinator."""
@@ -45,7 +43,6 @@ class HistoryStatsUpdateCoordinator(DataUpdateCoordinator[HistoryStatsState]):
super().__init__(
hass,
_LOGGER,
- config_entry=config_entry,
name=name,
update_interval=UPDATE_INTERVAL,
)
diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py
index e1241034aeb..4558da8722c 100644
--- a/homeassistant/components/history_stats/sensor.py
+++ b/homeassistant/components/history_stats/sensor.py
@@ -104,7 +104,7 @@ async def async_setup_platform(
unique_id: str | None = config.get(CONF_UNIQUE_ID)
history_stats = HistoryStats(hass, entity_id, entity_states, start, end, duration)
- coordinator = HistoryStatsUpdateCoordinator(hass, history_stats, None, name)
+ coordinator = HistoryStatsUpdateCoordinator(hass, history_stats, name)
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise PlatformNotReady from coordinator.last_exception
diff --git a/homeassistant/components/history_stats/strings.json b/homeassistant/components/history_stats/strings.json
index 8961d66118d..603a6b8c4dc 100644
--- a/homeassistant/components/history_stats/strings.json
+++ b/homeassistant/components/history_stats/strings.json
@@ -1,5 +1,4 @@
{
- "title": "History Stats",
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
diff --git a/homeassistant/components/hive/alarm_control_panel.py b/homeassistant/components/hive/alarm_control_panel.py
index 2b196ce820b..34d5d3d10c6 100644
--- a/homeassistant/components/hive/alarm_control_panel.py
+++ b/homeassistant/components/hive/alarm_control_panel.py
@@ -7,9 +7,14 @@ from datetime import timedelta
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -19,10 +24,10 @@ from .entity import HiveEntity
PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=15)
HIVETOHA = {
- "home": AlarmControlPanelState.DISARMED,
- "asleep": AlarmControlPanelState.ARMED_NIGHT,
- "away": AlarmControlPanelState.ARMED_AWAY,
- "sos": AlarmControlPanelState.TRIGGERED,
+ "home": STATE_ALARM_DISARMED,
+ "asleep": STATE_ALARM_ARMED_NIGHT,
+ "away": STATE_ALARM_ARMED_AWAY,
+ "sos": STATE_ALARM_TRIGGERED,
}
@@ -71,6 +76,6 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity):
self._attr_available = self.device["deviceData"].get("online")
if self._attr_available:
if self.device["status"]["state"]:
- self._attr_alarm_state = AlarmControlPanelState.TRIGGERED
+ self._attr_state = STATE_ALARM_TRIGGERED
else:
- self._attr_alarm_state = HIVETOHA[self.device["status"]["mode"]]
+ self._attr_state = HIVETOHA[self.device["status"]["mode"]]
diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py
index a997954f4cc..d6be2d1efab 100644
--- a/homeassistant/components/hive/config_flow.py
+++ b/homeassistant/components/hive/config_flow.py
@@ -182,6 +182,7 @@ class HiveOptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize Hive options flow."""
self.hive = None
+ self.config_entry = config_entry
self.interval = config_entry.options.get(CONF_SCAN_INTERVAL, 120)
async def async_step_init(
diff --git a/homeassistant/components/holiday/config_flow.py b/homeassistant/components/holiday/config_flow.py
index 27b13e34851..0284ac5c876 100644
--- a/homeassistant/components/holiday/config_flow.py
+++ b/homeassistant/components/holiday/config_flow.py
@@ -112,6 +112,12 @@ class HolidayConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle the re-configuration of a province."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the re-configuration of a province."""
reconfigure_entry = self._get_reconfigure_entry()
@@ -154,4 +160,6 @@ class HolidayConfigFlow(ConfigFlow, domain=DOMAIN):
}
)
- return self.async_show_form(step_id="reconfigure", data_schema=province_schema)
+ return self.async_show_form(
+ step_id="reconfigure_confirm", data_schema=province_schema
+ )
diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json
index 8c64f492d42..559f18b331a 100644
--- a/homeassistant/components/holiday/manifest.json
+++ b/homeassistant/components/holiday/manifest.json
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
- "requirements": ["holidays==0.60", "babel==2.15.0"]
+ "requirements": ["holidays==0.58", "babel==2.15.0"]
}
diff --git a/homeassistant/components/holiday/strings.json b/homeassistant/components/holiday/strings.json
index ae4930ecdb4..de013f44d60 100644
--- a/homeassistant/components/holiday/strings.json
+++ b/homeassistant/components/holiday/strings.json
@@ -16,7 +16,7 @@
"province": "Province"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"data": {
"province": "[%key:component::holiday::config::step::province::data::province%]"
}
diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py
index c60515eb57f..87f4bfa7799 100644
--- a/homeassistant/components/home_connect/__init__.py
+++ b/homeassistant/components/home_connect/__init__.py
@@ -10,7 +10,7 @@ from requests import HTTPError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_DEVICE_ID, Platform
+from homeassistant.const import ATTR_DEVICE_ID, CONF_DEVICE, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
config_entry_oauth2_flow,
@@ -79,14 +79,7 @@ SERVICE_PROGRAM_SCHEMA = vol.Any(
SERVICE_COMMAND_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_ID): str})
-PLATFORMS = [
- Platform.BINARY_SENSOR,
- Platform.LIGHT,
- Platform.NUMBER,
- Platform.SENSOR,
- Platform.SWITCH,
- Platform.TIME,
-]
+PLATFORMS = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH]
def _get_appliance_by_device_id(
@@ -94,7 +87,8 @@ def _get_appliance_by_device_id(
) -> api.HomeConnectDevice:
"""Return a Home Connect appliance instance given an device_id."""
for hc_api in hass.data[DOMAIN].values():
- for device in hc_api.devices:
+ for dev_dict in hc_api.devices:
+ device = dev_dict[CONF_DEVICE]
if device.device_id == device_id:
return device.appliance
raise ValueError(f"Appliance for device id {device_id} not found")
@@ -261,7 +255,9 @@ async def update_all_devices(hass: HomeAssistant, entry: ConfigEntry) -> None:
device_registry = dr.async_get(hass)
try:
await hass.async_add_executor_job(hc_api.get_devices)
- for device in hc_api.devices:
+ for device_dict in hc_api.devices:
+ device = device_dict["device"]
+
device_entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, device.appliance.haId)},
@@ -303,14 +299,3 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
_LOGGER.debug("Migration to version %s successful", config_entry.version)
return True
-
-
-def get_dict_from_home_connect_error(err: api.HomeConnectError) -> dict[str, Any]:
- """Return a dict from a Home Connect error."""
- return (
- err.args[0]
- if len(err.args) > 0 and isinstance(err.args[0], dict)
- else {"description": err.args[0]}
- if len(err.args) > 0 and isinstance(err.args[0], str)
- else {}
- )
diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py
index 453f926c402..4324edc8c1e 100644
--- a/homeassistant/components/home_connect/api.py
+++ b/homeassistant/components/home_connect/api.py
@@ -1,17 +1,50 @@
"""API for Home Connect bound to HASS OAuth."""
+from abc import abstractmethod
from asyncio import run_coroutine_threadsafe
import logging
+from typing import Any
import homeconnect
from homeconnect.api import HomeConnectAppliance, HomeConnectError
+from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_ICON,
+ CONF_DEVICE,
+ CONF_ENTITIES,
+ PERCENTAGE,
+ UnitOfTime,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.dispatcher import dispatcher_send
-from .const import ATTR_KEY, ATTR_VALUE, BSH_ACTIVE_PROGRAM, SIGNAL_UPDATE_ENTITIES
+from .const import (
+ ATTR_AMBIENT,
+ ATTR_BSH_KEY,
+ ATTR_DESC,
+ ATTR_DEVICE,
+ ATTR_KEY,
+ ATTR_SENSOR_TYPE,
+ ATTR_SIGN,
+ ATTR_UNIT,
+ ATTR_VALUE,
+ BSH_ACTIVE_PROGRAM,
+ BSH_AMBIENT_LIGHT_ENABLED,
+ BSH_COMMON_OPTION_DURATION,
+ BSH_COMMON_OPTION_PROGRAM_PROGRESS,
+ BSH_OPERATION_STATE,
+ BSH_POWER_OFF,
+ BSH_POWER_STANDBY,
+ BSH_REMAINING_PROGRAM_TIME,
+ BSH_REMOTE_CONTROL_ACTIVATION_STATE,
+ BSH_REMOTE_START_ALLOWANCE_STATE,
+ COOKING_LIGHTING,
+ SIGNAL_UPDATE_ENTITIES,
+)
_LOGGER = logging.getLogger(__name__)
@@ -32,7 +65,7 @@ class ConfigEntryAuth(homeconnect.HomeConnectAPI):
hass, config_entry, implementation
)
super().__init__(self.session.token)
- self.devices: list[HomeConnectDevice] = []
+ self.devices: list[dict[str, Any]] = []
def refresh_tokens(self) -> dict:
"""Refresh and return new Home Connect tokens using Home Assistant OAuth2 session."""
@@ -42,16 +75,55 @@ class ConfigEntryAuth(homeconnect.HomeConnectAPI):
return self.session.token
- def get_devices(self) -> list[HomeConnectAppliance]:
+ def get_devices(self) -> list[dict[str, Any]]:
"""Get a dictionary of devices."""
- appl: list[HomeConnectAppliance] = self.get_appliances()
- self.devices = [HomeConnectDevice(self.hass, app) for app in appl]
- return self.devices
+ appl = self.get_appliances()
+ devices = []
+ for app in appl:
+ device: HomeConnectDevice
+ if app.type == "Dryer":
+ device = Dryer(self.hass, app)
+ elif app.type == "Washer":
+ device = Washer(self.hass, app)
+ elif app.type == "WasherDryer":
+ device = WasherDryer(self.hass, app)
+ elif app.type == "Dishwasher":
+ device = Dishwasher(self.hass, app)
+ elif app.type == "FridgeFreezer":
+ device = FridgeFreezer(self.hass, app)
+ elif app.type == "Refrigerator":
+ device = Refrigerator(self.hass, app)
+ elif app.type == "Freezer":
+ device = Freezer(self.hass, app)
+ elif app.type == "Oven":
+ device = Oven(self.hass, app)
+ elif app.type == "CoffeeMaker":
+ device = CoffeeMaker(self.hass, app)
+ elif app.type == "Hood":
+ device = Hood(self.hass, app)
+ elif app.type == "Hob":
+ device = Hob(self.hass, app)
+ elif app.type == "CookProcessor":
+ device = CookProcessor(self.hass, app)
+ else:
+ _LOGGER.warning("Appliance type %s not implemented", app.type)
+ continue
+ devices.append(
+ {CONF_DEVICE: device, CONF_ENTITIES: device.get_entity_info()}
+ )
+ self.devices = devices
+ return devices
class HomeConnectDevice:
"""Generic Home Connect device."""
+ # for some devices, this is instead BSH_POWER_STANDBY
+ # see https://developer.home-connect.com/docs/settings/power_state
+ power_off_state = BSH_POWER_OFF
+ hass: HomeAssistant
+ appliance: HomeConnectAppliance
+
def __init__(self, hass: HomeAssistant, appliance: HomeConnectAppliance) -> None:
"""Initialize the device class."""
self.hass = hass
@@ -83,3 +155,378 @@ class HomeConnectDevice:
_LOGGER.debug("Update triggered on %s", appliance.name)
_LOGGER.debug(self.appliance.status)
dispatcher_send(self.hass, SIGNAL_UPDATE_ENTITIES, appliance.haId)
+
+ @abstractmethod
+ def get_entity_info(self) -> dict[str, list[dict[str, Any]]]:
+ """Get a dictionary with info about the associated entities."""
+ raise NotImplementedError
+
+
+class DeviceWithPrograms(HomeConnectDevice):
+ """Device with programs."""
+
+ def get_programs_available(self) -> list:
+ """Get the available programs."""
+ try:
+ programs_available = self.appliance.get_programs_available()
+ except (HomeConnectError, ValueError):
+ _LOGGER.debug("Unable to fetch available programs. Probably offline")
+ programs_available = []
+ return programs_available
+
+ def get_program_switches(self) -> list[dict[str, Any]]:
+ """Get a dictionary with info about program switches.
+
+ There will be one switch for each program.
+ """
+ programs = self.get_programs_available()
+ return [{ATTR_DEVICE: self, "program_name": p} for p in programs]
+
+ def get_program_sensors(self) -> list[dict[str, Any]]:
+ """Get a dictionary with info about program sensors.
+
+ There will be one of the four types of sensors for each
+ device.
+ """
+ sensors = {
+ BSH_REMAINING_PROGRAM_TIME: (
+ "Remaining Program Time",
+ None,
+ None,
+ SensorDeviceClass.TIMESTAMP,
+ 1,
+ ),
+ BSH_COMMON_OPTION_DURATION: (
+ "Duration",
+ UnitOfTime.SECONDS,
+ "mdi:update",
+ None,
+ 1,
+ ),
+ BSH_COMMON_OPTION_PROGRAM_PROGRESS: (
+ "Program Progress",
+ PERCENTAGE,
+ "mdi:progress-clock",
+ None,
+ 1,
+ ),
+ }
+ return [
+ {
+ ATTR_DEVICE: self,
+ ATTR_BSH_KEY: k,
+ ATTR_DESC: desc,
+ ATTR_UNIT: unit,
+ ATTR_ICON: icon,
+ ATTR_DEVICE_CLASS: device_class,
+ ATTR_SIGN: sign,
+ }
+ for k, (desc, unit, icon, device_class, sign) in sensors.items()
+ ]
+
+
+class DeviceWithOpState(HomeConnectDevice):
+ """Device that has an operation state sensor."""
+
+ def get_opstate_sensor(self) -> list[dict[str, Any]]:
+ """Get a list with info about operation state sensors."""
+
+ return [
+ {
+ ATTR_DEVICE: self,
+ ATTR_BSH_KEY: BSH_OPERATION_STATE,
+ ATTR_DESC: "Operation State",
+ ATTR_UNIT: None,
+ ATTR_ICON: "mdi:state-machine",
+ ATTR_DEVICE_CLASS: None,
+ ATTR_SIGN: 1,
+ }
+ ]
+
+
+class DeviceWithDoor(HomeConnectDevice):
+ """Device that has a door sensor."""
+
+ def get_door_entity(self) -> dict[str, Any]:
+ """Get a dictionary with info about the door binary sensor."""
+ return {
+ ATTR_DEVICE: self,
+ ATTR_BSH_KEY: "Door",
+ ATTR_DESC: "Door",
+ ATTR_SENSOR_TYPE: "door",
+ ATTR_DEVICE_CLASS: "door",
+ }
+
+
+class DeviceWithLight(HomeConnectDevice):
+ """Device that has lighting."""
+
+ def get_light_entity(self) -> dict[str, Any]:
+ """Get a dictionary with info about the lighting."""
+ return {
+ ATTR_DEVICE: self,
+ ATTR_BSH_KEY: COOKING_LIGHTING,
+ ATTR_DESC: "Light",
+ ATTR_AMBIENT: None,
+ }
+
+
+class DeviceWithAmbientLight(HomeConnectDevice):
+ """Device that has ambient lighting."""
+
+ def get_ambientlight_entity(self) -> dict[str, Any]:
+ """Get a dictionary with info about the ambient lighting."""
+ return {
+ ATTR_DEVICE: self,
+ ATTR_BSH_KEY: BSH_AMBIENT_LIGHT_ENABLED,
+ ATTR_DESC: "AmbientLight",
+ ATTR_AMBIENT: True,
+ }
+
+
+class DeviceWithRemoteControl(HomeConnectDevice):
+ """Device that has Remote Control binary sensor."""
+
+ def get_remote_control(self) -> dict[str, Any]:
+ """Get a dictionary with info about the remote control sensor."""
+ return {
+ ATTR_DEVICE: self,
+ ATTR_BSH_KEY: BSH_REMOTE_CONTROL_ACTIVATION_STATE,
+ ATTR_DESC: "Remote Control",
+ ATTR_SENSOR_TYPE: "remote_control",
+ }
+
+
+class DeviceWithRemoteStart(HomeConnectDevice):
+ """Device that has a Remote Start binary sensor."""
+
+ def get_remote_start(self) -> dict[str, Any]:
+ """Get a dictionary with info about the remote start sensor."""
+ return {
+ ATTR_DEVICE: self,
+ ATTR_BSH_KEY: BSH_REMOTE_START_ALLOWANCE_STATE,
+ ATTR_DESC: "Remote Start",
+ ATTR_SENSOR_TYPE: "remote_start",
+ }
+
+
+class Dryer(
+ DeviceWithDoor,
+ DeviceWithOpState,
+ DeviceWithPrograms,
+ DeviceWithRemoteControl,
+ DeviceWithRemoteStart,
+):
+ """Dryer class."""
+
+ def get_entity_info(self) -> dict[str, list[dict[str, Any]]]:
+ """Get a dictionary with infos about the associated entities."""
+ door_entity = self.get_door_entity()
+ remote_control = self.get_remote_control()
+ remote_start = self.get_remote_start()
+ op_state_sensor = self.get_opstate_sensor()
+ program_sensors = self.get_program_sensors()
+ program_switches = self.get_program_switches()
+ return {
+ "binary_sensor": [door_entity, remote_control, remote_start],
+ "switch": program_switches,
+ "sensor": program_sensors + op_state_sensor,
+ }
+
+
+class Dishwasher(
+ DeviceWithDoor,
+ DeviceWithAmbientLight,
+ DeviceWithOpState,
+ DeviceWithPrograms,
+ DeviceWithRemoteControl,
+ DeviceWithRemoteStart,
+):
+ """Dishwasher class."""
+
+ def get_entity_info(self) -> dict[str, list[dict[str, Any]]]:
+ """Get a dictionary with infos about the associated entities."""
+ door_entity = self.get_door_entity()
+ remote_control = self.get_remote_control()
+ remote_start = self.get_remote_start()
+ op_state_sensor = self.get_opstate_sensor()
+ program_sensors = self.get_program_sensors()
+ program_switches = self.get_program_switches()
+ return {
+ "binary_sensor": [door_entity, remote_control, remote_start],
+ "switch": program_switches,
+ "sensor": program_sensors + op_state_sensor,
+ }
+
+
+class Oven(
+ DeviceWithDoor,
+ DeviceWithOpState,
+ DeviceWithPrograms,
+ DeviceWithRemoteControl,
+ DeviceWithRemoteStart,
+):
+ """Oven class."""
+
+ power_off_state = BSH_POWER_STANDBY
+
+ def get_entity_info(self) -> dict[str, list[dict[str, Any]]]:
+ """Get a dictionary with infos about the associated entities."""
+ door_entity = self.get_door_entity()
+ remote_control = self.get_remote_control()
+ remote_start = self.get_remote_start()
+ op_state_sensor = self.get_opstate_sensor()
+ program_sensors = self.get_program_sensors()
+ program_switches = self.get_program_switches()
+ return {
+ "binary_sensor": [door_entity, remote_control, remote_start],
+ "switch": program_switches,
+ "sensor": program_sensors + op_state_sensor,
+ }
+
+
+class Washer(
+ DeviceWithDoor,
+ DeviceWithOpState,
+ DeviceWithPrograms,
+ DeviceWithRemoteControl,
+ DeviceWithRemoteStart,
+):
+ """Washer class."""
+
+ def get_entity_info(self) -> dict[str, list[dict[str, Any]]]:
+ """Get a dictionary with infos about the associated entities."""
+ door_entity = self.get_door_entity()
+ remote_control = self.get_remote_control()
+ remote_start = self.get_remote_start()
+ op_state_sensor = self.get_opstate_sensor()
+ program_sensors = self.get_program_sensors()
+ program_switches = self.get_program_switches()
+ return {
+ "binary_sensor": [door_entity, remote_control, remote_start],
+ "switch": program_switches,
+ "sensor": program_sensors + op_state_sensor,
+ }
+
+
+class WasherDryer(
+ DeviceWithDoor,
+ DeviceWithOpState,
+ DeviceWithPrograms,
+ DeviceWithRemoteControl,
+ DeviceWithRemoteStart,
+):
+ """WasherDryer class."""
+
+ def get_entity_info(self) -> dict[str, list[dict[str, Any]]]:
+ """Get a dictionary with infos about the associated entities."""
+ door_entity = self.get_door_entity()
+ remote_control = self.get_remote_control()
+ remote_start = self.get_remote_start()
+ op_state_sensor = self.get_opstate_sensor()
+ program_sensors = self.get_program_sensors()
+ program_switches = self.get_program_switches()
+ return {
+ "binary_sensor": [door_entity, remote_control, remote_start],
+ "switch": program_switches,
+ "sensor": program_sensors + op_state_sensor,
+ }
+
+
+class CoffeeMaker(DeviceWithOpState, DeviceWithPrograms, DeviceWithRemoteStart):
+ """Coffee maker class."""
+
+ power_off_state = BSH_POWER_STANDBY
+
+ def get_entity_info(self):
+ """Get a dictionary with infos about the associated entities."""
+ remote_start = self.get_remote_start()
+ op_state_sensor = self.get_opstate_sensor()
+ program_sensors = self.get_program_sensors()
+ program_switches = self.get_program_switches()
+ return {
+ "binary_sensor": [remote_start],
+ "switch": program_switches,
+ "sensor": program_sensors + op_state_sensor,
+ }
+
+
+class Hood(
+ DeviceWithLight,
+ DeviceWithAmbientLight,
+ DeviceWithOpState,
+ DeviceWithPrograms,
+ DeviceWithRemoteControl,
+ DeviceWithRemoteStart,
+):
+ """Hood class."""
+
+ def get_entity_info(self) -> dict[str, list[dict[str, Any]]]:
+ """Get a dictionary with infos about the associated entities."""
+ remote_control = self.get_remote_control()
+ remote_start = self.get_remote_start()
+ light_entity = self.get_light_entity()
+ ambientlight_entity = self.get_ambientlight_entity()
+ op_state_sensor = self.get_opstate_sensor()
+ program_sensors = self.get_program_sensors()
+ program_switches = self.get_program_switches()
+ return {
+ "binary_sensor": [remote_control, remote_start],
+ "switch": program_switches,
+ "sensor": program_sensors + op_state_sensor,
+ "light": [light_entity, ambientlight_entity],
+ }
+
+
+class FridgeFreezer(DeviceWithDoor):
+ """Fridge/Freezer class."""
+
+ def get_entity_info(self) -> dict[str, list[dict[str, Any]]]:
+ """Get a dictionary with infos about the associated entities."""
+ door_entity = self.get_door_entity()
+ return {"binary_sensor": [door_entity]}
+
+
+class Refrigerator(DeviceWithDoor):
+ """Refrigerator class."""
+
+ def get_entity_info(self) -> dict[str, list[dict[str, Any]]]:
+ """Get a dictionary with infos about the associated entities."""
+ door_entity = self.get_door_entity()
+ return {"binary_sensor": [door_entity]}
+
+
+class Freezer(DeviceWithDoor):
+ """Freezer class."""
+
+ def get_entity_info(self) -> dict[str, list[dict[str, Any]]]:
+ """Get a dictionary with infos about the associated entities."""
+ door_entity = self.get_door_entity()
+ return {"binary_sensor": [door_entity]}
+
+
+class Hob(DeviceWithOpState, DeviceWithPrograms, DeviceWithRemoteControl):
+ """Hob class."""
+
+ def get_entity_info(self) -> dict[str, list[dict[str, Any]]]:
+ """Get a dictionary with infos about the associated entities."""
+ remote_control = self.get_remote_control()
+ op_state_sensor = self.get_opstate_sensor()
+ program_sensors = self.get_program_sensors()
+ program_switches = self.get_program_switches()
+ return {
+ "binary_sensor": [remote_control],
+ "switch": program_switches,
+ "sensor": program_sensors + op_state_sensor,
+ }
+
+
+class CookProcessor(DeviceWithOpState):
+ """CookProcessor class."""
+
+ power_off_state = BSH_POWER_STANDBY
+
+ def get_entity_info(self) -> dict[str, list[dict[str, Any]]]:
+ """Get a dictionary with infos about the associated entities."""
+ op_state_sensor = self.get_opstate_sensor()
+ return {"sensor": op_state_sensor}
diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py
index 232b581d58b..7c99ee5421f 100644
--- a/homeassistant/components/home_connect/binary_sensor.py
+++ b/homeassistant/components/home_connect/binary_sensor.py
@@ -1,27 +1,21 @@
"""Provides a binary sensor for Home Connect."""
-from dataclasses import dataclass
+from dataclasses import dataclass, field
import logging
-from homeassistant.components.automation import automations_with_entity
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
-from homeassistant.components.script import scripts_with_entity
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_ENTITIES
from homeassistant.core import HomeAssistant
-from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.issue_registry import (
- IssueSeverity,
- async_create_issue,
- async_delete_issue,
-)
from .api import HomeConnectDevice
from .const import (
+ ATTR_DEVICE,
ATTR_VALUE,
BSH_DOOR_STATE,
BSH_DOOR_STATE_CLOSED,
@@ -39,79 +33,34 @@ from .const import (
from .entity import HomeConnectEntity
_LOGGER = logging.getLogger(__name__)
-REFRIGERATION_DOOR_BOOLEAN_MAP = {
- REFRIGERATION_STATUS_DOOR_CLOSED: False,
- REFRIGERATION_STATUS_DOOR_OPEN: True,
-}
@dataclass(frozen=True, kw_only=True)
class HomeConnectBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Entity Description class for binary sensors."""
- boolean_map: dict[str, bool] | None = None
+ desc: str
+ device_class: BinarySensorDeviceClass | None = BinarySensorDeviceClass.DOOR
+ boolean_map: dict[str, bool] = field(
+ default_factory=lambda: {
+ REFRIGERATION_STATUS_DOOR_CLOSED: False,
+ REFRIGERATION_STATUS_DOOR_OPEN: True,
+ }
+ )
-BINARY_SENSORS = (
- HomeConnectBinarySensorEntityDescription(
- key=BSH_REMOTE_CONTROL_ACTIVATION_STATE,
- translation_key="remote_control",
- ),
- HomeConnectBinarySensorEntityDescription(
- key=BSH_REMOTE_START_ALLOWANCE_STATE,
- translation_key="remote_start",
- ),
- HomeConnectBinarySensorEntityDescription(
- key="BSH.Common.Status.LocalControlActive",
- translation_key="local_control",
- ),
- HomeConnectBinarySensorEntityDescription(
- key="BSH.Common.Status.BatteryChargingState",
- device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
- boolean_map={
- "BSH.Common.EnumType.BatteryChargingState.Charging": True,
- "BSH.Common.EnumType.BatteryChargingState.Discharging": False,
- },
- translation_key="battery_charging_state",
- ),
- HomeConnectBinarySensorEntityDescription(
- key="BSH.Common.Status.ChargingConnection",
- device_class=BinarySensorDeviceClass.PLUG,
- boolean_map={
- "BSH.Common.EnumType.ChargingConnection.Connected": True,
- "BSH.Common.EnumType.ChargingConnection.Disconnected": False,
- },
- translation_key="charging_connection",
- ),
- HomeConnectBinarySensorEntityDescription(
- key="ConsumerProducts.CleaningRobot.Status.DustBoxInserted",
- translation_key="dust_box_inserted",
- ),
- HomeConnectBinarySensorEntityDescription(
- key="ConsumerProducts.CleaningRobot.Status.Lifted",
- translation_key="lifted",
- ),
- HomeConnectBinarySensorEntityDescription(
- key="ConsumerProducts.CleaningRobot.Status.Lost",
- translation_key="lost",
- ),
+BINARY_SENSORS: tuple[HomeConnectBinarySensorEntityDescription, ...] = (
HomeConnectBinarySensorEntityDescription(
key=REFRIGERATION_STATUS_DOOR_CHILLER,
- boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
- device_class=BinarySensorDeviceClass.DOOR,
- translation_key="chiller_door",
+ desc="Chiller Door",
),
HomeConnectBinarySensorEntityDescription(
key=REFRIGERATION_STATUS_DOOR_FREEZER,
- boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
- device_class=BinarySensorDeviceClass.DOOR,
- translation_key="freezer_door",
+ desc="Freezer Door",
),
HomeConnectBinarySensorEntityDescription(
key=REFRIGERATION_STATUS_DOOR_REFRIGERATOR,
- boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
- device_class=BinarySensorDeviceClass.DOOR,
- translation_key="refrigerator_door",
+ desc="Refrigerator Door",
),
)
@@ -126,14 +75,18 @@ async def async_setup_entry(
def get_entities() -> list[BinarySensorEntity]:
entities: list[BinarySensorEntity] = []
hc_api = hass.data[DOMAIN][config_entry.entry_id]
- for device in hc_api.devices:
+ for device_dict in hc_api.devices:
+ entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("binary_sensor", [])
+ entities += [HomeConnectBinarySensor(**d) for d in entity_dicts]
+ device: HomeConnectDevice = device_dict[ATTR_DEVICE]
+ # Auto-discover entities
entities.extend(
- HomeConnectBinarySensor(device, description)
+ HomeConnectFridgeDoorBinarySensor(
+ device=device, entity_description=description
+ )
for description in BINARY_SENSORS
if description.key in device.appliance.status
)
- if BSH_DOOR_STATE in device.appliance.status:
- entities.append(HomeConnectDoorBinarySensor(device))
return entities
async_add_entities(await hass.async_add_executor_job(get_entities), True)
@@ -142,7 +95,28 @@ async def async_setup_entry(
class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity):
"""Binary sensor for Home Connect."""
- entity_description: HomeConnectBinarySensorEntityDescription
+ def __init__(
+ self,
+ device: HomeConnectDevice,
+ bsh_key: str,
+ desc: str,
+ sensor_type: str,
+ device_class: BinarySensorDeviceClass | None = None,
+ ) -> None:
+ """Initialize the entity."""
+ super().__init__(device, bsh_key, desc)
+ self._attr_device_class = device_class
+ self._type = sensor_type
+ self._false_value_list = None
+ self._true_value_list = None
+ if self._type == "door":
+ self._update_key = BSH_DOOR_STATE
+ self._false_value_list = [BSH_DOOR_STATE_CLOSED, BSH_DOOR_STATE_LOCKED]
+ self._true_value_list = [BSH_DOOR_STATE_OPEN]
+ elif self._type == "remote_control":
+ self._update_key = BSH_REMOTE_CONTROL_ACTIVATION_STATE
+ elif self._type == "remote_start":
+ self._update_key = BSH_REMOTE_START_ALLOWANCE_STATE
@property
def available(self) -> bool:
@@ -151,90 +125,59 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity):
async def async_update(self) -> None:
"""Update the binary sensor's status."""
- if not self.device.appliance.status or not (
- status := self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE)
- ):
+ state = self.device.appliance.status.get(self._update_key, {})
+ if not state:
self._attr_is_on = None
return
- if self.entity_description.boolean_map:
- self._attr_is_on = self.entity_description.boolean_map.get(status)
- elif status not in [True, False]:
- self._attr_is_on = None
+
+ value = state.get(ATTR_VALUE)
+ if self._false_value_list and self._true_value_list:
+ if value in self._false_value_list:
+ self._attr_is_on = False
+ elif value in self._true_value_list:
+ self._attr_is_on = True
+ else:
+ _LOGGER.warning(
+ "Unexpected value for HomeConnect %s state: %s", self._type, state
+ )
+ self._attr_is_on = None
+ elif isinstance(value, bool):
+ self._attr_is_on = value
else:
- self._attr_is_on = status
+ _LOGGER.warning(
+ "Unexpected value for HomeConnect %s state: %s", self._type, state
+ )
+ self._attr_is_on = None
_LOGGER.debug("Updated, new state: %s", self._attr_is_on)
-class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
- """Binary sensor for Home Connect Generic Door."""
+class HomeConnectFridgeDoorBinarySensor(HomeConnectEntity, BinarySensorEntity):
+ """Binary sensor for Home Connect Fridge Doors."""
- _attr_has_entity_name = False
+ entity_description: HomeConnectBinarySensorEntityDescription
def __init__(
self,
device: HomeConnectDevice,
+ entity_description: HomeConnectBinarySensorEntityDescription,
) -> None:
"""Initialize the entity."""
- super().__init__(
- device,
- HomeConnectBinarySensorEntityDescription(
- key=BSH_DOOR_STATE,
- device_class=BinarySensorDeviceClass.DOOR,
- boolean_map={
- BSH_DOOR_STATE_CLOSED: False,
- BSH_DOOR_STATE_LOCKED: False,
- BSH_DOOR_STATE_OPEN: True,
- },
- ),
+ self.entity_description = entity_description
+ super().__init__(device, entity_description.key, entity_description.desc)
+
+ async def async_update(self) -> None:
+ """Update the binary sensor's status."""
+ _LOGGER.debug(
+ "Updating: %s, cur state: %s",
+ self._attr_unique_id,
+ self.state,
)
- self._attr_unique_id = f"{device.appliance.haId}-Door"
- self._attr_name = f"{device.appliance.name} Door"
-
- async def async_added_to_hass(self) -> None:
- """Call when entity is added to hass."""
- await super().async_added_to_hass()
- automations = automations_with_entity(self.hass, self.entity_id)
- scripts = scripts_with_entity(self.hass, self.entity_id)
- items = automations + scripts
- if not items:
- return
-
- entity_reg: er.EntityRegistry = er.async_get(self.hass)
- entity_automations = [
- automation_entity
- for automation_id in automations
- if (automation_entity := entity_reg.async_get(automation_id))
- ]
- entity_scripts = [
- script_entity
- for script_id in scripts
- if (script_entity := entity_reg.async_get(script_id))
- ]
-
- items_list = [
- f"- [{item.original_name}](/config/automation/edit/{item.unique_id})"
- for item in entity_automations
- ] + [
- f"- [{item.original_name}](/config/script/edit/{item.unique_id})"
- for item in entity_scripts
- ]
-
- async_create_issue(
- self.hass,
- DOMAIN,
- f"deprecated_binary_common_door_sensor_{self.entity_id}",
- breaks_in_ha_version="2025.5.0",
- is_fixable=False,
- severity=IssueSeverity.WARNING,
- translation_key="deprecated_binary_common_door_sensor",
- translation_placeholders={
- "entity": self.entity_id,
- "items": "\n".join(items_list),
- },
+ self._attr_is_on = self.entity_description.boolean_map.get(
+ self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE)
)
-
- async def async_will_remove_from_hass(self) -> None:
- """Call when entity will be removed from hass."""
- async_delete_issue(
- self.hass, DOMAIN, f"deprecated_binary_common_door_sensor_{self.entity_id}"
+ self._attr_available = self._attr_is_on is not None
+ _LOGGER.debug(
+ "Updated: %s, new state: %s",
+ self._attr_unique_id,
+ self.state,
)
diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py
index e49a56b9b97..1da9e517ad5 100644
--- a/homeassistant/components/home_connect/const.py
+++ b/homeassistant/components/home_connect/const.py
@@ -36,11 +36,6 @@ COFFEE_EVENT_BEAN_CONTAINER_EMPTY = (
COFFEE_EVENT_WATER_TANK_EMPTY = "ConsumerProducts.CoffeeMaker.Event.WaterTankEmpty"
COFFEE_EVENT_DRIP_TRAY_FULL = "ConsumerProducts.CoffeeMaker.Event.DripTrayFull"
-DISHWASHER_EVENT_SALT_NEARLY_EMPTY = "Dishcare.Dishwasher.Event.SaltNearlyEmpty"
-DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY = (
- "Dishcare.Dishwasher.Event.RinseAidNearlyEmpty"
-)
-
REFRIGERATION_INTERNAL_LIGHT_POWER = "Refrigeration.Common.Setting.Light.Internal.Power"
REFRIGERATION_INTERNAL_LIGHT_BRIGHTNESS = (
"Refrigeration.Common.Setting.Light.Internal.Brightness"
@@ -100,25 +95,17 @@ SERVICE_SELECT_PROGRAM = "select_program"
SERVICE_SETTING = "change_setting"
SERVICE_START_PROGRAM = "start_program"
-ATTR_ALLOWED_VALUES = "allowedvalues"
ATTR_AMBIENT = "ambient"
ATTR_BSH_KEY = "bsh_key"
-ATTR_CONSTRAINTS = "constraints"
ATTR_DESC = "desc"
ATTR_DEVICE = "device"
ATTR_KEY = "key"
ATTR_PROGRAM = "program"
ATTR_SENSOR_TYPE = "sensor_type"
ATTR_SIGN = "sign"
-ATTR_STEPSIZE = "stepsize"
ATTR_UNIT = "unit"
ATTR_VALUE = "value"
-SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME = "appliance_name"
-SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID = "entity_id"
-SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY = "setting_key"
-SVE_TRANSLATION_PLACEHOLDER_VALUE = "value"
-
OLD_NEW_UNIQUE_ID_SUFFIX_MAP = {
"ChildLock": BSH_CHILD_LOCK_STATE,
"Operation State": BSH_OPERATION_STATE,
diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py
index 0ae4a28b8d4..6cad310f76a 100644
--- a/homeassistant/components/home_connect/entity.py
+++ b/homeassistant/components/home_connect/entity.py
@@ -5,7 +5,7 @@ import logging
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.helpers.entity import Entity, EntityDescription
+from homeassistant.helpers.entity import Entity
from .api import HomeConnectDevice
from .const import DOMAIN, SIGNAL_UPDATE_ENTITIES
@@ -17,13 +17,13 @@ class HomeConnectEntity(Entity):
"""Generic Home Connect entity (base class)."""
_attr_should_poll = False
- _attr_has_entity_name = True
- def __init__(self, device: HomeConnectDevice, desc: EntityDescription) -> None:
+ def __init__(self, device: HomeConnectDevice, bsh_key: str, desc: str) -> None:
"""Initialize the entity."""
self.device = device
- self.entity_description = desc
- self._attr_unique_id = f"{device.appliance.haId}-{self.bsh_key}"
+ self.bsh_key = bsh_key
+ self._attr_name = f"{device.appliance.name} {desc}"
+ self._attr_unique_id = f"{device.appliance.haId}-{bsh_key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.appliance.haId)},
manufacturer=device.appliance.brand,
@@ -50,8 +50,3 @@ class HomeConnectEntity(Entity):
"""Update the entity."""
_LOGGER.debug("Entity update triggered on %s", self)
self.async_schedule_update_ha_state(True)
-
- @property
- def bsh_key(self) -> str:
- """Return the BSH key."""
- return self.entity_description.key
diff --git a/homeassistant/components/home_connect/icons.json b/homeassistant/components/home_connect/icons.json
index 166b2fe2c34..949b30919b5 100644
--- a/homeassistant/components/home_connect/icons.json
+++ b/homeassistant/components/home_connect/icons.json
@@ -23,135 +23,43 @@
}
},
"entity": {
- "binary_sensor": {
- "remote_control": {
- "default": "mdi:remote",
- "state": {
- "off": "mdi:remote-off"
- }
- },
- "remote_start": {
- "default": "mdi:remote",
- "state": {
- "off": "mdi:remote-off"
- }
- },
- "dust_box_inserted": {
- "default": "mdi:download"
- },
- "lifted": {
- "default": "mdi:arrow-up-right-bold"
- },
- "lost": {
- "default": "mdi:map-marker-remove-variant"
- }
- },
"sensor": {
- "operation_state": {
- "default": "mdi:state-machine",
- "state": {
- "inactive": "mdi:stop",
- "ready": "mdi:check-circle",
- "delayedstart": "mdi:progress-clock",
- "run": "mdi:play",
- "pause": "mdi:pause",
- "actionrequired": "mdi:gesture-tap",
- "finished": "mdi:flag-checkered",
- "error": "mdi:alert-circle",
- "aborting": "mdi:close-circle"
- }
- },
- "door": {
- "default": "mdi:door",
- "state": {
- "closed": "mdi:door-closed",
- "locked": "mdi:door-closed-lock",
- "open": "mdi:door-open"
- }
- },
- "program_progress": {
- "default": "mdi:progress-clock"
- },
- "coffee_counter": {
- "default": "mdi:coffee"
- },
- "powder_coffee_counter": {
- "default": "mdi:coffee"
- },
- "hot_water_counter": {
- "default": "mdi:cup-water"
- },
- "hot_water_cups_counter": {
- "default": "mdi:cup"
- },
- "hot_milk_counter": {
- "default": "mdi:cup"
- },
- "frothy_milk_counter": {
- "default": "mdi:cup"
- },
- "milk_counter": {
- "default": "mdi:cup"
- },
- "coffee_and_milk": {
- "default": "mdi:coffee"
- },
- "ristretto_espresso_counter": {
- "default": "mdi:coffee"
- },
- "camera_state": {
- "default": "mdi:camera",
- "state": {
- "disabled": "mdi:camera-off",
- "sleeping": "mdi:sleep",
- "error": "mdi:alert-circle-outline"
- }
- },
- "last_selected_map": {
- "default": "mdi:map",
- "state": {
- "tempmap": "mdi:map-clock-outline",
- "map1": "mdi:numeric-1",
- "map2": "mdi:numeric-2",
- "map3": "mdi:numeric-3"
- }
- },
- "refrigerator_door_alarm": {
+ "alarm_sensor_fridge": {
"default": "mdi:fridge",
"state": {
"confirmed": "mdi:fridge-alert-outline",
"present": "mdi:fridge-alert"
}
},
- "freezer_door_alarm": {
+ "alarm_sensor_freezer": {
"default": "mdi:snowflake",
"state": {
"confirmed": "mdi:snowflake-check",
"present": "mdi:snowflake-alert"
}
},
- "freezer_temperature_alarm": {
+ "alarm_sensor_temp": {
"default": "mdi:thermometer",
"state": {
"confirmed": "mdi:thermometer-check",
"present": "mdi:thermometer-alert"
}
},
- "bean_container_empty": {
+ "alarm_sensor_coffee_bean_container": {
"default": "mdi:coffee-maker",
"state": {
"confirmed": "mdi:coffee-maker-check",
"present": "mdi:coffee-maker-outline"
}
},
- "water_tank_empty": {
+ "alarm_sensor_coffee_water_tank": {
"default": "mdi:water",
"state": {
"confirmed": "mdi:water-check",
"present": "mdi:water-alert"
}
},
- "drip_tray_full": {
+ "alarm_sensor_coffee_drip_tray": {
"default": "mdi:tray",
"state": {
"confirmed": "mdi:tray-full",
@@ -160,51 +68,11 @@
}
},
"switch": {
- "power": {
- "default": "mdi:power"
- },
- "child_lock": {
- "default": "mdi:lock",
- "state": {
- "on": "mdi:lock",
- "off": "mdi:lock-off"
- }
- },
- "cup_warmer": {
- "default": "mdi:heat-wave"
- },
- "refrigerator_super_mode": {
- "default": "mdi:speedometer"
- },
- "freezer_super_mode": {
- "default": "mdi:speedometer"
- },
- "eco_mode": {
- "default": "mdi:sprout"
- },
- "cooking-oven-setting-sabbath_mode": {
- "default": "mdi:volume-mute"
- },
- "sabbath_mode": {
- "default": "mdi:volume-mute"
- },
- "vacation_mode": {
- "default": "mdi:beach"
- },
- "fresh_mode": {
- "default": "mdi:leaf"
- },
- "dispenser_enabled": {
+ "refrigeration_dispenser": {
"default": "mdi:snowflake",
"state": {
"off": "mdi:snowflake-off"
}
- },
- "door-assistant_fridge": {
- "default": "mdi:door"
- },
- "door-assistant_freezer": {
- "default": "mdi:door"
}
}
}
diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py
index 873e7d24f93..7f6ea1bb4be 100644
--- a/homeassistant/components/home_connect/light.py
+++ b/homeassistant/components/home_connect/light.py
@@ -10,34 +10,29 @@ from homeconnect.api import HomeConnectError
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_HS_COLOR,
- ATTR_RGB_COLOR,
ColorMode,
LightEntity,
LightEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_DEVICE, CONF_ENTITIES
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.color as color_util
-from . import get_dict_from_home_connect_error
-from .api import ConfigEntryAuth, HomeConnectDevice
+from .api import HomeConnectDevice
from .const import (
ATTR_VALUE,
BSH_AMBIENT_LIGHT_BRIGHTNESS,
BSH_AMBIENT_LIGHT_COLOR,
BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
BSH_AMBIENT_LIGHT_CUSTOM_COLOR,
- BSH_AMBIENT_LIGHT_ENABLED,
- COOKING_LIGHTING,
COOKING_LIGHTING_BRIGHTNESS,
DOMAIN,
REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS,
REFRIGERATION_EXTERNAL_LIGHT_POWER,
REFRIGERATION_INTERNAL_LIGHT_BRIGHTNESS,
REFRIGERATION_INTERNAL_LIGHT_POWER,
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
)
from .entity import HomeConnectEntity
@@ -48,40 +43,20 @@ _LOGGER = logging.getLogger(__name__)
class HomeConnectLightEntityDescription(LightEntityDescription):
"""Light entity description."""
- brightness_key: str | None = None
- color_key: str | None = None
- enable_custom_color_value_key: str | None = None
- custom_color_key: str | None = None
- brightness_scale: tuple[float, float] = (0.0, 100.0)
+ desc: str
+ brightness_key: str | None
LIGHTS: tuple[HomeConnectLightEntityDescription, ...] = (
HomeConnectLightEntityDescription(
key=REFRIGERATION_INTERNAL_LIGHT_POWER,
+ desc="Internal Light",
brightness_key=REFRIGERATION_INTERNAL_LIGHT_BRIGHTNESS,
- brightness_scale=(1.0, 100.0),
- translation_key="internal_light",
),
HomeConnectLightEntityDescription(
key=REFRIGERATION_EXTERNAL_LIGHT_POWER,
+ desc="External Light",
brightness_key=REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS,
- brightness_scale=(1.0, 100.0),
- translation_key="external_light",
- ),
- HomeConnectLightEntityDescription(
- key=COOKING_LIGHTING,
- brightness_key=COOKING_LIGHTING_BRIGHTNESS,
- brightness_scale=(10.0, 100.0),
- translation_key="cooking_lighting",
- ),
- HomeConnectLightEntityDescription(
- key=BSH_AMBIENT_LIGHT_ENABLED,
- brightness_key=BSH_AMBIENT_LIGHT_BRIGHTNESS,
- color_key=BSH_AMBIENT_LIGHT_COLOR,
- enable_custom_color_value_key=BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
- custom_color_key=BSH_AMBIENT_LIGHT_CUSTOM_COLOR,
- brightness_scale=(10.0, 100.0),
- translation_key="ambient_light",
),
)
@@ -95,13 +70,24 @@ async def async_setup_entry(
def get_entities() -> list[LightEntity]:
"""Get a list of entities."""
- hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id]
- return [
- HomeConnectLight(device, description)
- for description in LIGHTS
- for device in hc_api.devices
- if description.key in device.appliance.status
- ]
+ entities: list[LightEntity] = []
+ hc_api = hass.data[DOMAIN][config_entry.entry_id]
+ for device_dict in hc_api.devices:
+ entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("light", [])
+ entity_list = [HomeConnectLight(**d) for d in entity_dicts]
+ device: HomeConnectDevice = device_dict[CONF_DEVICE]
+ # Auto-discover entities
+ entities.extend(
+ HomeConnectCoolingLight(
+ device=device,
+ ambient=False,
+ entity_description=description,
+ )
+ for description in LIGHTS
+ if description.key in device.appliance.status
+ )
+ entities.extend(entity_list)
+ return entities
async_add_entities(await hass.async_add_executor_job(get_entities), True)
@@ -109,128 +95,78 @@ async def async_setup_entry(
class HomeConnectLight(HomeConnectEntity, LightEntity):
"""Light for Home Connect."""
- entity_description: LightEntityDescription
-
def __init__(
- self, device: HomeConnectDevice, desc: HomeConnectLightEntityDescription
+ self, device: HomeConnectDevice, bsh_key: str, desc: str, ambient: bool
) -> None:
"""Initialize the entity."""
- super().__init__(device, desc)
-
- def get_setting_key_if_setting_exists(setting_key: str | None) -> str | None:
- if setting_key and setting_key in device.appliance.status:
- return setting_key
- return None
-
- self._brightness_key = get_setting_key_if_setting_exists(desc.brightness_key)
- self._custom_color_key = get_setting_key_if_setting_exists(
- desc.custom_color_key
- )
- self._color_key = get_setting_key_if_setting_exists(desc.color_key)
- self._enable_custom_color_value_key = desc.enable_custom_color_value_key
- self._custom_color_key = get_setting_key_if_setting_exists(
- desc.custom_color_key
- )
- self._brightness_scale = desc.brightness_scale
-
- match (self._brightness_key, self._custom_color_key):
- case (None, None):
- self._attr_color_mode = ColorMode.ONOFF
- self._attr_supported_color_modes = {ColorMode.ONOFF}
- case (_, None):
- self._attr_color_mode = ColorMode.BRIGHTNESS
- self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
- case (_, _):
- self._attr_color_mode = ColorMode.HS
- self._attr_supported_color_modes = {ColorMode.HS, ColorMode.RGB}
+ super().__init__(device, bsh_key, desc)
+ self._ambient = ambient
+ self._percentage_scale = (10, 100)
+ self._brightness_key: str | None
+ self._custom_color_key: str | None
+ self._color_key: str | None
+ if ambient:
+ self._brightness_key = BSH_AMBIENT_LIGHT_BRIGHTNESS
+ self._custom_color_key = BSH_AMBIENT_LIGHT_CUSTOM_COLOR
+ self._color_key = BSH_AMBIENT_LIGHT_COLOR
+ self._attr_color_mode = ColorMode.HS
+ self._attr_supported_color_modes = {ColorMode.HS}
+ else:
+ self._brightness_key = COOKING_LIGHTING_BRIGHTNESS
+ self._custom_color_key = None
+ self._color_key = None
+ self._attr_color_mode = ColorMode.BRIGHTNESS
+ self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
async def async_turn_on(self, **kwargs: Any) -> None:
"""Switch the light on, change brightness, change color."""
- _LOGGER.debug("Switching light on for: %s", self.name)
- try:
- await self.hass.async_add_executor_job(
- self.device.appliance.set_setting, self.bsh_key, True
- )
- except HomeConnectError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="turn_on_light",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
- },
- ) from err
- if self._custom_color_key:
- if (
- ATTR_RGB_COLOR in kwargs or ATTR_HS_COLOR in kwargs
- ) and self._enable_custom_color_value_key:
+ if self._ambient:
+ _LOGGER.debug("Switching ambient light on for: %s", self.name)
+ try:
+ await self.hass.async_add_executor_job(
+ self.device.appliance.set_setting, self.bsh_key, True
+ )
+ except HomeConnectError as err:
+ _LOGGER.error("Error while trying to turn on ambient light: %s", err)
+ return
+ if ATTR_BRIGHTNESS in kwargs or ATTR_HS_COLOR in kwargs:
try:
await self.hass.async_add_executor_job(
self.device.appliance.set_setting,
self._color_key,
- self._enable_custom_color_value_key,
+ BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
)
except HomeConnectError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="select_light_custom_color",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
- },
- ) from err
+ _LOGGER.error("Error while trying selecting customcolor: %s", err)
+ if self._attr_brightness is not None:
+ brightness_arg = self._attr_brightness
+ if ATTR_BRIGHTNESS in kwargs:
+ brightness_arg = kwargs[ATTR_BRIGHTNESS]
- if ATTR_RGB_COLOR in kwargs:
- hex_val = color_util.color_rgb_to_hex(*kwargs[ATTR_RGB_COLOR])
- try:
- await self.hass.async_add_executor_job(
- self.device.appliance.set_setting,
- self._custom_color_key,
- f"#{hex_val}",
- )
- except HomeConnectError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="set_light_color",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
- },
- ) from err
- elif (ATTR_BRIGHTNESS in kwargs or ATTR_HS_COLOR in kwargs) and (
- self._attr_brightness is not None or ATTR_BRIGHTNESS in kwargs
- ):
- brightness = 10 + ceil(
- color_util.brightness_to_value(
- self._brightness_scale,
- kwargs.get(ATTR_BRIGHTNESS, self._attr_brightness),
- )
- )
-
- hs_color = kwargs.get(ATTR_HS_COLOR, self._attr_hs_color)
-
- if hs_color is not None:
- rgb = color_util.color_hsv_to_RGB(
- hs_color[0], hs_color[1], brightness
- )
- hex_val = color_util.color_rgb_to_hex(*rgb)
- try:
- await self.hass.async_add_executor_job(
- self.device.appliance.set_setting,
- self._custom_color_key,
- f"#{hex_val}",
+ brightness = ceil(
+ color_util.brightness_to_value(
+ self._percentage_scale, brightness_arg
)
- except HomeConnectError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="set_light_color",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
- },
- ) from err
+ )
+ hs_color = kwargs.get(ATTR_HS_COLOR, self._attr_hs_color)
- elif self._brightness_key and ATTR_BRIGHTNESS in kwargs:
+ if hs_color is not None:
+ rgb = color_util.color_hsv_to_RGB(
+ hs_color[0], hs_color[1], brightness
+ )
+ hex_val = color_util.color_rgb_to_hex(rgb[0], rgb[1], rgb[2])
+ try:
+ await self.hass.async_add_executor_job(
+ self.device.appliance.set_setting,
+ self._custom_color_key,
+ f"#{hex_val}",
+ )
+ except HomeConnectError as err:
+ _LOGGER.error(
+ "Error while trying setting the color: %s", err
+ )
+
+ elif ATTR_BRIGHTNESS in kwargs:
_LOGGER.debug(
"Changing brightness for: %s, to: %s",
self.name,
@@ -238,7 +174,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
)
brightness = ceil(
color_util.brightness_to_value(
- self._brightness_scale, kwargs[ATTR_BRIGHTNESS]
+ self._percentage_scale, kwargs[ATTR_BRIGHTNESS]
)
)
try:
@@ -246,14 +182,15 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
self.device.appliance.set_setting, self._brightness_key, brightness
)
except HomeConnectError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="set_light_brightness",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
- },
- ) from err
+ _LOGGER.error("Error while trying set the brightness: %s", err)
+ else:
+ _LOGGER.debug("Switching light on for: %s", self.name)
+ try:
+ await self.hass.async_add_executor_job(
+ self.device.appliance.set_setting, self.bsh_key, True
+ )
+ except HomeConnectError as err:
+ _LOGGER.error("Error while trying to turn on light: %s", err)
self.async_entity_update()
@@ -265,14 +202,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
self.device.appliance.set_setting, self.bsh_key, False
)
except HomeConnectError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="turn_off_light",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
- },
- ) from err
+ _LOGGER.error("Error while trying to turn off light: %s", err)
self.async_entity_update()
async def async_update(self) -> None:
@@ -288,33 +218,46 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
_LOGGER.debug("Updated, new light state: %s", self._attr_is_on)
- if self._custom_color_key:
+ if self._ambient:
color = self.device.appliance.status.get(self._custom_color_key, {})
if not color:
- self._attr_rgb_color = None
self._attr_hs_color = None
self._attr_brightness = None
else:
- color_value = color.get(ATTR_VALUE)[1:]
- rgb = color_util.rgb_hex_to_rgb_list(color_value)
- self._attr_rgb_color = (rgb[0], rgb[1], rgb[2])
- hsv = color_util.color_RGB_to_hsv(*rgb)
+ colorvalue = color.get(ATTR_VALUE)[1:]
+ rgb = color_util.rgb_hex_to_rgb_list(colorvalue)
+ hsv = color_util.color_RGB_to_hsv(rgb[0], rgb[1], rgb[2])
self._attr_hs_color = (hsv[0], hsv[1])
self._attr_brightness = color_util.value_to_brightness(
- self._brightness_scale, hsv[2]
+ self._percentage_scale, hsv[2]
)
- _LOGGER.debug(
- "Updated, new color (%s) and new brightness (%s) ",
- color_value,
- self._attr_brightness,
- )
- elif self._brightness_key:
+ _LOGGER.debug("Updated, new brightness: %s", self._attr_brightness)
+
+ else:
brightness = self.device.appliance.status.get(self._brightness_key, {})
if brightness is None:
self._attr_brightness = None
else:
self._attr_brightness = color_util.value_to_brightness(
- self._brightness_scale, brightness[ATTR_VALUE]
+ self._percentage_scale, brightness[ATTR_VALUE]
)
_LOGGER.debug("Updated, new brightness: %s", self._attr_brightness)
+
+
+class HomeConnectCoolingLight(HomeConnectLight):
+ """Light entity for Cooling Appliances."""
+
+ def __init__(
+ self,
+ device: HomeConnectDevice,
+ ambient: bool,
+ entity_description: HomeConnectLightEntityDescription,
+ ) -> None:
+ """Initialize Cooling Light Entity."""
+ super().__init__(
+ device, entity_description.key, entity_description.desc, ambient
+ )
+ self.entity_description = entity_description
+ self._brightness_key = entity_description.brightness_key
+ self._percentage_scale = (1, 100)
diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json
index e041e13d36b..389386e42af 100644
--- a/homeassistant/components/home_connect/manifest.json
+++ b/homeassistant/components/home_connect/manifest.json
@@ -1,7 +1,7 @@
{
"domain": "home_connect",
"name": "Home Connect",
- "codeowners": ["@DavidMStraub", "@Diegorro98"],
+ "codeowners": ["@DavidMStraub"],
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/home_connect",
diff --git a/homeassistant/components/home_connect/number.py b/homeassistant/components/home_connect/number.py
deleted file mode 100644
index ad853df77d0..00000000000
--- a/homeassistant/components/home_connect/number.py
+++ /dev/null
@@ -1,167 +0,0 @@
-"""Provides number enties for Home Connect."""
-
-import logging
-
-from homeconnect.api import HomeConnectError
-
-from homeassistant.components.number import (
- ATTR_MAX,
- ATTR_MIN,
- NumberDeviceClass,
- NumberEntity,
- NumberEntityDescription,
-)
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ServiceValidationError
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import get_dict_from_home_connect_error
-from .api import ConfigEntryAuth
-from .const import (
- ATTR_CONSTRAINTS,
- ATTR_STEPSIZE,
- ATTR_UNIT,
- ATTR_VALUE,
- DOMAIN,
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
- SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY,
- SVE_TRANSLATION_PLACEHOLDER_VALUE,
-)
-from .entity import HomeConnectEntity
-
-_LOGGER = logging.getLogger(__name__)
-
-
-NUMBERS = (
- NumberEntityDescription(
- key="Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator",
- device_class=NumberDeviceClass.TEMPERATURE,
- translation_key="refrigerator_setpoint_temperature",
- ),
- NumberEntityDescription(
- key="Refrigeration.FridgeFreezer.Setting.SetpointTemperatureFreezer",
- device_class=NumberDeviceClass.TEMPERATURE,
- translation_key="freezer_setpoint_temperature",
- ),
- NumberEntityDescription(
- key="Refrigeration.Common.Setting.BottleCooler.SetpointTemperature",
- device_class=NumberDeviceClass.TEMPERATURE,
- translation_key="bottle_cooler_setpoint_temperature",
- ),
- NumberEntityDescription(
- key="Refrigeration.Common.Setting.ChillerLeft.SetpointTemperature",
- device_class=NumberDeviceClass.TEMPERATURE,
- translation_key="chiller_left_setpoint_temperature",
- ),
- NumberEntityDescription(
- key="Refrigeration.Common.Setting.ChillerCommon.SetpointTemperature",
- device_class=NumberDeviceClass.TEMPERATURE,
- translation_key="chiller_setpoint_temperature",
- ),
- NumberEntityDescription(
- key="Refrigeration.Common.Setting.ChillerRight.SetpointTemperature",
- device_class=NumberDeviceClass.TEMPERATURE,
- translation_key="chiller_right_setpoint_temperature",
- ),
- NumberEntityDescription(
- key="Refrigeration.Common.Setting.WineCompartment.SetpointTemperature",
- device_class=NumberDeviceClass.TEMPERATURE,
- translation_key="wine_compartment_setpoint_temperature",
- ),
- NumberEntityDescription(
- key="Refrigeration.Common.Setting.WineCompartment2.SetpointTemperature",
- device_class=NumberDeviceClass.TEMPERATURE,
- translation_key="wine_compartment_2_setpoint_temperature",
- ),
- NumberEntityDescription(
- key="Refrigeration.Common.Setting.WineCompartment3.SetpointTemperature",
- device_class=NumberDeviceClass.TEMPERATURE,
- translation_key="wine_compartment_3_setpoint_temperature",
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the Home Connect number."""
-
- def get_entities() -> list[HomeConnectNumberEntity]:
- """Get a list of entities."""
- hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id]
- return [
- HomeConnectNumberEntity(device, description)
- for description in NUMBERS
- for device in hc_api.devices
- if description.key in device.appliance.status
- ]
-
- async_add_entities(await hass.async_add_executor_job(get_entities), True)
-
-
-class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
- """Number setting class for Home Connect."""
-
- async def async_set_native_value(self, value: float) -> None:
- """Set the native value of the entity."""
- _LOGGER.debug(
- "Tried to set value %s to %s for %s",
- value,
- self.bsh_key,
- self.entity_id,
- )
- try:
- await self.hass.async_add_executor_job(
- self.device.appliance.set_setting,
- self.bsh_key,
- value,
- )
- except HomeConnectError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="set_setting",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
- SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY: self.bsh_key,
- SVE_TRANSLATION_PLACEHOLDER_VALUE: str(value),
- },
- ) from err
-
- async def async_fetch_constraints(self) -> None:
- """Fetch the max and min values and step for the number entity."""
- try:
- data = await self.hass.async_add_executor_job(
- self.device.appliance.get, f"/settings/{self.bsh_key}"
- )
- except HomeConnectError as err:
- _LOGGER.error("An error occurred: %s", err)
- return
- if not data or not (constraints := data.get(ATTR_CONSTRAINTS)):
- return
- self._attr_native_max_value = constraints.get(ATTR_MAX)
- self._attr_native_min_value = constraints.get(ATTR_MIN)
- self._attr_native_step = constraints.get(ATTR_STEPSIZE)
- self._attr_native_unit_of_measurement = data.get(ATTR_UNIT)
-
- async def async_update(self) -> None:
- """Update the number setting status."""
- if not (data := self.device.appliance.status.get(self.bsh_key)):
- _LOGGER.error("No value for %s", self.bsh_key)
- self._attr_native_value = None
- return
- self._attr_native_value = data.get(ATTR_VALUE, None)
- _LOGGER.debug("Updated, new value: %s", self._attr_native_value)
-
- if (
- not hasattr(self, "_attr_native_min_value")
- or self._attr_native_min_value is None
- or not hasattr(self, "_attr_native_max_value")
- or self._attr_native_max_value is None
- or not hasattr(self, "_attr_native_step")
- or self._attr_native_step is None
- ):
- await self.async_fetch_constraints()
diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py
index 70096313d86..599156a6b3a 100644
--- a/homeassistant/components/home_connect/sensor.py
+++ b/homeassistant/components/home_connect/sensor.py
@@ -1,30 +1,26 @@
"""Provides a sensor for Home Connect."""
-import contextlib
-from dataclasses import dataclass
+from dataclasses import dataclass, field
from datetime import datetime, timedelta
import logging
from typing import cast
-from homeconnect.api import HomeConnectError
-
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
- SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import PERCENTAGE, UnitOfTime, UnitOfVolume
+from homeassistant.const import CONF_ENTITIES
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.util import slugify
import homeassistant.util.dt as dt_util
-from .api import ConfigEntryAuth
+from .api import ConfigEntryAuth, HomeConnectDevice
from .const import (
+ ATTR_DEVICE,
ATTR_VALUE,
- BSH_DOOR_STATE,
+ BSH_EVENT_PRESENT_STATE_OFF,
BSH_OPERATION_STATE,
BSH_OPERATION_STATE_FINISHED,
BSH_OPERATION_STATE_PAUSE,
@@ -32,8 +28,6 @@ from .const import (
COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
COFFEE_EVENT_DRIP_TRAY_FULL,
COFFEE_EVENT_WATER_TANK_EMPTY,
- DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY,
- DISHWASHER_EVENT_SALT_NEARLY_EMPTY,
DOMAIN,
REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR,
@@ -44,210 +38,49 @@ from .entity import HomeConnectEntity
_LOGGER = logging.getLogger(__name__)
-EVENT_OPTIONS = ["confirmed", "off", "present"]
-
-
@dataclass(frozen=True, kw_only=True)
class HomeConnectSensorEntityDescription(SensorEntityDescription):
"""Entity Description class for sensors."""
- default_value: str | None = None
- appliance_types: tuple[str, ...] | None = None
- sign: int = 1
+ device_class: SensorDeviceClass | None = SensorDeviceClass.ENUM
+ options: list[str] | None = field(
+ default_factory=lambda: ["confirmed", "off", "present"]
+ )
+ desc: str
+ appliance_types: tuple[str, ...]
-BSH_PROGRAM_SENSORS = (
- HomeConnectSensorEntityDescription(
- key="BSH.Common.Option.RemainingProgramTime",
- device_class=SensorDeviceClass.TIMESTAMP,
- sign=1,
- translation_key="program_finish_time",
- ),
- HomeConnectSensorEntityDescription(
- key="BSH.Common.Option.Duration",
- device_class=SensorDeviceClass.DURATION,
- native_unit_of_measurement=UnitOfTime.SECONDS,
- sign=1,
- ),
- HomeConnectSensorEntityDescription(
- key="BSH.Common.Option.ProgramProgress",
- native_unit_of_measurement=PERCENTAGE,
- sign=1,
- translation_key="program_progress",
- ),
-)
-
-SENSORS = (
- HomeConnectSensorEntityDescription(
- key=BSH_OPERATION_STATE,
- device_class=SensorDeviceClass.ENUM,
- options=[
- "inactive",
- "ready",
- "delayedstart",
- "run",
- "pause",
- "actionrequired",
- "finished",
- "error",
- "aborting",
- ],
- translation_key="operation_state",
- ),
- HomeConnectSensorEntityDescription(
- key=BSH_DOOR_STATE,
- device_class=SensorDeviceClass.ENUM,
- options=[
- "closed",
- "locked",
- "open",
- ],
- translation_key="door",
- ),
- HomeConnectSensorEntityDescription(
- key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterCoffee",
- state_class=SensorStateClass.TOTAL_INCREASING,
- translation_key="coffee_counter",
- ),
- HomeConnectSensorEntityDescription(
- key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterPowderCoffee",
- state_class=SensorStateClass.TOTAL_INCREASING,
- translation_key="powder_coffee_counter",
- ),
- HomeConnectSensorEntityDescription(
- key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotWater",
- native_unit_of_measurement=UnitOfVolume.MILLILITERS,
- device_class=SensorDeviceClass.VOLUME,
- state_class=SensorStateClass.TOTAL_INCREASING,
- translation_key="hot_water_counter",
- ),
- HomeConnectSensorEntityDescription(
- key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotWaterCups",
- state_class=SensorStateClass.TOTAL_INCREASING,
- translation_key="hot_water_cups_counter",
- ),
- HomeConnectSensorEntityDescription(
- key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotMilk",
- state_class=SensorStateClass.TOTAL_INCREASING,
- translation_key="hot_milk_counter",
- ),
- HomeConnectSensorEntityDescription(
- key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterFrothyMilk",
- state_class=SensorStateClass.TOTAL_INCREASING,
- translation_key="frothy_milk_counter",
- ),
- HomeConnectSensorEntityDescription(
- key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterMilk",
- state_class=SensorStateClass.TOTAL_INCREASING,
- translation_key="milk_counter",
- ),
- HomeConnectSensorEntityDescription(
- key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterCoffeeAndMilk",
- state_class=SensorStateClass.TOTAL_INCREASING,
- translation_key="coffee_and_milk_counter",
- ),
- HomeConnectSensorEntityDescription(
- key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterRistrettoEspresso",
- state_class=SensorStateClass.TOTAL_INCREASING,
- translation_key="ristretto_espresso_counter",
- ),
- HomeConnectSensorEntityDescription(
- key="BSH.Common.Status.BatteryLevel",
- device_class=SensorDeviceClass.BATTERY,
- translation_key="battery_level",
- ),
- HomeConnectSensorEntityDescription(
- key="BSH.Common.Status.Video.CameraState",
- device_class=SensorDeviceClass.ENUM,
- options=[
- "disabled",
- "sleeping",
- "ready",
- "streaminglocal",
- "streamingcloud",
- "streaminglocalancloud",
- "error",
- ],
- translation_key="camera_state",
- ),
- HomeConnectSensorEntityDescription(
- key="ConsumerProducts.CleaningRobot.Status.LastSelectedMap",
- device_class=SensorDeviceClass.ENUM,
- options=[
- "tempmap",
- "map1",
- "map2",
- "map3",
- ],
- translation_key="last_selected_map",
- ),
-)
-
-EVENT_SENSORS = (
+SENSORS: tuple[HomeConnectSensorEntityDescription, ...] = (
HomeConnectSensorEntityDescription(
key=REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
- device_class=SensorDeviceClass.ENUM,
- options=EVENT_OPTIONS,
- default_value="off",
- translation_key="freezer_door_alarm",
+ desc="Door Alarm Freezer",
appliance_types=("FridgeFreezer", "Freezer"),
),
HomeConnectSensorEntityDescription(
key=REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR,
- device_class=SensorDeviceClass.ENUM,
- options=EVENT_OPTIONS,
- default_value="off",
- translation_key="refrigerator_door_alarm",
+ desc="Door Alarm Refrigerator",
appliance_types=("FridgeFreezer", "Refrigerator"),
),
HomeConnectSensorEntityDescription(
key=REFRIGERATION_EVENT_TEMP_ALARM_FREEZER,
- device_class=SensorDeviceClass.ENUM,
- options=EVENT_OPTIONS,
- default_value="off",
- translation_key="freezer_temperature_alarm",
+ desc="Temperature Alarm Freezer",
appliance_types=("FridgeFreezer", "Freezer"),
),
HomeConnectSensorEntityDescription(
key=COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
- device_class=SensorDeviceClass.ENUM,
- options=EVENT_OPTIONS,
- default_value="off",
- translation_key="bean_container_empty",
+ desc="Bean Container Empty",
appliance_types=("CoffeeMaker",),
),
HomeConnectSensorEntityDescription(
key=COFFEE_EVENT_WATER_TANK_EMPTY,
- device_class=SensorDeviceClass.ENUM,
- options=EVENT_OPTIONS,
- default_value="off",
- translation_key="water_tank_empty",
+ desc="Water Tank Empty",
appliance_types=("CoffeeMaker",),
),
HomeConnectSensorEntityDescription(
key=COFFEE_EVENT_DRIP_TRAY_FULL,
- device_class=SensorDeviceClass.ENUM,
- options=EVENT_OPTIONS,
- default_value="off",
- translation_key="drip_tray_full",
+ desc="Drip Tray Full",
appliance_types=("CoffeeMaker",),
),
- HomeConnectSensorEntityDescription(
- key=DISHWASHER_EVENT_SALT_NEARLY_EMPTY,
- device_class=SensorDeviceClass.ENUM,
- options=EVENT_OPTIONS,
- default_value="off",
- translation_key="salt_nearly_empty",
- appliance_types=("Dishwasher",),
- ),
- HomeConnectSensorEntityDescription(
- key=DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY,
- device_class=SensorDeviceClass.ENUM,
- options=EVENT_OPTIONS,
- default_value="off",
- translation_key="rinse_aid_nearly_empty",
- appliance_types=("Dishwasher",),
- ),
)
@@ -262,25 +95,18 @@ async def async_setup_entry(
"""Get a list of entities."""
entities: list[SensorEntity] = []
hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id]
- for device in hc_api.devices:
+ for device_dict in hc_api.devices:
+ entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("sensor", [])
+ entities += [HomeConnectSensor(**d) for d in entity_dicts]
+ device: HomeConnectDevice = device_dict[ATTR_DEVICE]
+ # Auto-discover entities
entities.extend(
- HomeConnectSensor(
+ HomeConnectAlarmSensor(
device,
- description,
+ entity_description=description,
)
- for description in EVENT_SENSORS
- if description.appliance_types
- and device.appliance.type in description.appliance_types
- )
- with contextlib.suppress(HomeConnectError):
- if device.appliance.get_programs_available():
- entities.extend(
- HomeConnectSensor(device, desc) for desc in BSH_PROGRAM_SENSORS
- )
- entities.extend(
- HomeConnectSensor(device, description)
for description in SENSORS
- if description.key in device.appliance.status
+ if device.appliance.type in description.appliance_types
)
return entities
@@ -290,7 +116,25 @@ async def async_setup_entry(
class HomeConnectSensor(HomeConnectEntity, SensorEntity):
"""Sensor class for Home Connect."""
- entity_description: HomeConnectSensorEntityDescription
+ _key: str
+ _sign: int
+
+ def __init__(
+ self,
+ device: HomeConnectDevice,
+ bsh_key: str,
+ desc: str,
+ unit: str,
+ icon: str,
+ device_class: SensorDeviceClass,
+ sign: int = 1,
+ ) -> None:
+ """Initialize the entity."""
+ super().__init__(device, bsh_key, desc)
+ self._sign = sign
+ self._attr_native_unit_of_measurement = unit
+ self._attr_icon = icon
+ self._attr_device_class = device_class
@property
def available(self) -> bool:
@@ -299,52 +143,78 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
async def async_update(self) -> None:
"""Update the sensor's status."""
- appliance_status = self.device.appliance.status
- if (
- self.bsh_key not in appliance_status
- or ATTR_VALUE not in appliance_status[self.bsh_key]
- ):
- self._attr_native_value = self.entity_description.default_value
- _LOGGER.debug("Updated, new state: %s", self._attr_native_value)
- return
- status = appliance_status[self.bsh_key]
- match self.device_class:
- case SensorDeviceClass.TIMESTAMP:
- if ATTR_VALUE not in status:
- self._attr_native_value = None
- elif (
- self._attr_native_value is not None
- and self.entity_description.sign == 1
- and isinstance(self._attr_native_value, datetime)
- and self._attr_native_value < dt_util.utcnow()
- ):
- # if the date is supposed to be in the future but we're
- # already past it, set state to None.
- self._attr_native_value = None
- elif (
- BSH_OPERATION_STATE
- in (appliance_status := self.device.appliance.status)
- and ATTR_VALUE in appliance_status[BSH_OPERATION_STATE]
- and appliance_status[BSH_OPERATION_STATE][ATTR_VALUE]
- in [
- BSH_OPERATION_STATE_RUN,
- BSH_OPERATION_STATE_PAUSE,
- BSH_OPERATION_STATE_FINISHED,
- ]
- ):
- seconds = self.entity_description.sign * float(status[ATTR_VALUE])
- self._attr_native_value = dt_util.utcnow() + timedelta(
- seconds=seconds
- )
- else:
- self._attr_native_value = None
- case SensorDeviceClass.ENUM:
+ status = self.device.appliance.status
+ if self.bsh_key not in status:
+ self._attr_native_value = None
+ elif self.device_class == SensorDeviceClass.TIMESTAMP:
+ if ATTR_VALUE not in status[self.bsh_key]:
+ self._attr_native_value = None
+ elif (
+ self._attr_native_value is not None
+ and self._sign == 1
+ and isinstance(self._attr_native_value, datetime)
+ and self._attr_native_value < dt_util.utcnow()
+ ):
+ # if the date is supposed to be in the future but we're
+ # already past it, set state to None.
+ self._attr_native_value = None
+ elif (
+ BSH_OPERATION_STATE in status
+ and ATTR_VALUE in status[BSH_OPERATION_STATE]
+ and status[BSH_OPERATION_STATE][ATTR_VALUE]
+ in [
+ BSH_OPERATION_STATE_RUN,
+ BSH_OPERATION_STATE_PAUSE,
+ BSH_OPERATION_STATE_FINISHED,
+ ]
+ ):
+ seconds = self._sign * float(status[self.bsh_key][ATTR_VALUE])
+ self._attr_native_value = dt_util.utcnow() + timedelta(seconds=seconds)
+ else:
+ self._attr_native_value = None
+ else:
+ self._attr_native_value = status[self.bsh_key].get(ATTR_VALUE)
+ if self.bsh_key == BSH_OPERATION_STATE:
# Value comes back as an enum, we only really care about the
# last part, so split it off
# https://developer.home-connect.com/docs/status/operation_state
- self._attr_native_value = slugify(
- cast(str, status.get(ATTR_VALUE)).split(".")[-1]
- )
- case _:
- self._attr_native_value = status.get(ATTR_VALUE)
+ self._attr_native_value = cast(str, self._attr_native_value).split(".")[
+ -1
+ ]
_LOGGER.debug("Updated, new state: %s", self._attr_native_value)
+
+
+class HomeConnectAlarmSensor(HomeConnectEntity, SensorEntity):
+ """Sensor entity setup using SensorEntityDescription."""
+
+ entity_description: HomeConnectSensorEntityDescription
+
+ def __init__(
+ self,
+ device: HomeConnectDevice,
+ entity_description: HomeConnectSensorEntityDescription,
+ ) -> None:
+ """Initialize the entity."""
+ self.entity_description = entity_description
+ super().__init__(
+ device, self.entity_description.key, self.entity_description.desc
+ )
+
+ @property
+ def available(self) -> bool:
+ """Return true if the sensor is available."""
+ return self._attr_native_value is not None
+
+ async def async_update(self) -> None:
+ """Update the sensor's status."""
+ self._attr_native_value = (
+ self.device.appliance.status.get(self.bsh_key, {})
+ .get(ATTR_VALUE, BSH_EVENT_PRESENT_STATE_OFF)
+ .rsplit(".", maxsplit=1)[-1]
+ .lower()
+ )
+ _LOGGER.debug(
+ "Updated: %s, new state: %s",
+ self._attr_unique_id,
+ self._attr_native_value,
+ )
diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json
index eb57d822b15..1fcd95e9cb2 100644
--- a/homeassistant/components/home_connect/strings.json
+++ b/homeassistant/components/home_connect/strings.json
@@ -21,56 +21,6 @@
"default": "[%key:common::config_flow::create_entry::authenticated%]"
}
},
- "exceptions": {
- "turn_on_light": {
- "message": "Error while trying to turn on {entity_id}: {description}"
- },
- "turn_off_light": {
- "message": "Error while trying to turn off {entity_id}: {description}"
- },
- "set_light_brightness": {
- "message": "Error while trying to set brightness of {entity_id}: {description}"
- },
- "select_light_custom_color": {
- "message": "Error while trying to select custom color of {entity_id}: {description}"
- },
- "set_light_color": {
- "message": "Error while trying to set color of {entity_id}: {description}"
- },
- "set_setting": {
- "message": "Error while trying to assign the value \"{value}\" to the setting \"{key}\" for {entity_id}: {description}"
- },
- "turn_on": {
- "message": "Error while trying to turn on {entity_id} ({key}): {description}"
- },
- "turn_off": {
- "message": "Error while trying to turn off {entity_id} ({key}): {description}"
- },
- "start_program": {
- "message": "Error while trying to start program {program}: {description}"
- },
- "stop_program": {
- "message": "Error while trying to stop program {program}: {description}"
- },
- "power_on": {
- "message": "Error while trying to turn on {appliance_name}: {description}"
- },
- "power_off": {
- "message": "Error while trying to turn off {appliance_name} with value \"{value}\": {description}"
- },
- "turn_off_not_supported": {
- "message": "{appliance_name} does not support turning off or entering standby mode."
- },
- "unable_to_retrieve_turn_off": {
- "message": "Unable to turn off {appliance_name} because its support for turning off or entering standby mode could not be determined."
- }
- },
- "issues": {
- "deprecated_binary_common_door_sensor": {
- "title": "Deprecated binary door sensor detected in some automations or scripts",
- "description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
- }
- },
"services": {
"start_program": {
"name": "Start program",
@@ -185,277 +135,42 @@
}
},
"entity": {
- "binary_sensor": {
- "remote_control": {
- "name": "Remote control"
- },
- "remote_start": {
- "name": "Remote start"
- },
- "local_control": {
- "name": "Local control"
- },
- "battery_charging_state": {
- "name": "Battery charging state"
- },
- "charging_connection": {
- "name": "Charging connection"
- },
- "dust_box_inserted": {
- "name": "Dust box",
- "state": {
- "on": "Inserted",
- "off": "Not inserted"
- }
- },
- "lifted": {
- "name": "Lifted"
- },
- "lost": {
- "name": "Lost"
- },
- "chiller_door": {
- "name": "Chiller door"
- },
- "freezer_door": {
- "name": "Freezer door"
- },
- "refrigerator_door": {
- "name": "Refrigerator door"
- }
- },
- "light": {
- "cooking_lighting": {
- "name": "Functional light"
- },
- "ambient_light": {
- "name": "Ambient light"
- },
- "external_light": {
- "name": "External light"
- },
- "internal_light": {
- "name": "Internal light"
- }
- },
- "number": {
- "refrigerator_setpoint_temperature": {
- "name": "Refrigerator temperature"
- },
- "freezer_setpoint_temperature": {
- "name": "Freezer temperature"
- },
- "bottle_cooler_setpoint_temperature": {
- "name": "Bottle cooler temperature"
- },
- "chiller_left_setpoint_temperature": {
- "name": "Chiller left temperature"
- },
- "chiller_setpoint_temperature": {
- "name": "Chiller temperature"
- },
- "chiller_right_setpoint_temperature": {
- "name": "Chiller right temperature"
- },
- "wine_compartment_setpoint_temperature": {
- "name": "Wine compartment temperature"
- },
- "wine_compartment_2_setpoint_temperature": {
- "name": "Wine compartment 2 temperature"
- },
- "wine_compartment_3_setpoint_temperature": {
- "name": "Wine compartment 3 temperature"
- }
- },
"sensor": {
- "program_progress": {
- "name": "Program progress"
- },
- "program_finish_time": {
- "name": "Program finish time"
- },
- "operation_state": {
- "name": "Operation state",
- "state": {
- "inactive": "Inactive",
- "ready": "Ready",
- "delayedstart": "Delayed start",
- "run": "Run",
- "pause": "[%key:common::state::paused%]",
- "actionrequired": "Action required",
- "finished": "Finished",
- "error": "Error",
- "aborting": "Aborting"
- }
- },
- "door": {
- "name": "Door",
- "state": {
- "closed": "[%key:common::state::closed%]",
- "locked": "[%key:common::state::locked%]",
- "open": "[%key:common::state::open%]"
- }
- },
- "coffee_counter": {
- "name": "Coffees"
- },
- "powder_coffee_counter": {
- "name": "Powder coffees"
- },
- "hot_water_counter": {
- "name": "Hot water"
- },
- "hot_water_cups_counter": {
- "name": "Hot water cups"
- },
- "hot_milk_counter": {
- "name": "Hot milk cups"
- },
- "frothy_milk_counter": {
- "name": "Frothy milk cups"
- },
- "milk_counter": {
- "name": "Milk cups"
- },
- "coffee_and_milk_counter": {
- "name": "Coffee and milk cups"
- },
- "ristretto_espresso_counter": {
- "name": "Ristretto espresso cups"
- },
- "battery_level": {
- "name": "Battery level"
- },
- "camera_state": {
- "name": "Camera state",
- "state": {
- "disabled": "[%key:common::state::disabled%]",
- "sleeping": "Sleeping",
- "ready": "Ready",
- "streaminglocal": "Streaming local",
- "streamingcloud": "Streaming cloud",
- "streaminglocal_and_cloud": "Streaming local and cloud",
- "error": "Error"
- }
- },
- "last_selected_map": {
- "name": "Last selected map",
- "state": {
- "tempmap": "Temporary map",
- "map1": "Map 1",
- "map2": "Map 2",
- "map3": "Map 3"
- }
- },
- "freezer_door_alarm": {
- "name": "Freezer door alarm",
+ "alarm_sensor_fridge": {
"state": {
"confirmed": "[%key:component::home_connect::common::confirmed%]",
"present": "[%key:component::home_connect::common::present%]"
}
},
- "refrigerator_door_alarm": {
- "name": "Refrigerator door alarm",
+ "alarm_sensor_freezer": {
"state": {
- "off": "[%key:common::state::off%]",
"confirmed": "[%key:component::home_connect::common::confirmed%]",
"present": "[%key:component::home_connect::common::present%]"
}
},
- "freezer_temperature_alarm": {
- "name": "Freezer temperature alarm",
+ "alarm_sensor_temp": {
"state": {
- "off": "[%key:common::state::off%]",
"confirmed": "[%key:component::home_connect::common::confirmed%]",
"present": "[%key:component::home_connect::common::present%]"
}
},
- "bean_container_empty": {
- "name": "Bean container empty",
+ "alarm_sensor_coffee_bean_container": {
"state": {
- "off": "[%key:common::state::off%]",
"confirmed": "[%key:component::home_connect::common::confirmed%]",
"present": "[%key:component::home_connect::common::present%]"
}
},
- "water_tank_empty": {
- "name": "Water tank empty",
+ "alarm_sensor_coffee_water_tank": {
"state": {
- "off": "[%key:common::state::off%]",
"confirmed": "[%key:component::home_connect::common::confirmed%]",
"present": "[%key:component::home_connect::common::present%]"
}
},
- "drip_tray_full": {
- "name": "Drip tray full",
+ "alarm_sensor_coffee_drip_tray": {
"state": {
- "off": "[%key:common::state::off%]",
"confirmed": "[%key:component::home_connect::common::confirmed%]",
"present": "[%key:component::home_connect::common::present%]"
}
- },
- "salt_nearly_empty": {
- "name": "Salt nearly empty",
- "state": {
- "off": "[%key:common::state::off%]",
- "confirmed": "[%key:component::home_connect::common::confirmed%]",
- "present": "[%key:component::home_connect::common::present%]"
- }
- },
- "rinse_aid_nearly_empty": {
- "name": "Rinse aid nearly empty",
- "state": {
- "off": "[%key:common::state::off%]",
- "confirmed": "[%key:component::home_connect::common::confirmed%]",
- "present": "[%key:component::home_connect::common::present%]"
- }
- }
- },
- "switch": {
- "power": {
- "name": "Power"
- },
- "child_lock": {
- "name": "Child lock"
- },
- "cup_warmer": {
- "name": "Cup warmer"
- },
- "refrigerator_super_mode": {
- "name": "Refrigerator super mode"
- },
- "freezer_super_mode": {
- "name": "Freezer super mode"
- },
- "eco_mode": {
- "name": "Eco mode"
- },
- "sabbath_mode": {
- "name": "Sabbath mode"
- },
- "vacation_mode": {
- "name": "Vacation mode"
- },
- "fresh_mode": {
- "name": "Fresh mode"
- },
- "dispenser_enabled": {
- "name": "Dispenser",
- "state": {
- "off": "[%key:common::state::disabled%]",
- "on": "[%key:common::state::enabled%]"
- }
- },
- "door_assistant_fridge": {
- "name": "Fridge door assistant"
- },
- "door_assistant_freezer": {
- "name": "Freezer door assistant"
- }
- },
- "time": {
- "alarm_clock": {
- "name": "Alarm clock"
}
}
}
diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py
index 25bbb85278a..6e96b371b82 100644
--- a/homeassistant/components/home_connect/switch.py
+++ b/homeassistant/components/home_connect/switch.py
@@ -1,6 +1,6 @@
"""Provides a switch for Home Connect."""
-import contextlib
+from dataclasses import dataclass
import logging
from typing import Any
@@ -8,97 +8,47 @@ from homeconnect.api import HomeConnectError
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_DEVICE, CONF_ENTITIES
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import get_dict_from_home_connect_error
from .api import ConfigEntryAuth
from .const import (
- ATTR_ALLOWED_VALUES,
- ATTR_CONSTRAINTS,
ATTR_VALUE,
BSH_ACTIVE_PROGRAM,
BSH_CHILD_LOCK_STATE,
BSH_OPERATION_STATE,
- BSH_POWER_OFF,
BSH_POWER_ON,
- BSH_POWER_STANDBY,
BSH_POWER_STATE,
DOMAIN,
REFRIGERATION_DISPENSER,
REFRIGERATION_SUPERMODEFREEZER,
REFRIGERATION_SUPERMODEREFRIGERATOR,
- SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME,
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
- SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY,
- SVE_TRANSLATION_PLACEHOLDER_VALUE,
)
from .entity import HomeConnectDevice, HomeConnectEntity
_LOGGER = logging.getLogger(__name__)
-APPLIANCES_WITH_PROGRAMS = (
- "CleaningRobot",
- "CoffeeMaker",
- "Dishwasher",
- "Dryer",
- "Hood",
- "Oven",
- "WarmingDrawer",
- "Washer",
- "WasherDryer",
-)
+
+@dataclass(frozen=True, kw_only=True)
+class HomeConnectSwitchEntityDescription(SwitchEntityDescription):
+ """Switch entity description."""
+
+ desc: str
-SWITCHES = (
- SwitchEntityDescription(
- key=BSH_CHILD_LOCK_STATE,
- translation_key="child_lock",
- ),
- SwitchEntityDescription(
- key="ConsumerProducts.CoffeeMaker.Setting.CupWarmer",
- translation_key="cup_warmer",
- ),
- SwitchEntityDescription(
+SWITCHES: tuple[HomeConnectSwitchEntityDescription, ...] = (
+ HomeConnectSwitchEntityDescription(
key=REFRIGERATION_SUPERMODEFREEZER,
- translation_key="freezer_super_mode",
+ desc="Supermode Freezer",
),
- SwitchEntityDescription(
+ HomeConnectSwitchEntityDescription(
key=REFRIGERATION_SUPERMODEREFRIGERATOR,
- translation_key="refrigerator_super_mode",
+ desc="Supermode Refrigerator",
),
- SwitchEntityDescription(
- key="Refrigeration.Common.Setting.EcoMode",
- translation_key="eco_mode",
- ),
- SwitchEntityDescription(
- key="Cooking.Oven.Setting.SabbathMode",
- translation_key="sabbath_mode",
- ),
- SwitchEntityDescription(
- key="Refrigeration.Common.Setting.SabbathMode",
- translation_key="sabbath_mode",
- ),
- SwitchEntityDescription(
- key="Refrigeration.Common.Setting.VacationMode",
- translation_key="vacation_mode",
- ),
- SwitchEntityDescription(
- key="Refrigeration.Common.Setting.FreshMode",
- translation_key="fresh_mode",
- ),
- SwitchEntityDescription(
+ HomeConnectSwitchEntityDescription(
key=REFRIGERATION_DISPENSER,
- translation_key="dispenser_enabled",
- ),
- SwitchEntityDescription(
- key="Refrigeration.Common.Setting.Door.AssistantFridge",
- translation_key="door_assistant_fridge",
- ),
- SwitchEntityDescription(
- key="Refrigeration.Common.Setting.Door.AssistantFreezer",
- translation_key="door_assistant_freezer",
+ desc="Dispenser Enabled",
),
)
@@ -114,20 +64,17 @@ async def async_setup_entry(
"""Get a list of entities."""
entities: list[SwitchEntity] = []
hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id]
- for device in hc_api.devices:
- if device.appliance.type in APPLIANCES_WITH_PROGRAMS:
- with contextlib.suppress(HomeConnectError):
- programs = device.appliance.get_programs_available()
- if programs:
- entities.extend(
- HomeConnectProgramSwitch(device, program)
- for program in programs
- )
- entities.append(HomeConnectPowerSwitch(device))
+ for device_dict in hc_api.devices:
+ entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("switch", [])
+ entities.extend(HomeConnectProgramSwitch(**d) for d in entity_dicts)
+ entities.append(HomeConnectPowerSwitch(device_dict[CONF_DEVICE]))
+ entities.append(HomeConnectChildLockSwitch(device_dict[CONF_DEVICE]))
+ # Auto-discover entities
+ hc_device: HomeConnectDevice = device_dict[CONF_DEVICE]
entities.extend(
- HomeConnectSwitch(device, description)
+ HomeConnectSwitch(device=hc_device, entity_description=description)
for description in SWITCHES
- if description.key in device.appliance.status
+ if description.key in hc_device.appliance.status
)
return entities
@@ -138,6 +85,18 @@ async def async_setup_entry(
class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
"""Generic switch class for Home Connect Binary Settings."""
+ entity_description: HomeConnectSwitchEntityDescription
+
+ def __init__(
+ self,
+ device: HomeConnectDevice,
+ entity_description: HomeConnectSwitchEntityDescription,
+ ) -> None:
+ """Initialize the entity."""
+ self.entity_description = entity_description
+ self._attr_available = False
+ super().__init__(device, entity_description.key, entity_description.desc)
+
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on setting."""
@@ -147,16 +106,9 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
self.device.appliance.set_setting, self.entity_description.key, True
)
except HomeConnectError as err:
+ _LOGGER.error("Error while trying to turn on: %s", err)
self._attr_available = False
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="turn_on",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
- SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY: self.bsh_key,
- },
- ) from err
+ return
self._attr_available = True
self.async_entity_update()
@@ -172,15 +124,7 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
except HomeConnectError as err:
_LOGGER.error("Error while trying to turn off: %s", err)
self._attr_available = False
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="turn_off",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
- SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY: self.bsh_key,
- },
- ) from err
+ return
self._attr_available = True
self.async_entity_update()
@@ -209,10 +153,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
desc = " ".join(
["Program", program_name.split(".")[-3], program_name.split(".")[-1]]
)
- super().__init__(device, SwitchEntityDescription(key=program_name))
- self._attr_name = f"{device.appliance.name} {desc}"
- self._attr_unique_id = f"{device.appliance.haId}-{desc}"
- self._attr_has_entity_name = False
+ super().__init__(device, desc, desc)
self.program_name = program_name
async def async_turn_on(self, **kwargs: Any) -> None:
@@ -223,14 +164,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
self.device.appliance.start_program, self.program_name
)
except HomeConnectError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="start_program",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- "program": self.program_name,
- },
- ) from err
+ _LOGGER.error("Error while trying to start program: %s", err)
self.async_entity_update()
async def async_turn_off(self, **kwargs: Any) -> None:
@@ -239,14 +173,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
try:
await self.hass.async_add_executor_job(self.device.appliance.stop_program)
except HomeConnectError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="stop_program",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- "program": self.program_name,
- },
- ) from err
+ _LOGGER.error("Error while trying to stop program: %s", err)
self.async_entity_update()
async def async_update(self) -> None:
@@ -262,26 +189,9 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
"""Power switch class for Home Connect."""
- power_off_state: str | None
-
def __init__(self, device: HomeConnectDevice) -> None:
"""Initialize the entity."""
- super().__init__(
- device,
- SwitchEntityDescription(key=BSH_POWER_STATE, translation_key="power"),
- )
- if (
- power_state := device.appliance.status.get(BSH_POWER_STATE, {}).get(
- ATTR_VALUE
- )
- ) and power_state in [BSH_POWER_OFF, BSH_POWER_STANDBY]:
- self.power_off_state = power_state
-
- async def async_added_to_hass(self) -> None:
- """Add the entity to the hass instance."""
- await super().async_added_to_hass()
- if not hasattr(self, "power_off_state"):
- await self.async_fetch_power_off_state()
+ super().__init__(device, BSH_POWER_STATE, "Power")
async def async_turn_on(self, **kwargs: Any) -> None:
"""Switch the device on."""
@@ -291,54 +201,22 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
self.device.appliance.set_setting, BSH_POWER_STATE, BSH_POWER_ON
)
except HomeConnectError as err:
+ _LOGGER.error("Error while trying to turn on device: %s", err)
self._attr_is_on = False
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="power_on",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME: self.device.appliance.name,
- },
- ) from err
self.async_entity_update()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Switch the device off."""
- if not hasattr(self, "power_off_state"):
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="unable_to_retrieve_turn_off",
- translation_placeholders={
- SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME: self.device.appliance.name
- },
- )
-
- if self.power_off_state is None:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="turn_off_not_supported",
- translation_placeholders={
- SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME: self.device.appliance.name
- },
- )
_LOGGER.debug("tried to switch off %s", self.name)
try:
await self.hass.async_add_executor_job(
self.device.appliance.set_setting,
BSH_POWER_STATE,
- self.power_off_state,
+ self.device.power_off_state,
)
except HomeConnectError as err:
+ _LOGGER.error("Error while trying to turn off device: %s", err)
self._attr_is_on = True
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="power_off",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME: self.device.appliance.name,
- SVE_TRANSLATION_PLACEHOLDER_VALUE: self.power_off_state,
- },
- ) from err
self.async_entity_update()
async def async_update(self) -> None:
@@ -349,9 +227,8 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
):
self._attr_is_on = True
elif (
- hasattr(self, "power_off_state")
- and self.device.appliance.status.get(BSH_POWER_STATE, {}).get(ATTR_VALUE)
- == self.power_off_state
+ self.device.appliance.status.get(BSH_POWER_STATE, {}).get(ATTR_VALUE)
+ == self.device.power_off_state
):
self._attr_is_on = False
elif self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get(
@@ -375,23 +252,43 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
self._attr_is_on = None
_LOGGER.debug("Updated, new state: %s", self._attr_is_on)
- async def async_fetch_power_off_state(self) -> None:
- """Fetch the power off state."""
+
+class HomeConnectChildLockSwitch(HomeConnectEntity, SwitchEntity):
+ """Child lock switch class for Home Connect."""
+
+ def __init__(self, device: HomeConnectDevice) -> None:
+ """Initialize the entity."""
+ super().__init__(device, BSH_CHILD_LOCK_STATE, "ChildLock")
+
+ async def async_turn_on(self, **kwargs: Any) -> None:
+ """Switch child lock on."""
+ _LOGGER.debug("Tried to switch child lock on device: %s", self.name)
try:
- data = await self.hass.async_add_executor_job(
- self.device.appliance.get, f"/settings/{self.bsh_key}"
+ await self.hass.async_add_executor_job(
+ self.device.appliance.set_setting, BSH_CHILD_LOCK_STATE, True
)
except HomeConnectError as err:
- _LOGGER.error("An error occurred: %s", err)
- return
- if not data or not (
- allowed_values := data.get(ATTR_CONSTRAINTS, {}).get(ATTR_ALLOWED_VALUES)
- ):
- return
+ _LOGGER.error("Error while trying to turn on child lock on device: %s", err)
+ self._attr_is_on = False
+ self.async_entity_update()
- if BSH_POWER_OFF in allowed_values:
- self.power_off_state = BSH_POWER_OFF
- elif BSH_POWER_STANDBY in allowed_values:
- self.power_off_state = BSH_POWER_STANDBY
- else:
- self.power_off_state = None
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Switch child lock off."""
+ _LOGGER.debug("Tried to switch off child lock on device: %s", self.name)
+ try:
+ await self.hass.async_add_executor_job(
+ self.device.appliance.set_setting, BSH_CHILD_LOCK_STATE, False
+ )
+ except HomeConnectError as err:
+ _LOGGER.error(
+ "Error while trying to turn off child lock on device: %s", err
+ )
+ self._attr_is_on = True
+ self.async_entity_update()
+
+ async def async_update(self) -> None:
+ """Update the switch's status."""
+ self._attr_is_on = False
+ if self.device.appliance.status.get(BSH_CHILD_LOCK_STATE, {}).get(ATTR_VALUE):
+ self._attr_is_on = True
+ _LOGGER.debug("Updated child lock, new state: %s", self._attr_is_on)
diff --git a/homeassistant/components/home_connect/time.py b/homeassistant/components/home_connect/time.py
deleted file mode 100644
index 946a2354938..00000000000
--- a/homeassistant/components/home_connect/time.py
+++ /dev/null
@@ -1,109 +0,0 @@
-"""Provides time enties for Home Connect."""
-
-from datetime import time
-import logging
-
-from homeconnect.api import HomeConnectError
-
-from homeassistant.components.time import TimeEntity, TimeEntityDescription
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ServiceValidationError
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import get_dict_from_home_connect_error
-from .api import ConfigEntryAuth
-from .const import (
- ATTR_VALUE,
- DOMAIN,
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
- SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY,
- SVE_TRANSLATION_PLACEHOLDER_VALUE,
-)
-from .entity import HomeConnectEntity
-
-_LOGGER = logging.getLogger(__name__)
-
-
-TIME_ENTITIES = (
- TimeEntityDescription(
- key="BSH.Common.Setting.AlarmClock",
- translation_key="alarm_clock",
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the Home Connect switch."""
-
- def get_entities() -> list[HomeConnectTimeEntity]:
- """Get a list of entities."""
- hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id]
- return [
- HomeConnectTimeEntity(device, description)
- for description in TIME_ENTITIES
- for device in hc_api.devices
- if description.key in device.appliance.status
- ]
-
- async_add_entities(await hass.async_add_executor_job(get_entities), True)
-
-
-def seconds_to_time(seconds: int) -> time:
- """Convert seconds to a time object."""
- minutes, sec = divmod(seconds, 60)
- hours, minutes = divmod(minutes, 60)
- return time(hour=hours, minute=minutes, second=sec)
-
-
-def time_to_seconds(t: time) -> int:
- """Convert a time object to seconds."""
- return t.hour * 3600 + t.minute * 60 + t.second
-
-
-class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
- """Time setting class for Home Connect."""
-
- async def async_set_value(self, value: time) -> None:
- """Set the native value of the entity."""
- _LOGGER.debug(
- "Tried to set value %s to %s for %s",
- value,
- self.bsh_key,
- self.entity_id,
- )
- try:
- await self.hass.async_add_executor_job(
- self.device.appliance.set_setting,
- self.bsh_key,
- time_to_seconds(value),
- )
- except HomeConnectError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="set_setting",
- translation_placeholders={
- **get_dict_from_home_connect_error(err),
- SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
- SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY: self.bsh_key,
- SVE_TRANSLATION_PLACEHOLDER_VALUE: str(value),
- },
- ) from err
-
- async def async_update(self) -> None:
- """Update the Time setting status."""
- data = self.device.appliance.status.get(self.bsh_key)
- if data is None:
- _LOGGER.error("No value for %s", self.bsh_key)
- self._attr_native_value = None
- return
- seconds = data.get(ATTR_VALUE, None)
- if seconds is not None:
- self._attr_native_value = seconds_to_time(seconds)
- else:
- self._attr_native_value = None
- _LOGGER.debug("Updated, new value: %s", self._attr_native_value)
diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py
index dc33b0c63e3..6cec47152e5 100644
--- a/homeassistant/components/homeassistant/__init__.py
+++ b/homeassistant/components/homeassistant/__init__.py
@@ -8,9 +8,9 @@ from typing import Any
import voluptuous as vol
-from homeassistant import config as conf_util, core_config
from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL
from homeassistant.components import persistent_notification
+import homeassistant.config as conf_util
from homeassistant.const import (
ATTR_ELEVATION,
ATTR_ENTITY_ID,
@@ -269,7 +269,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
return
# auth only processed during startup
- await core_config.async_process_ha_core_config(hass, conf.get(DOMAIN) or {})
+ await conf_util.async_process_ha_core_config(hass, conf.get(DOMAIN) or {})
async_register_admin_service(
hass, DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config
@@ -282,7 +282,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
"longitude": call.data[ATTR_LONGITUDE],
}
- if (elevation := call.data.get(ATTR_ELEVATION)) is not None:
+ if elevation := call.data.get(ATTR_ELEVATION):
service_data["elevation"] = elevation
await hass.config.async_update(**service_data)
diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json
index 0dd4eff507d..29612bd61ed 100644
--- a/homeassistant/components/homeassistant/strings.json
+++ b/homeassistant/components/homeassistant/strings.json
@@ -57,14 +57,6 @@
"title": "[%key:common::config_flow::title::reauth%]",
"description": "Reauthentication is needed"
},
- "config_entry_unique_id_collision": {
- "title": "Multiple {domain} config entries with same unique ID",
- "description": "There are multiple {domain} config entries with the same unique ID.\nThe config entries are named {titles}.\n\nTo fix this error, [configure the integration]({configure_url}) and remove all except one of the duplicates.\n\nNote: Another group of duplicates may be revealed after removing these duplicates."
- },
- "config_entry_unique_id_collision_many": {
- "title": "[%key:component::homeassistant::issues::config_entry_unique_id_collision::title%]",
- "description": "There are multiple ({number_of_entries}) {domain} config entries with the same unique ID.\nThe first {title_limit} config entries are named {titles}.\n\nTo fix this error, [configure the integration]({configure_url}) and remove all except one of the duplicates.\n\nNote: Another group of duplicates may be revealed after removing these duplicates."
- },
"integration_not_found": {
"title": "Integration {domain} not found",
"fix_flow": {
diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py
index bea6e8a66a7..443d9c65d95 100644
--- a/homeassistant/components/homeassistant/triggers/time.py
+++ b/homeassistant/components/homeassistant/triggers/time.py
@@ -3,7 +3,7 @@
from collections.abc import Callable
from datetime import datetime, timedelta
from functools import partial
-from typing import Any, NamedTuple
+from typing import NamedTuple
import voluptuous as vol
@@ -26,8 +26,7 @@ from homeassistant.core import (
State,
callback,
)
-from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import config_validation as cv, template
+from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import (
async_track_point_in_time,
async_track_state_change_event,
@@ -38,7 +37,6 @@ from homeassistant.helpers.typing import ConfigType
import homeassistant.util.dt as dt_util
_TIME_TRIGGER_ENTITY = vol.All(str, cv.entity_domain(["input_datetime", "sensor"]))
-_TIME_AT_SCHEMA = vol.Any(cv.time, _TIME_TRIGGER_ENTITY)
_TIME_TRIGGER_ENTITY_WITH_OFFSET = vol.Schema(
{
@@ -47,29 +45,16 @@ _TIME_TRIGGER_ENTITY_WITH_OFFSET = vol.Schema(
}
)
-
-def valid_at_template(value: Any) -> template.Template:
- """Validate either a jinja2 template, valid time, or valid trigger entity."""
- tpl = cv.template(value)
-
- if tpl.is_static:
- _TIME_AT_SCHEMA(value)
-
- return tpl
-
-
_TIME_TRIGGER_SCHEMA = vol.Any(
cv.time,
_TIME_TRIGGER_ENTITY,
_TIME_TRIGGER_ENTITY_WITH_OFFSET,
- valid_at_template,
msg=(
"Expected HH:MM, HH:MM:SS, an Entity ID with domain 'input_datetime' or "
- "'sensor', a combination of a timestamp sensor entity and an offset, or Limited Template"
+ "'sensor', or a combination of a timestamp sensor entity and an offset."
),
)
-
TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "time",
@@ -93,7 +78,6 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration."""
trigger_data = trigger_info["trigger_data"]
- variables = trigger_info["variables"] or {}
entities: dict[tuple[str, timedelta], CALLBACK_TYPE] = {}
removes: list[CALLBACK_TYPE] = []
job = HassJob(action, f"time trigger {trigger_info}")
@@ -218,16 +202,6 @@ async def async_attach_trigger(
to_track: list[TrackEntity] = []
for at_time in config[CONF_AT]:
- if isinstance(at_time, template.Template):
- render = template.render_complex(at_time, variables, limited=True)
- try:
- at_time = _TIME_AT_SCHEMA(render)
- except vol.Invalid as exc:
- raise HomeAssistantError(
- f"Limited Template for 'at' rendered a unexpected value '{render}', expected HH:MM, "
- f"HH:MM:SS or Entity ID with domain 'input_datetime' or 'sensor'"
- ) from exc
-
if isinstance(at_time, str):
# entity
update_entity_trigger(at_time, new_state=hass.states.get(at_time))
diff --git a/homeassistant/components/homeassistant_alerts/coordinator.py b/homeassistant/components/homeassistant_alerts/coordinator.py
index a81824d2376..5d99e1c980f 100644
--- a/homeassistant/components/homeassistant_alerts/coordinator.py
+++ b/homeassistant/components/homeassistant_alerts/coordinator.py
@@ -5,11 +5,10 @@ import logging
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
-from homeassistant.components.hassio import get_supervisor_info
+from homeassistant.components.hassio import get_supervisor_info, is_hassio
from homeassistant.const import __version__
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, REQUEST_TIMEOUT, UPDATE_INTERVAL
diff --git a/homeassistant/components/homeassistant_alerts/manifest.json b/homeassistant/components/homeassistant_alerts/manifest.json
index 0412f43da69..96e419ad9a2 100644
--- a/homeassistant/components/homeassistant_alerts/manifest.json
+++ b/homeassistant/components/homeassistant_alerts/manifest.json
@@ -1,7 +1,6 @@
{
"domain": "homeassistant_alerts",
"name": "Home Assistant Alerts",
- "after_dependencies": ["hassio"],
"codeowners": ["@home-assistant/core"],
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts",
diff --git a/homeassistant/components/homeassistant_green/__init__.py b/homeassistant/components/homeassistant_green/__init__.py
index 79688f9d16a..2d35b5bbed3 100644
--- a/homeassistant/components/homeassistant_green/__init__.py
+++ b/homeassistant/components/homeassistant_green/__init__.py
@@ -2,11 +2,10 @@
from __future__ import annotations
-from homeassistant.components.hassio import get_os_info
+from homeassistant.components.hassio import get_os_info, is_hassio
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers.hassio import is_hassio
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
diff --git a/homeassistant/components/homeassistant_green/config_flow.py b/homeassistant/components/homeassistant_green/config_flow.py
index c9aed577365..4b71c7f1056 100644
--- a/homeassistant/components/homeassistant_green/config_flow.py
+++ b/homeassistant/components/homeassistant_green/config_flow.py
@@ -13,6 +13,7 @@ from homeassistant.components.hassio import (
HassioAPIError,
async_get_green_settings,
async_set_green_settings,
+ is_hassio,
)
from homeassistant.config_entries import (
ConfigEntry,
@@ -22,7 +23,6 @@ from homeassistant.config_entries import (
)
from homeassistant.core import callback
from homeassistant.helpers import selector
-from homeassistant.helpers.hassio import is_hassio
from .const import DOMAIN
@@ -55,6 +55,9 @@ class HomeAssistantGreenConfigFlow(ConfigFlow, domain=DOMAIN):
self, data: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
return self.async_create_entry(title="Home Assistant Green", data={})
diff --git a/homeassistant/components/homeassistant_green/manifest.json b/homeassistant/components/homeassistant_green/manifest.json
index 78da50603df..d543d562ee3 100644
--- a/homeassistant/components/homeassistant_green/manifest.json
+++ b/homeassistant/components/homeassistant_green/manifest.json
@@ -6,6 +6,5 @@
"config_flow": false,
"dependencies": ["hardware", "homeassistant_hardware"],
"documentation": "https://www.home-assistant.io/integrations/homeassistant_green",
- "integration_type": "hardware",
- "single_config_entry": true
+ "integration_type": "hardware"
}
diff --git a/homeassistant/components/homeassistant_green/strings.json b/homeassistant/components/homeassistant_green/strings.json
index 13507439e4b..9066ca64e5c 100644
--- a/homeassistant/components/homeassistant_green/strings.json
+++ b/homeassistant/components/homeassistant_green/strings.json
@@ -21,6 +21,7 @@
"abort": {
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
"read_hw_settings_error": "Failed to read hardware settings",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"write_hw_settings_error": "Failed to write hardware settings"
}
}
diff --git a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py
index a91fb00c142..b8dc4227ece 100644
--- a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py
+++ b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py
@@ -14,6 +14,7 @@ from homeassistant.components.hassio import (
AddonInfo,
AddonManager,
AddonState,
+ is_hassio,
)
from homeassistant.components.zha.repairs.wrong_silabs_firmware import (
probe_silabs_firmware_type,
@@ -24,10 +25,10 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow
-from homeassistant.helpers.hassio import is_hassio
from . import silabs_multiprotocol_addon
from .const import ZHA_DOMAIN
@@ -495,15 +496,13 @@ class BaseFirmwareConfigFlow(BaseFirmwareInstallFlow, ConfigFlow):
return await self.async_step_pick_firmware()
-class BaseFirmwareOptionsFlow(BaseFirmwareInstallFlow, OptionsFlow):
+class BaseFirmwareOptionsFlow(BaseFirmwareInstallFlow, OptionsFlowWithConfigEntry):
"""Zigbee and Thread options flow handlers."""
- def __init__(self, config_entry: ConfigEntry, *args: Any, **kwargs: Any) -> None:
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Instantiate options flow."""
super().__init__(*args, **kwargs)
- self._config_entry = config_entry
-
self._probed_firmware_type = ApplicationType(self.config_entry.data["firmware"])
# Make `context` a regular dictionary
diff --git a/homeassistant/components/homeassistant_hardware/manifest.json b/homeassistant/components/homeassistant_hardware/manifest.json
index f692094bc67..8898cece75a 100644
--- a/homeassistant/components/homeassistant_hardware/manifest.json
+++ b/homeassistant/components/homeassistant_hardware/manifest.json
@@ -1,7 +1,7 @@
{
"domain": "homeassistant_hardware",
"name": "Home Assistant Hardware",
- "after_dependencies": ["hassio", "zha"],
+ "after_dependencies": ["zha"],
"codeowners": ["@home-assistant/core"],
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
"integration_type": "system"
diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py
index 2b08031405f..31032ff6a8c 100644
--- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py
+++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py
@@ -17,6 +17,7 @@ from homeassistant.components.hassio import (
AddonManager,
AddonState,
hostname_from_addon_slug,
+ is_hassio,
)
from homeassistant.config_entries import (
ConfigEntry,
@@ -27,7 +28,6 @@ from homeassistant.config_entries import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.integration_platform import (
async_process_integration_platforms,
)
@@ -318,6 +318,7 @@ class OptionsFlowHandler(OptionsFlow, ABC):
self.start_task: asyncio.Task | None = None
self.stop_task: asyncio.Task | None = None
self._zha_migration_mgr: ZhaMultiPANMigrationHelper | None = None
+ self.config_entry = config_entry
self.original_addon_config: dict[str, Any] | None = None
self.revert_reason: str | None = None
diff --git a/homeassistant/components/homeassistant_hardware/util.py b/homeassistant/components/homeassistant_hardware/util.py
index 0c06ff05e5c..90cfee076e3 100644
--- a/homeassistant/components/homeassistant_hardware/util.py
+++ b/homeassistant/components/homeassistant_hardware/util.py
@@ -9,10 +9,9 @@ from typing import cast
from universal_silabs_flasher.const import ApplicationType
-from homeassistant.components.hassio import AddonError, AddonState
+from homeassistant.components.hassio import AddonError, AddonState, is_hassio
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.singleton import singleton
from .const import (
diff --git a/homeassistant/components/homeassistant_sky_connect/manifest.json b/homeassistant/components/homeassistant_sky_connect/manifest.json
index 27280c6aac3..f56fd24de61 100644
--- a/homeassistant/components/homeassistant_sky_connect/manifest.json
+++ b/homeassistant/components/homeassistant_sky_connect/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "homeassistant_sky_connect",
- "name": "Home Assistant Connect ZBT-1",
+ "name": "Home Assistant SkyConnect",
"codeowners": ["@home-assistant/core"],
"config_flow": true,
"dependencies": ["hardware", "usb", "homeassistant_hardware"],
diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py
index dc34cc4cdc9..04abe5a1dca 100644
--- a/homeassistant/components/homeassistant_yellow/__init__.py
+++ b/homeassistant/components/homeassistant_yellow/__init__.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import logging
-from homeassistant.components.hassio import get_os_info
+from homeassistant.components.hassio import get_os_info, is_hassio
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
check_multi_pan_addon,
)
@@ -16,7 +16,6 @@ from homeassistant.config_entries import SOURCE_HARDWARE, ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import discovery_flow
-from homeassistant.helpers.hassio import is_hassio
from .const import FIRMWARE, RADIO_DEVICE, ZHA_HW_DISCOVERY_DATA
diff --git a/homeassistant/components/homeassistant_yellow/config_flow.py b/homeassistant/components/homeassistant_yellow/config_flow.py
index 9edc5009171..1f4d150e49b 100644
--- a/homeassistant/components/homeassistant_yellow/config_flow.py
+++ b/homeassistant/components/homeassistant_yellow/config_flow.py
@@ -77,6 +77,9 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
self, data: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
# We do not actually use any portion of `BaseFirmwareConfigFlow` beyond this
await self._probe_firmware_type()
diff --git a/homeassistant/components/homeassistant_yellow/manifest.json b/homeassistant/components/homeassistant_yellow/manifest.json
index caf4d32c746..a9715003172 100644
--- a/homeassistant/components/homeassistant_yellow/manifest.json
+++ b/homeassistant/components/homeassistant_yellow/manifest.json
@@ -6,6 +6,5 @@
"config_flow": false,
"dependencies": ["hardware", "homeassistant_hardware"],
"documentation": "https://www.home-assistant.io/integrations/homeassistant_yellow",
- "integration_type": "hardware",
- "single_config_entry": true
+ "integration_type": "hardware"
}
diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py
index 53db7774821..a63e365ead7 100644
--- a/homeassistant/components/homekit/config_flow.py
+++ b/homeassistant/components/homekit/config_flow.py
@@ -362,14 +362,15 @@ class HomeKitConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Handle a option flow for homekit."""
- def __init__(self) -> None:
+ def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
+ self.config_entry = config_entry
self.hk_options: dict[str, Any] = {}
self.included_cameras: list[str] = []
diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json
index cf74bcc7d67..eebdc0026fd 100644
--- a/homeassistant/components/homekit/manifest.json
+++ b/homeassistant/components/homekit/manifest.json
@@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["pyhap"],
"requirements": [
- "HAP-python==4.9.2",
+ "HAP-python==4.9.1",
"fnv-hash-fast==1.0.2",
"PyQRCode==1.2.1",
"base36==0.1.1"
diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py
index cde80178c5e..6b57a03153c 100644
--- a/homeassistant/components/homekit/type_lights.py
+++ b/homeassistant/components/homekit/type_lights.py
@@ -171,9 +171,8 @@ class Light(HomeAccessory):
events = []
service = SERVICE_TURN_ON
params: dict[str, Any] = {ATTR_ENTITY_ID: self.entity_id}
- has_on = CHAR_ON in char_values
- if has_on:
+ if CHAR_ON in char_values:
if not char_values[CHAR_ON]:
service = SERVICE_TURN_OFF
events.append(f"Set state to {char_values[CHAR_ON]}")
@@ -181,10 +180,7 @@ class Light(HomeAccessory):
brightness_pct = None
if CHAR_BRIGHTNESS in char_values:
if char_values[CHAR_BRIGHTNESS] == 0:
- if has_on:
- events[-1] = "Set state to 0"
- else:
- events.append("Set state to 0")
+ events[-1] = "Set state to 0"
service = SERVICE_TURN_OFF
else:
brightness_pct = char_values[CHAR_BRIGHTNESS]
diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py
index 8634589cb5f..6ab521b6727 100644
--- a/homeassistant/components/homekit/type_security_systems.py
+++ b/homeassistant/components/homekit/type_security_systems.py
@@ -8,7 +8,6 @@ from pyhap.const import CATEGORY_ALARM_SYSTEM
from homeassistant.components.alarm_control_panel import (
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.const import (
ATTR_CODE,
@@ -18,8 +17,13 @@ from homeassistant.const import (
SERVICE_ALARM_ARM_HOME,
SERVICE_ALARM_ARM_NIGHT,
SERVICE_ALARM_DISARM,
- STATE_UNAVAILABLE,
- STATE_UNKNOWN,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
)
from homeassistant.core import State, callback
@@ -39,22 +43,22 @@ HK_ALARM_DISARMED = 3
HK_ALARM_TRIGGERED = 4
HASS_TO_HOMEKIT_CURRENT = {
- AlarmControlPanelState.ARMED_HOME: HK_ALARM_STAY_ARMED,
- AlarmControlPanelState.ARMED_VACATION: HK_ALARM_AWAY_ARMED,
- AlarmControlPanelState.ARMED_AWAY: HK_ALARM_AWAY_ARMED,
- AlarmControlPanelState.ARMED_NIGHT: HK_ALARM_NIGHT_ARMED,
- AlarmControlPanelState.ARMING: HK_ALARM_DISARMED,
- AlarmControlPanelState.DISARMED: HK_ALARM_DISARMED,
- AlarmControlPanelState.TRIGGERED: HK_ALARM_TRIGGERED,
+ STATE_ALARM_ARMED_HOME: HK_ALARM_STAY_ARMED,
+ STATE_ALARM_ARMED_VACATION: HK_ALARM_AWAY_ARMED,
+ STATE_ALARM_ARMED_AWAY: HK_ALARM_AWAY_ARMED,
+ STATE_ALARM_ARMED_NIGHT: HK_ALARM_NIGHT_ARMED,
+ STATE_ALARM_ARMING: HK_ALARM_DISARMED,
+ STATE_ALARM_DISARMED: HK_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED: HK_ALARM_TRIGGERED,
}
HASS_TO_HOMEKIT_TARGET = {
- AlarmControlPanelState.ARMED_HOME: HK_ALARM_STAY_ARMED,
- AlarmControlPanelState.ARMED_VACATION: HK_ALARM_AWAY_ARMED,
- AlarmControlPanelState.ARMED_AWAY: HK_ALARM_AWAY_ARMED,
- AlarmControlPanelState.ARMED_NIGHT: HK_ALARM_NIGHT_ARMED,
- AlarmControlPanelState.ARMING: HK_ALARM_AWAY_ARMED,
- AlarmControlPanelState.DISARMED: HK_ALARM_DISARMED,
+ STATE_ALARM_ARMED_HOME: HK_ALARM_STAY_ARMED,
+ STATE_ALARM_ARMED_VACATION: HK_ALARM_AWAY_ARMED,
+ STATE_ALARM_ARMED_AWAY: HK_ALARM_AWAY_ARMED,
+ STATE_ALARM_ARMED_NIGHT: HK_ALARM_NIGHT_ARMED,
+ STATE_ALARM_ARMING: HK_ALARM_AWAY_ARMED,
+ STATE_ALARM_DISARMED: HK_ALARM_DISARMED,
}
HASS_TO_HOMEKIT_SERVICES = {
@@ -120,7 +124,7 @@ class SecuritySystem(HomeAccessory):
self.char_current_state = serv_alarm.configure_char(
CHAR_CURRENT_SECURITY_STATE,
- value=HASS_TO_HOMEKIT_CURRENT[AlarmControlPanelState.DISARMED],
+ value=HASS_TO_HOMEKIT_CURRENT[STATE_ALARM_DISARMED],
valid_values={
key: val
for key, val in default_current_states.items()
@@ -154,16 +158,8 @@ class SecuritySystem(HomeAccessory):
@callback
def async_update_state(self, new_state: State) -> None:
"""Update security state after state changed."""
- hass_state: str | AlarmControlPanelState = new_state.state
- if hass_state in {"None", STATE_UNKNOWN, STATE_UNAVAILABLE}:
- # Bail out early for no state, unknown or unavailable
- return
- if hass_state is not None:
- hass_state = AlarmControlPanelState(hass_state)
- if (
- hass_state
- and (current_state := HASS_TO_HOMEKIT_CURRENT.get(hass_state)) is not None
- ):
+ hass_state = new_state.state
+ if (current_state := HASS_TO_HOMEKIT_CURRENT.get(hass_state)) is not None:
self.char_current_state.set_value(current_state)
_LOGGER.debug(
"%s: Updated current state to %s (%d)",
@@ -171,8 +167,5 @@ class SecuritySystem(HomeAccessory):
hass_state,
current_state,
)
- if (
- hass_state
- and (target_state := HASS_TO_HOMEKIT_TARGET.get(hass_state)) is not None
- ):
+ if (target_state := HASS_TO_HOMEKIT_TARGET.get(hass_state)) is not None:
self.char_target_state.set_value(target_state)
diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py
index 3cb80f2c817..1cb94926e8b 100644
--- a/homeassistant/components/homekit_controller/alarm_control_panel.py
+++ b/homeassistant/components/homekit_controller/alarm_control_panel.py
@@ -10,10 +10,17 @@ from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_BATTERY_LEVEL, Platform
+from homeassistant.const import (
+ ATTR_BATTERY_LEVEL,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+ Platform,
+)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -22,18 +29,18 @@ from .connection import HKDevice
from .entity import HomeKitEntity
CURRENT_STATE_MAP = {
- 0: AlarmControlPanelState.ARMED_HOME,
- 1: AlarmControlPanelState.ARMED_AWAY,
- 2: AlarmControlPanelState.ARMED_NIGHT,
- 3: AlarmControlPanelState.DISARMED,
- 4: AlarmControlPanelState.TRIGGERED,
+ 0: STATE_ALARM_ARMED_HOME,
+ 1: STATE_ALARM_ARMED_AWAY,
+ 2: STATE_ALARM_ARMED_NIGHT,
+ 3: STATE_ALARM_DISARMED,
+ 4: STATE_ALARM_TRIGGERED,
}
TARGET_STATE_MAP = {
- AlarmControlPanelState.ARMED_HOME: 0,
- AlarmControlPanelState.ARMED_AWAY: 1,
- AlarmControlPanelState.ARMED_NIGHT: 2,
- AlarmControlPanelState.DISARMED: 3,
+ STATE_ALARM_ARMED_HOME: 0,
+ STATE_ALARM_ARMED_AWAY: 1,
+ STATE_ALARM_ARMED_NIGHT: 2,
+ STATE_ALARM_DISARMED: 3,
}
@@ -79,7 +86,7 @@ class HomeKitAlarmControlPanelEntity(HomeKitEntity, AlarmControlPanelEntity):
]
@property
- def alarm_state(self) -> AlarmControlPanelState:
+ def state(self) -> str:
"""Return the state of the device."""
return CURRENT_STATE_MAP[
self.service.value(CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT)
@@ -87,23 +94,21 @@ class HomeKitAlarmControlPanelEntity(HomeKitEntity, AlarmControlPanelEntity):
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
- await self.set_alarm_state(AlarmControlPanelState.DISARMED, code)
+ await self.set_alarm_state(STATE_ALARM_DISARMED, code)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm command."""
- await self.set_alarm_state(AlarmControlPanelState.ARMED_AWAY, code)
+ await self.set_alarm_state(STATE_ALARM_ARMED_AWAY, code)
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send stay command."""
- await self.set_alarm_state(AlarmControlPanelState.ARMED_HOME, code)
+ await self.set_alarm_state(STATE_ALARM_ARMED_HOME, code)
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send night command."""
- await self.set_alarm_state(AlarmControlPanelState.ARMED_NIGHT, code)
+ await self.set_alarm_state(STATE_ALARM_ARMED_NIGHT, code)
- async def set_alarm_state(
- self, state: AlarmControlPanelState, code: str | None = None
- ) -> None:
+ async def set_alarm_state(self, state: str, code: str | None = None) -> None:
"""Send state command."""
await self.async_put_characteristics(
{CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET: TARGET_STATE_MAP[state]}
diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py
index 4e55c8212be..3be0af17dbd 100644
--- a/homeassistant/components/homekit_controller/climate.py
+++ b/homeassistant/components/homekit_controller/climate.py
@@ -8,7 +8,6 @@ from typing import Any, Final
from aiohomekit.model.characteristics import (
ActivationStateValues,
CharacteristicsTypes,
- CurrentFanStateValues,
CurrentHeaterCoolerStateValues,
HeatingCoolingCurrentValues,
HeatingCoolingTargetValues,
@@ -485,7 +484,6 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
CharacteristicsTypes.TEMPERATURE_TARGET,
CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT,
CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET,
- CharacteristicsTypes.FAN_STATE_CURRENT,
]
async def async_set_temperature(self, **kwargs: Any) -> None:
@@ -668,19 +666,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
return HVACAction.IDLE
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_CURRENT)
- current_hass_value = CURRENT_MODE_HOMEKIT_TO_HASS.get(value)
-
- # If a device has a fan state (such as an Ecobee thermostat)
- # show the Fan state when the device is otherwise idle.
- if (
- current_hass_value == HVACAction.IDLE
- and self.service.has(CharacteristicsTypes.FAN_STATE_CURRENT)
- and self.service.value(CharacteristicsTypes.FAN_STATE_CURRENT)
- == CurrentFanStateValues.ACTIVE
- ):
- return HVACAction.FAN
-
- return current_hass_value
+ return CURRENT_MODE_HOMEKIT_TO_HASS.get(value)
@property
def hvac_mode(self) -> HVACMode:
diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py
index 77deb07b3dd..aea5a6661ee 100644
--- a/homeassistant/components/homekit_controller/const.py
+++ b/homeassistant/components/homekit_controller/const.py
@@ -50,7 +50,6 @@ HOMEKIT_ACCESSORY_DISPATCH = {
ServicesTypes.FAN_V2: "fan",
ServicesTypes.OCCUPANCY_SENSOR: "binary_sensor",
ServicesTypes.TELEVISION: "media_player",
- ServicesTypes.FAUCET: "switch",
ServicesTypes.VALVE: "switch",
ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT: "camera",
ServicesTypes.DOORBELL: "event",
diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json
index cddd61a12c1..b2b215a98b9 100644
--- a/homeassistant/components/homekit_controller/manifest.json
+++ b/homeassistant/components/homekit_controller/manifest.json
@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
- "requirements": ["aiohomekit==3.2.6"],
+ "requirements": ["aiohomekit==3.2.3"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}
diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py
index 5abed2a5c79..9fa4782e061 100644
--- a/homeassistant/components/homekit_controller/switch.py
+++ b/homeassistant/components/homekit_controller/switch.py
@@ -102,27 +102,6 @@ class HomeKitSwitch(HomeKitEntity, SwitchEntity):
return None
-class HomeKitFaucet(HomeKitEntity, SwitchEntity):
- """Representation of a Homekit faucet."""
-
- def get_characteristic_types(self) -> list[str]:
- """Define the homekit characteristics the entity cares about."""
- return [CharacteristicsTypes.ACTIVE]
-
- @property
- def is_on(self) -> bool:
- """Return true if device is on."""
- return self.service.value(CharacteristicsTypes.ACTIVE)
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn the specified faucet on."""
- await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True})
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn the specified faucet off."""
- await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
-
-
class HomeKitValve(HomeKitEntity, SwitchEntity):
"""Represents a valve in an irrigation system."""
@@ -213,10 +192,9 @@ class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity):
)
-ENTITY_TYPES: dict[str, type[HomeKitSwitch | HomeKitFaucet | HomeKitValve]] = {
+ENTITY_TYPES: dict[str, type[HomeKitSwitch | HomeKitValve]] = {
ServicesTypes.SWITCH: HomeKitSwitch,
ServicesTypes.OUTLET: HomeKitSwitch,
- ServicesTypes.FAUCET: HomeKitFaucet,
ServicesTypes.VALVE: HomeKitValve,
}
@@ -235,7 +213,7 @@ async def async_setup_entry(
if not (entity_class := ENTITY_TYPES.get(service.type)):
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
- entity: HomeKitSwitch | HomeKitFaucet | HomeKitValve = entity_class(conn, info)
+ entity: HomeKitSwitch | HomeKitValve = entity_class(conn, info)
conn.async_migrate_unique_id(
entity.old_unique_id, entity.unique_id, Platform.SWITCH
)
diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py
index 4241316c2a4..35aa321f2a8 100644
--- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py
+++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py
@@ -9,9 +9,14 @@ from homematicip.functionalHomes import SecurityAndAlarmHome
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -60,21 +65,21 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity):
)
@property
- def alarm_state(self) -> AlarmControlPanelState:
+ def state(self) -> str:
"""Return the state of the alarm control panel."""
# check for triggered alarm
if self._security_and_alarm.alarmActive:
- return AlarmControlPanelState.TRIGGERED
+ return STATE_ALARM_TRIGGERED
activation_state = self._home.get_security_zones_activation()
# check arm_away
if activation_state == (True, True):
- return AlarmControlPanelState.ARMED_AWAY
+ return STATE_ALARM_ARMED_AWAY
# check arm_home
if activation_state == (False, True):
- return AlarmControlPanelState.ARMED_HOME
+ return STATE_ALARM_ARMED_HOME
- return AlarmControlPanelState.DISARMED
+ return STATE_ALARM_DISARMED
@property
def _security_and_alarm(self) -> SecurityAndAlarmHome:
diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py
index cf051103a10..5a56ae69377 100644
--- a/homeassistant/components/homematicip_cloud/light.py
+++ b/homeassistant/components/homematicip_cloud/light.py
@@ -14,14 +14,12 @@ from homematicip.aio.device import (
AsyncPluggableDimmer,
AsyncWiredDimmer3,
)
-from homematicip.base.enums import OpticalSignalBehaviour, RGBColorState
+from homematicip.base.enums import RGBColorState
from homematicip.base.functionalChannels import NotificationLightChannel
-from packaging.version import Version
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_NAME,
- ATTR_EFFECT,
ATTR_HS_COLOR,
ATTR_TRANSITION,
ColorMode,
@@ -49,22 +47,15 @@ async def async_setup_entry(
if isinstance(device, AsyncBrandSwitchMeasuring):
entities.append(HomematicipLightMeasuring(hap, device))
elif isinstance(device, AsyncBrandSwitchNotificationLight):
- device_version = Version(device.firmwareVersion)
entities.append(HomematicipLight(hap, device))
-
- entity_class = (
- HomematicipNotificationLightV2
- if device_version > Version("2.0.0")
- else HomematicipNotificationLight
- )
-
entities.append(
- entity_class(hap, device, device.topLightChannelIndex, "Top")
+ HomematicipNotificationLight(hap, device, device.topLightChannelIndex)
)
entities.append(
- entity_class(hap, device, device.bottomLightChannelIndex, "Bottom")
+ HomematicipNotificationLight(
+ hap, device, device.bottomLightChannelIndex
+ )
)
-
elif isinstance(device, (AsyncWiredDimmer3, AsyncDinRailDimmer3)):
entities.extend(
HomematicipMultiDimmer(hap, device, channel=channel)
@@ -167,9 +158,16 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity):
_attr_supported_color_modes = {ColorMode.HS}
_attr_supported_features = LightEntityFeature.TRANSITION
- def __init__(self, hap: HomematicipHAP, device, channel: int, post: str) -> None:
+ def __init__(self, hap: HomematicipHAP, device, channel: int) -> None:
"""Initialize the notification light entity."""
- super().__init__(hap, device, post=post, channel=channel, is_multi_channel=True)
+ if channel == 2:
+ super().__init__(
+ hap, device, post="Top", channel=channel, is_multi_channel=True
+ )
+ else:
+ super().__init__(
+ hap, device, post="Bottom", channel=channel, is_multi_channel=True
+ )
self._color_switcher: dict[str, tuple[float, float]] = {
RGBColorState.WHITE: (0.0, 0.0),
@@ -261,66 +259,6 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity):
)
-class HomematicipNotificationLightV2(HomematicipNotificationLight, LightEntity):
- """Representation of HomematicIP Cloud notification light."""
-
- _effect_list = [
- OpticalSignalBehaviour.BILLOW_MIDDLE,
- OpticalSignalBehaviour.BLINKING_MIDDLE,
- OpticalSignalBehaviour.FLASH_MIDDLE,
- OpticalSignalBehaviour.OFF,
- OpticalSignalBehaviour.ON,
- ]
-
- def __init__(self, hap: HomematicipHAP, device, channel: int, post: str) -> None:
- """Initialize the notification light entity."""
- super().__init__(hap, device, post=post, channel=channel)
- self._attr_supported_features |= LightEntityFeature.EFFECT
-
- @property
- def effect_list(self) -> list[str] | None:
- """Return the list of supported effects."""
- return self._effect_list
-
- @property
- def effect(self) -> str | None:
- """Return the current effect."""
- return self._func_channel.opticalSignalBehaviour
-
- @property
- def is_on(self) -> bool:
- """Return true if light is on."""
- return self._func_channel.on
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn the light on."""
- # Use hs_color from kwargs,
- # if not applicable use current hs_color.
- hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color)
- simple_rgb_color = _convert_color(hs_color)
-
- # If no kwargs, use default value.
- brightness = 255
- if ATTR_BRIGHTNESS in kwargs:
- brightness = kwargs[ATTR_BRIGHTNESS]
-
- # Minimum brightness is 10, otherwise the led is disabled
- brightness = max(10, brightness)
- dim_level = round(brightness / 255.0, 2)
-
- effect = self.effect
- if ATTR_EFFECT in kwargs:
- effect = kwargs[ATTR_EFFECT]
-
- await self._func_channel.async_set_optical_signal(
- opticalSignalBehaviour=effect, rgb=simple_rgb_color, dimLevel=dim_level
- )
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn the light off."""
- await self._func_channel.async_turn_off()
-
-
def _convert_color(color: tuple) -> RGBColorState:
"""Convert the given color to the reduced RGBColorState color.
diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py
index c44d280c190..a9c046e25bf 100644
--- a/homeassistant/components/homematicip_cloud/sensor.py
+++ b/homeassistant/components/homematicip_cloud/sensor.py
@@ -8,9 +8,6 @@ from typing import Any
from homematicip.aio.device import (
AsyncBrandSwitchMeasuring,
AsyncEnergySensorsInterface,
- AsyncFloorTerminalBlock6,
- AsyncFloorTerminalBlock10,
- AsyncFloorTerminalBlock12,
AsyncFullFlushSwitchMeasuring,
AsyncHeatingThermostat,
AsyncHeatingThermostatCompact,
@@ -31,13 +28,9 @@ from homematicip.aio.device import (
AsyncWeatherSensor,
AsyncWeatherSensorPlus,
AsyncWeatherSensorPro,
- AsyncWiredFloorTerminalBlock12,
)
from homematicip.base.enums import FunctionalChannelType, ValveState
-from homematicip.base.functionalChannels import (
- FloorTerminalBlockMechanicChannel,
- FunctionalChannel,
-)
+from homematicip.base.functionalChannels import FunctionalChannel
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -93,7 +86,7 @@ ILLUMINATION_DEVICE_ATTRIBUTES = {
}
-async def async_setup_entry( # noqa: C901
+async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
@@ -191,74 +184,10 @@ async def async_setup_entry( # noqa: C901
if ch.currentPowerConsumption is not None:
entities.append(HmipEsiLedCurrentPowerConsumption(hap, device))
entities.append(HmipEsiLedEnergyCounterHighTariff(hap, device))
- if isinstance(
- device,
- (
- AsyncFloorTerminalBlock6,
- AsyncFloorTerminalBlock10,
- AsyncFloorTerminalBlock12,
- AsyncWiredFloorTerminalBlock12,
- ),
- ):
- entities.extend(
- HomematicipFloorTerminalBlockMechanicChannelValve(
- hap, device, channel=channel.index
- )
- for channel in device.functionalChannels
- if isinstance(channel, FloorTerminalBlockMechanicChannel)
- and getattr(channel, "valvePosition", None) is not None
- )
async_add_entities(entities)
-class HomematicipFloorTerminalBlockMechanicChannelValve(
- HomematicipGenericEntity, SensorEntity
-):
- """Representation of the HomematicIP floor terminal block."""
-
- _attr_native_unit_of_measurement = PERCENTAGE
- _attr_state_class = SensorStateClass.MEASUREMENT
-
- def __init__(
- self, hap: HomematicipHAP, device, channel, is_multi_channel=True
- ) -> None:
- """Initialize floor terminal block 12 device."""
- super().__init__(
- hap,
- device,
- channel=channel,
- is_multi_channel=is_multi_channel,
- post="Valve Position",
- )
-
- @property
- def icon(self) -> str | None:
- """Return the icon."""
- if super().icon:
- return super().icon
- channel = next(
- channel
- for channel in self._device.functionalChannels
- if channel.index == self._channel
- )
- if channel.valveState != ValveState.ADAPTION_DONE:
- return "mdi:alert"
- return "mdi:heating-coil"
-
- @property
- def native_value(self) -> int | None:
- """Return the state of the floor terminal block mechanical channel valve position."""
- channel = next(
- channel
- for channel in self._device.functionalChannels
- if channel.index == self._channel
- )
- if channel.valveState != ValveState.ADAPTION_DONE:
- return None
- return round(channel.valvePosition * 100)
-
-
class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity):
"""Representation of then HomeMaticIP access point."""
@@ -420,7 +349,6 @@ class HomematicipWindspeedSensor(HomematicipGenericEntity, SensorEntity):
_attr_device_class = SensorDeviceClass.WIND_SPEED
_attr_native_unit_of_measurement = UnitOfSpeed.KILOMETERS_PER_HOUR
- _attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the windspeed sensor."""
@@ -452,7 +380,6 @@ class HomematicipTodayRainSensor(HomematicipGenericEntity, SensorEntity):
_attr_device_class = SensorDeviceClass.PRECIPITATION
_attr_native_unit_of_measurement = UnitOfPrecipitationDepth.MILLIMETERS
- _attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the device."""
diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py
index d52e53cf39b..06dbb9c8333 100644
--- a/homeassistant/components/homewizard/config_flow.py
+++ b/homeassistant/components/homewizard/config_flow.py
@@ -12,7 +12,7 @@ from homewizard_energy.models import Device
from voluptuous import Required, Schema
from homeassistant.components import onboarding, zeroconf
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_IP_ADDRESS, CONF_PATH
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
@@ -43,6 +43,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
discovery: DiscoveryData
+ entry: ConfigEntry | None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -150,6 +151,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-auth if API was disabled."""
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -158,17 +160,20 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
"""Confirm reauth dialog."""
errors: dict[str, str] | None = None
if user_input is not None:
- reauth_entry = self._get_reauth_entry()
+ assert self.entry is not None
try:
- await self._async_try_connect(reauth_entry.data[CONF_IP_ADDRESS])
+ await self._async_try_connect(self.entry.data[CONF_IP_ADDRESS])
except RecoverableError as ex:
_LOGGER.error(ex)
errors = {"base": ex.error_code}
else:
- await self.hass.config_entries.async_reload(reauth_entry.entry_id)
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_abort(reason="reauth_successful")
- return self.async_show_form(step_id="reauth_confirm", errors=errors)
+ return self.async_show_form(
+ step_id="reauth_confirm",
+ errors=errors,
+ )
@staticmethod
async def _async_try_connect(ip_address: str) -> Device:
diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py
index 61b304eb39c..db41d1dd128 100644
--- a/homeassistant/components/homewizard/coordinator.py
+++ b/homeassistant/components/homewizard/coordinator.py
@@ -74,8 +74,7 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry]
# Do not reload when performing first refresh
if self.data is not None:
- # Reload config entry to let init flow handle retrying and trigger repair flow
- self.hass.config_entries.async_schedule_reload(
+ await self.hass.config_entries.async_reload(
self.config_entry.entry_id
)
diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py
index 57071875edb..9bb61a467cb 100644
--- a/homeassistant/components/homewizard/sensor.py
+++ b/homeassistant/components/homewizard/sensor.py
@@ -19,6 +19,7 @@ from homeassistant.const import (
ATTR_VIA_DEVICE,
PERCENTAGE,
EntityCategory,
+ Platform,
UnitOfApparentPower,
UnitOfElectricCurrent,
UnitOfElectricPotential,
@@ -29,6 +30,7 @@ from homeassistant.const import (
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
@@ -623,6 +625,7 @@ async def async_setup_entry(
) -> None:
"""Initialize sensors."""
+ ent_reg = er.async_get(hass)
data = entry.runtime_data.data.data
# Initialize default sensors
@@ -636,6 +639,17 @@ async def async_setup_entry(
if data.external_devices is not None:
for unique_id, device in data.external_devices.items():
if description := EXTERNAL_SENSORS.get(device.meter_type):
+ # Migrate external devices to new unique_id
+ # This is to ensure that devices with same id but different type are unique
+ # Migration can be removed after 2024.11.0
+ if entity_id := ent_reg.async_get_entity_id(
+ Platform.SENSOR, DOMAIN, f"{DOMAIN}_{device.unique_id}"
+ ):
+ ent_reg.async_update_entity(
+ entity_id,
+ new_unique_id=f"{DOMAIN}_{unique_id}",
+ )
+
# Add external device
entities.append(
HomeWizardExternalSensorEntity(
diff --git a/homeassistant/components/homewizard/strings.json b/homeassistant/components/homewizard/strings.json
index 751c1ec450d..ca903330a44 100644
--- a/homeassistant/components/homewizard/strings.json
+++ b/homeassistant/components/homewizard/strings.json
@@ -22,10 +22,9 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "invalid_discovery_parameters": "Invalid discovery parameters",
+ "invalid_discovery_parameters": "Detected unsupported API version",
"device_not_supported": "This device is not supported",
"unknown_error": "[%key:common::config_flow::error::unknown%]",
- "unsupported_api_version": "Detected unsupported API version",
"reauth_successful": "Enabling API was successful"
}
},
diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py
index d1fa7774ef6..e08110cc8b0 100644
--- a/homeassistant/components/homeworks/config_flow.py
+++ b/homeassistant/components/homeworks/config_flow.py
@@ -558,25 +558,35 @@ class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for Lutron Homeworks."""
async def _validate_edit_controller(
- self, user_input: dict[str, Any], reconfigure_entry: ConfigEntry
+ self, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate controller setup."""
_validate_credentials(user_input)
user_input[CONF_PORT] = int(user_input[CONF_PORT])
- if any(
- entry.entry_id != reconfigure_entry.entry_id
- and user_input[CONF_HOST] == entry.options[CONF_HOST]
- and user_input[CONF_PORT] == entry.options[CONF_PORT]
- for entry in self._async_current_entries()
- ):
- raise SchemaFlowError("duplicated_host_port")
+ our_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+ assert our_entry
+ other_entries = self._async_current_entries()
+ for entry in other_entries:
+ if entry.entry_id == our_entry.entry_id:
+ continue
+ if (
+ user_input[CONF_HOST] == entry.options[CONF_HOST]
+ and user_input[CONF_PORT] == entry.options[CONF_PORT]
+ ):
+ raise SchemaFlowError("duplicated_host_port")
await _try_connection(user_input)
return user_input
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle a reconfigure flow."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfigure flow."""
errors = {}
@@ -596,7 +606,7 @@ class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_PASSWORD: user_input.get(CONF_PASSWORD),
}
try:
- await self._validate_edit_controller(user_input, reconfigure_entry)
+ await self._validate_edit_controller(user_input)
except SchemaFlowError as err:
errors["base"] = str(err)
else:
@@ -618,7 +628,7 @@ class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=self.add_suggested_values_to_schema(
DATA_SCHEMA_EDIT_CONTROLLER, suggested_values
),
diff --git a/homeassistant/components/homeworks/strings.json b/homeassistant/components/homeworks/strings.json
index 977e6be8afd..c2c8a14f77c 100644
--- a/homeassistant/components/homeworks/strings.json
+++ b/homeassistant/components/homeworks/strings.json
@@ -1,8 +1,5 @@
{
"config": {
- "abort": {
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
- },
"error": {
"connection_error": "Could not connect to the controller.",
"credentials_needed": "The controller needs credentials.",
@@ -25,7 +22,7 @@
"name": "[%key:component::homeworks::config::step::user::data_description::name%]"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
@@ -48,8 +45,8 @@
},
"data_description": {
"name": "A unique name identifying the Lutron Homeworks controller",
- "password": "[%key:component::homeworks::config::step::reconfigure::data_description::password%]",
- "username": "[%key:component::homeworks::config::step::reconfigure::data_description::username%]"
+ "password": "[%key:component::homeworks::config::step::reconfigure_confirm::data_description::password%]",
+ "username": "[%key:component::homeworks::config::step::reconfigure_confirm::data_description::username%]"
},
"description": "Add a Lutron Homeworks controller"
}
diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py
index 98cbae4eb7e..934d41b238e 100644
--- a/homeassistant/components/honeywell/climate.py
+++ b/homeassistant/components/honeywell/climate.py
@@ -49,10 +49,6 @@ from .const import (
RETRY,
)
-MODE_PERMANENT_HOLD = 2
-MODE_TEMPORARY_HOLD = 1
-MODE_HOLD = {MODE_PERMANENT_HOLD, MODE_TEMPORARY_HOLD}
-
ATTR_FAN_ACTION = "fan_action"
ATTR_PERMANENT_HOLD = "permanent_hold"
@@ -179,7 +175,6 @@ class HoneywellUSThermostat(ClimateEntity):
self._cool_away_temp = cool_away_temp
self._heat_away_temp = heat_away_temp
self._away = False
- self._away_hold = False
self._retry = 0
self._attr_unique_id = str(device.deviceid)
@@ -328,15 +323,11 @@ class HoneywellUSThermostat(ClimateEntity):
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
- if self._away and self._is_hold():
- self._away_hold = True
+ if self._away:
return PRESET_AWAY
- if self._is_hold():
+ if self._is_permanent_hold():
return PRESET_HOLD
- # Someone has changed the stat manually out of hold in away mode
- if self._away and self._away_hold:
- self._away = False
- self._away_hold = False
+
return PRESET_NONE
@property
@@ -344,15 +335,10 @@ class HoneywellUSThermostat(ClimateEntity):
"""Return the fan setting."""
return HW_FAN_MODE_TO_HA.get(self._device.fan_mode)
- def _is_hold(self) -> bool:
- heat_status = self._device.raw_ui_data.get("StatusHeat", 0)
- cool_status = self._device.raw_ui_data.get("StatusCool", 0)
- return heat_status in MODE_HOLD or cool_status in MODE_HOLD
-
def _is_permanent_hold(self) -> bool:
heat_status = self._device.raw_ui_data.get("StatusHeat", 0)
cool_status = self._device.raw_ui_data.get("StatusCool", 0)
- return MODE_PERMANENT_HOLD in (heat_status, cool_status)
+ return heat_status == 2 or cool_status == 2
async def _set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py
index c7cda500692..7f298aee632 100644
--- a/homeassistant/components/honeywell/config_flow.py
+++ b/homeassistant/components/honeywell/config_flow.py
@@ -38,11 +38,14 @@ class HoneywellConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a honeywell config flow."""
VERSION = 1
+ entry: ConfigEntry | None
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication with Honeywell."""
+
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -50,8 +53,8 @@ class HoneywellConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm re-authentication with Honeywell."""
errors: dict[str, str] = {}
+ assert self.entry is not None
- reauth_entry = self._get_reauth_entry()
if user_input:
try:
await self.is_valid(
@@ -69,14 +72,18 @@ class HoneywellConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
else:
return self.async_update_reload_and_abort(
- reauth_entry,
- data_updates=user_input,
+ self.entry,
+ data={
+ **self.entry.data,
+ **user_input,
+ },
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(
- REAUTH_SCHEMA, reauth_entry.data
+ REAUTH_SCHEMA,
+ self.entry.data,
),
errors=errors,
description_placeholders={"name": "Honeywell"},
@@ -129,12 +136,16 @@ class HoneywellConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> HoneywellOptionsFlowHandler:
"""Options callback for Honeywell."""
- return HoneywellOptionsFlowHandler()
+ return HoneywellOptionsFlowHandler(config_entry)
class HoneywellOptionsFlowHandler(OptionsFlow):
"""Config flow options for Honeywell."""
+ def __init__(self, entry: ConfigEntry) -> None:
+ """Initialize Honeywell options flow."""
+ self.config_entry = entry
+
async def async_step_init(self, user_input=None) -> ConfigFlowResult:
"""Manage the options."""
if user_input is not None:
diff --git a/homeassistant/components/honeywell/strings.json b/homeassistant/components/honeywell/strings.json
index a64f1a6fce0..aa6e53620a5 100644
--- a/homeassistant/components/honeywell/strings.json
+++ b/homeassistant/components/honeywell/strings.json
@@ -16,9 +16,6 @@
}
}
},
- "abort": {
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
- },
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py
index c8fc8ffb11b..dd5f1ed1b05 100644
--- a/homeassistant/components/http/ban.py
+++ b/homeassistant/components/http/ban.py
@@ -27,7 +27,6 @@ from homeassistant.config import load_yaml_config_file
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.hassio import get_supervisor_ip, is_hassio
from homeassistant.util import dt as dt_util, yaml
from .const import KEY_HASS
@@ -150,8 +149,12 @@ async def process_wrong_login(request: Request) -> None:
request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1
# Supervisor IP should never be banned
- if is_hassio(hass) and str(remote_addr) == get_supervisor_ip():
- return
+ if "hassio" in hass.config.components:
+ # pylint: disable-next=import-outside-toplevel
+ from homeassistant.components import hassio
+
+ if hassio.get_supervisor_ip() == str(remote_addr):
+ return
if (
request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr]
diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py
index 08fdae50c51..160b2a62b55 100644
--- a/homeassistant/components/huawei_lte/config_flow.py
+++ b/homeassistant/components/huawei_lte/config_flow.py
@@ -69,7 +69,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def _async_show_user_form(
self,
@@ -320,7 +320,8 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
- entry = self._get_reauth_entry()
+ entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+ assert entry
if not user_input:
return await self._async_show_reauth_form(
user_input={
@@ -339,12 +340,18 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
user_input=user_input, errors=errors
)
- return self.async_update_reload_and_abort(entry, data=new_data)
+ self.hass.config_entries.async_update_entry(entry, data=new_data)
+ await self.hass.config_entries.async_reload(entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
class OptionsFlowHandler(OptionsFlow):
"""Huawei LTE options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json
index 6720d6718ef..9a44024111c 100644
--- a/homeassistant/components/huawei_lte/manifest.json
+++ b/homeassistant/components/huawei_lte/manifest.json
@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["huawei_lte_api.Session"],
"requirements": [
- "huawei-lte-api==1.10.0",
+ "huawei-lte-api==1.7.3",
"stringcase==1.2.0",
"url-normalize==1.4.3"
],
diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py
index 8d17f810461..e73ae8fe11d 100644
--- a/homeassistant/components/hue/config_flow.py
+++ b/homeassistant/components/hue/config_flow.py
@@ -57,8 +57,8 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
) -> HueV1OptionsFlowHandler | HueV2OptionsFlowHandler:
"""Get the options flow for this handler."""
if config_entry.data.get(CONF_API_VERSION, 1) == 1:
- return HueV1OptionsFlowHandler()
- return HueV2OptionsFlowHandler()
+ return HueV1OptionsFlowHandler(config_entry)
+ return HueV2OptionsFlowHandler(config_entry)
def __init__(self) -> None:
"""Initialize the Hue flow."""
@@ -280,6 +280,10 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
class HueV1OptionsFlowHandler(OptionsFlow):
"""Handle Hue options for V1 implementation."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize Hue options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -311,6 +315,10 @@ class HueV1OptionsFlowHandler(OptionsFlow):
class HueV2OptionsFlowHandler(OptionsFlow):
"""Handle Hue options for V2 implementation."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize Hue options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json
index 2f7f2e55561..ab1d0fb58ad 100644
--- a/homeassistant/components/hue/strings.json
+++ b/homeassistant/components/hue/strings.json
@@ -137,15 +137,15 @@
"services": {
"hue_activate_scene": {
"name": "Activate scene",
- "description": "Activates a Hue scene stored in the Hue hub.",
+ "description": "Activates a hue scene stored in the hue hub.",
"fields": {
"group_name": {
"name": "Group",
- "description": "Name of Hue group/room from the Hue app."
+ "description": "Name of hue group/room from the hue app."
},
"scene_name": {
"name": "Scene",
- "description": "Name of Hue scene from the Hue app."
+ "description": "Name of hue scene from the hue app."
},
"dynamic": {
"name": "Dynamic",
diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py
index f9703f67df5..3e0c9845c92 100644
--- a/homeassistant/components/huisbaasje/__init__.py
+++ b/homeassistant/components/huisbaasje/__init__.py
@@ -54,7 +54,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name="sensor",
update_method=async_update_data,
update_interval=timedelta(seconds=POLLING_INTERVAL),
diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py
index d9358db2753..f8c7ac43b94 100644
--- a/homeassistant/components/hunterdouglas_powerview/__init__.py
+++ b/homeassistant/components/hunterdouglas_powerview/__init__.py
@@ -1,8 +1,9 @@
"""The Hunter Douglas PowerView integration."""
import logging
-from typing import TYPE_CHECKING
+from aiopvapi.helpers.aiorequest import AioRequest
+from aiopvapi.hub import Hub
from aiopvapi.resources.model import PowerviewData
from aiopvapi.rooms import Rooms
from aiopvapi.scenes import Scenes
@@ -11,13 +12,12 @@ from aiopvapi.shades import Shades
from homeassistant.const import CONF_API_VERSION, CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
-import homeassistant.helpers.entity_registry as er
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, HUB_EXCEPTIONS
from .coordinator import PowerviewShadeUpdateCoordinator
-from .model import PowerviewConfigEntry, PowerviewEntryData
+from .model import PowerviewConfigEntry, PowerviewDeviceInfo, PowerviewEntryData
from .shade_data import PowerviewShadeData
-from .util import async_connect_hub
PARALLEL_UPDATES = 1
@@ -35,23 +35,29 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: PowerviewConfigEntry) -> bool:
"""Set up Hunter Douglas PowerView from a config entry."""
+
config = entry.data
- hub_address: str = config[CONF_HOST]
- api_version: int | None = config.get(CONF_API_VERSION)
+
+ hub_address = config[CONF_HOST]
+ api_version = config.get(CONF_API_VERSION, None)
_LOGGER.debug("Connecting %s at %s with v%s api", DOMAIN, hub_address, api_version)
+ websession = async_get_clientsession(hass)
+
+ pv_request = AioRequest(
+ hub_address, loop=hass.loop, websession=websession, api_version=api_version
+ )
+
# default 15 second timeout for each call in upstream
try:
- api = await async_connect_hub(hass, hub_address, api_version)
+ hub = Hub(pv_request)
+ await hub.query_firmware()
+ device_info = await async_get_device_info(hub)
except HUB_EXCEPTIONS as err:
raise ConfigEntryNotReady(
f"Connection error to PowerView hub {hub_address}: {err}"
) from err
- hub = api.hub
- pv_request = api.pv_request
- device_info = api.device_info
-
if hub.role != "Primary":
# this should be caught in config_flow, but account for a hub changing roles
# this will only happen manually by a user
@@ -86,11 +92,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: PowerviewConfigEntry) ->
new_data[CONF_API_VERSION] = hub.api_version
hass.config_entries.async_update_entry(entry, data=new_data)
- if entry.unique_id is None:
- hass.config_entries.async_update_entry(
- entry, unique_id=device_info.serial_number
- )
-
coordinator = PowerviewShadeUpdateCoordinator(hass, shades, hub)
coordinator.async_set_updated_data(PowerviewShadeData())
# populate raw shade data into the coordinator for diagnostics
@@ -110,62 +111,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: PowerviewConfigEntry) ->
return True
+async def async_get_device_info(hub: Hub) -> PowerviewDeviceInfo:
+ """Determine device info."""
+ return PowerviewDeviceInfo(
+ name=hub.name,
+ mac_address=hub.mac_address,
+ serial_number=hub.serial_number,
+ firmware=hub.firmware,
+ model=hub.model,
+ hub_address=hub.ip,
+ )
+
+
async def async_unload_entry(hass: HomeAssistant, entry: PowerviewConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
-
-
-async def async_migrate_entry(hass: HomeAssistant, entry: PowerviewConfigEntry) -> bool:
- """Migrate entry."""
-
- _LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
-
- if entry.version == 1:
- # 1 -> 2: Unique ID from integer to string
- if entry.minor_version == 1:
- if entry.unique_id is None:
- await _async_add_missing_entry_unique_id(hass, entry)
- await _migrate_unique_ids(hass, entry)
- hass.config_entries.async_update_entry(entry, minor_version=2)
-
- _LOGGER.debug("Migrated to version %s.%s", entry.version, entry.minor_version)
-
- return True
-
-
-async def _async_add_missing_entry_unique_id(
- hass: HomeAssistant, entry: PowerviewConfigEntry
-) -> None:
- """Add the unique id if its missing."""
- address: str = entry.data[CONF_HOST]
- api_version: int | None = entry.data.get(CONF_API_VERSION)
- api = await async_connect_hub(hass, address, api_version)
- hass.config_entries.async_update_entry(
- entry, unique_id=api.device_info.serial_number
- )
-
-
-async def _migrate_unique_ids(hass: HomeAssistant, entry: PowerviewConfigEntry) -> None:
- """Migrate int based unique ids to str."""
- entity_registry = er.async_get(hass)
- registry_entries = er.async_entries_for_config_entry(
- entity_registry, entry.entry_id
- )
- if TYPE_CHECKING:
- assert entry.unique_id
- for reg_entry in registry_entries:
- if isinstance(reg_entry.unique_id, int) or (
- isinstance(reg_entry.unique_id, str)
- and not reg_entry.unique_id.startswith(entry.unique_id)
- ):
- _LOGGER.debug(
- "Migrating %s: %s to %s_%s",
- reg_entry.entity_id,
- reg_entry.unique_id,
- entry.unique_id,
- reg_entry.unique_id,
- )
- entity_registry.async_update_entity(
- reg_entry.entity_id,
- new_unique_id=f"{entry.unique_id}_{reg_entry.unique_id}",
- )
diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py
index debb9710dbd..1d4bcd9e2b8 100644
--- a/homeassistant/components/hunterdouglas_powerview/config_flow.py
+++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py
@@ -5,6 +5,8 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, Self
+from aiopvapi.helpers.aiorequest import AioRequest
+from aiopvapi.hub import Hub
import voluptuous as vol
from homeassistant.components import dhcp, zeroconf
@@ -12,15 +14,16 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_VERSION, CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from . import async_get_device_info
from .const import DOMAIN, HUB_EXCEPTIONS
-from .util import async_connect_hub
_LOGGER = logging.getLogger(__name__)
HAP_SUFFIX = "._hap._tcp.local."
POWERVIEW_G2_SUFFIX = "._powerview._tcp.local."
-POWERVIEW_G3_SUFFIX = "._PowerView-G3._tcp.local."
+POWERVIEW_G3_SUFFIX = "._powerview-g3._tcp.local."
async def validate_input(hass: HomeAssistant, hub_address: str) -> dict[str, str]:
@@ -28,9 +31,18 @@ async def validate_input(hass: HomeAssistant, hub_address: str) -> dict[str, str
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
- api = await async_connect_hub(hass, hub_address)
- hub = api.hub
- device_info = api.device_info
+
+ websession = async_get_clientsession(hass)
+
+ pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession)
+
+ try:
+ hub = Hub(pv_request)
+ await hub.query_firmware()
+ device_info = await async_get_device_info(hub)
+ except HUB_EXCEPTIONS as err:
+ raise CannotConnect from err
+
if hub.role != "Primary":
raise UnsupportedDevice(
f"{hub.name} ({hub.hub_address}) is the {hub.role} Hub. "
@@ -51,7 +63,6 @@ class PowerviewConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Hunter Douglas PowerView."""
VERSION = 1
- MINOR_VERSION = 2
def __init__(self) -> None:
"""Initialize the powerview config flow."""
@@ -99,7 +110,7 @@ class PowerviewConfigFlow(ConfigFlow, domain=DOMAIN):
try:
info = await validate_input(self.hass, host)
- except HUB_EXCEPTIONS:
+ except CannotConnect:
return None, "cannot_connect"
except UnsupportedDevice:
return None, "unsupported_device"
@@ -188,5 +199,9 @@ class PowerviewConfigFlow(ConfigFlow, domain=DOMAIN):
)
+class CannotConnect(HomeAssistantError):
+ """Error to indicate we cannot connect."""
+
+
class UnsupportedDevice(HomeAssistantError):
"""Error to indicate the device is not supported."""
diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py
index 197fb4e6223..6ee5fc92a41 100644
--- a/homeassistant/components/hunterdouglas_powerview/cover.py
+++ b/homeassistant/components/hunterdouglas_powerview/cover.py
@@ -595,7 +595,7 @@ class PowerViewShadeTDBUBottom(PowerViewShadeDualRailBase):
) -> None:
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
- self._attr_unique_id = f"{self._attr_unique_id}_bottom"
+ self._attr_unique_id = f"{self._shade.id}_bottom"
@callback
def _clamp_cover_limit(self, target_hass_position: int) -> int:
@@ -632,7 +632,7 @@ class PowerViewShadeTDBUTop(PowerViewShadeDualRailBase):
) -> None:
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
- self._attr_unique_id = f"{self._attr_unique_id}_top"
+ self._attr_unique_id = f"{self._shade.id}_top"
@property
def should_poll(self) -> bool:
@@ -740,7 +740,7 @@ class PowerViewShadeDualOverlappedCombined(PowerViewShadeDualOverlappedBase):
) -> None:
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
- self._attr_unique_id = f"{self._attr_unique_id}_combined"
+ self._attr_unique_id = f"{self._shade.id}_combined"
@property
def is_closed(self) -> bool:
@@ -806,7 +806,7 @@ class PowerViewShadeDualOverlappedFront(PowerViewShadeDualOverlappedBase):
) -> None:
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
- self._attr_unique_id = f"{self._attr_unique_id}_front"
+ self._attr_unique_id = f"{self._shade.id}_front"
@property
def should_poll(self) -> bool:
@@ -862,7 +862,7 @@ class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase):
) -> None:
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
- self._attr_unique_id = f"{self._attr_unique_id}_rear"
+ self._attr_unique_id = f"{self._shade.id}_rear"
@property
def should_poll(self) -> bool:
diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py
index ba572ecefce..424d314c4b9 100644
--- a/homeassistant/components/hunterdouglas_powerview/entity.py
+++ b/homeassistant/components/hunterdouglas_powerview/entity.py
@@ -26,12 +26,12 @@ class HDEntity(CoordinatorEntity[PowerviewShadeUpdateCoordinator]):
coordinator: PowerviewShadeUpdateCoordinator,
device_info: PowerviewDeviceInfo,
room_name: str,
- powerview_id: str,
+ unique_id: str,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._room_name = room_name
- self._attr_unique_id = f"{device_info.serial_number}_{powerview_id}"
+ self._attr_unique_id = unique_id
self._device_info = device_info
self._configuration_url = self.coordinator.hub.url
diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json
index a80708d9a3f..4120c55a7a7 100644
--- a/homeassistant/components/hunterdouglas_powerview/manifest.json
+++ b/homeassistant/components/hunterdouglas_powerview/manifest.json
@@ -19,5 +19,5 @@
"iot_class": "local_polling",
"loggers": ["aiopvapi"],
"requirements": ["aiopvapi==3.1.1"],
- "zeroconf": ["_powerview._tcp.local.", "_PowerView-G3._tcp.local."]
+ "zeroconf": ["_powerview._tcp.local.", "_powerview-g3._tcp.local."]
}
diff --git a/homeassistant/components/hunterdouglas_powerview/model.py b/homeassistant/components/hunterdouglas_powerview/model.py
index 407de86368f..86296b949f4 100644
--- a/homeassistant/components/hunterdouglas_powerview/model.py
+++ b/homeassistant/components/hunterdouglas_powerview/model.py
@@ -3,23 +3,20 @@
from __future__ import annotations
from dataclasses import dataclass
-from typing import TYPE_CHECKING
from aiopvapi.helpers.aiorequest import AioRequest
-from aiopvapi.hub import Hub
from aiopvapi.resources.room import Room
from aiopvapi.resources.scene import Scene
from aiopvapi.resources.shade import BaseShade
from homeassistant.config_entries import ConfigEntry
-if TYPE_CHECKING:
- from .coordinator import PowerviewShadeUpdateCoordinator
+from .coordinator import PowerviewShadeUpdateCoordinator
type PowerviewConfigEntry = ConfigEntry[PowerviewEntryData]
-@dataclass(slots=True)
+@dataclass
class PowerviewEntryData:
"""Define class for main domain information."""
@@ -31,7 +28,7 @@ class PowerviewEntryData:
device_info: PowerviewDeviceInfo
-@dataclass(slots=True)
+@dataclass
class PowerviewDeviceInfo:
"""Define class for device information."""
@@ -41,12 +38,3 @@ class PowerviewDeviceInfo:
firmware: str | None
model: str
hub_address: str
-
-
-@dataclass(slots=True)
-class PowerviewAPI:
- """Define class to hold the Powerview Hub API data."""
-
- hub: Hub
- pv_request: AioRequest
- device_info: PowerviewDeviceInfo
diff --git a/homeassistant/components/hunterdouglas_powerview/number.py b/homeassistant/components/hunterdouglas_powerview/number.py
index fb8c9f76d79..f893b04b2d1 100644
--- a/homeassistant/components/hunterdouglas_powerview/number.py
+++ b/homeassistant/components/hunterdouglas_powerview/number.py
@@ -95,7 +95,7 @@ class PowerViewNumber(ShadeEntity, RestoreNumber):
self.entity_description = description
self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
- async def async_set_native_value(self, value: float) -> None:
+ def set_native_value(self, value: float) -> None:
"""Update the current value."""
self._attr_native_value = value
self.entity_description.store_value_fn(self.coordinator, self._shade.id, value)
diff --git a/homeassistant/components/hunterdouglas_powerview/util.py b/homeassistant/components/hunterdouglas_powerview/util.py
index 360bd7f722b..1d670f46429 100644
--- a/homeassistant/components/hunterdouglas_powerview/util.py
+++ b/homeassistant/components/hunterdouglas_powerview/util.py
@@ -5,38 +5,12 @@ from __future__ import annotations
from collections.abc import Iterable
from typing import Any
-from aiopvapi.helpers.aiorequest import AioRequest
from aiopvapi.helpers.constants import ATTR_ID
-from aiopvapi.hub import Hub
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-
-from .model import PowerviewAPI, PowerviewDeviceInfo
+from homeassistant.core import callback
@callback
def async_map_data_by_id(data: Iterable[dict[str | int, Any]]):
"""Return a dict with the key being the id for a list of entries."""
return {entry[ATTR_ID]: entry for entry in data}
-
-
-async def async_connect_hub(
- hass: HomeAssistant, address: str, api_version: int | None = None
-) -> PowerviewAPI:
- """Create the hub and fetch the device info address."""
- websession = async_get_clientsession(hass)
- pv_request = AioRequest(
- address, loop=hass.loop, websession=websession, api_version=api_version
- )
- hub = Hub(pv_request)
- await hub.query_firmware()
- info = PowerviewDeviceInfo(
- name=hub.name,
- mac_address=hub.mac_address,
- serial_number=hub.serial_number,
- firmware=hub.firmware,
- model=hub.model,
- hub_address=hub.ip,
- )
- return PowerviewAPI(hub, pv_request, info)
diff --git a/homeassistant/components/husqvarna_automower/__init__.py b/homeassistant/components/husqvarna_automower/__init__.py
index 822f81f5f75..c7d69866313 100644
--- a/homeassistant/components/husqvarna_automower/__init__.py
+++ b/homeassistant/components/husqvarna_automower/__init__.py
@@ -13,9 +13,7 @@ from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
device_registry as dr,
- entity_registry as er,
)
-from homeassistant.util import dt as dt_util
from . import api
from .const import DOMAIN
@@ -50,11 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AutomowerConfigEntry) ->
aiohttp_client.async_get_clientsession(hass),
session,
)
- time_zone_str = str(dt_util.DEFAULT_TIME_ZONE)
- automower_api = AutomowerSession(
- api_api,
- await dt_util.async_get_time_zone(time_zone_str),
- )
+ automower_api = AutomowerSession(api_api)
try:
await api_api.async_get_access_token()
except ClientResponseError as err:
@@ -100,20 +94,3 @@ def cleanup_removed_devices(
device_reg.async_update_device(
device.id, remove_config_entry_id=config_entry.entry_id
)
-
-
-def remove_work_area_entities(
- hass: HomeAssistant,
- config_entry: ConfigEntry,
- removed_work_areas: set[int],
- mower_id: str,
-) -> None:
- """Remove all unused work area entities for the specified mower."""
- entity_reg = er.async_get(hass)
- for entity_entry in er.async_entries_for_config_entry(
- entity_reg, config_entry.entry_id
- ):
- for work_area_id in removed_work_areas:
- if entity_entry.unique_id.startswith(f"{mower_id}_{work_area_id}_"):
- _LOGGER.info("Deleting: %s", entity_entry.entity_id)
- entity_reg.async_remove(entity_entry.entity_id)
diff --git a/homeassistant/components/husqvarna_automower/binary_sensor.py b/homeassistant/components/husqvarna_automower/binary_sensor.py
index 5d1ccb6a074..922f7deb99b 100644
--- a/homeassistant/components/husqvarna_automower/binary_sensor.py
+++ b/homeassistant/components/husqvarna_automower/binary_sensor.py
@@ -28,7 +28,7 @@ class AutomowerBinarySensorEntityDescription(BinarySensorEntityDescription):
value_fn: Callable[[MowerAttributes], bool]
-MOWER_BINARY_SENSOR_TYPES: tuple[AutomowerBinarySensorEntityDescription, ...] = (
+BINARY_SENSOR_TYPES: tuple[AutomowerBinarySensorEntityDescription, ...] = (
AutomowerBinarySensorEntityDescription(
key="battery_charging",
value_fn=lambda data: data.mower.activity == MowerActivities.CHARGING,
@@ -57,7 +57,7 @@ async def async_setup_entry(
async_add_entities(
AutomowerBinarySensorEntity(mower_id, coordinator, description)
for mower_id in coordinator.data
- for description in MOWER_BINARY_SENSOR_TYPES
+ for description in BINARY_SENSOR_TYPES
)
diff --git a/homeassistant/components/husqvarna_automower/button.py b/homeassistant/components/husqvarna_automower/button.py
index 22a732ec54c..696c5ae85ea 100644
--- a/homeassistant/components/husqvarna_automower/button.py
+++ b/homeassistant/components/husqvarna_automower/button.py
@@ -11,6 +11,7 @@ from aioautomower.session import AutomowerSession
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.util import dt as dt_util
from . import AutomowerConfigEntry
from .coordinator import AutomowerDataUpdateCoordinator
@@ -23,6 +24,19 @@ from .entity import (
_LOGGER = logging.getLogger(__name__)
+async def _async_set_time(
+ session: AutomowerSession,
+ mower_id: str,
+) -> None:
+ """Set datetime for the mower."""
+ # dt_util returns the current (aware) local datetime, set in the frontend.
+ # We assume it's the timezone in which the mower is.
+ await session.commands.set_datetime(
+ mower_id,
+ dt_util.now(),
+ )
+
+
@dataclass(frozen=True, kw_only=True)
class AutomowerButtonEntityDescription(ButtonEntityDescription):
"""Describes Automower button entities."""
@@ -32,7 +46,7 @@ class AutomowerButtonEntityDescription(ButtonEntityDescription):
press_fn: Callable[[AutomowerSession, str], Awaitable[Any]]
-MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
+BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
AutomowerButtonEntityDescription(
key="confirm_error",
translation_key="confirm_error",
@@ -44,7 +58,7 @@ MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
key="sync_clock",
translation_key="sync_clock",
available_fn=_check_error_free,
- press_fn=lambda session, mower_id: session.commands.set_datetime(mower_id),
+ press_fn=_async_set_time,
),
)
@@ -59,7 +73,7 @@ async def async_setup_entry(
async_add_entities(
AutomowerButtonEntity(mower_id, coordinator, description)
for mower_id in coordinator.data
- for description in MOWER_BUTTON_TYPES
+ for description in BUTTON_TYPES
if description.exists_fn(coordinator.data[mower_id])
)
diff --git a/homeassistant/components/husqvarna_automower/calendar.py b/homeassistant/components/husqvarna_automower/calendar.py
index d4162af0c5c..87fac58beb2 100644
--- a/homeassistant/components/husqvarna_automower/calendar.py
+++ b/homeassistant/components/husqvarna_automower/calendar.py
@@ -60,8 +60,8 @@ class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
]
return CalendarEvent(
summary=make_name_string(work_area_name, program_event.schedule_no),
- start=program_event.start,
- end=program_event.end,
+ start=program_event.start.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE),
+ end=program_event.end.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE),
rrule=program_event.rrule_str,
)
diff --git a/homeassistant/components/husqvarna_automower/config_flow.py b/homeassistant/components/husqvarna_automower/config_flow.py
index 3e76b9ac812..63e78b5d508 100644
--- a/homeassistant/components/husqvarna_automower/config_flow.py
+++ b/homeassistant/components/husqvarna_automower/config_flow.py
@@ -6,7 +6,7 @@ from typing import Any
from aioautomower.utils import structure_token
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, CONF_TOKEN
from homeassistant.helpers import config_entry_oauth2_flow
@@ -26,29 +26,27 @@ class HusqvarnaConfigFlowHandler(
VERSION = 1
DOMAIN = DOMAIN
+ reauth_entry: ConfigEntry | None = None
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create an entry for the flow."""
token = data[CONF_TOKEN]
- if "amc:api" not in token["scope"] and self.source != SOURCE_REAUTH:
+ if "amc:api" not in token["scope"] and not self.reauth_entry:
return self.async_abort(reason="missing_amc_scope")
user_id = token[CONF_USER_ID]
- await self.async_set_unique_id(user_id)
-
- if self.source == SOURCE_REAUTH:
- reauth_entry = self._get_reauth_entry()
+ if self.reauth_entry:
if "amc:api" not in token["scope"]:
return self.async_update_reload_and_abort(
- reauth_entry, data=data, reason="missing_amc_scope"
+ self.reauth_entry, data=data, reason="missing_amc_scope"
)
- self._abort_if_unique_id_mismatch(reason="wrong_account")
- return self.async_update_reload_and_abort(reauth_entry, data=data)
-
- self._abort_if_unique_id_configured()
-
+ if self.reauth_entry.unique_id != user_id:
+ return self.async_abort(reason="wrong_account")
+ return self.async_update_reload_and_abort(self.reauth_entry, data=data)
structured_token = structure_token(token[CONF_ACCESS_TOKEN])
first_name = structured_token.user.first_name
last_name = structured_token.user.last_name
+ await self.async_set_unique_id(user_id)
+ self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"{NAME} of {first_name} {last_name}",
data=data,
@@ -63,8 +61,12 @@ class HusqvarnaConfigFlowHandler(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
- if "amc:api" not in entry_data["token"]["scope"]:
- return await self.async_step_missing_scope()
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
+ if self.reauth_entry is not None:
+ if "amc:api" not in self.reauth_entry.data["token"]["scope"]:
+ return await self.async_step_missing_scope()
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -72,9 +74,10 @@ class HusqvarnaConfigFlowHandler(
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
if user_input is None:
+ assert self.reauth_entry
return self.async_show_form(
step_id="reauth_confirm",
- description_placeholders={CONF_NAME: self._get_reauth_entry().title},
+ description_placeholders={CONF_NAME: self.reauth_entry.title},
)
return await self.async_step_user()
@@ -82,9 +85,9 @@ class HusqvarnaConfigFlowHandler(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauth for missing scope."""
- if user_input is None and self.source == SOURCE_REAUTH:
+ if user_input is None and self.reauth_entry is not None:
token_structured = structure_token(
- self._get_reauth_entry().data["token"]["access_token"]
+ self.reauth_entry.data["token"]["access_token"]
)
return self.async_show_form(
step_id="missing_scope",
diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py
index c19f37a040d..458ff50dac9 100644
--- a/homeassistant/components/husqvarna_automower/coordinator.py
+++ b/homeassistant/components/husqvarna_automower/coordinator.py
@@ -8,7 +8,6 @@ from aioautomower.exceptions import (
ApiException,
AuthException,
HusqvarnaWSServerHandshakeError,
- TimeoutException,
)
from aioautomower.model import MowerAttributes
from aioautomower.session import AutomowerSession
@@ -23,7 +22,6 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
MAX_WS_RECONNECT_TIME = 600
SCAN_INTERVAL = timedelta(minutes=8)
-DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time
class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]):
@@ -42,8 +40,8 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
update_interval=SCAN_INTERVAL,
)
self.api = api
+
self.ws_connected: bool = False
- self.reconnect_time = DEFAULT_RECONNECT_TIME
async def _async_update_data(self) -> dict[str, MowerAttributes]:
"""Subscribe for websocket and poll data from the API."""
@@ -68,28 +66,24 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
hass: HomeAssistant,
entry: ConfigEntry,
automower_client: AutomowerSession,
+ reconnect_time: int = 2,
) -> None:
"""Listen with the client."""
try:
await automower_client.auth.websocket_connect()
- # Reset reconnect time after successful connection
- self.reconnect_time = DEFAULT_RECONNECT_TIME
+ reconnect_time = 2
await automower_client.start_listening()
except HusqvarnaWSServerHandshakeError as err:
_LOGGER.debug(
- "Failed to connect to websocket. Trying to reconnect: %s",
- err,
- )
- except TimeoutException as err:
- _LOGGER.debug(
- "Failed to listen to websocket. Trying to reconnect: %s",
- err,
+ "Failed to connect to websocket. Trying to reconnect: %s", err
)
+
if not hass.is_stopping:
- await asyncio.sleep(self.reconnect_time)
- self.reconnect_time = min(self.reconnect_time * 2, MAX_WS_RECONNECT_TIME)
- entry.async_create_background_task(
- hass,
- self.client_listen(hass, entry, automower_client),
- "reconnect_task",
+ await asyncio.sleep(reconnect_time)
+ reconnect_time = min(reconnect_time * 2, MAX_WS_RECONNECT_TIME)
+ await self.client_listen(
+ hass=hass,
+ entry=entry,
+ automower_client=automower_client,
+ reconnect_time=reconnect_time,
)
diff --git a/homeassistant/components/husqvarna_automower/entity.py b/homeassistant/components/husqvarna_automower/entity.py
index da6c0ae59ce..ea3fff079eb 100644
--- a/homeassistant/components/husqvarna_automower/entity.py
+++ b/homeassistant/components/husqvarna_automower/entity.py
@@ -9,12 +9,13 @@ from typing import TYPE_CHECKING, Any
from aioautomower.exceptions import ApiException
from aioautomower.model import MowerActivities, MowerAttributes, MowerStates, WorkArea
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from . import AutomowerDataUpdateCoordinator
+from . import AutomowerConfigEntry, AutomowerDataUpdateCoordinator
from .const import DOMAIN, EXECUTION_TIME_DELAY
_LOGGER = logging.getLogger(__name__)
@@ -52,6 +53,30 @@ def _work_area_translation_key(work_area_id: int, key: str) -> str:
return f"work_area_{key}"
+@callback
+def async_remove_work_area_entities(
+ hass: HomeAssistant,
+ coordinator: AutomowerDataUpdateCoordinator,
+ entry: AutomowerConfigEntry,
+ mower_id: str,
+) -> None:
+ """Remove deleted work areas from Home Assistant."""
+ entity_reg = er.async_get(hass)
+ active_work_areas = set()
+ _work_areas = coordinator.data[mower_id].work_areas
+ if _work_areas is not None:
+ for work_area_id in _work_areas:
+ uid = f"{mower_id}_{work_area_id}_cutting_height_work_area"
+ active_work_areas.add(uid)
+ for entity_entry in er.async_entries_for_config_entry(entity_reg, entry.entry_id):
+ if (
+ (split := entity_entry.unique_id.split("_"))[0] == mower_id
+ and split[-1] == "area"
+ and entity_entry.unique_id not in active_work_areas
+ ):
+ entity_reg.async_remove(entity_entry.entity_id)
+
+
def handle_sending_exception(
poll_after_sending: bool = False,
) -> Callable[
@@ -100,9 +125,7 @@ class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, mower_id)},
manufacturer="Husqvarna",
- model=self.mower_attributes.system.model.removeprefix(
- "HUSQVARNA "
- ).removeprefix("Husqvarna "),
+ model=self.mower_attributes.system.model,
name=self.mower_attributes.system.name,
serial_number=self.mower_attributes.system.serial_number,
suggested_area="Garden",
diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json
index d22d23583ba..85acfaf66a2 100644
--- a/homeassistant/components/husqvarna_automower/manifest.json
+++ b/homeassistant/components/husqvarna_automower/manifest.json
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
- "requirements": ["aioautomower==2024.10.3"]
+ "requirements": ["aioautomower==2024.9.3"]
}
diff --git a/homeassistant/components/husqvarna_automower/number.py b/homeassistant/components/husqvarna_automower/number.py
index d6d794f2d83..c22bb4d37f7 100644
--- a/homeassistant/components/husqvarna_automower/number.py
+++ b/homeassistant/components/husqvarna_automower/number.py
@@ -13,12 +13,13 @@ from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from . import AutomowerConfigEntry, remove_work_area_entities
+from . import AutomowerConfigEntry
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import (
AutomowerControlEntity,
WorkAreaControlEntity,
_work_area_translation_key,
+ async_remove_work_area_entities,
handle_sending_exception,
)
@@ -64,7 +65,7 @@ class AutomowerNumberEntityDescription(NumberEntityDescription):
set_value_fn: Callable[[AutomowerSession, str, float], Awaitable[Any]]
-MOWER_NUMBER_TYPES: tuple[AutomowerNumberEntityDescription, ...] = (
+NUMBER_TYPES: tuple[AutomowerNumberEntityDescription, ...] = (
AutomowerNumberEntityDescription(
key="cutting_height",
translation_key="cutting_height",
@@ -80,7 +81,7 @@ MOWER_NUMBER_TYPES: tuple[AutomowerNumberEntityDescription, ...] = (
@dataclass(frozen=True, kw_only=True)
-class WorkAreaNumberEntityDescription(NumberEntityDescription):
+class AutomowerWorkAreaNumberEntityDescription(NumberEntityDescription):
"""Describes Automower work area number entity."""
value_fn: Callable[[WorkArea], int]
@@ -90,8 +91,8 @@ class WorkAreaNumberEntityDescription(NumberEntityDescription):
]
-WORK_AREA_NUMBER_TYPES: tuple[WorkAreaNumberEntityDescription, ...] = (
- WorkAreaNumberEntityDescription(
+WORK_AREA_NUMBER_TYPES: tuple[AutomowerWorkAreaNumberEntityDescription, ...] = (
+ AutomowerWorkAreaNumberEntityDescription(
key="cutting_height_work_area",
translation_key_fn=_work_area_translation_key,
entity_category=EntityCategory.CONFIG,
@@ -109,44 +110,26 @@ async def async_setup_entry(
) -> None:
"""Set up number platform."""
coordinator = entry.runtime_data
- current_work_areas: dict[str, set[int]] = {}
+ entities: list[NumberEntity] = []
- async_add_entities(
- AutomowerNumberEntity(mower_id, coordinator, description)
- for mower_id in coordinator.data
- for description in MOWER_NUMBER_TYPES
- if description.exists_fn(coordinator.data[mower_id])
- )
-
- def _async_work_area_listener() -> None:
- """Listen for new work areas and add/remove entities as needed."""
- for mower_id in coordinator.data:
- if (
- coordinator.data[mower_id].capabilities.work_areas
- and (_work_areas := coordinator.data[mower_id].work_areas) is not None
- ):
- received_work_areas = set(_work_areas.keys())
- current_work_area_set = current_work_areas.setdefault(mower_id, set())
-
- new_work_areas = received_work_areas - current_work_area_set
- removed_work_areas = current_work_area_set - received_work_areas
-
- if new_work_areas:
- current_work_area_set.update(new_work_areas)
- async_add_entities(
- WorkAreaNumberEntity(
- mower_id, coordinator, description, work_area_id
- )
- for description in WORK_AREA_NUMBER_TYPES
- for work_area_id in new_work_areas
+ for mower_id in coordinator.data:
+ if coordinator.data[mower_id].capabilities.work_areas:
+ _work_areas = coordinator.data[mower_id].work_areas
+ if _work_areas is not None:
+ entities.extend(
+ AutomowerWorkAreaNumberEntity(
+ mower_id, coordinator, description, work_area_id
)
-
- if removed_work_areas:
- remove_work_area_entities(hass, entry, removed_work_areas, mower_id)
- current_work_area_set.difference_update(removed_work_areas)
-
- coordinator.async_add_listener(_async_work_area_listener)
- _async_work_area_listener()
+ for description in WORK_AREA_NUMBER_TYPES
+ for work_area_id in _work_areas
+ )
+ async_remove_work_area_entities(hass, coordinator, entry, mower_id)
+ entities.extend(
+ AutomowerNumberEntity(mower_id, coordinator, description)
+ for description in NUMBER_TYPES
+ if description.exists_fn(coordinator.data[mower_id])
+ )
+ async_add_entities(entities)
class AutomowerNumberEntity(AutomowerControlEntity, NumberEntity):
@@ -178,16 +161,16 @@ class AutomowerNumberEntity(AutomowerControlEntity, NumberEntity):
)
-class WorkAreaNumberEntity(WorkAreaControlEntity, NumberEntity):
- """Defining the WorkAreaNumberEntity with WorkAreaNumberEntityDescription."""
+class AutomowerWorkAreaNumberEntity(WorkAreaControlEntity, NumberEntity):
+ """Defining the AutomowerWorkAreaNumberEntity with AutomowerWorkAreaNumberEntityDescription."""
- entity_description: WorkAreaNumberEntityDescription
+ entity_description: AutomowerWorkAreaNumberEntityDescription
def __init__(
self,
mower_id: str,
coordinator: AutomowerDataUpdateCoordinator,
- description: WorkAreaNumberEntityDescription,
+ description: AutomowerWorkAreaNumberEntityDescription,
work_area_id: int,
) -> None:
"""Set up AutomowerNumberEntity."""
diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py
index ebb68033918..b9a6fb16486 100644
--- a/homeassistant/components/husqvarna_automower/sensor.py
+++ b/homeassistant/components/husqvarna_automower/sensor.py
@@ -4,8 +4,8 @@ from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import datetime
import logging
-from operator import attrgetter
from typing import TYPE_CHECKING, Any
+from zoneinfo import ZoneInfo
from aioautomower.model import (
MowerAttributes,
@@ -14,6 +14,7 @@ from aioautomower.model import (
RestrictedReasons,
WorkArea,
)
+from aioautomower.utils import naive_to_aware
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -25,6 +26,7 @@ from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfLength, UnitOf
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
+from homeassistant.util import dt as dt_util
from . import AutomowerConfigEntry
from .coordinator import AutomowerDataUpdateCoordinator
@@ -194,16 +196,16 @@ ERROR_STATES = {
}
RESTRICTED_REASONS: list = [
- RestrictedReasons.ALL_WORK_AREAS_COMPLETED,
- RestrictedReasons.DAILY_LIMIT,
- RestrictedReasons.EXTERNAL,
- RestrictedReasons.FOTA,
- RestrictedReasons.FROST,
- RestrictedReasons.NONE,
- RestrictedReasons.NOT_APPLICABLE,
- RestrictedReasons.PARK_OVERRIDE,
- RestrictedReasons.SENSOR,
- RestrictedReasons.WEEK_SCHEDULE,
+ RestrictedReasons.ALL_WORK_AREAS_COMPLETED.lower(),
+ RestrictedReasons.DAILY_LIMIT.lower(),
+ RestrictedReasons.EXTERNAL.lower(),
+ RestrictedReasons.FOTA.lower(),
+ RestrictedReasons.FROST.lower(),
+ RestrictedReasons.NONE.lower(),
+ RestrictedReasons.NOT_APPLICABLE.lower(),
+ RestrictedReasons.PARK_OVERRIDE.lower(),
+ RestrictedReasons.SENSOR.lower(),
+ RestrictedReasons.WEEK_SCHEDULE.lower(),
]
STATE_NO_WORK_AREA_ACTIVE = "no_work_area_active"
@@ -270,15 +272,15 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
- value_fn=attrgetter("battery.battery_percent"),
+ value_fn=lambda data: data.battery.battery_percent,
),
AutomowerSensorEntityDescription(
key="mode",
translation_key="mode",
device_class=SensorDeviceClass.ENUM,
- option_fn=lambda data: list(MowerModes),
+ option_fn=lambda data: [option.lower() for option in list(MowerModes)],
value_fn=(
- lambda data: data.mower.mode
+ lambda data: data.mower.mode.lower()
if data.mower.mode != MowerModes.UNKNOWN
else None
),
@@ -291,7 +293,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.cutting_blade_usage_time is not None,
- value_fn=attrgetter("statistics.cutting_blade_usage_time"),
+ value_fn=lambda data: data.statistics.cutting_blade_usage_time,
),
AutomowerSensorEntityDescription(
key="total_charging_time",
@@ -302,7 +304,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.total_charging_time is not None,
- value_fn=attrgetter("statistics.total_charging_time"),
+ value_fn=lambda data: data.statistics.total_charging_time,
),
AutomowerSensorEntityDescription(
key="total_cutting_time",
@@ -313,7 +315,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.total_cutting_time is not None,
- value_fn=attrgetter("statistics.total_cutting_time"),
+ value_fn=lambda data: data.statistics.total_cutting_time,
),
AutomowerSensorEntityDescription(
key="total_running_time",
@@ -324,7 +326,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.total_running_time is not None,
- value_fn=attrgetter("statistics.total_running_time"),
+ value_fn=lambda data: data.statistics.total_running_time,
),
AutomowerSensorEntityDescription(
key="total_searching_time",
@@ -335,7 +337,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.total_searching_time is not None,
- value_fn=attrgetter("statistics.total_searching_time"),
+ value_fn=lambda data: data.statistics.total_searching_time,
),
AutomowerSensorEntityDescription(
key="number_of_charging_cycles",
@@ -343,7 +345,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL,
exists_fn=lambda data: data.statistics.number_of_charging_cycles is not None,
- value_fn=attrgetter("statistics.number_of_charging_cycles"),
+ value_fn=lambda data: data.statistics.number_of_charging_cycles,
),
AutomowerSensorEntityDescription(
key="number_of_collisions",
@@ -351,7 +353,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL,
exists_fn=lambda data: data.statistics.number_of_collisions is not None,
- value_fn=attrgetter("statistics.number_of_collisions"),
+ value_fn=lambda data: data.statistics.number_of_collisions,
),
AutomowerSensorEntityDescription(
key="total_drive_distance",
@@ -362,13 +364,16 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfLength.METERS,
suggested_unit_of_measurement=UnitOfLength.KILOMETERS,
exists_fn=lambda data: data.statistics.total_drive_distance is not None,
- value_fn=attrgetter("statistics.total_drive_distance"),
+ value_fn=lambda data: data.statistics.total_drive_distance,
),
AutomowerSensorEntityDescription(
key="next_start_timestamp",
translation_key="next_start_timestamp",
device_class=SensorDeviceClass.TIMESTAMP,
- value_fn=attrgetter("planner.next_start_datetime"),
+ value_fn=lambda data: naive_to_aware(
+ data.planner.next_start_datetime_naive,
+ ZoneInfo(str(dt_util.DEFAULT_TIME_ZONE)),
+ ),
),
AutomowerSensorEntityDescription(
key="error",
@@ -382,7 +387,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
translation_key="restricted_reason",
device_class=SensorDeviceClass.ENUM,
option_fn=lambda data: RESTRICTED_REASONS,
- value_fn=attrgetter("planner.restricted_reason"),
+ value_fn=lambda data: data.planner.restricted_reason.lower(),
),
AutomowerSensorEntityDescription(
key="work_area",
@@ -412,14 +417,17 @@ WORK_AREA_SENSOR_TYPES: tuple[WorkAreaSensorEntityDescription, ...] = (
exists_fn=lambda data: data.progress is not None,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
- value_fn=attrgetter("progress"),
+ value_fn=lambda data: data.progress,
),
WorkAreaSensorEntityDescription(
key="last_time_completed",
translation_key_fn=_work_area_translation_key,
- exists_fn=lambda data: data.last_time_completed is not None,
+ exists_fn=lambda data: data.last_time_completed_naive is not None,
device_class=SensorDeviceClass.TIMESTAMP,
- value_fn=attrgetter("last_time_completed"),
+ value_fn=lambda data: naive_to_aware(
+ data.last_time_completed_naive,
+ ZoneInfo(str(dt_util.DEFAULT_TIME_ZONE)),
+ ),
),
)
@@ -431,44 +439,25 @@ async def async_setup_entry(
) -> None:
"""Set up sensor platform."""
coordinator = entry.runtime_data
- current_work_areas: dict[str, set[int]] = {}
-
- async_add_entities(
- AutomowerSensorEntity(mower_id, coordinator, description)
- for mower_id, data in coordinator.data.items()
- for description in MOWER_SENSOR_TYPES
- if description.exists_fn(data)
- )
-
- def _async_work_area_listener() -> None:
- """Listen for new work areas and add sensor entities if they did not exist.
-
- Listening for deletable work areas is managed in the number platform.
- """
- for mower_id in coordinator.data:
- if (
- coordinator.data[mower_id].capabilities.work_areas
- and (_work_areas := coordinator.data[mower_id].work_areas) is not None
- ):
- received_work_areas = set(_work_areas.keys())
- new_work_areas = received_work_areas - current_work_areas.get(
- mower_id, set()
+ entities: list[SensorEntity] = []
+ for mower_id in coordinator.data:
+ if coordinator.data[mower_id].capabilities.work_areas:
+ _work_areas = coordinator.data[mower_id].work_areas
+ if _work_areas is not None:
+ entities.extend(
+ WorkAreaSensorEntity(
+ mower_id, coordinator, description, work_area_id
+ )
+ for description in WORK_AREA_SENSOR_TYPES
+ for work_area_id in _work_areas
+ if description.exists_fn(_work_areas[work_area_id])
)
- if new_work_areas:
- current_work_areas.setdefault(mower_id, set()).update(
- new_work_areas
- )
- async_add_entities(
- WorkAreaSensorEntity(
- mower_id, coordinator, description, work_area_id
- )
- for description in WORK_AREA_SENSOR_TYPES
- for work_area_id in new_work_areas
- if description.exists_fn(_work_areas[work_area_id])
- )
-
- coordinator.async_add_listener(_async_work_area_listener)
- _async_work_area_listener()
+ entities.extend(
+ AutomowerSensorEntity(mower_id, coordinator, description)
+ for description in MOWER_SENSOR_TYPES
+ if description.exists_fn(coordinator.data[mower_id])
+ )
+ async_add_entities(entities)
class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity):
diff --git a/homeassistant/components/husqvarna_automower/switch.py b/homeassistant/components/husqvarna_automower/switch.py
index 2bbe5c87624..1808b651d3d 100644
--- a/homeassistant/components/husqvarna_automower/switch.py
+++ b/homeassistant/components/husqvarna_automower/switch.py
@@ -6,7 +6,8 @@ from typing import TYPE_CHECKING, Any
from aioautomower.model import MowerModes, StayOutZones, Zone
from homeassistant.components.switch import SwitchEntity
-from homeassistant.core import HomeAssistant
+from homeassistant.const import Platform
+from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -29,82 +30,30 @@ async def async_setup_entry(
) -> None:
"""Set up switch platform."""
coordinator = entry.runtime_data
- current_work_areas: dict[str, set[int]] = {}
- current_stay_out_zones: dict[str, set[str]] = {}
-
- async_add_entities(
+ entities: list[SwitchEntity] = []
+ entities.extend(
AutomowerScheduleSwitchEntity(mower_id, coordinator)
for mower_id in coordinator.data
)
-
- def _async_work_area_listener() -> None:
- """Listen for new work areas and add switch entities if they did not exist.
-
- Listening for deletable work areas is managed in the number platform.
- """
- for mower_id in coordinator.data:
- if (
- coordinator.data[mower_id].capabilities.work_areas
- and (_work_areas := coordinator.data[mower_id].work_areas) is not None
- ):
- received_work_areas = set(_work_areas.keys())
- new_work_areas = received_work_areas - current_work_areas.get(
- mower_id, set()
+ for mower_id in coordinator.data:
+ if coordinator.data[mower_id].capabilities.stay_out_zones:
+ _stay_out_zones = coordinator.data[mower_id].stay_out_zones
+ if _stay_out_zones is not None:
+ entities.extend(
+ AutomowerStayOutZoneSwitchEntity(
+ coordinator, mower_id, stay_out_zone_uid
+ )
+ for stay_out_zone_uid in _stay_out_zones.zones
)
- if new_work_areas:
- current_work_areas.setdefault(mower_id, set()).update(
- new_work_areas
- )
- async_add_entities(
- WorkAreaSwitchEntity(coordinator, mower_id, work_area_id)
- for work_area_id in new_work_areas
- )
-
- def _remove_stay_out_zone_entities(
- removed_stay_out_zones: set, mower_id: str
- ) -> None:
- """Remove all unused stay-out zones for all platforms."""
- entity_reg = er.async_get(hass)
- for entity_entry in er.async_entries_for_config_entry(
- entity_reg, entry.entry_id
- ):
- for stay_out_zone_uid in removed_stay_out_zones:
- if entity_entry.unique_id.startswith(f"{mower_id}_{stay_out_zone_uid}"):
- entity_reg.async_remove(entity_entry.entity_id)
-
- def _async_stay_out_zone_listener() -> None:
- """Listen for new stay-out zones and add/remove switch entities if they did not exist."""
- for mower_id in coordinator.data:
- if (
- coordinator.data[mower_id].capabilities.stay_out_zones
- and (_stay_out_zones := coordinator.data[mower_id].stay_out_zones)
- is not None
- ):
- received_stay_out_zones = set(_stay_out_zones.zones)
- current_stay_out_zones_set = current_stay_out_zones.get(mower_id, set())
- new_stay_out_zones = (
- received_stay_out_zones - current_stay_out_zones_set
+ async_remove_entities(hass, coordinator, entry, mower_id)
+ if coordinator.data[mower_id].capabilities.work_areas:
+ _work_areas = coordinator.data[mower_id].work_areas
+ if _work_areas is not None:
+ entities.extend(
+ WorkAreaSwitchEntity(coordinator, mower_id, work_area_id)
+ for work_area_id in _work_areas
)
- removed_stay_out_zones = (
- current_stay_out_zones_set - received_stay_out_zones
- )
- if new_stay_out_zones:
- current_stay_out_zones.setdefault(mower_id, set()).update(
- new_stay_out_zones
- )
- async_add_entities(
- StayOutZoneSwitchEntity(
- coordinator, mower_id, stay_out_zone_uid
- )
- for stay_out_zone_uid in new_stay_out_zones
- )
- if removed_stay_out_zones:
- _remove_stay_out_zone_entities(removed_stay_out_zones, mower_id)
-
- coordinator.async_add_listener(_async_work_area_listener)
- coordinator.async_add_listener(_async_stay_out_zone_listener)
- _async_work_area_listener()
- _async_stay_out_zone_listener()
+ async_add_entities(entities)
class AutomowerScheduleSwitchEntity(AutomowerControlEntity, SwitchEntity):
@@ -137,7 +86,7 @@ class AutomowerScheduleSwitchEntity(AutomowerControlEntity, SwitchEntity):
await self.coordinator.api.commands.resume_schedule(self.mower_id)
-class StayOutZoneSwitchEntity(AutomowerControlEntity, SwitchEntity):
+class AutomowerStayOutZoneSwitchEntity(AutomowerControlEntity, SwitchEntity):
"""Defining the Automower stay out zone switch."""
_attr_translation_key = "stay_out_zones"
@@ -233,3 +182,28 @@ class WorkAreaSwitchEntity(WorkAreaControlEntity, SwitchEntity):
await self.coordinator.api.commands.workarea_settings(
self.mower_id, self.work_area_id, enabled=True
)
+
+
+@callback
+def async_remove_entities(
+ hass: HomeAssistant,
+ coordinator: AutomowerDataUpdateCoordinator,
+ entry: AutomowerConfigEntry,
+ mower_id: str,
+) -> None:
+ """Remove deleted stay-out-zones from Home Assistant."""
+ entity_reg = er.async_get(hass)
+ active_zones = set()
+ _zones = coordinator.data[mower_id].stay_out_zones
+ if _zones is not None:
+ for zones_uid in _zones.zones:
+ uid = f"{mower_id}_{zones_uid}_stay_out_zones"
+ active_zones.add(uid)
+ for entity_entry in er.async_entries_for_config_entry(entity_reg, entry.entry_id):
+ if (
+ entity_entry.domain == Platform.SWITCH
+ and (split := entity_entry.unique_id.split("_"))[0] == mower_id
+ and split[-1] == "zones"
+ and entity_entry.unique_id not in active_zones
+ ):
+ entity_reg.async_remove(entity_entry.entity_id)
diff --git a/homeassistant/components/husqvarna_automower_ble/__init__.py b/homeassistant/components/husqvarna_automower_ble/__init__.py
deleted file mode 100644
index 2025ba64cf1..00000000000
--- a/homeassistant/components/husqvarna_automower_ble/__init__.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""The Husqvarna Autoconnect Bluetooth integration."""
-
-from __future__ import annotations
-
-from automower_ble.mower import Mower
-from bleak import BleakError
-from bleak_retry_connector import close_stale_connections_by_address, get_device
-
-from homeassistant.components import bluetooth
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, Platform
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
-
-from .const import LOGGER
-from .coordinator import HusqvarnaCoordinator
-
-PLATFORMS = [
- Platform.LAWN_MOWER,
-]
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
- """Set up Husqvarna Autoconnect Bluetooth from a config entry."""
- address = entry.data[CONF_ADDRESS]
- channel_id = entry.data[CONF_CLIENT_ID]
-
- mower = Mower(channel_id, address)
-
- await close_stale_connections_by_address(address)
-
- LOGGER.debug("connecting to %s with channel ID %s", address, str(channel_id))
- try:
- device = bluetooth.async_ble_device_from_address(
- hass, address, connectable=True
- ) or await get_device(address)
- if not await mower.connect(device):
- raise ConfigEntryNotReady
- except (TimeoutError, BleakError) as exception:
- raise ConfigEntryNotReady(
- f"Unable to connect to device {address} due to {exception}"
- ) from exception
- LOGGER.debug("connected and paired")
-
- model = await mower.get_model()
- LOGGER.debug("Connected to Automower: %s", model)
-
- coordinator = HusqvarnaCoordinator(hass, mower, address, channel_id, model)
-
- 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: ConfigEntry) -> bool:
- """Unload a config entry."""
- if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
- coordinator: HusqvarnaCoordinator = entry.runtime_data
- await coordinator.async_shutdown()
-
- return unload_ok
diff --git a/homeassistant/components/husqvarna_automower_ble/config_flow.py b/homeassistant/components/husqvarna_automower_ble/config_flow.py
deleted file mode 100644
index 72835c22334..00000000000
--- a/homeassistant/components/husqvarna_automower_ble/config_flow.py
+++ /dev/null
@@ -1,121 +0,0 @@
-"""Config flow for Husqvarna Bluetooth integration."""
-
-from __future__ import annotations
-
-import random
-from typing import Any
-
-from automower_ble.mower import Mower
-from bleak import BleakError
-import voluptuous as vol
-
-from homeassistant.components import bluetooth
-from homeassistant.components.bluetooth import BluetoothServiceInfo
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID
-
-from .const import DOMAIN, LOGGER
-
-
-def _is_supported(discovery_info: BluetoothServiceInfo):
- """Check if device is supported."""
-
- LOGGER.debug(
- "%s manufacturer data: %s",
- discovery_info.address,
- discovery_info.manufacturer_data,
- )
-
- manufacturer = any(key == 1062 for key in discovery_info.manufacturer_data)
- service_husqvarna = any(
- service == "98bd0001-0b0e-421a-84e5-ddbf75dc6de4"
- for service in discovery_info.service_uuids
- )
- service_generic = any(
- service == "00001800-0000-1000-8000-00805f9b34fb"
- for service in discovery_info.service_uuids
- )
-
- return manufacturer and service_husqvarna and service_generic
-
-
-class HusqvarnaAutomowerBleConfigFlow(ConfigFlow, domain=DOMAIN):
- """Handle a config flow for Husqvarna Bluetooth."""
-
- VERSION = 1
-
- def __init__(self) -> None:
- """Initialize the config flow."""
- self.address: str | None
-
- async def async_step_bluetooth(
- self, discovery_info: BluetoothServiceInfo
- ) -> ConfigFlowResult:
- """Handle the bluetooth discovery step."""
-
- LOGGER.debug("Discovered device: %s", discovery_info)
- if not _is_supported(discovery_info):
- return self.async_abort(reason="no_devices_found")
-
- self.address = discovery_info.address
- await self.async_set_unique_id(self.address)
- self._abort_if_unique_id_configured()
- return await self.async_step_confirm()
-
- async def async_step_confirm(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Confirm discovery."""
- assert self.address
-
- device = bluetooth.async_ble_device_from_address(
- self.hass, self.address, connectable=True
- )
- channel_id = random.randint(1, 0xFFFFFFFF)
-
- try:
- (manufacturer, device_type, model) = await Mower(
- channel_id, self.address
- ).probe_gatts(device)
- except (BleakError, TimeoutError) as exception:
- LOGGER.exception("Failed to connect to device: %s", exception)
- return self.async_abort(reason="cannot_connect")
-
- title = manufacturer + " " + device_type
-
- LOGGER.debug("Found device: %s", title)
-
- if user_input is not None:
- return self.async_create_entry(
- title=title,
- data={CONF_ADDRESS: self.address, CONF_CLIENT_ID: channel_id},
- )
-
- self.context["title_placeholders"] = {
- "name": title,
- }
-
- self._set_confirm_only()
- return self.async_show_form(
- step_id="confirm",
- description_placeholders=self.context["title_placeholders"],
- )
-
- async def async_step_user(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle the initial step."""
- if user_input is not None:
- self.address = user_input[CONF_ADDRESS]
- await self.async_set_unique_id(self.address, raise_on_progress=False)
- self._abort_if_unique_id_configured()
- return await self.async_step_confirm()
-
- return self.async_show_form(
- step_id="user",
- data_schema=vol.Schema(
- {
- vol.Required(CONF_ADDRESS): str,
- },
- ),
- )
diff --git a/homeassistant/components/husqvarna_automower_ble/const.py b/homeassistant/components/husqvarna_automower_ble/const.py
deleted file mode 100644
index 7117d0c9e29..00000000000
--- a/homeassistant/components/husqvarna_automower_ble/const.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""Constants for the Husqvarna Automower Bluetooth integration."""
-
-import logging
-
-DOMAIN = "husqvarna_automower_ble"
-MANUFACTURER = "Husqvarna"
-
-LOGGER = logging.getLogger(__package__)
diff --git a/homeassistant/components/husqvarna_automower_ble/coordinator.py b/homeassistant/components/husqvarna_automower_ble/coordinator.py
deleted file mode 100644
index c577ccd9196..00000000000
--- a/homeassistant/components/husqvarna_automower_ble/coordinator.py
+++ /dev/null
@@ -1,100 +0,0 @@
-"""Provides the DataUpdateCoordinator."""
-
-from __future__ import annotations
-
-from datetime import timedelta
-
-from automower_ble.mower import Mower
-from bleak import BleakError
-from bleak_retry_connector import close_stale_connections_by_address
-
-from homeassistant.components import bluetooth
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-
-from .const import DOMAIN, LOGGER
-
-SCAN_INTERVAL = timedelta(seconds=60)
-
-
-class HusqvarnaCoordinator(DataUpdateCoordinator[dict[str, bytes]]):
- """Class to manage fetching data."""
-
- def __init__(
- self,
- hass: HomeAssistant,
- mower: Mower,
- address: str,
- channel_id: str,
- model: str,
- ) -> None:
- """Initialize global data updater."""
- super().__init__(
- hass=hass,
- logger=LOGGER,
- name=DOMAIN,
- update_interval=SCAN_INTERVAL,
- )
- self.address = address
- self.channel_id = channel_id
- self.model = model
- self.mower = mower
-
- async def async_shutdown(self) -> None:
- """Shutdown coordinator and any connection."""
- LOGGER.debug("Shutdown")
- await super().async_shutdown()
- if self.mower.is_connected():
- await self.mower.disconnect()
-
- async def _async_find_device(self):
- LOGGER.debug("Trying to reconnect")
- await close_stale_connections_by_address(self.address)
-
- device = bluetooth.async_ble_device_from_address(
- self.hass, self.address, connectable=True
- )
-
- try:
- if not await self.mower.connect(device):
- raise UpdateFailed("Failed to connect")
- except BleakError as err:
- raise UpdateFailed("Failed to connect") from err
-
- async def _async_update_data(self) -> dict[str, bytes]:
- """Poll the device."""
- LOGGER.debug("Polling device")
-
- data: dict[str, bytes] = {}
-
- try:
- if not self.mower.is_connected():
- await self._async_find_device()
- except BleakError as err:
- raise UpdateFailed("Failed to connect") from err
-
- try:
- data["battery_level"] = await self.mower.battery_level()
- LOGGER.debug("battery_level" + str(data["battery_level"]))
- if data["battery_level"] is None:
- await self._async_find_device()
- raise UpdateFailed("Error getting data from device")
-
- data["activity"] = await self.mower.mower_activity()
- LOGGER.debug("activity:" + str(data["activity"]))
- if data["activity"] is None:
- await self._async_find_device()
- raise UpdateFailed("Error getting data from device")
-
- data["state"] = await self.mower.mower_state()
- LOGGER.debug("state:" + str(data["state"]))
- if data["state"] is None:
- await self._async_find_device()
- raise UpdateFailed("Error getting data from device")
-
- except BleakError as err:
- LOGGER.error("Error getting data from device")
- await self._async_find_device()
- raise UpdateFailed("Error getting data from device") from err
-
- return data
diff --git a/homeassistant/components/husqvarna_automower_ble/entity.py b/homeassistant/components/husqvarna_automower_ble/entity.py
deleted file mode 100644
index d2873d933ff..00000000000
--- a/homeassistant/components/husqvarna_automower_ble/entity.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Provides the HusqvarnaAutomowerBleEntity."""
-
-from __future__ import annotations
-
-from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from .const import DOMAIN, MANUFACTURER
-from .coordinator import HusqvarnaCoordinator
-
-
-class HusqvarnaAutomowerBleEntity(CoordinatorEntity[HusqvarnaCoordinator]):
- """HusqvarnaCoordinator entity for Husqvarna Automower Bluetooth."""
-
- _attr_has_entity_name = True
-
- def __init__(self, coordinator: HusqvarnaCoordinator) -> None:
- """Initialize coordinator entity."""
- super().__init__(coordinator)
-
- self._attr_device_info = DeviceInfo(
- identifiers={(DOMAIN, f"{coordinator.address}_{coordinator.channel_id}")},
- manufacturer=MANUFACTURER,
- model_id=coordinator.model,
- )
-
- @property
- def available(self) -> bool:
- """Return if entity is available."""
- return super().available and self.coordinator.mower.is_connected()
diff --git a/homeassistant/components/husqvarna_automower_ble/lawn_mower.py b/homeassistant/components/husqvarna_automower_ble/lawn_mower.py
deleted file mode 100644
index 980efc6f069..00000000000
--- a/homeassistant/components/husqvarna_automower_ble/lawn_mower.py
+++ /dev/null
@@ -1,153 +0,0 @@
-"""The Husqvarna Autoconnect Bluetooth lawn mower platform."""
-
-from __future__ import annotations
-
-from automower_ble.protocol import MowerActivity, MowerState
-
-from homeassistant.components import bluetooth
-from homeassistant.components.lawn_mower import (
- LawnMowerActivity,
- LawnMowerEntity,
- LawnMowerEntityFeature,
-)
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from .const import LOGGER
-from .coordinator import HusqvarnaCoordinator
-from .entity import HusqvarnaAutomowerBleEntity
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up AutomowerLawnMower integration from a config entry."""
- coordinator: HusqvarnaCoordinator = config_entry.runtime_data
- address = coordinator.address
-
- async_add_entities(
- [
- AutomowerLawnMower(
- coordinator,
- address,
- ),
- ]
- )
-
-
-class AutomowerLawnMower(HusqvarnaAutomowerBleEntity, LawnMowerEntity):
- """Husqvarna Automower."""
-
- _attr_name = None
- _attr_supported_features = (
- LawnMowerEntityFeature.PAUSE
- | LawnMowerEntityFeature.START_MOWING
- | LawnMowerEntityFeature.DOCK
- )
-
- def __init__(
- self,
- coordinator: HusqvarnaCoordinator,
- address: str,
- ) -> None:
- """Initialize the lawn mower."""
- super().__init__(coordinator)
- self._attr_unique_id = str(address)
-
- def _get_activity(self) -> LawnMowerActivity | None:
- """Return the current lawn mower activity."""
- if self.coordinator.data is None:
- return None
-
- state = self.coordinator.data["state"]
- activity = self.coordinator.data["activity"]
-
- if state is None or activity is None:
- return None
-
- if state == MowerState.PAUSED:
- return LawnMowerActivity.PAUSED
- if state in (MowerState.STOPPED, MowerState.OFF, MowerState.WAIT_FOR_SAFETYPIN):
- # This is actually stopped, but that isn't an option
- return LawnMowerActivity.ERROR
- if state in (
- MowerState.RESTRICTED,
- MowerState.IN_OPERATION,
- MowerState.PENDING_START,
- ):
- if activity in (
- MowerActivity.CHARGING,
- MowerActivity.PARKED,
- MowerActivity.NONE,
- ):
- return LawnMowerActivity.DOCKED
- if activity in (MowerActivity.GOING_OUT, MowerActivity.MOWING):
- return LawnMowerActivity.MOWING
- if activity == MowerActivity.GOING_HOME:
- return LawnMowerActivity.RETURNING
- return LawnMowerActivity.ERROR
-
- @callback
- def _handle_coordinator_update(self) -> None:
- """Handle updated data from the coordinator."""
- LOGGER.debug("AutomowerLawnMower: _handle_coordinator_update")
-
- self._attr_activity = self._get_activity()
- self._attr_available = self._attr_activity is not None
- super()._handle_coordinator_update()
-
- async def async_start_mowing(self) -> None:
- """Start mowing."""
- LOGGER.debug("Starting mower")
-
- if not self.coordinator.mower.is_connected():
- device = bluetooth.async_ble_device_from_address(
- self.coordinator.hass, self.coordinator.address, connectable=True
- )
- if not await self.coordinator.mower.connect(device):
- return
-
- await self.coordinator.mower.mower_resume()
- if self._attr_activity is LawnMowerActivity.DOCKED:
- await self.coordinator.mower.mower_override()
- await self.coordinator.async_request_refresh()
-
- self._attr_activity = self._get_activity()
- self.async_write_ha_state()
-
- async def async_dock(self) -> None:
- """Start docking."""
- LOGGER.debug("Start docking")
-
- if not self.coordinator.mower.is_connected():
- device = bluetooth.async_ble_device_from_address(
- self.coordinator.hass, self.coordinator.address, connectable=True
- )
- if not await self.coordinator.mower.connect(device):
- return
-
- await self.coordinator.mower.mower_park()
- await self.coordinator.async_request_refresh()
-
- self._attr_activity = self._get_activity()
- self.async_write_ha_state()
-
- async def async_pause(self) -> None:
- """Pause mower."""
- LOGGER.debug("Pausing mower")
-
- if not self.coordinator.mower.is_connected():
- device = bluetooth.async_ble_device_from_address(
- self.coordinator.hass, self.coordinator.address, connectable=True
- )
- if not await self.coordinator.mower.connect(device):
- return
-
- await self.coordinator.mower.mower_pause()
- await self.coordinator.async_request_refresh()
-
- self._attr_activity = self._get_activity()
- self.async_write_ha_state()
diff --git a/homeassistant/components/husqvarna_automower_ble/manifest.json b/homeassistant/components/husqvarna_automower_ble/manifest.json
deleted file mode 100644
index 3e72d9707c7..00000000000
--- a/homeassistant/components/husqvarna_automower_ble/manifest.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "domain": "husqvarna_automower_ble",
- "name": "Husqvarna Automower BLE",
- "bluetooth": [
- {
- "service_uuid": "98bd0001-0b0e-421a-84e5-ddbf75dc6de4",
- "connectable": true
- }
- ],
- "codeowners": ["@alistair23"],
- "config_flow": true,
- "dependencies": ["bluetooth_adapters"],
- "documentation": "https://www.home-assistant.io/integrations/???",
- "iot_class": "local_polling",
- "requirements": ["automower-ble==0.2.0"]
-}
diff --git a/homeassistant/components/husqvarna_automower_ble/strings.json b/homeassistant/components/husqvarna_automower_ble/strings.json
deleted file mode 100644
index de0a140933a..00000000000
--- a/homeassistant/components/husqvarna_automower_ble/strings.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "config": {
- "flow_title": "{name} ({address})",
- "step": {
- "user": {
- "data": {
- "address": "Device BLE address"
- }
- },
- "confirm": {
- "description": "Do you want to set up {name}? Make sure the mower is in pairing mode"
- }
- },
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "no_devices_found": "Ensure the mower is in pairing mode and try again. It can take a few attempts.",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- }
- }
-}
diff --git a/homeassistant/components/huum/manifest.json b/homeassistant/components/huum/manifest.json
index 38562e1a072..7629f529b91 100644
--- a/homeassistant/components/huum/manifest.json
+++ b/homeassistant/components/huum/manifest.json
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/huum",
"iot_class": "cloud_polling",
- "requirements": ["huum==0.7.12"]
+ "requirements": ["huum==0.7.10"]
}
diff --git a/homeassistant/components/hvv_departures/config_flow.py b/homeassistant/components/hvv_departures/config_flow.py
index 536b8f18259..3e1b98d9a38 100644
--- a/homeassistant/components/hvv_departures/config_flow.py
+++ b/homeassistant/components/hvv_departures/config_flow.py
@@ -141,14 +141,16 @@ class HVVDeparturesConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Options flow handler."""
- def __init__(self) -> None:
+ def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize HVV Departures options flow."""
+ self.config_entry = config_entry
+ self.options = dict(config_entry.options)
self.departure_filters: dict[str, Any] = {}
async def async_step_init(
diff --git a/homeassistant/components/hydrawise/config_flow.py b/homeassistant/components/hydrawise/config_flow.py
index 242763e81e3..a5e7d616fcf 100644
--- a/homeassistant/components/hydrawise/config_flow.py
+++ b/homeassistant/components/hydrawise/config_flow.py
@@ -10,7 +10,7 @@ from pydrawise import auth, client
from pydrawise.exceptions import NotAuthorizedError
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import DOMAIN, LOGGER
@@ -21,6 +21,10 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ def __init__(self) -> None:
+ """Construct a ConfigFlow."""
+ self.reauth_entry: ConfigEntry | None = None
+
async def _create_or_update_entry(
self,
username: str,
@@ -45,17 +49,20 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(f"hydrawise-{user.customer_id}")
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
self._abort_if_unique_id_configured()
return self.async_create_entry(
title="Hydrawise",
data={CONF_USERNAME: username, CONF_PASSWORD: password},
)
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
- data_updates={CONF_USERNAME: username, CONF_PASSWORD: password},
+ self.hass.config_entries.async_update_entry(
+ self.reauth_entry,
+ data=self.reauth_entry.data
+ | {CONF_USERNAME: username, CONF_PASSWORD: password},
)
+ await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -86,4 +93,7 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth after updating config to username/password."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_user()
diff --git a/homeassistant/components/hydrawise/const.py b/homeassistant/components/hydrawise/const.py
index 47b9bef845e..f731ecf278c 100644
--- a/homeassistant/components/hydrawise/const.py
+++ b/homeassistant/components/hydrawise/const.py
@@ -10,7 +10,7 @@ DEFAULT_WATERING_TIME = timedelta(minutes=15)
MANUFACTURER = "Hydrawise"
-SCAN_INTERVAL = timedelta(seconds=60)
+SCAN_INTERVAL = timedelta(seconds=30)
SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update"
diff --git a/homeassistant/components/hydrawise/strings.json b/homeassistant/components/hydrawise/strings.json
index 4d50f10bcb2..b6df36ad4ff 100644
--- a/homeassistant/components/hydrawise/strings.json
+++ b/homeassistant/components/hydrawise/strings.json
@@ -13,8 +13,7 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
- "unknown": "[%key:common::config_flow::error::unknown%]",
- "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py
index b2b7dbdf531..161c531328d 100644
--- a/homeassistant/components/hyperion/config_flow.py
+++ b/homeassistant/components/hyperion/config_flow.py
@@ -424,22 +424,24 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> HyperionOptionsFlow:
+ def async_get_options_flow(config_entry: ConfigEntry) -> HyperionOptionsFlow:
"""Get the Hyperion Options flow."""
- return HyperionOptionsFlow()
+ return HyperionOptionsFlow(config_entry)
class HyperionOptionsFlow(OptionsFlow):
"""Hyperion options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize a Hyperion options flow."""
+ self._config_entry = config_entry
+
def _create_client(self) -> client.HyperionClient:
"""Create and connect a client instance."""
return create_hyperion_client(
- self.config_entry.data[CONF_HOST],
- self.config_entry.data[CONF_PORT],
- token=self.config_entry.data.get(CONF_TOKEN),
+ self._config_entry.data[CONF_HOST],
+ self._config_entry.data[CONF_PORT],
+ token=self._config_entry.data.get(CONF_TOKEN),
)
async def async_step_init(
@@ -468,7 +470,8 @@ class HyperionOptionsFlow(OptionsFlow):
return self.async_create_entry(title="", data=user_input)
default_effect_show_list = list(
- set(effects) - set(self.config_entry.options.get(CONF_EFFECT_HIDE_LIST, []))
+ set(effects)
+ - set(self._config_entry.options.get(CONF_EFFECT_HIDE_LIST, []))
)
return self.async_show_form(
@@ -477,7 +480,7 @@ class HyperionOptionsFlow(OptionsFlow):
{
vol.Optional(
CONF_PRIORITY,
- default=self.config_entry.options.get(
+ default=self._config_entry.options.get(
CONF_PRIORITY, DEFAULT_PRIORITY
),
): vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
diff --git a/homeassistant/components/hyperion/strings.json b/homeassistant/components/hyperion/strings.json
index 01682648277..79c226b71eb 100644
--- a/homeassistant/components/hyperion/strings.json
+++ b/homeassistant/components/hyperion/strings.json
@@ -52,9 +52,6 @@
"effect_show_list": "Hyperion effects to show"
}
}
- },
- "abort": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"entity": {
diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py
index 4ae3787dc1d..912f04a1d1e 100644
--- a/homeassistant/components/ialarm/alarm_control_panel.py
+++ b/homeassistant/components/ialarm/alarm_control_panel.py
@@ -5,7 +5,6 @@ from __future__ import annotations
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -51,7 +50,7 @@ class IAlarmPanel(
self._attr_unique_id = coordinator.mac
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the device."""
return self.coordinator.state
diff --git a/homeassistant/components/ialarm/const.py b/homeassistant/components/ialarm/const.py
index 1b8074c34f0..d1561cc86d5 100644
--- a/homeassistant/components/ialarm/const.py
+++ b/homeassistant/components/ialarm/const.py
@@ -2,7 +2,12 @@
from pyialarm import IAlarm
-from homeassistant.components.alarm_control_panel import AlarmControlPanelState
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+)
DATA_COORDINATOR = "ialarm"
@@ -11,8 +16,8 @@ DEFAULT_PORT = 18034
DOMAIN = "ialarm"
IALARM_TO_HASS = {
- IAlarm.ARMED_AWAY: AlarmControlPanelState.ARMED_AWAY,
- IAlarm.ARMED_STAY: AlarmControlPanelState.ARMED_HOME,
- IAlarm.DISARMED: AlarmControlPanelState.DISARMED,
- IAlarm.TRIGGERED: AlarmControlPanelState.TRIGGERED,
+ IAlarm.ARMED_AWAY: STATE_ALARM_ARMED_AWAY,
+ IAlarm.ARMED_STAY: STATE_ALARM_ARMED_HOME,
+ IAlarm.DISARMED: STATE_ALARM_DISARMED,
+ IAlarm.TRIGGERED: STATE_ALARM_TRIGGERED,
}
diff --git a/homeassistant/components/ialarm/coordinator.py b/homeassistant/components/ialarm/coordinator.py
index ad0f2298a3b..2aec99c98c4 100644
--- a/homeassistant/components/ialarm/coordinator.py
+++ b/homeassistant/components/ialarm/coordinator.py
@@ -7,10 +7,7 @@ import logging
from pyialarm import IAlarm
-from homeassistant.components.alarm_control_panel import (
- SCAN_INTERVAL,
- AlarmControlPanelState,
-)
+from homeassistant.components.alarm_control_panel import SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -25,7 +22,7 @@ class IAlarmDataUpdateCoordinator(DataUpdateCoordinator[None]):
def __init__(self, hass: HomeAssistant, ialarm: IAlarm, mac: str) -> None:
"""Initialize global iAlarm data updater."""
self.ialarm = ialarm
- self.state: AlarmControlPanelState | None = None
+ self.state: str | None = None
self.host: str = ialarm.host
self.mac = mac
diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py
index 2cb1ba4b5d7..3605c328903 100644
--- a/homeassistant/components/iaqualink/config_flow.py
+++ b/homeassistant/components/iaqualink/config_flow.py
@@ -27,6 +27,11 @@ class AqualinkFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow start."""
+ # Supporting a single account.
+ entries = self._async_current_entries()
+ if entries:
+ return self.async_abort(reason="single_instance_allowed")
+
errors = {}
if user_input is not None:
diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json
index 2531632075c..8834a538be9 100644
--- a/homeassistant/components/iaqualink/manifest.json
+++ b/homeassistant/components/iaqualink/manifest.json
@@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/iaqualink",
"iot_class": "cloud_polling",
"loggers": ["iaqualink"],
- "requirements": ["iaqualink==0.5.0", "h2==4.1.0"],
- "single_config_entry": true
+ "requirements": ["iaqualink==0.5.0", "h2==4.1.0"]
}
diff --git a/homeassistant/components/iaqualink/strings.json b/homeassistant/components/iaqualink/strings.json
index 032e1a592d9..85b49996f51 100644
--- a/homeassistant/components/iaqualink/strings.json
+++ b/homeassistant/components/iaqualink/strings.json
@@ -13,6 +13,9 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}
diff --git a/homeassistant/components/ibeacon/config_flow.py b/homeassistant/components/ibeacon/config_flow.py
index c00398e39b0..424befa81ec 100644
--- a/homeassistant/components/ibeacon/config_flow.py
+++ b/homeassistant/components/ibeacon/config_flow.py
@@ -30,6 +30,9 @@ class IBeaconConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
if not bluetooth.async_scanner_count(self.hass, connectable=False):
return self.async_abort(reason="bluetooth_not_available")
@@ -44,12 +47,16 @@ class IBeaconConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Get the options flow for this handler."""
- return IBeaconOptionsFlow()
+ return IBeaconOptionsFlow(config_entry)
class IBeaconOptionsFlow(OptionsFlow):
"""Handle options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(self, user_input: dict | None = None) -> ConfigFlowResult:
"""Manage the options."""
errors = {}
diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json
index 8bd7e3ab9cc..8dbc99c8ada 100644
--- a/homeassistant/components/ibeacon/manifest.json
+++ b/homeassistant/components/ibeacon/manifest.json
@@ -13,6 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/ibeacon",
"iot_class": "local_push",
"loggers": ["bleak"],
- "requirements": ["ibeacon-ble==1.2.0"],
- "single_config_entry": true
+ "requirements": ["ibeacon-ble==1.2.0"]
}
diff --git a/homeassistant/components/ibeacon/strings.json b/homeassistant/components/ibeacon/strings.json
index 9307f848644..440df8292a9 100644
--- a/homeassistant/components/ibeacon/strings.json
+++ b/homeassistant/components/ibeacon/strings.json
@@ -6,7 +6,8 @@
}
},
"abort": {
- "bluetooth_not_available": "At least one Bluetooth adapter or remote must be configured to use iBeacon Tracker."
+ "bluetooth_not_available": "At least one Bluetooth adapter or remote must be configured to use iBeacon Tracker.",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"options": {
diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py
index 739352485bd..1af23d716c8 100644
--- a/homeassistant/components/ifttt/alarm_control_panel.py
+++ b/homeassistant/components/ifttt/alarm_control_panel.py
@@ -10,7 +10,6 @@ from homeassistant.components.alarm_control_panel import (
PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA,
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
from homeassistant.const import (
@@ -19,6 +18,10 @@ from homeassistant.const import (
CONF_CODE,
CONF_NAME,
CONF_OPTIMISTIC,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
)
from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv
@@ -30,10 +33,10 @@ from . import ATTR_EVENT, DOMAIN, SERVICE_PUSH_ALARM_STATE, SERVICE_TRIGGER
_LOGGER = logging.getLogger(__name__)
ALLOWED_STATES = [
- AlarmControlPanelState.DISARMED,
- AlarmControlPanelState.ARMED_NIGHT,
- AlarmControlPanelState.ARMED_AWAY,
- AlarmControlPanelState.ARMED_HOME,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
]
DATA_IFTTT_ALARM = "ifttt_alarm"
@@ -165,41 +168,40 @@ class IFTTTAlarmPanel(AlarmControlPanelEntity):
"""Send disarm command."""
if not self._check_code(code):
return
- self.set_alarm_state(self._event_disarm, AlarmControlPanelState.DISARMED)
+ self.set_alarm_state(self._event_disarm, STATE_ALARM_DISARMED)
def alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
if self._code_arm_required and not self._check_code(code):
return
- self.set_alarm_state(self._event_away, AlarmControlPanelState.ARMED_AWAY)
+ self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY)
def alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
if self._code_arm_required and not self._check_code(code):
return
- self.set_alarm_state(self._event_home, AlarmControlPanelState.ARMED_HOME)
+ self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME)
def alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
if self._code_arm_required and not self._check_code(code):
return
- self.set_alarm_state(self._event_night, AlarmControlPanelState.ARMED_NIGHT)
+ self.set_alarm_state(self._event_night, STATE_ALARM_ARMED_NIGHT)
- def set_alarm_state(self, event: str, state: AlarmControlPanelState) -> None:
+ def set_alarm_state(self, event: str, state: str) -> None:
"""Call the IFTTT trigger service to change the alarm state."""
data = {ATTR_EVENT: event}
self.hass.services.call(DOMAIN, SERVICE_TRIGGER, data)
_LOGGER.debug("Called IFTTT integration to trigger event %s", event)
if self._optimistic:
- self._attr_alarm_state = state
+ self._attr_state = state
def push_alarm_state(self, value: str) -> None:
"""Push the alarm state to the given value."""
- value = AlarmControlPanelState(value)
if value in ALLOWED_STATES:
_LOGGER.debug("Pushed the alarm state to %s", value)
- self._attr_alarm_state = value
+ self._attr_state = value
def _check_code(self, code: str | None) -> bool:
return self._code is None or self._code == code
diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py
index dbb5962eabf..47019f3e92e 100644
--- a/homeassistant/components/image/__init__.py
+++ b/homeassistant/components/image/__init__.py
@@ -8,27 +8,19 @@ from contextlib import suppress
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
-import os
from random import SystemRandom
from typing import Final, final
from aiohttp import hdrs, web
import httpx
from propcache import cached_property
-import voluptuous as vol
from homeassistant.components.http import KEY_AUTHENTICATED, KEY_HASS, HomeAssistantView
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONTENT_TYPE_MULTIPART, EVENT_HOMEASSISTANT_STOP
-from homeassistant.core import (
- Event,
- EventStateChangedData,
- HomeAssistant,
- ServiceCall,
- callback,
-)
+from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
-import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import (
@@ -36,26 +28,17 @@ from homeassistant.helpers.event import (
async_track_time_interval,
)
from homeassistant.helpers.httpx_client import get_async_client
-from homeassistant.helpers.typing import (
- UNDEFINED,
- ConfigType,
- UndefinedType,
- VolDictType,
-)
+from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
from .const import DATA_COMPONENT, DOMAIN, IMAGE_TIMEOUT
_LOGGER = logging.getLogger(__name__)
-SERVICE_SNAPSHOT: Final = "snapshot"
-
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
SCAN_INTERVAL: Final = timedelta(seconds=30)
-ATTR_FILENAME: Final = "filename"
-
DEFAULT_CONTENT_TYPE: Final = "image/jpeg"
ENTITY_IMAGE_URL: Final = "/api/image_proxy/{0}?token={1}"
@@ -68,8 +51,6 @@ FRAME_BOUNDARY = "frame-boundary"
FRAME_SEPARATOR = bytes(f"\r\n--{FRAME_BOUNDARY}\r\n", "utf-8")
LAST_FRAME_MARKER = bytes(f"\r\n--{FRAME_BOUNDARY}--\r\n", "utf-8")
-IMAGE_SERVICE_SNAPSHOT: VolDictType = {vol.Required(ATTR_FILENAME): cv.string}
-
class ImageEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes image entities."""
@@ -134,10 +115,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unsub_track_time_interval)
- component.async_register_entity_service(
- SERVICE_SNAPSHOT, IMAGE_SERVICE_SNAPSHOT, async_handle_snapshot_service
- )
-
return True
@@ -403,34 +380,3 @@ class ImageStreamView(ImageView):
) -> web.StreamResponse:
"""Serve image stream."""
return await async_get_still_stream(request, image_entity)
-
-
-async def async_handle_snapshot_service(
- image: ImageEntity, service_call: ServiceCall
-) -> None:
- """Handle snapshot services calls."""
- hass = image.hass
- snapshot_file: str = service_call.data[ATTR_FILENAME]
-
- # check if we allow to access to that file
- if not hass.config.is_allowed_path(snapshot_file):
- raise HomeAssistantError(
- f"Cannot write `{snapshot_file}`, no access to path; `allowlist_external_dirs` may need to be adjusted in `configuration.yaml`"
- )
-
- async with asyncio.timeout(IMAGE_TIMEOUT):
- image_data = await image.async_image()
-
- if image_data is None:
- return
-
- def _write_image(to_file: str, image_data: bytes) -> None:
- """Executor helper to write image."""
- os.makedirs(os.path.dirname(to_file), exist_ok=True)
- with open(to_file, "wb") as img_file:
- img_file.write(image_data)
-
- try:
- await hass.async_add_executor_job(_write_image, snapshot_file, image_data)
- except OSError as err:
- raise HomeAssistantError("Can't write image to file") from err
diff --git a/homeassistant/components/image/icons.json b/homeassistant/components/image/icons.json
index 4434f3c180c..cec9c99d765 100644
--- a/homeassistant/components/image/icons.json
+++ b/homeassistant/components/image/icons.json
@@ -3,10 +3,5 @@
"_": {
"default": "mdi:image"
}
- },
- "services": {
- "snapshot": {
- "service": "mdi:camera"
- }
}
}
diff --git a/homeassistant/components/image/services.yaml b/homeassistant/components/image/services.yaml
deleted file mode 100644
index 8eef055cd89..00000000000
--- a/homeassistant/components/image/services.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-# Describes the format for available image services
-
-snapshot:
- target:
- entity:
- domain: image
- fields:
- filename:
- required: true
- example: "/tmp/image_snapshot.jpg"
- selector:
- text:
diff --git a/homeassistant/components/image/strings.json b/homeassistant/components/image/strings.json
index 011102f5b9e..ea7ecd16956 100644
--- a/homeassistant/components/image/strings.json
+++ b/homeassistant/components/image/strings.json
@@ -4,17 +4,5 @@
"_": {
"name": "[%key:component::image::title%]"
}
- },
- "services": {
- "snapshot": {
- "name": "Take snapshot",
- "description": "Takes a snapshot from an image.",
- "fields": {
- "filename": {
- "name": "Filename",
- "description": "Template of a filename. Variable available is `entity_id`."
- }
- }
- }
}
}
diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py
index 0ac8d39813b..2c1d0f9304c 100644
--- a/homeassistant/components/image_processing/__init__.py
+++ b/homeassistant/components/image_processing/__init__.py
@@ -223,7 +223,7 @@ class ImageProcessingFaceEntity(ImageProcessingEntity):
confidence = f_co
for attr in (ATTR_NAME, ATTR_MOTION):
if attr in face:
- state = face[attr]
+ state = face[attr] # type: ignore[literal-required]
break
return state
diff --git a/homeassistant/components/image_upload/manifest.json b/homeassistant/components/image_upload/manifest.json
index bb8c33ba749..963721a0476 100644
--- a/homeassistant/components/image_upload/manifest.json
+++ b/homeassistant/components/image_upload/manifest.json
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/image_upload",
"integration_type": "system",
"quality_scale": "internal",
- "requirements": ["Pillow==11.0.0"]
+ "requirements": ["Pillow==10.4.0"]
}
diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py
index 994c53b5b3e..6f93ce71d84 100644
--- a/homeassistant/components/imap/config_flow.py
+++ b/homeassistant/components/imap/config_flow.py
@@ -13,15 +13,9 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
-)
-from homeassistant.const import (
- CONF_NAME,
- CONF_PASSWORD,
- CONF_PORT,
- CONF_USERNAME,
- CONF_VERIFY_SSL,
+ OptionsFlowWithConfigEntry,
)
+from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers import config_validation as cv
@@ -150,6 +144,7 @@ class IMAPConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for imap."""
VERSION = 1
+ _reauth_entry: ConfigEntry | None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -182,6 +177,9 @@ class IMAPConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -189,16 +187,17 @@ class IMAPConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
errors = {}
- reauth_entry = self._get_reauth_entry()
+ assert self._reauth_entry
if user_input is not None:
- user_input = {**reauth_entry.data, **user_input}
+ user_input = {**self._reauth_entry.data, **user_input}
if not (errors := await validate_input(self.hass, user_input)):
- return self.async_update_reload_and_abort(reauth_entry, data=user_input)
+ return self.async_update_reload_and_abort(
+ self._reauth_entry, data=user_input
+ )
return self.async_show_form(
description_placeholders={
- CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
- CONF_NAME: reauth_entry.title,
+ CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME]
},
step_id="reauth_confirm",
data_schema=vol.Schema(
@@ -213,12 +212,12 @@ class IMAPConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
- ) -> ImapOptionsFlow:
+ ) -> OptionsFlow:
"""Get the options flow for this handler."""
- return ImapOptionsFlow()
+ return OptionsFlow(config_entry)
-class ImapOptionsFlow(OptionsFlow):
+class OptionsFlow(OptionsFlowWithConfigEntry):
"""Option flow handler."""
async def async_step_init(
@@ -226,13 +225,13 @@ class ImapOptionsFlow(OptionsFlow):
) -> ConfigFlowResult:
"""Manage the options."""
errors: dict[str, str] | None = None
- entry_data: dict[str, Any] = dict(self.config_entry.data)
+ entry_data: dict[str, Any] = dict(self._config_entry.data)
if user_input is not None:
try:
self._async_abort_entries_match(
{
- CONF_SERVER: self.config_entry.data[CONF_SERVER],
- CONF_USERNAME: self.config_entry.data[CONF_USERNAME],
+ CONF_SERVER: self._config_entry.data[CONF_SERVER],
+ CONF_USERNAME: self._config_entry.data[CONF_USERNAME],
CONF_FOLDER: user_input[CONF_FOLDER],
CONF_SEARCH: user_input[CONF_SEARCH],
}
diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json
index 7c4a0d9a973..115d46f3d0e 100644
--- a/homeassistant/components/imap/strings.json
+++ b/homeassistant/components/imap/strings.json
@@ -104,7 +104,7 @@
"services": {
"fetch": {
"name": "Fetch message",
- "description": "Fetch an email message from the server.",
+ "description": "Fetch the email message from the server.",
"fields": {
"entry": {
"name": "Entry",
diff --git a/homeassistant/components/improv_ble/config_flow.py b/homeassistant/components/improv_ble/config_flow.py
index 05dd1de449a..f38f4830ace 100644
--- a/homeassistant/components/improv_ble/config_flow.py
+++ b/homeassistant/components/improv_ble/config_flow.py
@@ -120,22 +120,12 @@ class ImprovBLEConfigFlow(ConfigFlow, domain=DOMAIN):
assert self._discovery_info is not None
service_data = self._discovery_info.service_data
- try:
- improv_service_data = ImprovServiceData.from_bytes(
- service_data[SERVICE_DATA_UUID]
- )
- except improv_ble_errors.InvalidCommand as err:
- _LOGGER.warning(
- "Aborting improv flow, device %s sent invalid improv data: '%s'",
- self._discovery_info.address,
- service_data[SERVICE_DATA_UUID].hex(),
- )
- raise AbortFlow("invalid_improv_data") from err
-
+ improv_service_data = ImprovServiceData.from_bytes(
+ service_data[SERVICE_DATA_UUID]
+ )
if improv_service_data.state in (State.PROVISIONING, State.PROVISIONED):
_LOGGER.debug(
- "Aborting improv flow, device %s is already provisioned: %s",
- self._discovery_info.address,
+ "Aborting improv flow, device is already provisioned: %s",
improv_service_data.state,
)
raise AbortFlow("already_provisioned")
diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py
index e7620ac2a1a..28424069d1c 100644
--- a/homeassistant/components/incomfort/water_heater.py
+++ b/homeassistant/components/incomfort/water_heater.py
@@ -54,16 +54,12 @@ class IncomfortWaterHeater(IncomfortBoilerEntity, WaterHeaterEntity):
return {k: v for k, v in self._heater.status.items() if k in HEATER_ATTRS}
@property
- def current_temperature(self) -> float | None:
+ def current_temperature(self) -> float:
"""Return the current temperature."""
if self._heater.is_tapping:
return self._heater.tap_temp
if self._heater.is_pumping:
return self._heater.heater_temp
- if self._heater.heater_temp is None:
- return self._heater.tap_temp
- if self._heater.tap_temp is None:
- return self._heater.heater_temp
return max(self._heater.heater_temp, self._heater.tap_temp)
@property
diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py
index 143a9e2a5e2..9b486ad01e3 100644
--- a/homeassistant/components/insteon/config_flow.py
+++ b/homeassistant/components/insteon/config_flow.py
@@ -59,6 +59,8 @@ class InsteonFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Init the config flow."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
modem_types = [STEP_PLM, STEP_HUB_V1, STEP_HUB_V2]
return self.async_show_menu(step_id="user", menu_options=modem_types)
@@ -133,6 +135,9 @@ class InsteonFlowHandler(ConfigFlow, domain=DOMAIN):
self, discovery_info: usb.UsbServiceInfo
) -> ConfigFlowResult:
"""Handle USB discovery."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
self._device_path = discovery_info.device
self._device_name = usb.human_readable_device_name(
discovery_info.device,
diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json
index c9127640250..c5791573195 100644
--- a/homeassistant/components/insteon/manifest.json
+++ b/homeassistant/components/insteon/manifest.json
@@ -20,7 +20,6 @@
"pyinsteon==1.6.3",
"insteon-frontend-home-assistant==0.5.0"
],
- "single_config_entry": true,
"usb": [
{
"vid": "10BF"
diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json
index 4df997ac939..37cdd5c0343 100644
--- a/homeassistant/components/insteon/strings.json
+++ b/homeassistant/components/insteon/strings.json
@@ -44,6 +44,7 @@
},
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"not_insteon_device": "Discovered device not an Insteon device"
}
},
@@ -112,7 +113,7 @@
"services": {
"add_all_link": {
"name": "Add all link",
- "description": "Tells the Insteon Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.",
+ "description": "Tells the Insteom Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.",
"fields": {
"group": {
"name": "Group",
diff --git a/homeassistant/components/intellifire/binary_sensor.py b/homeassistant/components/intellifire/binary_sensor.py
index 7d00bdfc26d..f0a5d84fa62 100644
--- a/homeassistant/components/intellifire/binary_sensor.py
+++ b/homeassistant/components/intellifire/binary_sensor.py
@@ -5,6 +5,8 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
+from intellifire4py.model import IntelliFirePollData
+
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
@@ -24,7 +26,7 @@ from .entity import IntellifireEntity
class IntellifireBinarySensorRequiredKeysMixin:
"""Mixin for required keys."""
- value_fn: Callable[[IntellifireDataUpdateCoordinator], bool | None]
+ value_fn: Callable[[IntelliFirePollData], bool]
@dataclass(frozen=True)
@@ -38,114 +40,100 @@ INTELLIFIRE_BINARY_SENSORS: tuple[IntellifireBinarySensorEntityDescription, ...]
IntellifireBinarySensorEntityDescription(
key="on_off", # This is the sensor name
translation_key="flame", # This is the translation key
- value_fn=lambda coordinator: coordinator.data.is_on,
+ value_fn=lambda data: data.is_on,
),
IntellifireBinarySensorEntityDescription(
key="timer_on",
translation_key="timer_on",
- value_fn=lambda coordinator: coordinator.data.timer_on,
+ value_fn=lambda data: data.timer_on,
),
IntellifireBinarySensorEntityDescription(
key="pilot_light_on",
translation_key="pilot_light_on",
- value_fn=lambda coordinator: coordinator.data.pilot_on,
+ value_fn=lambda data: data.pilot_on,
),
IntellifireBinarySensorEntityDescription(
key="thermostat_on",
translation_key="thermostat_on",
- value_fn=lambda coordinator: coordinator.data.thermostat_on,
+ value_fn=lambda data: data.thermostat_on,
),
IntellifireBinarySensorEntityDescription(
key="error_pilot_flame",
translation_key="pilot_flame_error",
entity_category=EntityCategory.DIAGNOSTIC,
- value_fn=lambda coordinator: coordinator.data.error_pilot_flame,
+ value_fn=lambda data: data.error_pilot_flame,
device_class=BinarySensorDeviceClass.PROBLEM,
),
IntellifireBinarySensorEntityDescription(
key="error_flame",
translation_key="flame_error",
entity_category=EntityCategory.DIAGNOSTIC,
- value_fn=lambda coordinator: coordinator.data.error_flame,
+ value_fn=lambda data: data.error_flame,
device_class=BinarySensorDeviceClass.PROBLEM,
),
IntellifireBinarySensorEntityDescription(
key="error_fan_delay",
translation_key="fan_delay_error",
entity_category=EntityCategory.DIAGNOSTIC,
- value_fn=lambda coordinator: coordinator.data.error_fan_delay,
+ value_fn=lambda data: data.error_fan_delay,
device_class=BinarySensorDeviceClass.PROBLEM,
),
IntellifireBinarySensorEntityDescription(
key="error_maintenance",
translation_key="maintenance_error",
entity_category=EntityCategory.DIAGNOSTIC,
- value_fn=lambda coordinator: coordinator.data.error_maintenance,
+ value_fn=lambda data: data.error_maintenance,
device_class=BinarySensorDeviceClass.PROBLEM,
),
IntellifireBinarySensorEntityDescription(
key="error_disabled",
translation_key="disabled_error",
entity_category=EntityCategory.DIAGNOSTIC,
- value_fn=lambda coordinator: coordinator.data.error_disabled,
+ value_fn=lambda data: data.error_disabled,
device_class=BinarySensorDeviceClass.PROBLEM,
),
IntellifireBinarySensorEntityDescription(
key="error_fan",
translation_key="fan_error",
entity_category=EntityCategory.DIAGNOSTIC,
- value_fn=lambda coordinator: coordinator.data.error_fan,
+ value_fn=lambda data: data.error_fan,
device_class=BinarySensorDeviceClass.PROBLEM,
),
IntellifireBinarySensorEntityDescription(
key="error_lights",
translation_key="lights_error",
entity_category=EntityCategory.DIAGNOSTIC,
- value_fn=lambda coordinator: coordinator.data.error_lights,
+ value_fn=lambda data: data.error_lights,
device_class=BinarySensorDeviceClass.PROBLEM,
),
IntellifireBinarySensorEntityDescription(
key="error_accessory",
translation_key="accessory_error",
entity_category=EntityCategory.DIAGNOSTIC,
- value_fn=lambda coordinator: coordinator.data.error_accessory,
+ value_fn=lambda data: data.error_accessory,
device_class=BinarySensorDeviceClass.PROBLEM,
),
IntellifireBinarySensorEntityDescription(
key="error_soft_lock_out",
translation_key="soft_lock_out_error",
entity_category=EntityCategory.DIAGNOSTIC,
- value_fn=lambda coordinator: coordinator.data.error_soft_lock_out,
+ value_fn=lambda data: data.error_soft_lock_out,
device_class=BinarySensorDeviceClass.PROBLEM,
),
IntellifireBinarySensorEntityDescription(
key="error_ecm_offline",
translation_key="ecm_offline_error",
entity_category=EntityCategory.DIAGNOSTIC,
- value_fn=lambda coordinator: coordinator.data.error_ecm_offline,
+ value_fn=lambda data: data.error_ecm_offline,
device_class=BinarySensorDeviceClass.PROBLEM,
),
IntellifireBinarySensorEntityDescription(
key="error_offline",
translation_key="offline_error",
entity_category=EntityCategory.DIAGNOSTIC,
- value_fn=lambda coordinator: coordinator.data.error_offline,
+ value_fn=lambda data: data.error_offline,
device_class=BinarySensorDeviceClass.PROBLEM,
),
- IntellifireBinarySensorEntityDescription(
- key="local_connectivity",
- translation_key="local_connectivity",
- entity_category=EntityCategory.DIAGNOSTIC,
- device_class=BinarySensorDeviceClass.CONNECTIVITY,
- value_fn=lambda coordinator: coordinator.fireplace.local_connectivity,
- ),
- IntellifireBinarySensorEntityDescription(
- key="cloud_connectivity",
- translation_key="cloud_connectivity",
- entity_category=EntityCategory.DIAGNOSTIC,
- device_class=BinarySensorDeviceClass.CONNECTIVITY,
- value_fn=lambda coordinator: coordinator.fireplace.cloud_connectivity,
- ),
)
@@ -169,6 +157,6 @@ class IntellifireBinarySensor(IntellifireEntity, BinarySensorEntity):
entity_description: IntellifireBinarySensorEntityDescription
@property
- def is_on(self) -> bool | None:
+ def is_on(self) -> bool:
"""Use this to get the correct value."""
- return self.entity_description.value_fn(self.coordinator)
+ return self.entity_description.value_fn(self.coordinator.read_api.data)
diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py
index a6b63f3b3e8..56f0d5ca6a5 100644
--- a/homeassistant/components/intellifire/config_flow.py
+++ b/homeassistant/components/intellifire/config_flow.py
@@ -14,7 +14,7 @@ from intellifire4py.model import IntelliFireCommonFireplaceData
import voluptuous as vol
from homeassistant.components.dhcp import DhcpServiceInfo
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
@@ -79,6 +79,7 @@ class IntelliFireConfigFlow(ConfigFlow, domain=DOMAIN):
self._dhcp_discovered_serial: str = "" # used only in discovery mode
self._discovered_host: DiscoveredHostInfo
self._dhcp_mode = False
+ self._is_reauth = False
self._not_configured_hosts: list[DiscoveredHostInfo] = []
self._reauth_needed: DiscoveredHostInfo
@@ -181,6 +182,14 @@ class IntelliFireConfigFlow(ConfigFlow, domain=DOMAIN):
# If there is a single fireplace configure it
if len(available_fireplaces) == 1:
+ if self._is_reauth:
+ reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
+ return await self._async_create_config_entry_from_common_data(
+ fireplace=available_fireplaces[0], existing_entry=reauth_entry
+ )
+
return await self._async_create_config_entry_from_common_data(
fireplace=available_fireplaces[0]
)
@@ -198,7 +207,9 @@ class IntelliFireConfigFlow(ConfigFlow, domain=DOMAIN):
)
async def _async_create_config_entry_from_common_data(
- self, fireplace: IntelliFireCommonFireplaceData
+ self,
+ fireplace: IntelliFireCommonFireplaceData,
+ existing_entry: ConfigEntry | None = None,
) -> ConfigFlowResult:
"""Construct a config entry based on an object of IntelliFireCommonFireplaceData."""
@@ -215,9 +226,9 @@ class IntelliFireConfigFlow(ConfigFlow, domain=DOMAIN):
options = {CONF_READ_MODE: API_MODE_LOCAL, CONF_CONTROL_MODE: API_MODE_LOCAL}
- if self.source == SOURCE_REAUTH:
+ if existing_entry:
return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=data, options=options
+ existing_entry, data=data, options=options
)
return self.async_create_entry(
title=f"Fireplace {fireplace.serial}", data=data, options=options
@@ -228,9 +239,11 @@ class IntelliFireConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
LOGGER.debug("STEP: reauth")
+ self._is_reauth = True
+ entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
# populate the expected vars
- self._dhcp_discovered_serial = self._get_reauth_entry().data[CONF_SERIAL]
+ self._dhcp_discovered_serial = entry.data[CONF_SERIAL] # type: ignore[union-attr]
placeholders = {"serial": self._dhcp_discovered_serial}
self.context["title_placeholders"] = placeholders
diff --git a/homeassistant/components/intellifire/icons.json b/homeassistant/components/intellifire/icons.json
index fd6a2c149a7..6dca69484b6 100644
--- a/homeassistant/components/intellifire/icons.json
+++ b/homeassistant/components/intellifire/icons.json
@@ -18,20 +18,6 @@
},
"fan_error": {
"default": "mdi:fan-alert"
- },
- "local_connectivity": {
- "default": "mdi:lan-pending",
- "state": {
- "on": "mdi:lan-connect",
- "off": "mdi:lan-disconnect"
- }
- },
- "cloud_connectivity": {
- "default": "mdi:cloud-question",
- "state": {
- "on": "mdi:cloud-check-variant-outline",
- "off": "mdi:cloud-alert-outline"
- }
}
},
"number": {
diff --git a/homeassistant/components/intellifire/strings.json b/homeassistant/components/intellifire/strings.json
index 423d2c0788d..2eeb2b50b93 100644
--- a/homeassistant/components/intellifire/strings.json
+++ b/homeassistant/components/intellifire/strings.json
@@ -73,12 +73,6 @@
},
"offline_error": {
"name": "Offline error"
- },
- "cloud_connectivity": {
- "name": "Cloud connectivity"
- },
- "local_connectivity": {
- "name": "Local connectivity"
}
},
"fan": {
diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py
index 1322576f115..001f2515ebf 100644
--- a/homeassistant/components/intent/__init__.py
+++ b/homeassistant/components/intent/__init__.py
@@ -137,7 +137,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
intent.async_register(hass, TimerStatusIntentHandler())
intent.async_register(hass, GetCurrentDateIntentHandler())
intent.async_register(hass, GetCurrentTimeIntentHandler())
- intent.async_register(hass, HelloIntentHandler())
return True
@@ -240,8 +239,6 @@ class GetStateIntentHandler(intent.IntentHandler):
vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]),
vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]),
vol.Optional("state"): vol.All(cv.ensure_list, [cv.string]),
- vol.Optional("preferred_area_id"): cv.string,
- vol.Optional("preferred_floor_id"): cv.string,
}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
@@ -283,13 +280,7 @@ class GetStateIntentHandler(intent.IntentHandler):
device_classes=device_classes,
assistant=intent_obj.assistant,
)
- match_preferences = intent.MatchTargetsPreferences(
- area_id=slots.get("preferred_area_id", {}).get("value"),
- floor_id=slots.get("preferred_floor_id", {}).get("value"),
- )
- match_result = intent.async_match_targets(
- hass, match_constraints, match_preferences
- )
+ match_result = intent.async_match_targets(hass, match_constraints)
if (
(not match_result.is_match)
and (match_result.no_match_reason is not None)
@@ -365,7 +356,7 @@ class NevermindIntentHandler(intent.IntentHandler):
description = "Cancels the current request and does nothing"
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
- """Do nothing and produces an empty response."""
+ """Doe not do anything, and produces an empty response."""
return intent_obj.create_response()
@@ -421,17 +412,6 @@ class GetCurrentTimeIntentHandler(intent.IntentHandler):
return response
-class HelloIntentHandler(intent.IntentHandler):
- """Responds with no action."""
-
- intent_type = intent.INTENT_RESPOND
- description = "Returns the provided response with no action."
-
- async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
- """Return the provided response, but take no action."""
- return intent_obj.create_response()
-
-
async def _async_process_intent(
hass: HomeAssistant, domain: str, platform: IntentPlatformProtocol
) -> None:
diff --git a/homeassistant/components/iotty/api.py b/homeassistant/components/iotty/api.py
index d87fda57731..03e18a02903 100644
--- a/homeassistant/components/iotty/api.py
+++ b/homeassistant/components/iotty/api.py
@@ -33,6 +33,8 @@ class IottyProxy(CloudApi):
async def async_get_access_token(self) -> Any:
"""Return a valid access token."""
- await self._oauth_session.async_ensure_token_valid()
+
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token["access_token"]
diff --git a/homeassistant/components/iotty/coordinator.py b/homeassistant/components/iotty/coordinator.py
index 420248f7724..12764ac1cf6 100644
--- a/homeassistant/components/iotty/coordinator.py
+++ b/homeassistant/components/iotty/coordinator.py
@@ -61,12 +61,14 @@ class IottyDataUpdateCoordinator(DataUpdateCoordinator[IottyData]):
)
self._device_registry = dr.async_get(hass)
- async def _async_setup(self) -> None:
- """Get devices."""
+ async def async_config_entry_first_refresh(self) -> None:
+ """Override the first refresh to also fetch iotty devices list."""
_LOGGER.debug("Fetching devices list from iottyCloud")
self._devices = await self.iotty.get_devices()
_LOGGER.debug("There are %d devices", len(self._devices))
+ await super().async_config_entry_first_refresh()
+
async def _async_update_data(self) -> IottyData:
"""Fetch data from iottyCloud device."""
_LOGGER.debug("Fetching devices status from iottyCloud")
diff --git a/homeassistant/components/iotty/strings.json b/homeassistant/components/iotty/strings.json
index cb0dc509d9a..569e148a5a3 100644
--- a/homeassistant/components/iotty/strings.json
+++ b/homeassistant/components/iotty/strings.json
@@ -12,8 +12,7 @@
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
- "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
- "missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]"
+ "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json
index baa41cf00bd..2ba82b2cfec 100644
--- a/homeassistant/components/ipp/manifest.json
+++ b/homeassistant/components/ipp/manifest.json
@@ -8,6 +8,6 @@
"iot_class": "local_polling",
"loggers": ["deepmerge", "pyipp"],
"quality_scale": "platinum",
- "requirements": ["pyipp==0.17.0"],
+ "requirements": ["pyipp==0.16.0"],
"zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."]
}
diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py
index a2792c7749b..e872fc7977f 100644
--- a/homeassistant/components/ipp/sensor.py
+++ b/homeassistant/components/ipp/sensor.py
@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
-from datetime import datetime
+from datetime import datetime, timedelta
from typing import Any
from pyipp import Marker, Printer
@@ -19,6 +19,7 @@ from homeassistant.const import ATTR_LOCATION, PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
+from homeassistant.util.dt import utcnow
from . import IPPConfigEntry
from .const import (
@@ -79,7 +80,7 @@ PRINTER_SENSORS: tuple[IPPSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
- value_fn=lambda printer: printer.booted_at,
+ value_fn=lambda printer: (utcnow() - timedelta(seconds=printer.info.uptime)),
),
)
diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py
index 3fabb88b041..8b72d6f8784 100644
--- a/homeassistant/components/iqvia/__init__.py
+++ b/homeassistant/components/iqvia/__init__.py
@@ -76,7 +76,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = coordinators[sensor_type] = DataUpdateCoordinator(
hass,
LOGGER,
- config_entry=entry,
name=f"{entry.data[CONF_ZIP_CODE]} {sensor_type}",
update_interval=DEFAULT_SCAN_INTERVAL,
update_method=partial(async_get_data_from_api, api_coro),
diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json
index 11c99a7428f..6142fa1349e 100644
--- a/homeassistant/components/iqvia/manifest.json
+++ b/homeassistant/components/iqvia/manifest.json
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pyiqvia"],
- "requirements": ["numpy==2.1.3", "pyiqvia==2022.04.0"]
+ "requirements": ["numpy==1.26.4", "pyiqvia==2022.04.0"]
}
diff --git a/homeassistant/components/iron_os/__init__.py b/homeassistant/components/iron_os/__init__.py
index 56a83117e68..11d99a1558a 100644
--- a/homeassistant/components/iron_os/__init__.py
+++ b/homeassistant/components/iron_os/__init__.py
@@ -5,7 +5,6 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING
-from aiogithubapi import GitHubAPI
from pynecil import Pynecil
from homeassistant.components import bluetooth
@@ -13,36 +12,17 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.typing import ConfigType
-from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN
-from .coordinator import IronOSFirmwareUpdateCoordinator, IronOSLiveDataCoordinator
+from .coordinator import IronOSCoordinator
-PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.UPDATE]
+PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR]
-
-type IronOSConfigEntry = ConfigEntry[IronOSLiveDataCoordinator]
-IRON_OS_KEY: HassKey[IronOSFirmwareUpdateCoordinator] = HassKey(DOMAIN)
-
-CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
+type IronOSConfigEntry = ConfigEntry[IronOSCoordinator]
_LOGGER = logging.getLogger(__name__)
-async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
- """Set up IronOS firmware update coordinator."""
-
- session = async_get_clientsession(hass)
- github = GitHubAPI(session=session)
-
- hass.data[IRON_OS_KEY] = IronOSFirmwareUpdateCoordinator(hass, github)
- await hass.data[IRON_OS_KEY].async_request_refresh()
- return True
-
-
async def async_setup_entry(hass: HomeAssistant, entry: IronOSConfigEntry) -> bool:
"""Set up IronOS from a config entry."""
if TYPE_CHECKING:
@@ -59,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: IronOSConfigEntry) -> bo
device = Pynecil(ble_device)
- coordinator = IronOSLiveDataCoordinator(hass, device)
+ coordinator = IronOSCoordinator(hass, device)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
diff --git a/homeassistant/components/iron_os/coordinator.py b/homeassistant/components/iron_os/coordinator.py
index 699f5a01704..aefb14b689b 100644
--- a/homeassistant/components/iron_os/coordinator.py
+++ b/homeassistant/components/iron_os/coordinator.py
@@ -4,9 +4,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
-from typing import TYPE_CHECKING
-from aiogithubapi import GitHubAPI, GitHubException, GitHubReleaseModel
from pynecil import CommunicationError, DeviceInfoResponse, LiveDataResponse, Pynecil
from homeassistant.config_entries import ConfigEntry
@@ -18,11 +16,10 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
-SCAN_INTERVAL_GITHUB = timedelta(hours=3)
-class IronOSLiveDataCoordinator(DataUpdateCoordinator[LiveDataResponse]):
- """IronOS live data coordinator."""
+class IronOSCoordinator(DataUpdateCoordinator[LiveDataResponse]):
+ """IronOS coordinator."""
device_info: DeviceInfoResponse
config_entry: ConfigEntry
@@ -41,42 +38,16 @@ class IronOSLiveDataCoordinator(DataUpdateCoordinator[LiveDataResponse]):
"""Fetch data from Device."""
try:
- # device info is cached and won't be refetched on every
- # coordinator refresh, only after the device has disconnected
- # the device info is refetched
- self.device_info = await self.device.get_device_info()
return await self.device.get_live_data()
except CommunicationError as e:
raise UpdateFailed("Cannot connect to device") from e
-
-class IronOSFirmwareUpdateCoordinator(DataUpdateCoordinator[GitHubReleaseModel]):
- """IronOS coordinator for retrieving update information from github."""
-
- def __init__(self, hass: HomeAssistant, github: GitHubAPI) -> None:
- """Initialize IronOS coordinator."""
- super().__init__(
- hass,
- _LOGGER,
- config_entry=None,
- name=DOMAIN,
- update_interval=SCAN_INTERVAL_GITHUB,
- )
- self.github = github
-
- async def _async_update_data(self) -> GitHubReleaseModel:
- """Fetch data from Github."""
+ async def _async_setup(self) -> None:
+ """Set up the coordinator."""
try:
- release = await self.github.repos.releases.latest("Ralim/IronOS")
+ self.device_info = await self.device.get_device_info()
- except GitHubException as e:
- raise UpdateFailed(
- "Failed to retrieve latest release data from Github"
- ) from e
-
- if TYPE_CHECKING:
- assert release.data
-
- return release.data
+ except CommunicationError as e:
+ raise UpdateFailed("Cannot connect to device") from e
diff --git a/homeassistant/components/iron_os/entity.py b/homeassistant/components/iron_os/entity.py
index 77bebda9390..5a24b0a5567 100644
--- a/homeassistant/components/iron_os/entity.py
+++ b/homeassistant/components/iron_os/entity.py
@@ -9,17 +9,17 @@ from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import MANUFACTURER, MODEL
-from .coordinator import IronOSLiveDataCoordinator
+from .coordinator import IronOSCoordinator
-class IronOSBaseEntity(CoordinatorEntity[IronOSLiveDataCoordinator]):
+class IronOSBaseEntity(CoordinatorEntity[IronOSCoordinator]):
"""Base IronOS entity."""
_attr_has_entity_name = True
def __init__(
self,
- coordinator: IronOSLiveDataCoordinator,
+ coordinator: IronOSCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize the sensor."""
diff --git a/homeassistant/components/iron_os/manifest.json b/homeassistant/components/iron_os/manifest.json
index 4ec08a43b61..cfaf36880f2 100644
--- a/homeassistant/components/iron_os/manifest.json
+++ b/homeassistant/components/iron_os/manifest.json
@@ -12,6 +12,6 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/iron_os",
"iot_class": "local_polling",
- "loggers": ["pynecil", "aiogithubapi"],
- "requirements": ["pynecil==0.2.1", "aiogithubapi==24.6.0"]
+ "loggers": ["pynecil"],
+ "requirements": ["pynecil==0.2.0"]
}
diff --git a/homeassistant/components/iron_os/update.py b/homeassistant/components/iron_os/update.py
deleted file mode 100644
index 786ba86f730..00000000000
--- a/homeassistant/components/iron_os/update.py
+++ /dev/null
@@ -1,98 +0,0 @@
-"""Update platform for IronOS integration."""
-
-from __future__ import annotations
-
-from homeassistant.components.update import (
- UpdateDeviceClass,
- UpdateEntity,
- UpdateEntityDescription,
- UpdateEntityFeature,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import IRON_OS_KEY, IronOSConfigEntry, IronOSLiveDataCoordinator
-from .coordinator import IronOSFirmwareUpdateCoordinator
-from .entity import IronOSBaseEntity
-
-UPDATE_DESCRIPTION = UpdateEntityDescription(
- key="firmware",
- device_class=UpdateDeviceClass.FIRMWARE,
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: IronOSConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up IronOS update platform."""
-
- coordinator = entry.runtime_data
-
- async_add_entities(
- [IronOSUpdate(coordinator, hass.data[IRON_OS_KEY], UPDATE_DESCRIPTION)]
- )
-
-
-class IronOSUpdate(IronOSBaseEntity, UpdateEntity):
- """Representation of an IronOS update entity."""
-
- _attr_supported_features = UpdateEntityFeature.RELEASE_NOTES
-
- def __init__(
- self,
- coordinator: IronOSLiveDataCoordinator,
- firmware_update: IronOSFirmwareUpdateCoordinator,
- entity_description: UpdateEntityDescription,
- ) -> None:
- """Initialize the sensor."""
- self.firmware_update = firmware_update
- super().__init__(coordinator, entity_description)
-
- @property
- def installed_version(self) -> str | None:
- """IronOS version on the device."""
-
- return self.coordinator.device_info.build
-
- @property
- def title(self) -> str | None:
- """Title of the IronOS release."""
-
- return f"IronOS {self.firmware_update.data.name}"
-
- @property
- def release_url(self) -> str | None:
- """URL to the full release notes of the latest IronOS version available."""
-
- return self.firmware_update.data.html_url
-
- @property
- def latest_version(self) -> str | None:
- """Latest IronOS version available for install."""
-
- return self.firmware_update.data.tag_name
-
- async def async_release_notes(self) -> str | None:
- """Return the release notes."""
-
- return self.firmware_update.data.body
-
- async def async_added_to_hass(self) -> None:
- """When entity is added to hass.
-
- Register extra update listener for the firmware update coordinator.
- """
- await super().async_added_to_hass()
- self.async_on_remove(
- self.firmware_update.async_add_listener(self._handle_coordinator_update)
- )
-
- @property
- def available(self) -> bool:
- """Return if entity is available."""
- return (
- self.installed_version is not None
- and self.firmware_update.last_update_success
- )
diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py
index ce911ccc49d..2db89183499 100644
--- a/homeassistant/components/islamic_prayer_times/config_flow.py
+++ b/homeassistant/components/islamic_prayer_times/config_flow.py
@@ -52,7 +52,7 @@ class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> IslamicPrayerOptionsFlowHandler:
"""Get the options flow for this handler."""
- return IslamicPrayerOptionsFlowHandler()
+ return IslamicPrayerOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -93,6 +93,10 @@ class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN):
class IslamicPrayerOptionsFlowHandler(OptionsFlow):
"""Handle Islamic Prayer client options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/iss/__init__.py b/homeassistant/components/iss/__init__.py
index dbbcc8b6c51..606263ce769 100644
--- a/homeassistant/components/iss/__init__.py
+++ b/homeassistant/components/iss/__init__.py
@@ -53,7 +53,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=DOMAIN,
update_method=async_update,
update_interval=timedelta(seconds=60),
diff --git a/homeassistant/components/iss/config_flow.py b/homeassistant/components/iss/config_flow.py
index eaf01a6d094..80644698239 100644
--- a/homeassistant/components/iss/config_flow.py
+++ b/homeassistant/components/iss/config_flow.py
@@ -1,7 +1,5 @@
"""Config flow to configure iss component."""
-from __future__ import annotations
-
import voluptuous as vol
from homeassistant.config_entries import (
@@ -25,12 +23,16 @@ class ISSConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
- ) -> OptionsFlowHandler:
+ ) -> OptionsFlow:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
+ # Check if already configured
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
if user_input is not None:
return self.async_create_entry(
title=DEFAULT_NAME,
@@ -44,10 +46,16 @@ class ISSConfigFlow(ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(OptionsFlow):
"""Config flow options handler for iss."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+ self.options = dict(config_entry.options)
+
async def async_step_init(self, user_input=None) -> ConfigFlowResult:
"""Manage the options."""
if user_input is not None:
- return self.async_create_entry(data=self.config_entry.options | user_input)
+ self.options.update(user_input)
+ return self.async_create_entry(title="", data=self.options)
return self.async_show_form(
step_id="init",
diff --git a/homeassistant/components/iss/manifest.json b/homeassistant/components/iss/manifest.json
index bf36a15db46..1dc885c9df6 100644
--- a/homeassistant/components/iss/manifest.json
+++ b/homeassistant/components/iss/manifest.json
@@ -7,6 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pyiss"],
- "requirements": ["pyiss==1.0.1"],
- "single_config_entry": true
+ "requirements": ["pyiss==1.0.1"]
}
diff --git a/homeassistant/components/iss/strings.json b/homeassistant/components/iss/strings.json
index 17e86587e85..e0c7d85efa4 100644
--- a/homeassistant/components/iss/strings.json
+++ b/homeassistant/components/iss/strings.json
@@ -6,6 +6,7 @@
}
},
"abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"latitude_longitude_not_defined": "Latitude and longitude are not defined in Home Assistant."
}
},
diff --git a/homeassistant/components/ista_ecotrend/config_flow.py b/homeassistant/components/ista_ecotrend/config_flow.py
index c11c43070df..15222995a37 100644
--- a/homeassistant/components/ista_ecotrend/config_flow.py
+++ b/homeassistant/components/ista_ecotrend/config_flow.py
@@ -17,6 +17,7 @@ from homeassistant.helpers.selector import (
TextSelectorType,
)
+from . import IstaConfigEntry
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -42,6 +43,8 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
class IstaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for ista EcoTrend."""
+ reauth_entry: IstaConfigEntry | None = None
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -85,6 +88,9 @@ class IstaConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -92,8 +98,9 @@ class IstaConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
+ if TYPE_CHECKING:
+ assert self.reauth_entry
- reauth_entry = self._get_reauth_entry()
if user_input is not None:
ista = PyEcotrendIsta(
user_input[CONF_EMAIL],
@@ -110,7 +117,9 @@ class IstaConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
- return self.async_update_reload_and_abort(reauth_entry, data=user_input)
+ return self.async_update_reload_and_abort(
+ self.reauth_entry, data=user_input
+ )
return self.async_show_form(
step_id="reauth_confirm",
@@ -119,12 +128,12 @@ class IstaConfigFlow(ConfigFlow, domain=DOMAIN):
suggested_values={
CONF_EMAIL: user_input[CONF_EMAIL]
if user_input is not None
- else reauth_entry.data[CONF_EMAIL]
+ else self.reauth_entry.data[CONF_EMAIL]
},
),
description_placeholders={
- CONF_NAME: reauth_entry.title,
- CONF_EMAIL: reauth_entry.data[CONF_EMAIL],
+ CONF_NAME: self.reauth_entry.title,
+ CONF_EMAIL: self.reauth_entry.data[CONF_EMAIL],
},
errors=errors,
)
diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py
index 3575fa99a55..0239926f5e3 100644
--- a/homeassistant/components/isy994/config_flow.py
+++ b/homeassistant/components/isy994/config_flow.py
@@ -140,7 +140,7 @@ class Isy994ConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -314,6 +314,10 @@ class Isy994ConfigFlow(ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(OptionsFlow):
"""Handle a option flow for ISY/IoX."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json
index f0e55881652..ec7d78edd53 100644
--- a/homeassistant/components/isy994/strings.json
+++ b/homeassistant/components/isy994/strings.json
@@ -29,8 +29,7 @@
"invalid_host": "The host entry was not in full URL format, e.g., http://192.168.10.100:80"
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
diff --git a/homeassistant/components/jellyfin/config_flow.py b/homeassistant/components/jellyfin/config_flow.py
index 0c170d2485f..7b5426cffde 100644
--- a/homeassistant/components/jellyfin/config_flow.py
+++ b/homeassistant/components/jellyfin/config_flow.py
@@ -8,7 +8,11 @@ from typing import Any
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
+from homeassistant.config_entries import (
+ ConfigFlow,
+ ConfigFlowResult,
+ OptionsFlowWithConfigEntry,
+)
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.util.uuid import random_uuid_hex
@@ -52,6 +56,7 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the Jellyfin config flow."""
self.client_device_id: str | None = None
+ self.entry: JellyfinConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -103,6 +108,7 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -112,8 +118,8 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input is not None:
- reauth_entry = self._get_reauth_entry()
- new_input = reauth_entry.data | user_input
+ assert self.entry is not None
+ new_input = self.entry.data | user_input
if self.client_device_id is None:
self.client_device_id = _generate_client_device_id()
@@ -129,7 +135,10 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
_LOGGER.exception("Unexpected exception")
else:
- return self.async_update_reload_and_abort(reauth_entry, data=new_input)
+ self.hass.config_entries.async_update_entry(self.entry, data=new_input)
+
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm", data_schema=REAUTH_DATA_SCHEMA, errors=errors
@@ -139,12 +148,12 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: JellyfinConfigEntry,
- ) -> OptionsFlowHandler:
+ ) -> OptionsFlowWithConfigEntry:
"""Create the options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
-class OptionsFlowHandler(OptionsFlow):
+class OptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle an option flow for jellyfin."""
async def async_step_init(
diff --git a/homeassistant/components/jellyfin/const.py b/homeassistant/components/jellyfin/const.py
index cdddaa46ad1..34fb040115f 100644
--- a/homeassistant/components/jellyfin/const.py
+++ b/homeassistant/components/jellyfin/const.py
@@ -83,5 +83,5 @@ MEDIA_CLASS_MAP = {
"Season": MediaClass.SEASON,
}
-PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE, Platform.SENSOR]
+PLATFORMS = [Platform.MEDIA_PLAYER, Platform.SENSOR]
LOGGER = logging.getLogger(__package__)
diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py
index 20428250254..a9b0a8b7031 100644
--- a/homeassistant/components/jellyfin/coordinator.py
+++ b/homeassistant/components/jellyfin/coordinator.py
@@ -41,7 +41,6 @@ class JellyfinDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, An
self.user_id: str = user_id
self.session_ids: set[str] = set()
- self.remote_session_ids: set[str] = set()
self.device_ids: set[str] = set()
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
diff --git a/homeassistant/components/jellyfin/remote.py b/homeassistant/components/jellyfin/remote.py
deleted file mode 100644
index ae33d58cc0c..00000000000
--- a/homeassistant/components/jellyfin/remote.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""Support for Jellyfin remote commands."""
-
-from __future__ import annotations
-
-from collections.abc import Iterable
-import time
-from typing import Any
-
-from homeassistant.components.remote import (
- ATTR_DELAY_SECS,
- ATTR_NUM_REPEATS,
- DEFAULT_DELAY_SECS,
- DEFAULT_NUM_REPEATS,
- RemoteEntity,
-)
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import JellyfinConfigEntry
-from .const import LOGGER
-from .coordinator import JellyfinDataUpdateCoordinator
-from .entity import JellyfinClientEntity
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: JellyfinConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up Jellyfin remote from a config entry."""
- coordinator = entry.runtime_data
-
- @callback
- def handle_coordinator_update() -> None:
- """Add remote per session."""
- entities: list[RemoteEntity] = []
- for session_id, session_data in coordinator.data.items():
- if (
- session_id not in coordinator.remote_session_ids
- and session_data["SupportsRemoteControl"]
- ):
- entity = JellyfinRemote(coordinator, session_id)
- LOGGER.debug("Creating remote for session: %s", session_id)
- coordinator.remote_session_ids.add(session_id)
- entities.append(entity)
- async_add_entities(entities)
-
- handle_coordinator_update()
-
- entry.async_on_unload(coordinator.async_add_listener(handle_coordinator_update))
-
-
-class JellyfinRemote(JellyfinClientEntity, RemoteEntity):
- """Defines a Jellyfin remote entity."""
-
- def __init__(
- self,
- coordinator: JellyfinDataUpdateCoordinator,
- session_id: str,
- ) -> None:
- """Initialize the Jellyfin Remote entity."""
- super().__init__(coordinator, session_id)
- self._attr_unique_id = f"{coordinator.server_id}-{session_id}"
-
- @property
- def is_on(self) -> bool:
- """Return if the client is on."""
- return self.session_data["IsActive"] if self.session_data else False
-
- def send_command(self, command: Iterable[str], **kwargs: Any) -> None:
- """Send a command to the client."""
- num_repeats = kwargs.get(ATTR_NUM_REPEATS, DEFAULT_NUM_REPEATS)
- delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
-
- for _ in range(num_repeats):
- for single_command in command:
- self.coordinator.api_client.jellyfin.command(
- self.session_id, single_command
- )
- time.sleep(delay)
diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py
index 823e9bd59be..fd238e8d615 100644
--- a/homeassistant/components/jewish_calendar/__init__.py
+++ b/homeassistant/components/jewish_calendar/__init__.py
@@ -5,17 +5,26 @@ from __future__ import annotations
from functools import partial
from hdate import Location
+import voluptuous as vol
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_ELEVATION,
CONF_LANGUAGE,
CONF_LATITUDE,
+ CONF_LOCATION,
CONF_LONGITUDE,
+ CONF_NAME,
CONF_TIME_ZONE,
Platform,
)
-from homeassistant.core import HomeAssistant
+from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
+import homeassistant.helpers.config_validation as cv
+import homeassistant.helpers.entity_registry as er
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
+from homeassistant.helpers.typing import ConfigType
+from .binary_sensor import BINARY_SENSORS
from .const import (
CONF_CANDLE_LIGHT_MINUTES,
CONF_DIASPORA,
@@ -24,15 +33,94 @@ from .const import (
DEFAULT_DIASPORA,
DEFAULT_HAVDALAH_OFFSET_MINUTES,
DEFAULT_LANGUAGE,
+ DEFAULT_NAME,
+ DOMAIN,
)
-from .entity import JewishCalendarConfigEntry, JewishCalendarData
+from .sensor import INFO_SENSORS, TIME_SENSORS
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.All(
+ cv.deprecated(DOMAIN),
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_DIASPORA, default=DEFAULT_DIASPORA): cv.boolean,
+ vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude,
+ vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude,
+ vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(
+ ["hebrew", "english"]
+ ),
+ vol.Optional(
+ CONF_CANDLE_LIGHT_MINUTES, default=DEFAULT_CANDLE_LIGHT
+ ): int,
+ # Default of 0 means use 8.5 degrees / 'three_stars' time.
+ vol.Optional(
+ CONF_HAVDALAH_OFFSET_MINUTES,
+ default=DEFAULT_HAVDALAH_OFFSET_MINUTES,
+ ): int,
+ },
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
-async def async_setup_entry(
- hass: HomeAssistant, config_entry: JewishCalendarConfigEntry
-) -> bool:
+
+def get_unique_prefix(
+ location: Location,
+ language: str,
+ candle_lighting_offset: int | None,
+ havdalah_offset: int | None,
+) -> str:
+ """Create a prefix for unique ids."""
+ # location.altitude was unset before 2024.6 when this method
+ # was used to create the unique id. As such it would always
+ # use the default altitude of 754.
+ config_properties = [
+ location.latitude,
+ location.longitude,
+ location.timezone,
+ 754,
+ location.diaspora,
+ language,
+ candle_lighting_offset,
+ havdalah_offset,
+ ]
+ prefix = "_".join(map(str, config_properties))
+ return f"{prefix}"
+
+
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+ """Set up the Jewish Calendar component."""
+ if DOMAIN not in config:
+ return True
+
+ async_create_issue(
+ hass,
+ HOMEASSISTANT_DOMAIN,
+ f"deprecated_yaml_{DOMAIN}",
+ is_fixable=False,
+ issue_domain=DOMAIN,
+ breaks_in_ha_version="2024.12.0",
+ severity=IssueSeverity.WARNING,
+ translation_key="deprecated_yaml",
+ translation_placeholders={
+ "domain": DOMAIN,
+ "integration_title": DEFAULT_NAME,
+ },
+ )
+
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
+ )
+ )
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up a configuration entry for Jewish calendar."""
language = config_entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE)
diaspora = config_entry.data.get(CONF_DIASPORA, DEFAULT_DIASPORA)
@@ -55,19 +143,27 @@ async def async_setup_entry(
)
)
- config_entry.runtime_data = JewishCalendarData(
- language,
- diaspora,
- location,
- candle_lighting_offset,
- havdalah_offset,
+ hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = {
+ CONF_LANGUAGE: language,
+ CONF_DIASPORA: diaspora,
+ CONF_LOCATION: location,
+ CONF_CANDLE_LIGHT_MINUTES: candle_lighting_offset,
+ CONF_HAVDALAH_OFFSET_MINUTES: havdalah_offset,
+ }
+
+ # Update unique ID to be unrelated to user defined options
+ old_prefix = get_unique_prefix(
+ location, language, candle_lighting_offset, havdalah_offset
)
+ ent_reg = er.async_get(hass)
+ entries = er.async_entries_for_config_entry(ent_reg, config_entry.entry_id)
+ if not entries or any(entry.unique_id.startswith(old_prefix) for entry in entries):
+ async_update_unique_ids(ent_reg, config_entry.entry_id, old_prefix)
+
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
- async def update_listener(
- hass: HomeAssistant, config_entry: JewishCalendarConfigEntry
- ) -> None:
+ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
# Trigger update of states for all platforms
await hass.config_entries.async_reload(config_entry.entry_id)
@@ -75,8 +171,35 @@ async def async_setup_entry(
return True
-async def async_unload_entry(
- hass: HomeAssistant, config_entry: JewishCalendarConfigEntry
-) -> bool:
+async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
+ unload_ok = await hass.config_entries.async_unload_platforms(
+ config_entry, PLATFORMS
+ )
+
+ if unload_ok:
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+
+ return unload_ok
+
+
+@callback
+def async_update_unique_ids(
+ ent_reg: er.EntityRegistry, new_prefix: str, old_prefix: str
+) -> None:
+ """Update unique ID to be unrelated to user defined options.
+
+ Introduced with release 2024.6
+ """
+ platform_descriptions = {
+ Platform.BINARY_SENSOR: BINARY_SENSORS,
+ Platform.SENSOR: (*INFO_SENSORS, *TIME_SENSORS),
+ }
+ for platform, descriptions in platform_descriptions.items():
+ for description in descriptions:
+ new_unique_id = f"{new_prefix}-{description.key}"
+ old_unique_id = f"{old_prefix}_{description.key}"
+ if entity_id := ent_reg.async_get_entity_id(
+ platform, DOMAIN, old_unique_id
+ ):
+ ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py
index 9fd1371f8a8..060650ee25c 100644
--- a/homeassistant/components/jewish_calendar/binary_sensor.py
+++ b/homeassistant/components/jewish_calendar/binary_sensor.py
@@ -14,13 +14,15 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import event
from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.dt as dt_util
-from .entity import JewishCalendarConfigEntry, JewishCalendarEntity
+from .const import DOMAIN
+from .entity import JewishCalendarEntity
@dataclass(frozen=True)
@@ -61,12 +63,14 @@ BINARY_SENSORS: tuple[JewishCalendarBinarySensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
- config_entry: JewishCalendarConfigEntry,
+ config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Jewish Calendar binary sensors."""
+ entry = hass.data[DOMAIN][config_entry.entry_id]
+
async_add_entities(
- JewishCalendarBinarySensor(config_entry, description)
+ JewishCalendarBinarySensor(config_entry, entry, description)
for description in BINARY_SENSORS
)
diff --git a/homeassistant/components/jewish_calendar/config_flow.py b/homeassistant/components/jewish_calendar/config_flow.py
index a2eadbf57bd..f96699d01bd 100644
--- a/homeassistant/components/jewish_calendar/config_flow.py
+++ b/homeassistant/components/jewish_calendar/config_flow.py
@@ -12,7 +12,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import (
CONF_ELEVATION,
@@ -90,21 +90,32 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> JewishCalendarOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowWithConfigEntry:
"""Get the options flow for this handler."""
- return JewishCalendarOptionsFlowHandler()
+ return JewishCalendarOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
if user_input is not None:
+ _options = {}
+ if CONF_CANDLE_LIGHT_MINUTES in user_input:
+ _options[CONF_CANDLE_LIGHT_MINUTES] = user_input[
+ CONF_CANDLE_LIGHT_MINUTES
+ ]
+ del user_input[CONF_CANDLE_LIGHT_MINUTES]
+ if CONF_HAVDALAH_OFFSET_MINUTES in user_input:
+ _options[CONF_HAVDALAH_OFFSET_MINUTES] = user_input[
+ CONF_HAVDALAH_OFFSET_MINUTES
+ ]
+ del user_input[CONF_HAVDALAH_OFFSET_MINUTES]
if CONF_LOCATION in user_input:
user_input[CONF_LATITUDE] = user_input[CONF_LOCATION][CONF_LATITUDE]
user_input[CONF_LONGITUDE] = user_input[CONF_LOCATION][CONF_LONGITUDE]
- return self.async_create_entry(title=DEFAULT_NAME, data=user_input)
+ return self.async_create_entry(
+ title=DEFAULT_NAME, data=user_input, options=_options
+ )
return self.async_show_form(
step_id="user",
@@ -113,6 +124,10 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
),
)
+ async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
+ """Import a config entry from configuration.yaml."""
+ return await self.async_step_user(import_data)
+
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -130,7 +145,7 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_update_reload_and_abort(reconfigure_entry, data=user_input)
-class JewishCalendarOptionsFlowHandler(OptionsFlow):
+class JewishCalendarOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle Jewish Calendar options."""
async def async_step_init(
diff --git a/homeassistant/components/jewish_calendar/entity.py b/homeassistant/components/jewish_calendar/entity.py
index ad5ac8e2137..c11925df954 100644
--- a/homeassistant/components/jewish_calendar/entity.py
+++ b/homeassistant/components/jewish_calendar/entity.py
@@ -1,27 +1,18 @@
"""Entity representing a Jewish Calendar sensor."""
-from dataclasses import dataclass
-
-from hdate import Location
+from typing import Any
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_LANGUAGE, CONF_LOCATION
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
-from .const import DOMAIN
-
-type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarData]
-
-
-@dataclass
-class JewishCalendarData:
- """Jewish Calendar runtime dataclass."""
-
- language: str
- diaspora: bool
- location: Location
- candle_lighting_offset: int
- havdalah_offset: int
+from .const import (
+ CONF_CANDLE_LIGHT_MINUTES,
+ CONF_DIASPORA,
+ CONF_HAVDALAH_OFFSET_MINUTES,
+ DOMAIN,
+)
class JewishCalendarEntity(Entity):
@@ -31,7 +22,8 @@ class JewishCalendarEntity(Entity):
def __init__(
self,
- config_entry: JewishCalendarConfigEntry,
+ config_entry: ConfigEntry,
+ data: dict[str, Any],
description: EntityDescription,
) -> None:
"""Initialize a Jewish Calendar entity."""
@@ -40,10 +32,10 @@ class JewishCalendarEntity(Entity):
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, config_entry.entry_id)},
+ name=config_entry.title,
)
- data = config_entry.runtime_data
- self._location = data.location
- self._hebrew = data.language == "hebrew"
- self._candle_lighting_offset = data.candle_lighting_offset
- self._havdalah_offset = data.havdalah_offset
- self._diaspora = data.diaspora
+ self._location = data[CONF_LOCATION]
+ self._hebrew = data[CONF_LANGUAGE] == "hebrew"
+ self._candle_lighting_offset = data[CONF_CANDLE_LIGHT_MINUTES]
+ self._havdalah_offset = data[CONF_HAVDALAH_OFFSET_MINUTES]
+ self._diaspora = data[CONF_DIASPORA]
diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py
index c32647af07c..87b4375b8b2 100644
--- a/homeassistant/components/jewish_calendar/sensor.py
+++ b/homeassistant/components/jewish_calendar/sensor.py
@@ -14,13 +14,15 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import SUN_EVENT_SUNSET, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.sun import get_astral_event_date
import homeassistant.util.dt as dt_util
-from .entity import JewishCalendarConfigEntry, JewishCalendarEntity
+from .const import DOMAIN
+from .entity import JewishCalendarEntity
_LOGGER = logging.getLogger(__name__)
@@ -167,15 +169,17 @@ TIME_SENSORS: tuple[SensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
- config_entry: JewishCalendarConfigEntry,
+ config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Jewish calendar sensors ."""
+ entry = hass.data[DOMAIN][config_entry.entry_id]
sensors = [
- JewishCalendarSensor(config_entry, description) for description in INFO_SENSORS
+ JewishCalendarSensor(config_entry, entry, description)
+ for description in INFO_SENSORS
]
sensors.extend(
- JewishCalendarTimeSensor(config_entry, description)
+ JewishCalendarTimeSensor(config_entry, entry, description)
for description in TIME_SENSORS
)
@@ -189,11 +193,12 @@ class JewishCalendarSensor(JewishCalendarEntity, SensorEntity):
def __init__(
self,
- config_entry: JewishCalendarConfigEntry,
+ config_entry: ConfigEntry,
+ data: dict[str, Any],
description: SensorEntityDescription,
) -> None:
"""Initialize the Jewish calendar sensor."""
- super().__init__(config_entry, description)
+ super().__init__(config_entry, data, description)
self._attrs: dict[str, str] = {}
async def async_update(self) -> None:
diff --git a/homeassistant/components/jewish_calendar/strings.json b/homeassistant/components/jewish_calendar/strings.json
index 1b7b86c0056..e5367b5819e 100644
--- a/homeassistant/components/jewish_calendar/strings.json
+++ b/homeassistant/components/jewish_calendar/strings.json
@@ -27,8 +27,7 @@
}
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py
index fcfca7f2492..445d04e67ec 100644
--- a/homeassistant/components/juicenet/__init__.py
+++ b/homeassistant/components/juicenet/__init__.py
@@ -83,7 +83,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name="JuiceNet",
update_method=async_update_data,
update_interval=timedelta(seconds=30),
diff --git a/homeassistant/components/justnimbus/config_flow.py b/homeassistant/components/justnimbus/config_flow.py
index 7b0d3f8e5db..8c816c1ac1b 100644
--- a/homeassistant/components/justnimbus/config_flow.py
+++ b/homeassistant/components/justnimbus/config_flow.py
@@ -9,7 +9,7 @@ from typing import Any
import justnimbus
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_CLIENT_ID
from homeassistant.helpers import config_validation as cv
@@ -29,6 +29,7 @@ class JustNimbusConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for JustNimbus."""
VERSION = 1
+ reauth_entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -43,7 +44,7 @@ class JustNimbusConfigFlow(ConfigFlow, domain=DOMAIN):
unique_id = f"{user_input[CONF_CLIENT_ID]}{user_input[CONF_ZIP_CODE]}"
await self.async_set_unique_id(unique_id=unique_id)
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
self._abort_if_unique_id_configured()
client = justnimbus.JustNimbusClient(
@@ -59,12 +60,18 @@ class JustNimbusConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
return self.async_create_entry(title="JustNimbus", data=user_input)
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=user_input, unique_id=unique_id
+ self.hass.config_entries.async_update_entry(
+ self.reauth_entry, data=user_input, unique_id=unique_id
)
+ # Reload the config entry otherwise devices will remain unavailable
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
+
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
@@ -73,4 +80,7 @@ class JustNimbusConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_user()
diff --git a/homeassistant/components/jvc_projector/config_flow.py b/homeassistant/components/jvc_projector/config_flow.py
index 5d9bedd7591..253aa640f71 100644
--- a/homeassistant/components/jvc_projector/config_flow.py
+++ b/homeassistant/components/jvc_projector/config_flow.py
@@ -9,7 +9,7 @@ from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorConnec
from jvcprojector.projector import DEFAULT_PORT
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.helpers.device_registry import format_mac
from homeassistant.util.network import is_host_valid
@@ -22,6 +22,8 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ _reauth_entry: ConfigEntry | None = None
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -75,18 +77,22 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth on password authentication error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: Mapping[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
+ assert self._reauth_entry
+
errors = {}
if user_input is not None:
- reauth_entry = self._get_reauth_entry()
- host = reauth_entry.data[CONF_HOST]
- port = reauth_entry.data[CONF_PORT]
+ host = self._reauth_entry.data[CONF_HOST]
+ port = self._reauth_entry.data[CONF_PORT]
password = user_input[CONF_PASSWORD]
try:
@@ -96,9 +102,12 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN):
except JvcProjectorAuthError:
errors["base"] = "invalid_auth"
else:
- return self.async_update_reload_and_abort(
- reauth_entry, data_updates=user_input
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry,
+ data={CONF_HOST: host, CONF_PORT: port, CONF_PASSWORD: password},
)
+ await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
diff --git a/homeassistant/components/jvc_projector/strings.json b/homeassistant/components/jvc_projector/strings.json
index b517bf064e1..b89139cbab3 100644
--- a/homeassistant/components/jvc_projector/strings.json
+++ b/homeassistant/components/jvc_projector/strings.json
@@ -24,7 +24,6 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py
index d11fedac385..69e81bf292d 100644
--- a/homeassistant/components/keenetic_ndms2/config_flow.py
+++ b/homeassistant/components/keenetic_ndms2/config_flow.py
@@ -55,7 +55,7 @@ class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> KeeneticOptionsFlowHandler:
"""Get the options flow for this handler."""
- return KeeneticOptionsFlowHandler()
+ return KeeneticOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -138,8 +138,9 @@ class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN):
class KeeneticOptionsFlowHandler(OptionsFlow):
"""Handle options."""
- def __init__(self) -> None:
+ def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
+ self.config_entry = config_entry
self._interface_options: dict[str, str] = {}
async def async_step_init(
diff --git a/homeassistant/components/kitchen_sink/config_flow.py b/homeassistant/components/kitchen_sink/config_flow.py
index 019d1dddcad..8cff9321729 100644
--- a/homeassistant/components/kitchen_sink/config_flow.py
+++ b/homeassistant/components/kitchen_sink/config_flow.py
@@ -12,7 +12,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.core import callback
@@ -33,10 +33,13 @@ class KitchenSinkConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Set the config entry up from yaml."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
return self.async_create_entry(title="Kitchen Sink", data=import_data)
async def async_step_reauth(
@@ -54,7 +57,7 @@ class KitchenSinkConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="reauth_successful")
-class OptionsFlowHandler(OptionsFlow):
+class OptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle options."""
async def async_step_init(
@@ -68,7 +71,8 @@ class OptionsFlowHandler(OptionsFlow):
) -> ConfigFlowResult:
"""Manage the options."""
if user_input is not None:
- return self.async_create_entry(data=self.config_entry.options | user_input)
+ self.options.update(user_input)
+ return await self._update_options()
return self.async_show_form(
step_id="options_1",
@@ -94,3 +98,7 @@ class OptionsFlowHandler(OptionsFlow):
}
),
)
+
+ async def _update_options(self) -> ConfigFlowResult:
+ """Update config entry options."""
+ return self.async_create_entry(title="", data=self.options)
diff --git a/homeassistant/components/kitchen_sink/manifest.json b/homeassistant/components/kitchen_sink/manifest.json
index ae2462afbbd..e2f9468f7e0 100644
--- a/homeassistant/components/kitchen_sink/manifest.json
+++ b/homeassistant/components/kitchen_sink/manifest.json
@@ -5,6 +5,5 @@
"codeowners": ["@home-assistant/core"],
"documentation": "https://www.home-assistant.io/integrations/kitchen_sink",
"iot_class": "calculated",
- "quality_scale": "internal",
- "single_config_entry": true
+ "quality_scale": "internal"
}
diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py
index edec0b32af2..5f93de3c60e 100644
--- a/homeassistant/components/kmtronic/__init__.py
+++ b/homeassistant/components/kmtronic/__init__.py
@@ -44,7 +44,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"{MANUFACTURER} {hub.name}",
update_method=async_update_data,
update_interval=timedelta(seconds=30),
diff --git a/homeassistant/components/kmtronic/config_flow.py b/homeassistant/components/kmtronic/config_flow.py
index 56b1d4675bc..6bf0b878f72 100644
--- a/homeassistant/components/kmtronic/config_flow.py
+++ b/homeassistant/components/kmtronic/config_flow.py
@@ -66,7 +66,7 @@ class KmtronicConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> KMTronicOptionsFlow:
"""Get the options flow for this handler."""
- return KMTronicOptionsFlow()
+ return KMTronicOptionsFlow(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -102,6 +102,10 @@ class InvalidAuth(HomeAssistantError):
class KMTronicOptionsFlow(OptionsFlow):
"""Handle options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/knocki/strings.json b/homeassistant/components/knocki/strings.json
index 8e6fb722281..8f5d0161166 100644
--- a/homeassistant/components/knocki/strings.json
+++ b/homeassistant/components/knocki/strings.json
@@ -10,7 +10,6 @@
},
"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%]"
},
"abort": {
diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py
index fe6f3ad8892..736c5f6cb9d 100644
--- a/homeassistant/components/knx/__init__.py
+++ b/homeassistant/components/knx/__init__.py
@@ -29,6 +29,7 @@ from homeassistant.const import (
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.reload import async_integration_yaml_config
@@ -192,6 +193,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
},
)
+ # set up notify service for backwards compatibility - remove 2024.11
+ if NotifySchema.PLATFORM in config:
+ hass.async_create_task(
+ discovery.async_load_platform(
+ hass, Platform.NOTIFY, DOMAIN, {}, hass.data[DATA_HASS_CONFIG]
+ )
+ )
+
await register_panel(hass)
return True
diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py
index 0e0da4d5c0c..879e1421bd4 100644
--- a/homeassistant/components/knx/climate.py
+++ b/homeassistant/components/knx/climate.py
@@ -136,9 +136,6 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
ClimateSchema.CONF_FAN_SPEED_STATE_ADDRESS
),
fan_speed_mode=config[ClimateSchema.CONF_FAN_SPEED_MODE],
- group_address_humidity_state=config.get(
- ClimateSchema.CONF_HUMIDITY_STATE_ADDRESS
- ),
)
@@ -400,11 +397,6 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
await self._device.set_fan_speed(self._fan_modes_percentages[fan_mode_index])
- @property
- def current_humidity(self) -> float | None:
- """Return the current humidity."""
- return self._device.humidity.value
-
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return device specific state attributes."""
diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py
index feeb7626577..4a71c600824 100644
--- a/homeassistant/components/knx/config_flow.py
+++ b/homeassistant/components/knx/config_flow.py
@@ -770,6 +770,7 @@ class KNXOptionsFlow(KNXCommonFlow, OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize KNX options flow."""
+ self.config_entry = config_entry
super().__init__(initial_data=config_entry.data) # type: ignore[arg-type]
@callback
diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json
index df895282a2b..aa0178b2c4a 100644
--- a/homeassistant/components/knx/manifest.json
+++ b/homeassistant/components/knx/manifest.json
@@ -11,8 +11,8 @@
"loggers": ["xknx", "xknxproject"],
"quality_scale": "platinum",
"requirements": [
- "xknx==3.3.0",
- "xknxproject==3.8.1",
+ "xknx==3.2.0",
+ "xknxproject==3.8.0",
"knx-frontend==2024.9.10.221729"
],
"single_config_entry": true
diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py
index 245de2e937e..46abbaa1454 100644
--- a/homeassistant/components/knx/notify.py
+++ b/homeassistant/components/knx/notify.py
@@ -2,21 +2,86 @@
from __future__ import annotations
+from typing import Any
+
from xknx import XKNX
from xknx.devices import Notification as XknxNotification
from homeassistant import config_entries
-from homeassistant.components.notify import NotifyEntity
+from homeassistant.components.notify import (
+ BaseNotificationService,
+ NotifyEntity,
+ migrate_notify_issue,
+)
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import ConfigType
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import KNXModule
-from .const import KNX_ADDRESS, KNX_MODULE_KEY
+from .const import DOMAIN, KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
+async def async_get_service(
+ hass: HomeAssistant,
+ config: ConfigType,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> KNXNotificationService | None:
+ """Get the KNX notification service."""
+ if discovery_info is None:
+ return None
+
+ knx_module = hass.data[KNX_MODULE_KEY]
+ if platform_config := knx_module.config_yaml.get(Platform.NOTIFY):
+ xknx: XKNX = hass.data[KNX_MODULE_KEY].xknx
+
+ notification_devices = [
+ _create_notification_instance(xknx, device_config)
+ for device_config in platform_config
+ ]
+ return KNXNotificationService(notification_devices)
+
+ return None
+
+
+class KNXNotificationService(BaseNotificationService):
+ """Implement notification service."""
+
+ def __init__(self, devices: list[XknxNotification]) -> None:
+ """Initialize the service."""
+ self.devices = devices
+
+ @property
+ def targets(self) -> dict[str, str]:
+ """Return a dictionary of registered targets."""
+ ret = {}
+ for device in self.devices:
+ ret[device.name] = device.name
+ return ret
+
+ async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
+ """Send a notification to knx bus."""
+ migrate_notify_issue(
+ self.hass, DOMAIN, "KNX", "2024.11.0", service_name=self._service_name
+ )
+ if "target" in kwargs:
+ await self._async_send_to_device(message, kwargs["target"])
+ else:
+ await self._async_send_to_all_devices(message)
+
+ async def _async_send_to_all_devices(self, message: str) -> None:
+ """Send a notification to knx bus to all connected devices."""
+ for device in self.devices:
+ await device.set(message)
+
+ async def _async_send_to_device(self, message: str, names: str) -> None:
+ """Send a notification to knx bus to device with given names."""
+ for device in self.devices:
+ if device.name in names:
+ await device.set(message)
+
+
async def async_setup_entry(
hass: HomeAssistant,
config_entry: config_entries.ConfigEntry,
diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py
index bf2fc55e5c9..cc65a399da7 100644
--- a/homeassistant/components/knx/schema.py
+++ b/homeassistant/components/knx/schema.py
@@ -347,7 +347,6 @@ class ClimateSchema(KNXPlatformSchema):
CONF_FAN_MAX_STEP = "fan_max_step"
CONF_FAN_SPEED_MODE = "fan_speed_mode"
CONF_FAN_ZERO_MODE = "fan_zero_mode"
- CONF_HUMIDITY_STATE_ADDRESS = "humidity_state_address"
DEFAULT_NAME = "KNX Climate"
DEFAULT_SETPOINT_SHIFT_MODE = "DPT6010"
@@ -440,7 +439,6 @@ class ClimateSchema(KNXPlatformSchema):
vol.Optional(CONF_FAN_ZERO_MODE, default=FAN_OFF): vol.Coerce(
FanZeroMode
),
- vol.Optional(CONF_HUMIDITY_STATE_ADDRESS): ga_list_validator,
}
),
)
diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py
index 65dd7cf39b3..3f1ef99c6fb 100644
--- a/homeassistant/components/konnected/config_flow.py
+++ b/homeassistant/components/konnected/config_flow.py
@@ -402,10 +402,9 @@ class OptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
- self.model = config_entry.data[CONF_MODEL]
- self.current_opt = (
- config_entry.options or config_entry.data[CONF_DEFAULT_OPTIONS]
- )
+ self.entry = config_entry
+ self.model = self.entry.data[CONF_MODEL]
+ self.current_opt = self.entry.options or self.entry.data[CONF_DEFAULT_OPTIONS]
# as config proceeds we'll build up new options and then replace what's in the config entry
self.new_opt: dict[str, Any] = {CONF_IO: {}}
@@ -476,7 +475,7 @@ class OptionsFlowHandler(OptionsFlow):
),
description_placeholders={
"model": KONN_PANEL_MODEL_NAMES[self.model],
- "host": self.config_entry.data[CONF_HOST],
+ "host": self.entry.data[CONF_HOST],
},
errors=errors,
)
@@ -512,7 +511,7 @@ class OptionsFlowHandler(OptionsFlow):
),
description_placeholders={
"model": KONN_PANEL_MODEL_NAMES[self.model],
- "host": self.config_entry.data[CONF_HOST],
+ "host": self.entry.data[CONF_HOST],
},
errors=errors,
)
@@ -572,7 +571,7 @@ class OptionsFlowHandler(OptionsFlow):
),
description_placeholders={
"model": KONN_PANEL_MODEL_NAMES[self.model],
- "host": self.config_entry.data[CONF_HOST],
+ "host": self.entry.data[CONF_HOST],
},
errors=errors,
)
diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py
index 67de34f2fce..fbbfb03fb3e 100644
--- a/homeassistant/components/kostal_plenticore/sensor.py
+++ b/homeassistant/components/kostal_plenticore/sensor.py
@@ -17,7 +17,6 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PERCENTAGE,
- EntityCategory,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
@@ -748,15 +747,6 @@ SENSOR_PROCESS_DATA = [
state_class=SensorStateClass.TOTAL_INCREASING,
formatter="format_energy",
),
- PlenticoreSensorEntityDescription(
- module_id="scb:event",
- key="Event:ActiveErrorCnt",
- name="Active Alarms",
- entity_category=EntityCategory.DIAGNOSTIC,
- entity_registry_enabled_default=False,
- icon="mdi:alert",
- formatter="format_round",
- ),
PlenticoreSensorEntityDescription(
module_id="_virt_",
key="pv_P",
diff --git a/homeassistant/components/kraken/config_flow.py b/homeassistant/components/kraken/config_flow.py
index 54a817f0a50..67778515273 100644
--- a/homeassistant/components/kraken/config_flow.py
+++ b/homeassistant/components/kraken/config_flow.py
@@ -33,7 +33,7 @@ class KrakenConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> KrakenOptionsFlowHandler:
"""Get the options flow for this handler."""
- return KrakenOptionsFlowHandler()
+ return KrakenOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -53,6 +53,10 @@ class KrakenConfigFlow(ConfigFlow, domain=DOMAIN):
class KrakenOptionsFlowHandler(OptionsFlow):
"""Handle Kraken client options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize Kraken options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/kraken/manifest.json b/homeassistant/components/kraken/manifest.json
index fed16a673b5..98347f7681b 100644
--- a/homeassistant/components/kraken/manifest.json
+++ b/homeassistant/components/kraken/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/kraken",
"iot_class": "cloud_polling",
"loggers": ["krakenex", "pykrakenapi"],
- "requirements": ["krakenex==2.2.2", "pykrakenapi==0.1.8"]
+ "requirements": ["krakenex==2.1.0", "pykrakenapi==0.1.8"]
}
diff --git a/homeassistant/components/lacrosse_view/config_flow.py b/homeassistant/components/lacrosse_view/config_flow.py
index ecf30f9a197..5a3fe4a03ca 100644
--- a/homeassistant/components/lacrosse_view/config_flow.py
+++ b/homeassistant/components/lacrosse_view/config_flow.py
@@ -9,7 +9,7 @@ from typing import Any
from lacrosse_view import LaCrosse, Location, LoginError
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -54,6 +54,7 @@ class LaCrosseViewConfigFlow(ConfigFlow, domain=DOMAIN):
"""Initialize the config flow."""
self.data: dict[str, str] = {}
self.locations: list[Location] = []
+ self._reauth_entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -82,10 +83,12 @@ class LaCrosseViewConfigFlow(ConfigFlow, domain=DOMAIN):
self.locations = info
# Check if we are reauthenticating
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=self.data
+ if self._reauth_entry is not None:
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry, data=self._reauth_entry.data | self.data
)
+ await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
_LOGGER.debug("Moving on to location step")
return await self.async_step_location()
@@ -136,6 +139,9 @@ class LaCrosseViewConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Reauth in case of a password change or other error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_user()
diff --git a/homeassistant/components/lacrosse_view/manifest.json b/homeassistant/components/lacrosse_view/manifest.json
index 453a0855229..1cf8794237d 100644
--- a/homeassistant/components/lacrosse_view/manifest.json
+++ b/homeassistant/components/lacrosse_view/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/lacrosse_view",
"iot_class": "cloud_polling",
"loggers": ["lacrosse_view"],
- "requirements": ["lacrosse-view==1.0.3"]
+ "requirements": ["lacrosse-view==1.0.2"]
}
diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py
index da513bc8cff..8df7a2f5d0e 100644
--- a/homeassistant/components/lamarzocco/__init__.py
+++ b/homeassistant/components/lamarzocco/__init__.py
@@ -2,12 +2,12 @@
import logging
+from lmcloud.client_bluetooth import LaMarzoccoBluetoothClient
+from lmcloud.client_cloud import LaMarzoccoCloudClient
+from lmcloud.client_local import LaMarzoccoLocalClient
+from lmcloud.const import BT_MODEL_PREFIXES, FirmwareType
+from lmcloud.exceptions import AuthFail, RequestNotSuccessful
from packaging import version
-from pylamarzocco.client_bluetooth import LaMarzoccoBluetoothClient
-from pylamarzocco.client_cloud import LaMarzoccoCloudClient
-from pylamarzocco.client_local import LaMarzoccoLocalClient
-from pylamarzocco.const import BT_MODEL_PREFIXES, FirmwareType
-from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from homeassistant.components.bluetooth import async_discovered_service_info
from homeassistant.config_entries import ConfigEntry
@@ -26,7 +26,7 @@ from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.httpx_client import get_async_client
from .const import CONF_USE_BLUETOOTH, DOMAIN
-from .coordinator import LaMarzoccoConfigEntry, LaMarzoccoUpdateCoordinator
+from .coordinator import LaMarzoccoUpdateCoordinator
PLATFORMS = [
Platform.BINARY_SENSOR,
@@ -41,6 +41,8 @@ PLATFORMS = [
_LOGGER = logging.getLogger(__name__)
+type LaMarzoccoConfigEntry = ConfigEntry[LaMarzoccoUpdateCoordinator]
+
async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -> bool:
"""Set up La Marzocco as config entry."""
@@ -101,7 +103,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
coordinator = LaMarzoccoUpdateCoordinator(
hass=hass,
- entry=entry,
local_client=local_client,
cloud_client=cloud_client,
bluetooth_client=bluetooth_client,
diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py
index 444e4d0723b..81ac3672a0f 100644
--- a/homeassistant/components/lamarzocco/binary_sensor.py
+++ b/homeassistant/components/lamarzocco/binary_sensor.py
@@ -3,7 +3,7 @@
from collections.abc import Callable
from dataclasses import dataclass
-from pylamarzocco.models import LaMarzoccoMachineConfig
+from lmcloud.models import LaMarzoccoMachineConfig
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -14,7 +14,7 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from .coordinator import LaMarzoccoConfigEntry
+from . import LaMarzoccoConfigEntry
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
diff --git a/homeassistant/components/lamarzocco/button.py b/homeassistant/components/lamarzocco/button.py
index ae79e21897f..56fcca98cb3 100644
--- a/homeassistant/components/lamarzocco/button.py
+++ b/homeassistant/components/lamarzocco/button.py
@@ -1,23 +1,21 @@
"""Button platform for La Marzocco espresso machines."""
-import asyncio
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
-from pylamarzocco.exceptions import RequestNotSuccessful
+from lmcloud.exceptions import RequestNotSuccessful
+from lmcloud.lm_machine import LaMarzoccoMachine
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from . import LaMarzoccoConfigEntry
from .const import DOMAIN
-from .coordinator import LaMarzoccoConfigEntry, LaMarzoccoUpdateCoordinator
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
-BACKFLUSH_ENABLED_DURATION = 15
-
@dataclass(frozen=True, kw_only=True)
class LaMarzoccoButtonEntityDescription(
@@ -26,25 +24,14 @@ class LaMarzoccoButtonEntityDescription(
):
"""Description of a La Marzocco button."""
- press_fn: Callable[[LaMarzoccoUpdateCoordinator], Coroutine[Any, Any, None]]
-
-
-async def async_backflush_and_update(coordinator: LaMarzoccoUpdateCoordinator) -> None:
- """Press backflush button."""
- await coordinator.device.start_backflush()
- # lib will set state optimistically
- coordinator.async_set_updated_data(None)
- # backflush is enabled for 15 seconds
- # then turns off automatically
- await asyncio.sleep(BACKFLUSH_ENABLED_DURATION + 1)
- await coordinator.async_request_refresh()
+ press_fn: Callable[[LaMarzoccoMachine], Coroutine[Any, Any, None]]
ENTITIES: tuple[LaMarzoccoButtonEntityDescription, ...] = (
LaMarzoccoButtonEntityDescription(
key="start_backflush",
translation_key="start_backflush",
- press_fn=async_backflush_and_update,
+ press_fn=lambda machine: machine.start_backflush(),
),
)
@@ -72,7 +59,7 @@ class LaMarzoccoButtonEntity(LaMarzoccoEntity, ButtonEntity):
async def async_press(self) -> None:
"""Press button."""
try:
- await self.entity_description.press_fn(self.coordinator)
+ await self.entity_description.press_fn(self.coordinator.device)
except RequestNotSuccessful as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
@@ -81,3 +68,4 @@ class LaMarzoccoButtonEntity(LaMarzoccoEntity, ButtonEntity):
"key": self.entity_description.key,
},
) from exc
+ await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/lamarzocco/calendar.py b/homeassistant/components/lamarzocco/calendar.py
index 0ec9b55a9a1..8b3240ff7a1 100644
--- a/homeassistant/components/lamarzocco/calendar.py
+++ b/homeassistant/components/lamarzocco/calendar.py
@@ -3,14 +3,15 @@
from collections.abc import Iterator
from datetime import datetime, timedelta
-from pylamarzocco.models import LaMarzoccoWakeUpSleepEntry
+from lmcloud.models import LaMarzoccoWakeUpSleepEntry
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
-from .coordinator import LaMarzoccoConfigEntry, LaMarzoccoUpdateCoordinator
+from . import LaMarzoccoConfigEntry
+from .coordinator import LaMarzoccoUpdateCoordinator
from .entity import LaMarzoccoBaseEntity
CALENDAR_KEY = "auto_on_off_schedule"
diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py
index 04e705edbdc..0c359a53631 100644
--- a/homeassistant/components/lamarzocco/config_flow.py
+++ b/homeassistant/components/lamarzocco/config_flow.py
@@ -1,22 +1,19 @@
"""Config flow for La Marzocco integration."""
-from __future__ import annotations
-
from collections.abc import Mapping
import logging
from typing import Any
-from pylamarzocco.client_cloud import LaMarzoccoCloudClient
-from pylamarzocco.client_local import LaMarzoccoLocalClient
-from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
-from pylamarzocco.models import LaMarzoccoDeviceInfo
+from lmcloud.client_cloud import LaMarzoccoCloudClient
+from lmcloud.client_local import LaMarzoccoLocalClient
+from lmcloud.exceptions import AuthFail, RequestNotSuccessful
+from lmcloud.models import LaMarzoccoDeviceInfo
import voluptuous as vol
from homeassistant.components.bluetooth import (
BluetoothServiceInfo,
async_discovered_service_info,
)
-from homeassistant.components.dhcp import DhcpServiceInfo
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
@@ -24,6 +21,7 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import (
CONF_HOST,
@@ -105,15 +103,6 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "machine_not_found"
else:
self._config = data
- # if DHCP discovery was used, auto fill machine selection
- if CONF_HOST in self._discovered:
- return await self.async_step_machine_selection(
- user_input={
- CONF_HOST: self._discovered[CONF_HOST],
- CONF_MACHINE: self._discovered[CONF_MACHINE],
- }
- )
- # if Bluetooth discovery was used, only select host
return self.async_show_form(
step_id="machine_selection",
data_schema=vol.Schema(
@@ -269,27 +258,6 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
return await self.async_step_user()
- async def async_step_dhcp(
- self, discovery_info: DhcpServiceInfo
- ) -> ConfigFlowResult:
- """Handle discovery via dhcp."""
-
- serial = discovery_info.hostname.upper()
-
- await self.async_set_unique_id(serial)
- self._abort_if_unique_id_configured()
-
- _LOGGER.debug(
- "Discovered La Marzocco machine %s through DHCP at address %s",
- discovery_info.hostname,
- discovery_info.ip,
- )
-
- self._discovered[CONF_MACHINE] = serial
- self._discovered[CONF_HOST] = discovery_info.ip
-
- return await self.async_step_user()
-
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
@@ -316,10 +284,16 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Perform reconfiguration of the config entry."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Confirm reconfiguration of the device."""
if not user_input:
reconfigure_entry = self._get_reconfigure_entry()
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=vol.Schema(
{
vol.Required(
@@ -340,12 +314,12 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
- ) -> LmOptionsFlowHandler:
+ ) -> OptionsFlow:
"""Create the options flow."""
- return LmOptionsFlowHandler()
+ return LmOptionsFlowHandler(config_entry)
-class LmOptionsFlowHandler(OptionsFlow):
+class LmOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handles options flow for the component."""
async def async_step_init(
@@ -359,7 +333,7 @@ class LmOptionsFlowHandler(OptionsFlow):
{
vol.Optional(
CONF_USE_BLUETOOTH,
- default=self.config_entry.options.get(CONF_USE_BLUETOOTH, True),
+ default=self.options.get(CONF_USE_BLUETOOTH, True),
): cv.boolean,
}
)
diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py
index 05fee98c599..f255276b192 100644
--- a/homeassistant/components/lamarzocco/coordinator.py
+++ b/homeassistant/components/lamarzocco/coordinator.py
@@ -1,18 +1,16 @@
"""Coordinator for La Marzocco API."""
-from __future__ import annotations
-
from collections.abc import Callable, Coroutine
from datetime import timedelta
import logging
from time import time
from typing import Any
-from pylamarzocco.client_bluetooth import LaMarzoccoBluetoothClient
-from pylamarzocco.client_cloud import LaMarzoccoCloudClient
-from pylamarzocco.client_local import LaMarzoccoLocalClient
-from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
-from pylamarzocco.lm_machine import LaMarzoccoMachine
+from lmcloud.client_bluetooth import LaMarzoccoBluetoothClient
+from lmcloud.client_cloud import LaMarzoccoCloudClient
+from lmcloud.client_local import LaMarzoccoLocalClient
+from lmcloud.exceptions import AuthFail, RequestNotSuccessful
+from lmcloud.lm_machine import LaMarzoccoMachine
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODEL, CONF_NAME, EVENT_HOMEASSISTANT_STOP
@@ -28,30 +26,21 @@ STATISTICS_UPDATE_INTERVAL = 300
_LOGGER = logging.getLogger(__name__)
-type LaMarzoccoConfigEntry = ConfigEntry[LaMarzoccoUpdateCoordinator]
-
class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
"""Class to handle fetching data from the La Marzocco API centrally."""
- config_entry: LaMarzoccoConfigEntry
+ config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
- entry: LaMarzoccoConfigEntry,
cloud_client: LaMarzoccoCloudClient,
local_client: LaMarzoccoLocalClient | None,
bluetooth_client: LaMarzoccoBluetoothClient | None,
) -> None:
"""Initialize coordinator."""
- super().__init__(
- hass,
- _LOGGER,
- config_entry=entry,
- name=DOMAIN,
- update_interval=SCAN_INTERVAL,
- )
+ super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)
self.local_connection_configured = local_client is not None
assert self.config_entry.unique_id
diff --git a/homeassistant/components/lamarzocco/diagnostics.py b/homeassistant/components/lamarzocco/diagnostics.py
index 43ae51ee192..4293fdca615 100644
--- a/homeassistant/components/lamarzocco/diagnostics.py
+++ b/homeassistant/components/lamarzocco/diagnostics.py
@@ -5,12 +5,12 @@ from __future__ import annotations
from dataclasses import asdict
from typing import Any, TypedDict
-from pylamarzocco.const import FirmwareType
+from lmcloud.const import FirmwareType
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
-from .coordinator import LaMarzoccoConfigEntry
+from . import LaMarzoccoConfigEntry
TO_REDACT = {
"serial_number",
diff --git a/homeassistant/components/lamarzocco/entity.py b/homeassistant/components/lamarzocco/entity.py
index 1ea84302a17..9cc2ce8ef6b 100644
--- a/homeassistant/components/lamarzocco/entity.py
+++ b/homeassistant/components/lamarzocco/entity.py
@@ -3,8 +3,8 @@
from collections.abc import Callable
from dataclasses import dataclass
-from pylamarzocco.const import FirmwareType
-from pylamarzocco.lm_machine import LaMarzoccoMachine
+from lmcloud.const import FirmwareType
+from lmcloud.lm_machine import LaMarzoccoMachine
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
@@ -43,7 +43,6 @@ class LaMarzoccoBaseEntity(
name=device.name,
manufacturer="La Marzocco",
model=device.full_model_name,
- model_id=device.model,
serial_number=device.serial_number,
sw_version=device.firmware[FirmwareType.MACHINE].current_version,
)
diff --git a/homeassistant/components/lamarzocco/icons.json b/homeassistant/components/lamarzocco/icons.json
index 860da12ddd9..bc7d621d91d 100644
--- a/homeassistant/components/lamarzocco/icons.json
+++ b/homeassistant/components/lamarzocco/icons.json
@@ -43,9 +43,6 @@
"preinfusion_off": {
"default": "mdi:water"
},
- "smart_standby_time": {
- "default": "mdi:timer"
- },
"steam_temp": {
"default": "mdi:thermometer-water"
},
@@ -54,13 +51,6 @@
}
},
"select": {
- "smart_standby_mode": {
- "default": "mdi:power",
- "state": {
- "poweron": "mdi:power",
- "lastbrewing": "mdi:coffee"
- }
- },
"steam_temp_select": {
"default": "mdi:thermometer",
"state": {
@@ -110,12 +100,6 @@
"off": "mdi:alarm-off"
}
},
- "smart_standby_enabled": {
- "state": {
- "on": "mdi:sleep",
- "off": "mdi:sleep-off"
- }
- },
"steam_boiler": {
"default": "mdi:water-boiler",
"state": {
diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json
index 6b226051118..a1da8982cd8 100644
--- a/homeassistant/components/lamarzocco/manifest.json
+++ b/homeassistant/components/lamarzocco/manifest.json
@@ -18,20 +18,9 @@
"codeowners": ["@zweckj"],
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
- "dhcp": [
- {
- "hostname": "gs[0-9][0-9][0-9][0-9][0-9][0-9]"
- },
- {
- "hostname": "lm[0-9][0-9][0-9][0-9][0-9][0-9]"
- },
- {
- "hostname": "mr[0-9][0-9][0-9][0-9][0-9][0-9]"
- }
- ],
"documentation": "https://www.home-assistant.io/integrations/lamarzocco",
"integration_type": "device",
"iot_class": "cloud_polling",
- "loggers": ["pylamarzocco"],
- "requirements": ["pylamarzocco==1.2.3"]
+ "loggers": ["lmcloud"],
+ "requirements": ["lmcloud==1.2.3"]
}
diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py
index 825c5d6deb0..e607d856193 100644
--- a/homeassistant/components/lamarzocco/number.py
+++ b/homeassistant/components/lamarzocco/number.py
@@ -4,16 +4,16 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
-from pylamarzocco.const import (
+from lmcloud.const import (
KEYS_PER_MODEL,
BoilerType,
MachineModel,
PhysicalKey,
PrebrewMode,
)
-from pylamarzocco.exceptions import RequestNotSuccessful
-from pylamarzocco.lm_machine import LaMarzoccoMachine
-from pylamarzocco.models import LaMarzoccoMachineConfig
+from lmcloud.exceptions import RequestNotSuccessful
+from lmcloud.lm_machine import LaMarzoccoMachine
+from lmcloud.models import LaMarzoccoMachineConfig
from homeassistant.components.number import (
NumberDeviceClass,
@@ -31,8 +31,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from . import LaMarzoccoConfigEntry
from .const import DOMAIN
-from .coordinator import LaMarzoccoConfigEntry, LaMarzoccoUpdateCoordinator
+from .coordinator import LaMarzoccoUpdateCoordinator
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
@@ -108,22 +109,6 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
MachineModel.GS3_MP,
),
),
- LaMarzoccoNumberEntityDescription(
- key="smart_standby_time",
- translation_key="smart_standby_time",
- device_class=NumberDeviceClass.DURATION,
- native_unit_of_measurement=UnitOfTime.MINUTES,
- native_step=10,
- native_min_value=10,
- native_max_value=240,
- entity_category=EntityCategory.CONFIG,
- set_value_fn=lambda machine, value: machine.set_smart_standby(
- enabled=machine.config.smart_standby.enabled,
- mode=machine.config.smart_standby.mode,
- minutes=int(value),
- ),
- native_value_fn=lambda config: config.smart_standby.minutes,
- ),
)
diff --git a/homeassistant/components/lamarzocco/select.py b/homeassistant/components/lamarzocco/select.py
index 1889ba38d6b..7a410796285 100644
--- a/homeassistant/components/lamarzocco/select.py
+++ b/homeassistant/components/lamarzocco/select.py
@@ -4,10 +4,10 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
-from pylamarzocco.const import MachineModel, PrebrewMode, SmartStandbyMode, SteamLevel
-from pylamarzocco.exceptions import RequestNotSuccessful
-from pylamarzocco.lm_machine import LaMarzoccoMachine
-from pylamarzocco.models import LaMarzoccoMachineConfig
+from lmcloud.const import MachineModel, PrebrewMode, SteamLevel
+from lmcloud.exceptions import RequestNotSuccessful
+from lmcloud.lm_machine import LaMarzoccoMachine
+from lmcloud.models import LaMarzoccoMachineConfig
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
@@ -15,8 +15,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from . import LaMarzoccoConfigEntry
from .const import DOMAIN
-from .coordinator import LaMarzoccoConfigEntry
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
STEAM_LEVEL_HA_TO_LM = {
@@ -25,7 +25,11 @@ STEAM_LEVEL_HA_TO_LM = {
"3": SteamLevel.LEVEL_3,
}
-STEAM_LEVEL_LM_TO_HA = {value: key for key, value in STEAM_LEVEL_HA_TO_LM.items()}
+STEAM_LEVEL_LM_TO_HA = {
+ SteamLevel.LEVEL_1: "1",
+ SteamLevel.LEVEL_2: "2",
+ SteamLevel.LEVEL_3: "3",
+}
PREBREW_MODE_HA_TO_LM = {
"disabled": PrebrewMode.DISABLED,
@@ -33,15 +37,12 @@ PREBREW_MODE_HA_TO_LM = {
"preinfusion": PrebrewMode.PREINFUSION,
}
-PREBREW_MODE_LM_TO_HA = {value: key for key, value in PREBREW_MODE_HA_TO_LM.items()}
-
-STANDBY_MODE_HA_TO_LM = {
- "power_on": SmartStandbyMode.POWER_ON,
- "last_brewing": SmartStandbyMode.LAST_BREWING,
+PREBREW_MODE_LM_TO_HA = {
+ PrebrewMode.DISABLED: "disabled",
+ PrebrewMode.PREBREW: "prebrew",
+ PrebrewMode.PREINFUSION: "preinfusion",
}
-STANDBY_MODE_LM_TO_HA = {value: key for key, value in STANDBY_MODE_HA_TO_LM.items()}
-
@dataclass(frozen=True, kw_only=True)
class LaMarzoccoSelectEntityDescription(
@@ -82,20 +83,6 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
MachineModel.LINEA_MINI,
),
),
- LaMarzoccoSelectEntityDescription(
- key="smart_standby_mode",
- translation_key="smart_standby_mode",
- entity_category=EntityCategory.CONFIG,
- options=["power_on", "last_brewing"],
- select_option_fn=lambda machine, option: machine.set_smart_standby(
- enabled=machine.config.smart_standby.enabled,
- mode=STANDBY_MODE_HA_TO_LM[option],
- minutes=machine.config.smart_standby.minutes,
- ),
- current_option_fn=lambda config: STANDBY_MODE_LM_TO_HA[
- config.smart_standby.mode
- ],
- ),
)
diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py
index 04b095e798c..225f0a43c5c 100644
--- a/homeassistant/components/lamarzocco/sensor.py
+++ b/homeassistant/components/lamarzocco/sensor.py
@@ -3,8 +3,8 @@
from collections.abc import Callable
from dataclasses import dataclass
-from pylamarzocco.const import BoilerType, MachineModel, PhysicalKey
-from pylamarzocco.lm_machine import LaMarzoccoMachine
+from lmcloud.const import BoilerType, MachineModel, PhysicalKey
+from lmcloud.lm_machine import LaMarzoccoMachine
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -16,7 +16,7 @@ from homeassistant.const import EntityCategory, UnitOfTemperature, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from .coordinator import LaMarzoccoConfigEntry
+from . import LaMarzoccoConfigEntry
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json
index 959dda265a9..71b13e2b789 100644
--- a/homeassistant/components/lamarzocco/strings.json
+++ b/homeassistant/components/lamarzocco/strings.json
@@ -8,7 +8,6 @@
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "machine_not_found": "Discovered machine not found in given account",
"no_machines": "No machines found in account",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
@@ -48,7 +47,7 @@
"password": "[%key:component::lamarzocco::config::step::user::data_description::password%]"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
@@ -117,9 +116,6 @@
"preinfusion_off_key": {
"name": "Preinfusion time Key {key}"
},
- "smart_standby_time": {
- "name": "Smart standby time"
- },
"steam_temp": {
"name": "Steam target temperature"
},
@@ -136,13 +132,6 @@
"preinfusion": "Preinfusion"
}
},
- "smart_standby_mode": {
- "name": "Smart standby mode",
- "state": {
- "last_brewing": "Last brewing",
- "power_on": "Power on"
- }
- },
"steam_temp_select": {
"name": "Steam level",
"state": {
@@ -173,9 +162,6 @@
"auto_on_off": {
"name": "Auto on/off ({id})"
},
- "smart_standby_enabled": {
- "name": "Smart standby enabled"
- },
"steam_boiler": {
"name": "Steam boiler"
}
diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py
index f7690885f05..dda0f0f1d58 100644
--- a/homeassistant/components/lamarzocco/switch.py
+++ b/homeassistant/components/lamarzocco/switch.py
@@ -4,10 +4,10 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
-from pylamarzocco.const import BoilerType
-from pylamarzocco.exceptions import RequestNotSuccessful
-from pylamarzocco.lm_machine import LaMarzoccoMachine
-from pylamarzocco.models import LaMarzoccoMachineConfig
+from lmcloud.const import BoilerType
+from lmcloud.exceptions import RequestNotSuccessful
+from lmcloud.lm_machine import LaMarzoccoMachine
+from lmcloud.models import LaMarzoccoMachineConfig
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
@@ -15,8 +15,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from . import LaMarzoccoConfigEntry
from .const import DOMAIN
-from .coordinator import LaMarzoccoConfigEntry, LaMarzoccoUpdateCoordinator
+from .coordinator import LaMarzoccoUpdateCoordinator
from .entity import LaMarzoccoBaseEntity, LaMarzoccoEntity, LaMarzoccoEntityDescription
@@ -45,17 +46,6 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = (
control_fn=lambda machine, state: machine.set_steam(state),
is_on_fn=lambda config: config.boilers[BoilerType.STEAM].enabled,
),
- LaMarzoccoSwitchEntityDescription(
- key="smart_standby_enabled",
- translation_key="smart_standby_enabled",
- entity_category=EntityCategory.CONFIG,
- control_fn=lambda machine, state: machine.set_smart_standby(
- enabled=state,
- mode=machine.config.smart_standby.mode,
- minutes=machine.config.smart_standby.minutes,
- ),
- is_on_fn=lambda config: config.smart_standby.enabled,
- ),
)
diff --git a/homeassistant/components/lamarzocco/update.py b/homeassistant/components/lamarzocco/update.py
index 371ff679bae..0bf8ea3264f 100644
--- a/homeassistant/components/lamarzocco/update.py
+++ b/homeassistant/components/lamarzocco/update.py
@@ -3,8 +3,8 @@
from dataclasses import dataclass
from typing import Any
-from pylamarzocco.const import FirmwareType
-from pylamarzocco.exceptions import RequestNotSuccessful
+from lmcloud.const import FirmwareType
+from lmcloud.exceptions import RequestNotSuccessful
from homeassistant.components.update import (
UpdateDeviceClass,
@@ -17,8 +17,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from . import LaMarzoccoConfigEntry
from .const import DOMAIN
-from .coordinator import LaMarzoccoConfigEntry
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py
index 36dcdf26ed6..8dbd5279bc6 100644
--- a/homeassistant/components/lametric/config_flow.py
+++ b/homeassistant/components/lametric/config_flow.py
@@ -29,7 +29,7 @@ from homeassistant.components.ssdp import (
ATTR_UPNP_SERIAL,
SsdpServiceInfo,
)
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -59,6 +59,7 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
discovered_host: str
discovered_serial: str
discovered: bool = False
+ reauth_entry: ConfigEntry | None = None
@property
def logger(self) -> logging.Logger:
@@ -112,6 +113,9 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle initiation of re-authentication with LaMetric."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_choice_enter_manual_or_fetch_cloud()
async def async_step_choice_enter_manual_or_fetch_cloud(
@@ -134,8 +138,8 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
if user_input is not None:
if self.discovered:
host = self.discovered_host
- elif self.source == SOURCE_REAUTH:
- host = self._get_reauth_entry().data[CONF_HOST]
+ elif self.reauth_entry:
+ host = self.reauth_entry.data[CONF_HOST]
else:
host = user_input[CONF_HOST]
@@ -158,7 +162,7 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
TextSelectorConfig(type=TextSelectorType.PASSWORD)
)
}
- if not self.discovered and self.source != SOURCE_REAUTH:
+ if not self.discovered and not self.reauth_entry:
schema = {vol.Required(CONF_HOST): TextSelector()} | schema
return self.async_show_form(
@@ -191,11 +195,10 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Handle device selection from devices offered by the cloud."""
if self.discovered:
user_input = {CONF_DEVICE: self.discovered_serial}
- elif self.source == SOURCE_REAUTH:
- reauth_unique_id = self._get_reauth_entry().unique_id
- if reauth_unique_id not in self.devices:
+ elif self.reauth_entry:
+ if self.reauth_entry.unique_id not in self.devices:
return self.async_abort(reason="reauth_device_not_found")
- user_input = {CONF_DEVICE: reauth_unique_id}
+ user_input = {CONF_DEVICE: self.reauth_entry.unique_id}
elif len(self.devices) == 1:
user_input = {CONF_DEVICE: list(self.devices.values())[0].serial_number}
@@ -248,7 +251,7 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
device = await lametric.device()
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
await self.async_set_unique_id(device.serial_number)
self._abort_if_unique_id_configured(
updates={CONF_HOST: lametric.host, CONF_API_KEY: lametric.api_key}
@@ -270,14 +273,19 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
)
)
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
- data_updates={
+ if self.reauth_entry:
+ self.hass.config_entries.async_update_entry(
+ self.reauth_entry,
+ data={
+ **self.reauth_entry.data,
CONF_HOST: lametric.host,
CONF_API_KEY: lametric.api_key,
},
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
return self.async_create_entry(
title=device.name,
diff --git a/homeassistant/components/landisgyr_heat_meter/strings.json b/homeassistant/components/landisgyr_heat_meter/strings.json
index 31f08ded79f..4bae2490006 100644
--- a/homeassistant/components/landisgyr_heat_meter/strings.json
+++ b/homeassistant/components/landisgyr_heat_meter/strings.json
@@ -12,9 +12,6 @@
}
}
},
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
- },
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
diff --git a/homeassistant/components/lastfm/config_flow.py b/homeassistant/components/lastfm/config_flow.py
index 0e1f680dd63..c6ea120242d 100644
--- a/homeassistant/components/lastfm/config_flow.py
+++ b/homeassistant/components/lastfm/config_flow.py
@@ -11,7 +11,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_API_KEY
from homeassistant.core import callback
@@ -80,7 +80,7 @@ class LastFmConfigFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> LastFmOptionsFlowHandler:
"""Get the options flow for this handler."""
- return LastFmOptionsFlowHandler()
+ return LastFmOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -155,7 +155,7 @@ class LastFmConfigFlowHandler(ConfigFlow, domain=DOMAIN):
)
-class LastFmOptionsFlowHandler(OptionsFlow):
+class LastFmOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""LastFm Options flow handler."""
async def async_step_init(
@@ -163,25 +163,24 @@ class LastFmOptionsFlowHandler(OptionsFlow):
) -> ConfigFlowResult:
"""Initialize form."""
errors: dict[str, str] = {}
- options = self.config_entry.options
if user_input is not None:
users, errors = validate_lastfm_users(
- options[CONF_API_KEY], user_input[CONF_USERS]
+ self.options[CONF_API_KEY], user_input[CONF_USERS]
)
user_input[CONF_USERS] = users
if not errors:
return self.async_create_entry(
title="LastFM",
data={
- **options,
+ **self.options,
CONF_USERS: user_input[CONF_USERS],
},
)
- if options[CONF_MAIN_USER]:
+ if self.options[CONF_MAIN_USER]:
try:
main_user, _ = get_lastfm_user(
- options[CONF_API_KEY],
- options[CONF_MAIN_USER],
+ self.options[CONF_API_KEY],
+ self.options[CONF_MAIN_USER],
)
friends_response = await self.hass.async_add_executor_job(
main_user.get_friends
@@ -207,6 +206,6 @@ class LastFmOptionsFlowHandler(OptionsFlow):
),
}
),
- user_input or options,
+ user_input or self.options,
),
)
diff --git a/homeassistant/components/launch_library/__init__.py b/homeassistant/components/launch_library/__init__.py
index 6bfd3bc9adf..66e7eb832fe 100644
--- a/homeassistant/components/launch_library/__init__.py
+++ b/homeassistant/components/launch_library/__init__.py
@@ -51,7 +51,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=DOMAIN,
update_method=async_update,
update_interval=timedelta(hours=1),
diff --git a/homeassistant/components/launch_library/config_flow.py b/homeassistant/components/launch_library/config_flow.py
index 37b80fbff8a..3cdff3650b3 100644
--- a/homeassistant/components/launch_library/config_flow.py
+++ b/homeassistant/components/launch_library/config_flow.py
@@ -18,6 +18,10 @@ class LaunchLibraryFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
+ # Check if already configured
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
if user_input is not None:
return self.async_create_entry(title="Launch Library", data=user_input)
diff --git a/homeassistant/components/launch_library/manifest.json b/homeassistant/components/launch_library/manifest.json
index 3258a9a34fb..00f11f95a44 100644
--- a/homeassistant/components/launch_library/manifest.json
+++ b/homeassistant/components/launch_library/manifest.json
@@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/launch_library",
"integration_type": "service",
"iot_class": "cloud_polling",
- "requirements": ["pylaunches==2.0.0"],
- "single_config_entry": true
+ "requirements": ["pylaunches==2.0.0"]
}
diff --git a/homeassistant/components/launch_library/strings.json b/homeassistant/components/launch_library/strings.json
index a587544f836..f3cca9fc581 100644
--- a/homeassistant/components/launch_library/strings.json
+++ b/homeassistant/components/launch_library/strings.json
@@ -4,6 +4,9 @@
"user": {
"description": "Do you want to configure the Launch Library?"
}
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"entity": {
diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py
index eb26ef48e4e..a8d75fe5635 100644
--- a/homeassistant/components/lcn/__init__.py
+++ b/homeassistant/components/lcn/__init__.py
@@ -8,28 +8,23 @@ import logging
import pypck
from pypck.connection import PchkConnectionManager
-from homeassistant.config_entries import ConfigEntry
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_DEVICE_ID,
- CONF_DOMAIN,
- CONF_ENTITIES,
CONF_IP_ADDRESS,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
- Platform,
)
from homeassistant.core import HomeAssistant
-from homeassistant.helpers import config_validation as cv, device_registry as dr
+from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.typing import ConfigType
from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_ACKNOWLEDGE,
CONF_DIM_MODE,
- CONF_DOMAIN_DATA,
CONF_SK_NUM_TRIES,
- CONF_TRANSITION,
CONNECTION,
DOMAIN,
PLATFORMS,
@@ -39,29 +34,40 @@ from .helpers import (
InputType,
async_update_config_entry,
generate_unique_id,
+ import_lcn_config,
register_lcn_address_devices,
register_lcn_host_device,
)
-from .services import register_services
+from .schemas import CONFIG_SCHEMA # noqa: F401
+from .services import SERVICES
from .websocket import register_panel_and_ws_api
_LOGGER = logging.getLogger(__name__)
-CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
-
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LCN component."""
- hass.data.setdefault(DOMAIN, {})
+ if DOMAIN not in config:
+ return True
- await register_services(hass)
- await register_panel_and_ws_api(hass)
+ # initialize a config_flow for all LCN configurations read from
+ # configuration.yaml
+ config_entries_data = import_lcn_config(config[DOMAIN])
+ for config_entry_data in config_entries_data:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config_entry_data,
+ )
+ )
return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up a connection to PCHK host from a config entry."""
+ hass.data.setdefault(DOMAIN, {})
if config_entry.entry_id in hass.data[DOMAIN]:
return False
@@ -121,6 +127,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
)
lcn_connection.register_for_inputs(input_received)
+ # register service calls
+ for service_name, service in SERVICES:
+ if not hass.services.has_service(DOMAIN, service_name):
+ hass.services.async_register(
+ DOMAIN, service_name, service(hass).async_call_service, service.schema
+ )
+
+ await register_panel_and_ws_api(hass)
+
return True
@@ -132,25 +147,15 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
config_entry.minor_version,
)
- new_data = {**config_entry.data}
-
if config_entry.version == 1:
- # update to 1.2 (add acknowledge flag)
+ new_data = {**config_entry.data}
+
if config_entry.minor_version < 2:
new_data[CONF_ACKNOWLEDGE] = False
- # update to 2.1 (fix transitions for lights and switches)
- new_entities_data = [*new_data[CONF_ENTITIES]]
- for entity in new_entities_data:
- if entity[CONF_DOMAIN] in [Platform.LIGHT, Platform.SCENE]:
- if entity[CONF_DOMAIN_DATA][CONF_TRANSITION] is None:
- entity[CONF_DOMAIN_DATA][CONF_TRANSITION] = 0
- entity[CONF_DOMAIN_DATA][CONF_TRANSITION] /= 1000.0
- new_data[CONF_ENTITIES] = new_entities_data
-
- hass.config_entries.async_update_entry(
- config_entry, data=new_data, minor_version=1, version=2
- )
+ hass.config_entries.async_update_entry(
+ config_entry, data=new_data, minor_version=2, version=1
+ )
_LOGGER.debug(
"Migration to configuration version %s.%s successful",
@@ -171,6 +176,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
host = hass.data[DOMAIN].pop(config_entry.entry_id)
await host[CONNECTION].async_close()
+ # unregister service calls
+ if unload_ok and not hass.data[DOMAIN]: # check if this is the last entry to unload
+ for service_name, _ in SERVICES:
+ hass.services.async_remove(DOMAIN, service_name)
+
return unload_ok
diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py
index d0ce4815f19..106e74fd060 100644
--- a/homeassistant/components/lcn/binary_sensor.py
+++ b/homeassistant/components/lcn/binary_sensor.py
@@ -5,21 +5,14 @@ from functools import partial
import pypck
-from homeassistant.components.automation import automations_with_entity
from homeassistant.components.binary_sensor import (
DOMAIN as DOMAIN_BINARY_SENSOR,
BinarySensorEntity,
)
-from homeassistant.components.script import scripts_with_entity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES, CONF_SOURCE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.issue_registry import (
- IssueSeverity,
- async_create_issue,
- async_delete_issue,
-)
from homeassistant.helpers.typing import ConfigType
from .const import (
@@ -90,28 +83,11 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity):
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
-
if not self.device_connection.is_group:
await self.device_connection.activate_status_request_handler(
self.setpoint_variable
)
- entity_automations = automations_with_entity(self.hass, self.entity_id)
- entity_scripts = scripts_with_entity(self.hass, self.entity_id)
- if entity_automations + entity_scripts:
- async_create_issue(
- self.hass,
- DOMAIN,
- f"deprecated_binary_sensor_{self.entity_id}",
- breaks_in_ha_version="2025.5.0",
- is_fixable=False,
- severity=IssueSeverity.WARNING,
- translation_key="deprecated_regulatorlock_sensor",
- translation_placeholders={
- "entity": f"{DOMAIN_BINARY_SENSOR}.{self.name.lower().replace(' ', '_')}",
- },
- )
-
async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
await super().async_will_remove_from_hass()
@@ -119,9 +95,6 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity):
await self.device_connection.cancel_status_request_handler(
self.setpoint_variable
)
- async_delete_issue(
- self.hass, DOMAIN, f"deprecated_binary_sensor_{self.entity_id}"
- )
def input_received(self, input_obj: InputType) -> None:
"""Set sensor value when LCN input object (command) is received."""
@@ -183,34 +156,14 @@ class LcnLockKeysSensor(LcnEntity, BinarySensorEntity):
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
-
if not self.device_connection.is_group:
await self.device_connection.activate_status_request_handler(self.source)
- entity_automations = automations_with_entity(self.hass, self.entity_id)
- entity_scripts = scripts_with_entity(self.hass, self.entity_id)
- if entity_automations + entity_scripts:
- async_create_issue(
- self.hass,
- DOMAIN,
- f"deprecated_binary_sensor_{self.entity_id}",
- breaks_in_ha_version="2025.5.0",
- is_fixable=False,
- severity=IssueSeverity.WARNING,
- translation_key="deprecated_keylock_sensor",
- translation_placeholders={
- "entity": f"{DOMAIN_BINARY_SENSOR}.{self.name.lower().replace(' ', '_')}",
- },
- )
-
async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
await super().async_will_remove_from_hass()
if not self.device_connection.is_group:
await self.device_connection.cancel_status_request_handler(self.source)
- async_delete_issue(
- self.hass, DOMAIN, f"deprecated_binary_sensor_{self.entity_id}"
- )
def input_received(self, input_obj: InputType) -> None:
"""Set sensor value when LCN input object (command) is received."""
diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py
index 008265e62ae..e8b462bd321 100644
--- a/homeassistant/components/lcn/config_flow.py
+++ b/homeassistant/components/lcn/config_flow.py
@@ -9,6 +9,7 @@ import pypck
import voluptuous as vol
from homeassistant import config_entries
+from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import (
CONF_BASE,
CONF_DEVICES,
@@ -19,12 +20,14 @@ from homeassistant.const import (
CONF_PORT,
CONF_USERNAME,
)
-from homeassistant.core import HomeAssistant
+from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType
from . import PchkConnectionManager
from .const import CONF_ACKNOWLEDGE, CONF_DIM_MODE, CONF_SK_NUM_TRIES, DIM_MODES, DOMAIN
+from .helpers import purge_device_registry, purge_entity_registry
_LOGGER = logging.getLogger(__name__)
@@ -107,8 +110,57 @@ async def validate_connection(data: ConfigType) -> str | None:
class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a LCN config flow."""
- VERSION = 2
- MINOR_VERSION = 1
+ VERSION = 1
+ MINOR_VERSION = 2
+
+ async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
+ """Import existing configuration from LCN."""
+ # validate the imported connection parameters
+ if error := await validate_connection(import_data):
+ async_create_issue(
+ self.hass,
+ DOMAIN,
+ error,
+ is_fixable=False,
+ issue_domain=DOMAIN,
+ severity=IssueSeverity.ERROR,
+ translation_key=error,
+ translation_placeholders={
+ "url": "/config/integrations/dashboard/add?domain=lcn"
+ },
+ )
+ return self.async_abort(reason=error)
+
+ async_create_issue(
+ self.hass,
+ HOMEASSISTANT_DOMAIN,
+ f"deprecated_yaml_{DOMAIN}",
+ breaks_in_ha_version="2024.12.0",
+ is_fixable=False,
+ is_persistent=False,
+ issue_domain=DOMAIN,
+ severity=IssueSeverity.WARNING,
+ translation_key="deprecated_yaml",
+ translation_placeholders={
+ "domain": DOMAIN,
+ "integration_title": "LCN",
+ },
+ )
+
+ # check if we already have a host with the same address configured
+ if entry := get_config_entry(self.hass, import_data):
+ entry.source = config_entries.SOURCE_IMPORT
+ # Cleanup entity and device registry, if we imported from configuration.yaml to
+ # remove orphans when entities were removed from configuration
+ purge_entity_registry(self.hass, entry.entry_id, import_data)
+ purge_device_registry(self.hass, entry.entry_id, import_data)
+
+ self.hass.config_entries.async_update_entry(entry, data=import_data)
+ return self.async_abort(reason="existing_configuration_updated")
+
+ return self.async_create_entry(
+ title=f"{import_data[CONF_HOST]}", data=import_data
+ )
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -142,6 +194,12 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> config_entries.ConfigFlowResult:
+ """Reconfigure LCN configuration."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Reconfigure LCN configuration."""
reconfigure_entry = self._get_reconfigure_entry()
@@ -161,7 +219,7 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await self.hass.config_entries.async_setup(reconfigure_entry.entry_id)
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=self.add_suggested_values_to_schema(
CONFIG_SCHEMA, reconfigure_entry.data
),
diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py
index 97aeeecd8b5..707d0f29ba3 100644
--- a/homeassistant/components/lcn/const.py
+++ b/homeassistant/components/lcn/const.py
@@ -42,7 +42,6 @@ CONF_LED = "led"
CONF_KEYS = "keys"
CONF_TIME = "time"
CONF_TIME_UNIT = "time_unit"
-CONF_LOCK_TIME = "lock_time"
CONF_TABLE = "table"
CONF_ROW = "row"
CONF_TEXT = "text"
diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py
index 6a9c63ea212..7da047682ac 100644
--- a/homeassistant/components/lcn/helpers.py
+++ b/homeassistant/components/lcn/helpers.py
@@ -9,6 +9,7 @@ import re
from typing import cast
import pypck
+import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -18,12 +19,17 @@ from homeassistant.const import (
CONF_DEVICES,
CONF_DOMAIN,
CONF_ENTITIES,
+ CONF_HOST,
+ CONF_IP_ADDRESS,
CONF_LIGHTS,
CONF_NAME,
+ CONF_PASSWORD,
+ CONF_PORT,
CONF_RESOURCE,
CONF_SENSORS,
CONF_SOURCE,
CONF_SWITCHES,
+ CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -31,13 +37,19 @@ from homeassistant.helpers.typing import ConfigType
from .const import (
BINSENSOR_PORTS,
+ CONF_ACKNOWLEDGE,
CONF_CLIMATES,
+ CONF_CONNECTIONS,
+ CONF_DIM_MODE,
+ CONF_DOMAIN_DATA,
CONF_HARDWARE_SERIAL,
CONF_HARDWARE_TYPE,
CONF_OUTPUT,
CONF_SCENES,
+ CONF_SK_NUM_TRIES,
CONF_SOFTWARE_SERIAL,
CONNECTION,
+ DEFAULT_NAME,
DOMAIN,
LED_PORTS,
LOGICOP_PORTS,
@@ -134,6 +146,110 @@ def generate_unique_id(
return unique_id
+def import_lcn_config(lcn_config: ConfigType) -> list[ConfigType]:
+ """Convert lcn settings from configuration.yaml to config_entries data.
+
+ Create a list of config_entry data structures like:
+
+ "data": {
+ "host": "pchk",
+ "ip_address": "192.168.2.41",
+ "port": 4114,
+ "username": "lcn",
+ "password": "lcn,
+ "sk_num_tries: 0,
+ "dim_mode: "STEPS200",
+ "acknowledge": False,
+ "devices": [
+ {
+ "address": (0, 7, False)
+ "name": "",
+ "hardware_serial": -1,
+ "software_serial": -1,
+ "hardware_type": -1
+ }, ...
+ ],
+ "entities": [
+ {
+ "address": (0, 7, False)
+ "name": "Light_Output1",
+ "resource": "output1",
+ "domain": "light",
+ "domain_data": {
+ "output": "OUTPUT1",
+ "dimmable": True,
+ "transition": 5000.0
+ }
+ }, ...
+ ]
+ }
+ """
+ data = {}
+ for connection in lcn_config[CONF_CONNECTIONS]:
+ host = {
+ CONF_HOST: connection[CONF_NAME],
+ CONF_IP_ADDRESS: connection[CONF_HOST],
+ CONF_PORT: connection[CONF_PORT],
+ CONF_USERNAME: connection[CONF_USERNAME],
+ CONF_PASSWORD: connection[CONF_PASSWORD],
+ CONF_SK_NUM_TRIES: connection[CONF_SK_NUM_TRIES],
+ CONF_DIM_MODE: connection[CONF_DIM_MODE],
+ CONF_ACKNOWLEDGE: False,
+ CONF_DEVICES: [],
+ CONF_ENTITIES: [],
+ }
+ data[connection[CONF_NAME]] = host
+
+ for confkey, domain_config in lcn_config.items():
+ if confkey == CONF_CONNECTIONS:
+ continue
+ domain = DOMAIN_LOOKUP[confkey]
+ # loop over entities in configuration.yaml
+ for domain_data in domain_config:
+ # remove name and address from domain_data
+ entity_name = domain_data.pop(CONF_NAME)
+ address, host_name = domain_data.pop(CONF_ADDRESS)
+
+ if host_name is None:
+ host_name = DEFAULT_NAME
+
+ # check if we have a new device config
+ for device_config in data[host_name][CONF_DEVICES]:
+ if address == device_config[CONF_ADDRESS]:
+ break
+ else: # create new device_config
+ device_config = {
+ CONF_ADDRESS: address,
+ CONF_NAME: "",
+ CONF_HARDWARE_SERIAL: -1,
+ CONF_SOFTWARE_SERIAL: -1,
+ CONF_HARDWARE_TYPE: -1,
+ }
+
+ data[host_name][CONF_DEVICES].append(device_config)
+
+ # insert entity config
+ resource = get_resource(domain, domain_data).lower()
+ for entity_config in data[host_name][CONF_ENTITIES]:
+ if (
+ address == entity_config[CONF_ADDRESS]
+ and resource == entity_config[CONF_RESOURCE]
+ and domain == entity_config[CONF_DOMAIN]
+ ):
+ break
+ else: # create new entity_config
+ entity_config = {
+ CONF_ADDRESS: address,
+ CONF_NAME: entity_name,
+ CONF_RESOURCE: resource,
+ CONF_DOMAIN: domain,
+ CONF_DOMAIN_DATA: domain_data.copy(),
+ }
+ data[host_name][CONF_ENTITIES].append(entity_config)
+
+ return list(data.values())
+
+
def purge_entity_registry(
hass: HomeAssistant, entry_id: str, imported_entry_data: ConfigType
) -> None:
@@ -320,6 +436,26 @@ def get_device_config(
return None
+def has_unique_host_names(hosts: list[ConfigType]) -> list[ConfigType]:
+ """Validate that all connection names are unique.
+
+ Use 'pchk' as default connection_name (or add a numeric suffix if
+ pchk' is already in use.
+ """
+ suffix = 0
+ for host in hosts:
+ if host.get(CONF_NAME) is None:
+ if suffix == 0:
+ host[CONF_NAME] = DEFAULT_NAME
+ else:
+ host[CONF_NAME] = f"{DEFAULT_NAME}{suffix:d}"
+ suffix += 1
+
+ schema = vol.Schema(vol.Unique())
+ schema([host.get(CONF_NAME) for host in hosts])
+ return hosts
+
+
def is_address(value: str) -> tuple[AddressType, str]:
"""Validate the given address string.
diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py
index 9ec660325c8..943e3c69acf 100644
--- a/homeassistant/components/lcn/light.py
+++ b/homeassistant/components/lcn/light.py
@@ -90,7 +90,7 @@ class LcnOutputLight(LcnEntity, LightEntity):
self.output = pypck.lcn_defs.OutputPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]]
self._transition = pypck.lcn_defs.time_to_ramp_value(
- config[CONF_DOMAIN_DATA][CONF_TRANSITION] * 1000.0
+ config[CONF_DOMAIN_DATA][CONF_TRANSITION]
)
self.dimmable = config[CONF_DOMAIN_DATA][CONF_DIMMABLE]
diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json
index 695a35df871..8f6b59e0a04 100644
--- a/homeassistant/components/lcn/manifest.json
+++ b/homeassistant/components/lcn/manifest.json
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/lcn",
"iot_class": "local_push",
"loggers": ["pypck"],
- "requirements": ["pypck==0.7.24", "lcn-frontend==0.2.2"]
+ "requirements": ["pypck==0.7.24", "lcn-frontend==0.1.6"]
}
diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py
index 0f40926cf17..241493ec108 100644
--- a/homeassistant/components/lcn/scene.py
+++ b/homeassistant/components/lcn/scene.py
@@ -87,7 +87,7 @@ class LcnScene(LcnEntity, Scene):
self.transition = None
else:
self.transition = pypck.lcn_defs.time_to_ramp_value(
- config[CONF_DOMAIN_DATA][CONF_TRANSITION] * 1000.0
+ config[CONF_DOMAIN_DATA][CONF_TRANSITION]
)
async def async_activate(self, **kwargs: Any) -> None:
diff --git a/homeassistant/components/lcn/schemas.py b/homeassistant/components/lcn/schemas.py
index c9c91b9843d..0539e83dea8 100644
--- a/homeassistant/components/lcn/schemas.py
+++ b/homeassistant/components/lcn/schemas.py
@@ -4,9 +4,20 @@ import voluptuous as vol
from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP
from homeassistant.const import (
+ CONF_ADDRESS,
+ CONF_BINARY_SENSORS,
+ CONF_COVERS,
+ CONF_HOST,
+ CONF_LIGHTS,
+ CONF_NAME,
+ CONF_PASSWORD,
+ CONF_PORT,
CONF_SCENE,
+ CONF_SENSORS,
CONF_SOURCE,
+ CONF_SWITCHES,
CONF_UNIT_OF_MEASUREMENT,
+ CONF_USERNAME,
UnitOfTemperature,
)
import homeassistant.helpers.config_validation as cv
@@ -14,6 +25,9 @@ from homeassistant.helpers.typing import VolDictType
from .const import (
BINSENSOR_PORTS,
+ CONF_CLIMATES,
+ CONF_CONNECTIONS,
+ CONF_DIM_MODE,
CONF_DIMMABLE,
CONF_LOCKABLE,
CONF_MAX_TEMP,
@@ -23,8 +37,12 @@ from .const import (
CONF_OUTPUTS,
CONF_REGISTER,
CONF_REVERSE_TIME,
+ CONF_SCENES,
CONF_SETPOINT,
+ CONF_SK_NUM_TRIES,
CONF_TRANSITION,
+ DIM_MODES,
+ DOMAIN,
KEYS,
LED_PORTS,
LOGICOP_PORTS,
@@ -38,6 +56,7 @@ from .const import (
VAR_UNITS,
VARIABLES,
)
+from .helpers import has_unique_host_names, is_address
ADDRESS_SCHEMA = vol.Coerce(tuple)
@@ -76,7 +95,7 @@ DOMAIN_DATA_LIGHT: VolDictType = {
vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS)),
vol.Optional(CONF_DIMMABLE, default=False): vol.Coerce(bool),
vol.Optional(CONF_TRANSITION, default=0): vol.All(
- vol.Coerce(float), vol.Range(min=0.0, max=486.0)
+ vol.Coerce(float), vol.Range(min=0.0, max=486.0), lambda value: value * 1000
),
}
@@ -87,8 +106,13 @@ DOMAIN_DATA_SCENE: VolDictType = {
vol.Optional(CONF_OUTPUTS, default=[]): vol.All(
cv.ensure_list, [vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS))]
),
- vol.Optional(CONF_TRANSITION, default=0): vol.Any(
- vol.All(vol.Coerce(int), vol.Range(min=0.0, max=486.0))
+ vol.Optional(CONF_TRANSITION, default=None): vol.Any(
+ vol.All(
+ vol.Coerce(int),
+ vol.Range(min=0.0, max=486.0),
+ lambda value: value * 1000,
+ ),
+ None,
),
}
@@ -106,8 +130,73 @@ DOMAIN_DATA_SENSOR: VolDictType = {
DOMAIN_DATA_SWITCH: VolDictType = {
- vol.Required(CONF_OUTPUT): vol.All(
- vol.Upper,
- vol.In(OUTPUT_PORTS + RELAY_PORTS + SETPOINTS + KEYS),
- ),
+ vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS)),
}
+
+#
+# Configuration
+#
+
+DOMAIN_DATA_BASE: VolDictType = {
+ vol.Required(CONF_NAME): cv.string,
+ vol.Required(CONF_ADDRESS): is_address,
+}
+
+BINARY_SENSORS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_BINARY_SENSOR})
+
+CLIMATES_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_CLIMATE})
+
+COVERS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_COVER})
+
+LIGHTS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_LIGHT})
+
+SCENES_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_SCENE})
+
+SENSORS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_SENSOR})
+
+SWITCHES_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_SWITCH})
+
+CONNECTION_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_PORT): cv.port,
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_SK_NUM_TRIES, default=0): cv.positive_int,
+ vol.Optional(CONF_DIM_MODE, default="steps50"): vol.All(
+ vol.Upper, vol.In(DIM_MODES)
+ ),
+ vol.Optional(CONF_NAME): cv.string,
+ }
+)
+
+CONFIG_SCHEMA = vol.Schema(
+ vol.All(
+ cv.deprecated(DOMAIN),
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_CONNECTIONS): vol.All(
+ cv.ensure_list, has_unique_host_names, [CONNECTION_SCHEMA]
+ ),
+ vol.Optional(CONF_BINARY_SENSORS): vol.All(
+ cv.ensure_list, [BINARY_SENSORS_SCHEMA]
+ ),
+ vol.Optional(CONF_CLIMATES): vol.All(
+ cv.ensure_list, [CLIMATES_SCHEMA]
+ ),
+ vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]),
+ vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]),
+ vol.Optional(CONF_SCENES): vol.All(cv.ensure_list, [SCENES_SCHEMA]),
+ vol.Optional(CONF_SENSORS): vol.All(
+ cv.ensure_list, [SENSORS_SCHEMA]
+ ),
+ vol.Optional(CONF_SWITCHES): vol.All(
+ cv.ensure_list, [SWITCHES_SCHEMA]
+ ),
+ },
+ )
+ },
+ ),
+ extra=vol.ALLOW_EXTRA,
+)
diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py
index ada0857742c..5a360d44b8c 100644
--- a/homeassistant/components/lcn/sensor.py
+++ b/homeassistant/components/lcn/sensor.py
@@ -126,11 +126,7 @@ class LcnVariableSensor(LcnEntity, SensorEntity):
):
return
- is_regulator = self.variable.name in SETPOINTS
- self._attr_native_value = input_obj.get_value().to_var_unit(
- self.unit, is_regulator
- )
-
+ self._attr_native_value = input_obj.get_value().to_var_unit(self.unit)
self.async_write_ha_state()
diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py
index 92f5863c47e..611a7353bcd 100644
--- a/homeassistant/components/lcn/services.py
+++ b/homeassistant/components/lcn/services.py
@@ -429,11 +429,3 @@ SERVICES = (
(LcnService.DYN_TEXT, DynText),
(LcnService.PCK, Pck),
)
-
-
-async def register_services(hass: HomeAssistant) -> None:
- """Register services for LCN."""
- for service_name, service in SERVICES:
- hass.services.async_register(
- DOMAIN, service_name, service(hass).async_call_service, service.schema
- )
diff --git a/homeassistant/components/lcn/strings.json b/homeassistant/components/lcn/strings.json
index 088a3654500..90650c2aed1 100644
--- a/homeassistant/components/lcn/strings.json
+++ b/homeassistant/components/lcn/strings.json
@@ -34,7 +34,7 @@
"acknowledge": "Retry sendig commands if no response is received (increases bus traffic)."
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"title": "Reconfigure LCN host",
"description": "Reconfigure connection to LCN host.",
"data": {
@@ -63,13 +63,17 @@
}
},
"issues": {
- "deprecated_regulatorlock_sensor": {
- "title": "Deprecated LCN regulator lock binary sensor",
- "description": "Your LCN regulator lock binary sensor entity `{entity}` is beeing used in automations or scripts. A regulator lock switch entity is available and should be used going forward.\n\nPlease adjust your automations or scripts to fix this issue."
+ "authentication_error": {
+ "title": "Authentication failed.",
+ "description": "Configuring LCN using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure username and password are correct.\n\nConsider removing the LCN YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
},
- "deprecated_keylock_sensor": {
- "title": "Deprecated LCN key lock binary sensor",
- "description": "Your LCN key lock binary sensor entity `{entity}` is beeing used in automations or scripts. A key lock switch entity is available and should be used going forward.\n\nPlease adjust your automations or scripts to fix this issue."
+ "license_error": {
+ "title": "Maximum number of connections was reached.",
+ "description": "Configuring LCN using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure sufficient PCHK licenses are registered and restart Home Assistant.\n\nConsider removing the LCN YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
+ },
+ "connection_refused": {
+ "title": "Unable to connect to PCHK.",
+ "description": "Configuring LCN using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure the connection (IP and port) to the LCN bus coupler is correct.\n\nConsider removing the LCN YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
}
},
"services": {
diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py
index dd940bd38b3..6ad5977855e 100644
--- a/homeassistant/components/lcn/switch.py
+++ b/homeassistant/components/lcn/switch.py
@@ -19,8 +19,6 @@ from .const import (
CONF_OUTPUT,
DOMAIN,
OUTPUT_PORTS,
- RELAY_PORTS,
- SETPOINTS,
)
from .entity import LcnEntity
from .helpers import InputType
@@ -34,18 +32,12 @@ def add_lcn_switch_entities(
entity_configs: Iterable[ConfigType],
) -> None:
"""Add entities for this domain."""
- entities: list[
- LcnOutputSwitch | LcnRelaySwitch | LcnRegulatorLockSwitch | LcnKeyLockSwitch
- ] = []
+ entities: list[LcnOutputSwitch | LcnRelaySwitch] = []
for entity_config in entity_configs:
if entity_config[CONF_DOMAIN_DATA][CONF_OUTPUT] in OUTPUT_PORTS:
entities.append(LcnOutputSwitch(entity_config, config_entry))
- elif entity_config[CONF_DOMAIN_DATA][CONF_OUTPUT] in RELAY_PORTS:
+ else: # in RELAY_PORTS
entities.append(LcnRelaySwitch(entity_config, config_entry))
- elif entity_config[CONF_DOMAIN_DATA][CONF_OUTPUT] in SETPOINTS:
- entities.append(LcnRegulatorLockSwitch(entity_config, config_entry))
- else: # in KEYS
- entities.append(LcnKeyLockSwitch(entity_config, config_entry))
async_add_entities(entities)
@@ -172,118 +164,3 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity):
self._attr_is_on = input_obj.get_state(self.output.value)
self.async_write_ha_state()
-
-
-class LcnRegulatorLockSwitch(LcnEntity, SwitchEntity):
- """Representation of a LCN switch for regulator locks."""
-
- _attr_is_on = False
-
- def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None:
- """Initialize the LCN switch."""
- super().__init__(config, config_entry)
-
- self.setpoint_variable = pypck.lcn_defs.Var[
- config[CONF_DOMAIN_DATA][CONF_OUTPUT]
- ]
- self.reg_id = pypck.lcn_defs.Var.to_set_point_id(self.setpoint_variable)
-
- async def async_added_to_hass(self) -> None:
- """Run when entity about to be added to hass."""
- await super().async_added_to_hass()
- if not self.device_connection.is_group:
- await self.device_connection.activate_status_request_handler(
- self.setpoint_variable
- )
-
- async def async_will_remove_from_hass(self) -> None:
- """Run when entity will be removed from hass."""
- await super().async_will_remove_from_hass()
- if not self.device_connection.is_group:
- await self.device_connection.cancel_status_request_handler(
- self.setpoint_variable
- )
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn the entity on."""
- if not await self.device_connection.lock_regulator(self.reg_id, True):
- return
- self._attr_is_on = True
- self.async_write_ha_state()
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn the entity off."""
- if not await self.device_connection.lock_regulator(self.reg_id, False):
- return
- self._attr_is_on = False
- self.async_write_ha_state()
-
- def input_received(self, input_obj: InputType) -> None:
- """Set switch state when LCN input object (command) is received."""
- if (
- not isinstance(input_obj, pypck.inputs.ModStatusVar)
- or input_obj.get_var() != self.setpoint_variable
- ):
- return
-
- self._attr_is_on = input_obj.get_value().is_locked_regulator()
- self.async_write_ha_state()
-
-
-class LcnKeyLockSwitch(LcnEntity, SwitchEntity):
- """Representation of a LCN switch for key locks."""
-
- _attr_is_on = False
-
- def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None:
- """Initialize the LCN switch."""
- super().__init__(config, config_entry)
-
- self.key = pypck.lcn_defs.Key[config[CONF_DOMAIN_DATA][CONF_OUTPUT]]
- self.table_id = ord(self.key.name[0]) - 65
- self.key_id = int(self.key.name[1]) - 1
-
- async def async_added_to_hass(self) -> None:
- """Run when entity about to be added to hass."""
- await super().async_added_to_hass()
- if not self.device_connection.is_group:
- await self.device_connection.activate_status_request_handler(self.key)
-
- async def async_will_remove_from_hass(self) -> None:
- """Run when entity will be removed from hass."""
- await super().async_will_remove_from_hass()
- if not self.device_connection.is_group:
- await self.device_connection.cancel_status_request_handler(self.key)
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn the entity on."""
- states = [pypck.lcn_defs.KeyLockStateModifier.NOCHANGE] * 8
- states[self.key_id] = pypck.lcn_defs.KeyLockStateModifier.ON
-
- if not await self.device_connection.lock_keys(self.table_id, states):
- return
-
- self._attr_is_on = True
- self.async_write_ha_state()
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn the entity off."""
- states = [pypck.lcn_defs.KeyLockStateModifier.NOCHANGE] * 8
- states[self.key_id] = pypck.lcn_defs.KeyLockStateModifier.OFF
-
- if not await self.device_connection.lock_keys(self.table_id, states):
- return
-
- self._attr_is_on = False
- self.async_write_ha_state()
-
- def input_received(self, input_obj: InputType) -> None:
- """Set switch state when LCN input object (command) is received."""
- if (
- not isinstance(input_obj, pypck.inputs.ModStatusKeyLocks)
- or self.key not in pypck.lcn_defs.Key
- ):
- return
-
- self._attr_is_on = input_obj.get_state(self.table_id, self.key_id)
- self.async_write_ha_state()
diff --git a/homeassistant/components/led_ble/__init__.py b/homeassistant/components/led_ble/__init__.py
index 84d7369d706..d09f88b145a 100644
--- a/homeassistant/components/led_ble/__init__.py
+++ b/homeassistant/components/led_ble/__init__.py
@@ -66,7 +66,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=led_ble.name,
update_method=_async_update,
update_interval=timedelta(seconds=UPDATE_SECONDS),
diff --git a/homeassistant/components/lektrico/__init__.py b/homeassistant/components/lektrico/__init__.py
index 475b6132541..0691bfef72a 100644
--- a/homeassistant/components/lektrico/__init__.py
+++ b/homeassistant/components/lektrico/__init__.py
@@ -12,11 +12,9 @@ from .coordinator import LektricoDeviceDataUpdateCoordinator
# List the platforms that charger supports.
CHARGERS_PLATFORMS: list[Platform] = [
- Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.NUMBER,
Platform.SENSOR,
- Platform.SWITCH,
]
# List the platforms that load balancer device supports.
diff --git a/homeassistant/components/lektrico/binary_sensor.py b/homeassistant/components/lektrico/binary_sensor.py
deleted file mode 100644
index d0a3e39690c..00000000000
--- a/homeassistant/components/lektrico/binary_sensor.py
+++ /dev/null
@@ -1,139 +0,0 @@
-"""Support for Lektrico binary sensors entities."""
-
-from collections.abc import Callable
-from dataclasses import dataclass
-from typing import Any
-
-from homeassistant.components.binary_sensor import (
- BinarySensorDeviceClass,
- BinarySensorEntity,
- BinarySensorEntityDescription,
-)
-from homeassistant.const import ATTR_SERIAL_NUMBER, CONF_TYPE, EntityCategory
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import LektricoConfigEntry, LektricoDeviceDataUpdateCoordinator
-from .entity import LektricoEntity
-
-
-@dataclass(frozen=True, kw_only=True)
-class LektricoBinarySensorEntityDescription(BinarySensorEntityDescription):
- """Describes Lektrico binary sensor entity."""
-
- value_fn: Callable[[dict[str, Any]], bool]
-
-
-BINARY_SENSORS: tuple[LektricoBinarySensorEntityDescription, ...] = (
- LektricoBinarySensorEntityDescription(
- key="state_e_activated",
- translation_key="state_e_activated",
- entity_category=EntityCategory.DIAGNOSTIC,
- device_class=BinarySensorDeviceClass.PROBLEM,
- value_fn=lambda data: bool(data["state_e_activated"]),
- ),
- LektricoBinarySensorEntityDescription(
- key="overtemp",
- translation_key="overtemp",
- entity_category=EntityCategory.DIAGNOSTIC,
- device_class=BinarySensorDeviceClass.PROBLEM,
- value_fn=lambda data: bool(data["overtemp"]),
- ),
- LektricoBinarySensorEntityDescription(
- key="critical_temp",
- translation_key="critical_temp",
- entity_category=EntityCategory.DIAGNOSTIC,
- device_class=BinarySensorDeviceClass.PROBLEM,
- value_fn=lambda data: bool(data["critical_temp"]),
- ),
- LektricoBinarySensorEntityDescription(
- key="overcurrent",
- translation_key="overcurrent",
- entity_category=EntityCategory.DIAGNOSTIC,
- device_class=BinarySensorDeviceClass.PROBLEM,
- value_fn=lambda data: bool(data["overcurrent"]),
- ),
- LektricoBinarySensorEntityDescription(
- key="meter_fault",
- translation_key="meter_fault",
- entity_category=EntityCategory.DIAGNOSTIC,
- device_class=BinarySensorDeviceClass.PROBLEM,
- value_fn=lambda data: bool(data["meter_fault"]),
- ),
- LektricoBinarySensorEntityDescription(
- key="undervoltage",
- translation_key="undervoltage",
- entity_category=EntityCategory.DIAGNOSTIC,
- device_class=BinarySensorDeviceClass.PROBLEM,
- value_fn=lambda data: bool(data["undervoltage_error"]),
- ),
- LektricoBinarySensorEntityDescription(
- key="overvoltage",
- translation_key="overvoltage",
- entity_category=EntityCategory.DIAGNOSTIC,
- device_class=BinarySensorDeviceClass.PROBLEM,
- value_fn=lambda data: bool(data["overvoltage_error"]),
- ),
- LektricoBinarySensorEntityDescription(
- key="rcd_error",
- translation_key="rcd_error",
- entity_category=EntityCategory.DIAGNOSTIC,
- device_class=BinarySensorDeviceClass.PROBLEM,
- value_fn=lambda data: bool(data["rcd_error"]),
- ),
- LektricoBinarySensorEntityDescription(
- key="cp_diode_failure",
- translation_key="cp_diode_failure",
- entity_category=EntityCategory.DIAGNOSTIC,
- device_class=BinarySensorDeviceClass.PROBLEM,
- value_fn=lambda data: bool(data["cp_diode_failure"]),
- ),
- LektricoBinarySensorEntityDescription(
- key="contactor_failure",
- translation_key="contactor_failure",
- entity_category=EntityCategory.DIAGNOSTIC,
- device_class=BinarySensorDeviceClass.PROBLEM,
- value_fn=lambda data: bool(data["contactor_failure"]),
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: LektricoConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up Lektrico binary sensor entities based on a config entry."""
- coordinator = entry.runtime_data
-
- async_add_entities(
- LektricoBinarySensor(
- description,
- coordinator,
- f"{entry.data[CONF_TYPE]}_{entry.data[ATTR_SERIAL_NUMBER]}",
- )
- for description in BINARY_SENSORS
- )
-
-
-class LektricoBinarySensor(LektricoEntity, BinarySensorEntity):
- """Defines a Lektrico binary sensor entity."""
-
- entity_description: LektricoBinarySensorEntityDescription
-
- def __init__(
- self,
- description: LektricoBinarySensorEntityDescription,
- coordinator: LektricoDeviceDataUpdateCoordinator,
- device_name: str,
- ) -> None:
- """Initialize Lektrico binary sensor."""
- super().__init__(coordinator, device_name)
- self.entity_description = description
- self._coordinator = coordinator
- self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
-
- @property
- def is_on(self) -> bool:
- """Return the state of the binary sensor."""
- return self.entity_description.value_fn(self.coordinator.data)
diff --git a/homeassistant/components/lektrico/manifest.json b/homeassistant/components/lektrico/manifest.json
index d34915d66ba..d96b8cc4b69 100644
--- a/homeassistant/components/lektrico/manifest.json
+++ b/homeassistant/components/lektrico/manifest.json
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/lektrico",
"integration_type": "device",
"iot_class": "local_polling",
- "requirements": ["lektricowifi==0.0.43"],
+ "requirements": ["lektricowifi==0.0.42"],
"zeroconf": [
{
"type": "_http._tcp.local.",
diff --git a/homeassistant/components/lektrico/sensor.py b/homeassistant/components/lektrico/sensor.py
index d55d91c4cd4..a26a3676d8b 100644
--- a/homeassistant/components/lektrico/sensor.py
+++ b/homeassistant/components/lektrico/sensor.py
@@ -62,13 +62,11 @@ SENSORS_FOR_CHARGERS: tuple[LektricoSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.ENUM,
options=[
"available",
- "charging",
"connected",
- "error",
- "locked",
"need_auth",
"paused",
- "paused_by_scheduler",
+ "charging",
+ "error",
"updating_firmware",
],
translation_key="state",
diff --git a/homeassistant/components/lektrico/strings.json b/homeassistant/components/lektrico/strings.json
index e24700c9b09..b749ea23490 100644
--- a/homeassistant/components/lektrico/strings.json
+++ b/homeassistant/components/lektrico/strings.json
@@ -22,38 +22,6 @@
}
},
"entity": {
- "binary_sensor": {
- "state_e_activated": {
- "name": "Ev error"
- },
- "overtemp": {
- "name": "Thermal throttling"
- },
- "critical_temp": {
- "name": "Overheating"
- },
- "overcurrent": {
- "name": "Overcurrent"
- },
- "meter_fault": {
- "name": "Metering error"
- },
- "undervoltage": {
- "name": "Undervoltage"
- },
- "overvoltage": {
- "name": "Overvoltage"
- },
- "rcd_error": {
- "name": "Rcd error"
- },
- "cp_diode_failure": {
- "name": "Ev diode short"
- },
- "contactor_failure": {
- "name": "Relay contacts welded"
- }
- },
"button": {
"charge_start": {
"name": "Charge start"
@@ -86,13 +54,11 @@
"name": "State",
"state": {
"available": "Available",
- "charging": "Charging",
"connected": "Connected",
- "error": "Error",
- "locked": "Locked",
"need_auth": "Waiting for authentication",
"paused": "Paused",
- "paused_by_scheduler": "Paused by scheduler",
+ "charging": "Charging",
+ "error": "Error",
"updating_firmware": "Updating firmware"
}
},
@@ -160,17 +126,6 @@
"pf_l3": {
"name": "Power factor L3"
}
- },
- "switch": {
- "authentication": {
- "name": "Authentication"
- },
- "force_single_phase": {
- "name": "Force single phase"
- },
- "lock": {
- "name": "Lock"
- }
}
}
}
diff --git a/homeassistant/components/lektrico/switch.py b/homeassistant/components/lektrico/switch.py
deleted file mode 100644
index 0fdfbd2ad41..00000000000
--- a/homeassistant/components/lektrico/switch.py
+++ /dev/null
@@ -1,116 +0,0 @@
-"""Support for Lektrico switch entities."""
-
-from collections.abc import Callable, Coroutine
-from dataclasses import dataclass
-from typing import Any
-
-from lektricowifi import Device
-
-from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
-from homeassistant.const import ATTR_SERIAL_NUMBER, CONF_TYPE, EntityCategory
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import LektricoConfigEntry, LektricoDeviceDataUpdateCoordinator
-from .entity import LektricoEntity
-
-
-@dataclass(frozen=True, kw_only=True)
-class LektricoSwitchEntityDescription(SwitchEntityDescription):
- """Describes Lektrico switch entity."""
-
- value_fn: Callable[[dict[str, Any]], bool]
- set_value_fn: Callable[[Device, dict[Any, Any], bool], Coroutine[Any, Any, Any]]
-
-
-SWITCHS_FOR_ALL_CHARGERS: tuple[LektricoSwitchEntityDescription, ...] = (
- LektricoSwitchEntityDescription(
- key="authentication",
- translation_key="authentication",
- entity_category=EntityCategory.CONFIG,
- value_fn=lambda data: bool(data["require_auth"]),
- set_value_fn=lambda device, data, value: device.set_auth(not value),
- ),
- LektricoSwitchEntityDescription(
- key="lock",
- translation_key="lock",
- entity_category=EntityCategory.CONFIG,
- value_fn=lambda data: str(data["charger_state"]) == "locked",
- set_value_fn=lambda device, data, value: device.set_charger_locked(value),
- ),
-)
-
-
-SWITCHS_FOR_3_PHASE_CHARGERS: tuple[LektricoSwitchEntityDescription, ...] = (
- LektricoSwitchEntityDescription(
- key="force_single_phase",
- translation_key="force_single_phase",
- entity_category=EntityCategory.CONFIG,
- value_fn=lambda data: data["relay_mode"] == 1,
- set_value_fn=lambda device, data, value: (
- device.set_relay_mode(data["dynamic_current"], 1)
- if value
- else device.set_relay_mode(data["dynamic_current"], 3)
- ),
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: LektricoConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up Lektrico switch entities based on a config entry."""
- coordinator = entry.runtime_data
-
- switchs_to_be_used: tuple[LektricoSwitchEntityDescription, ...]
- if coordinator.device_type == Device.TYPE_3P22K:
- switchs_to_be_used = SWITCHS_FOR_ALL_CHARGERS + SWITCHS_FOR_3_PHASE_CHARGERS
- else:
- switchs_to_be_used = SWITCHS_FOR_ALL_CHARGERS
-
- async_add_entities(
- LektricoSwitch(
- description,
- coordinator,
- f"{entry.data[CONF_TYPE]}_{entry.data[ATTR_SERIAL_NUMBER]}",
- )
- for description in switchs_to_be_used
- )
-
-
-class LektricoSwitch(LektricoEntity, SwitchEntity):
- """Defines a Lektrico switch entity."""
-
- entity_description: LektricoSwitchEntityDescription
-
- def __init__(
- self,
- description: LektricoSwitchEntityDescription,
- coordinator: LektricoDeviceDataUpdateCoordinator,
- device_name: str,
- ) -> None:
- """Initialize Lektrico switch."""
- super().__init__(coordinator, device_name)
- self.entity_description = description
- self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
-
- @property
- def is_on(self) -> bool:
- """Return the state of the switch."""
- return self.entity_description.value_fn(self.coordinator.data)
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn the switch on."""
- await self.entity_description.set_value_fn(
- self.coordinator.device, self.coordinator.data, True
- )
- await self.coordinator.async_request_refresh()
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn the switch off."""
- await self.entity_description.set_value_fn(
- self.coordinator.device, self.coordinator.data, False
- )
- await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/lg_netcast/config_flow.py b/homeassistant/components/lg_netcast/config_flow.py
index d5e28f3c057..4b1780d41ae 100644
--- a/homeassistant/components/lg_netcast/config_flow.py
+++ b/homeassistant/components/lg_netcast/config_flow.py
@@ -18,9 +18,10 @@ from homeassistant.const import (
CONF_MODEL,
CONF_NAME,
)
-from homeassistant.core import CALLBACK_TYPE, callback
+from homeassistant.core import CALLBACK_TYPE, DOMAIN as HOMEASSISTANT_DOMAIN, callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.util.network import is_host_valid
from .const import DEFAULT_NAME, DOMAIN
@@ -67,6 +68,56 @@ class LGNetCast(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors,
)
+ async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
+ """Import configuration from yaml."""
+ self.device_config = {
+ CONF_HOST: import_data[CONF_HOST],
+ CONF_NAME: import_data[CONF_NAME],
+ }
+
+ def _create_issue():
+ async_create_issue(
+ self.hass,
+ HOMEASSISTANT_DOMAIN,
+ f"deprecated_yaml_{DOMAIN}",
+ breaks_in_ha_version="2024.11.0",
+ is_fixable=False,
+ issue_domain=DOMAIN,
+ severity=IssueSeverity.WARNING,
+ translation_key="deprecated_yaml",
+ translation_placeholders={
+ "domain": DOMAIN,
+ "integration_title": "LG Netcast",
+ },
+ )
+
+ try:
+ result: ConfigFlowResult = await self.async_step_authorize(import_data)
+ except AbortFlow as err:
+ if err.reason != "already_configured":
+ async_create_issue(
+ self.hass,
+ DOMAIN,
+ "deprecated_yaml_import_issue_{err.reason}",
+ breaks_in_ha_version="2024.11.0",
+ is_fixable=False,
+ issue_domain=DOMAIN,
+ severity=IssueSeverity.WARNING,
+ translation_key=f"deprecated_yaml_import_issue_{err.reason}",
+ translation_placeholders={
+ "domain": DOMAIN,
+ "integration_title": "LG Netcast",
+ "error_type": err.reason,
+ },
+ )
+ else:
+ _create_issue()
+ raise
+
+ _create_issue()
+
+ return result
+
async def async_discover_client(self):
"""Handle Discovery step."""
self.create_client()
diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py
index b3f8f8e0437..4dc694cd085 100644
--- a/homeassistant/components/lg_netcast/media_player.py
+++ b/homeassistant/components/lg_netcast/media_player.py
@@ -7,20 +7,26 @@ from typing import TYPE_CHECKING, Any
from pylgnetcast import LG_COMMAND, LgNetCastClient, LgNetCastError
from requests import RequestException
+import voluptuous as vol
from homeassistant.components.media_player import (
+ PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
MediaPlayerDeviceClass,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
)
-from homeassistant.config_entries import ConfigEntry
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_MODEL, CONF_NAME
from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import FlowResultType
+from homeassistant.exceptions import PlatformNotReady
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.trigger import PluggableAction
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import ATTR_MANUFACTURER, DOMAIN
from .triggers.turn_on import async_get_turn_on_trigger
@@ -43,6 +49,15 @@ SUPPORT_LGTV = (
| MediaPlayerEntityFeature.STOP
)
+PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_ACCESS_TOKEN): vol.All(cv.string, vol.Length(max=6)),
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ }
+)
+
async def async_setup_entry(
hass: HomeAssistant,
@@ -64,6 +79,27 @@ async def async_setup_entry(
async_add_entities([LgTVDevice(client, name, model, unique_id=unique_id)])
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: ConfigType,
+ async_add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> None:
+ """Set up the LG TV platform."""
+
+ host = config.get(CONF_HOST)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=config
+ )
+
+ if (
+ result.get("type") == FlowResultType.ABORT
+ and result.get("reason") == "cannot_connect"
+ ):
+ raise PlatformNotReady(f"Connection error while connecting to {host}")
+
+
class LgTVDevice(MediaPlayerEntity):
"""Representation of a LG TV."""
diff --git a/homeassistant/components/lg_netcast/strings.json b/homeassistant/components/lg_netcast/strings.json
index 209c3837261..77003f60f43 100644
--- a/homeassistant/components/lg_netcast/strings.json
+++ b/homeassistant/components/lg_netcast/strings.json
@@ -28,6 +28,16 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
+ "issues": {
+ "deprecated_yaml_import_issue_cannot_connect": {
+ "title": "The {integration_title} is not online for YAML migration to complete",
+ "description": "Migrating {integration_title} from YAML cannot complete until the TV is online.\n\nPlease turn on your TV for migration to complete."
+ },
+ "deprecated_yaml_import_issue_invalid_host": {
+ "title": "The {integration_title} YAML configuration has an invalid host.",
+ "description": "Configuring {integration_title} using YAML is being removed but the device returned an invalid response.\n\nPlease check or manually remove the YAML configuration."
+ }
+ },
"device_automation": {
"trigger_type": {
"lg_netcast.turn_on": "Device is requested to turn on"
diff --git a/homeassistant/components/lg_thinq/__init__.py b/homeassistant/components/lg_thinq/__init__.py
deleted file mode 100644
index a8d3fe175ef..00000000000
--- a/homeassistant/components/lg_thinq/__init__.py
+++ /dev/null
@@ -1,166 +0,0 @@
-"""Support for LG ThinQ Connect device."""
-
-from __future__ import annotations
-
-import asyncio
-from dataclasses import dataclass, field
-import logging
-
-from thinqconnect import ThinQApi, ThinQAPIException
-from thinqconnect.integration import async_get_ha_bridge_list
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import (
- CONF_ACCESS_TOKEN,
- CONF_COUNTRY,
- EVENT_HOMEASSISTANT_STOP,
- Platform,
-)
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers import device_registry as dr
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.event import async_track_time_interval
-
-from .const import CONF_CONNECT_CLIENT_ID, MQTT_SUBSCRIPTION_INTERVAL
-from .coordinator import DeviceDataUpdateCoordinator, async_setup_device_coordinator
-from .mqtt import ThinQMQTT
-
-
-@dataclass(kw_only=True)
-class ThinqData:
- """A class that holds runtime data."""
-
- coordinators: dict[str, DeviceDataUpdateCoordinator] = field(default_factory=dict)
- mqtt_client: ThinQMQTT | None = None
-
-
-type ThinqConfigEntry = ConfigEntry[ThinqData]
-
-PLATFORMS = [
- Platform.BINARY_SENSOR,
- Platform.CLIMATE,
- Platform.EVENT,
- Platform.FAN,
- Platform.NUMBER,
- Platform.SELECT,
- Platform.SENSOR,
- Platform.SWITCH,
- Platform.VACUUM,
-]
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: ThinqConfigEntry) -> bool:
- """Set up an entry."""
- entry.runtime_data = ThinqData()
-
- access_token = entry.data[CONF_ACCESS_TOKEN]
- client_id = entry.data[CONF_CONNECT_CLIENT_ID]
- country_code = entry.data[CONF_COUNTRY]
-
- thinq_api = ThinQApi(
- session=async_get_clientsession(hass),
- access_token=access_token,
- country_code=country_code,
- client_id=client_id,
- )
-
- # Setup coordinators and register devices.
- await async_setup_coordinators(hass, entry, thinq_api)
-
- # Set up all platforms for this device/entry.
- await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
-
- # Set up MQTT connection.
- await async_setup_mqtt(hass, entry, thinq_api, client_id)
-
- # Clean up devices they are no longer in use.
- async_cleanup_device_registry(hass, entry)
-
- return True
-
-
-async def async_setup_coordinators(
- hass: HomeAssistant,
- entry: ThinqConfigEntry,
- thinq_api: ThinQApi,
-) -> None:
- """Set up coordinators and register devices."""
- # Get a list of ha bridge.
- try:
- bridge_list = await async_get_ha_bridge_list(thinq_api)
- except ThinQAPIException as exc:
- raise ConfigEntryNotReady(exc.message) from exc
-
- if not bridge_list:
- return
-
- # Setup coordinator per device.
- task_list = [
- hass.async_create_task(async_setup_device_coordinator(hass, bridge))
- for bridge in bridge_list
- ]
- task_result = await asyncio.gather(*task_list)
- for coordinator in task_result:
- entry.runtime_data.coordinators[coordinator.unique_id] = coordinator
-
-
-@callback
-def async_cleanup_device_registry(hass: HomeAssistant, entry: ThinqConfigEntry) -> None:
- """Clean up device registry."""
- new_device_unique_ids = [
- coordinator.unique_id
- for coordinator in entry.runtime_data.coordinators.values()
- ]
- device_registry = dr.async_get(hass)
- existing_entries = dr.async_entries_for_config_entry(
- device_registry, entry.entry_id
- )
-
- # Remove devices that are no longer exist.
- for old_entry in existing_entries:
- old_unique_id = next(iter(old_entry.identifiers))[1]
- if old_unique_id not in new_device_unique_ids:
- device_registry.async_remove_device(old_entry.id)
- _LOGGER.debug("Remove device_registry: device_id=%s", old_entry.id)
-
-
-async def async_setup_mqtt(
- hass: HomeAssistant, entry: ThinqConfigEntry, thinq_api: ThinQApi, client_id: str
-) -> None:
- """Set up MQTT connection."""
- mqtt_client = ThinQMQTT(hass, thinq_api, client_id, entry.runtime_data.coordinators)
- entry.runtime_data.mqtt_client = mqtt_client
-
- # Try to connect.
- result = await mqtt_client.async_connect()
- if not result:
- _LOGGER.error("Failed to set up mqtt connection")
- return
-
- # Ready to subscribe.
- await mqtt_client.async_start_subscribes()
-
- entry.async_on_unload(
- async_track_time_interval(
- hass,
- mqtt_client.async_refresh_subscribe,
- MQTT_SUBSCRIPTION_INTERVAL,
- cancel_on_shutdown=True,
- )
- )
- entry.async_on_unload(
- hass.bus.async_listen_once(
- EVENT_HOMEASSISTANT_STOP, mqtt_client.async_disconnect
- )
- )
-
-
-async def async_unload_entry(hass: HomeAssistant, entry: ThinqConfigEntry) -> bool:
- """Unload the entry."""
- if entry.runtime_data.mqtt_client:
- await entry.runtime_data.mqtt_client.async_disconnect()
-
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/lg_thinq/binary_sensor.py b/homeassistant/components/lg_thinq/binary_sensor.py
deleted file mode 100644
index 845bf8c3079..00000000000
--- a/homeassistant/components/lg_thinq/binary_sensor.py
+++ /dev/null
@@ -1,181 +0,0 @@
-"""Support for binary sensor entities."""
-
-from __future__ import annotations
-
-from dataclasses import dataclass
-import logging
-
-from thinqconnect import DeviceType
-from thinqconnect.devices.const import Property as ThinQProperty
-from thinqconnect.integration import ActiveMode
-
-from homeassistant.components.binary_sensor import (
- BinarySensorDeviceClass,
- BinarySensorEntity,
- BinarySensorEntityDescription,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import ThinqConfigEntry
-from .entity import ThinQEntity
-
-
-@dataclass(frozen=True, kw_only=True)
-class ThinQBinarySensorEntityDescription(BinarySensorEntityDescription):
- """Describes ThinQ sensor entity."""
-
- on_key: str | None = None
-
-
-BINARY_SENSOR_DESC: dict[ThinQProperty, ThinQBinarySensorEntityDescription] = {
- ThinQProperty.RINSE_REFILL: ThinQBinarySensorEntityDescription(
- key=ThinQProperty.RINSE_REFILL,
- translation_key=ThinQProperty.RINSE_REFILL,
- ),
- ThinQProperty.ECO_FRIENDLY_MODE: ThinQBinarySensorEntityDescription(
- key=ThinQProperty.ECO_FRIENDLY_MODE,
- translation_key=ThinQProperty.ECO_FRIENDLY_MODE,
- ),
- ThinQProperty.POWER_SAVE_ENABLED: ThinQBinarySensorEntityDescription(
- key=ThinQProperty.POWER_SAVE_ENABLED,
- translation_key=ThinQProperty.POWER_SAVE_ENABLED,
- ),
- ThinQProperty.REMOTE_CONTROL_ENABLED: ThinQBinarySensorEntityDescription(
- key=ThinQProperty.REMOTE_CONTROL_ENABLED,
- translation_key=ThinQProperty.REMOTE_CONTROL_ENABLED,
- ),
- ThinQProperty.SABBATH_MODE: ThinQBinarySensorEntityDescription(
- key=ThinQProperty.SABBATH_MODE,
- translation_key=ThinQProperty.SABBATH_MODE,
- ),
- ThinQProperty.DOOR_STATE: ThinQBinarySensorEntityDescription(
- key=ThinQProperty.DOOR_STATE,
- device_class=BinarySensorDeviceClass.DOOR,
- on_key="open",
- ),
- ThinQProperty.MACHINE_CLEAN_REMINDER: ThinQBinarySensorEntityDescription(
- key=ThinQProperty.MACHINE_CLEAN_REMINDER,
- translation_key=ThinQProperty.MACHINE_CLEAN_REMINDER,
- on_key="mcreminder_on",
- ),
- ThinQProperty.SIGNAL_LEVEL: ThinQBinarySensorEntityDescription(
- key=ThinQProperty.SIGNAL_LEVEL,
- translation_key=ThinQProperty.SIGNAL_LEVEL,
- on_key="signallevel_on",
- ),
- ThinQProperty.CLEAN_LIGHT_REMINDER: ThinQBinarySensorEntityDescription(
- key=ThinQProperty.CLEAN_LIGHT_REMINDER,
- translation_key=ThinQProperty.CLEAN_LIGHT_REMINDER,
- on_key="cleanlreminder_on",
- ),
- ThinQProperty.HOOD_OPERATION_MODE: ThinQBinarySensorEntityDescription(
- key=ThinQProperty.HOOD_OPERATION_MODE,
- translation_key="operation_mode",
- on_key="power_on",
- ),
- ThinQProperty.WATER_HEATER_OPERATION_MODE: ThinQBinarySensorEntityDescription(
- key=ThinQProperty.WATER_HEATER_OPERATION_MODE,
- translation_key="operation_mode",
- on_key="power_on",
- ),
- ThinQProperty.ONE_TOUCH_FILTER: ThinQBinarySensorEntityDescription(
- key=ThinQProperty.ONE_TOUCH_FILTER,
- translation_key=ThinQProperty.ONE_TOUCH_FILTER,
- on_key="on",
- ),
-}
-
-DEVICE_TYPE_BINARY_SENSOR_MAP: dict[
- DeviceType, tuple[ThinQBinarySensorEntityDescription, ...]
-] = {
- DeviceType.COOKTOP: (BINARY_SENSOR_DESC[ThinQProperty.REMOTE_CONTROL_ENABLED],),
- DeviceType.DISH_WASHER: (
- BINARY_SENSOR_DESC[ThinQProperty.DOOR_STATE],
- BINARY_SENSOR_DESC[ThinQProperty.RINSE_REFILL],
- BINARY_SENSOR_DESC[ThinQProperty.REMOTE_CONTROL_ENABLED],
- BINARY_SENSOR_DESC[ThinQProperty.MACHINE_CLEAN_REMINDER],
- BINARY_SENSOR_DESC[ThinQProperty.SIGNAL_LEVEL],
- BINARY_SENSOR_DESC[ThinQProperty.CLEAN_LIGHT_REMINDER],
- ),
- DeviceType.DRYER: (BINARY_SENSOR_DESC[ThinQProperty.REMOTE_CONTROL_ENABLED],),
- DeviceType.HOOD: (BINARY_SENSOR_DESC[ThinQProperty.HOOD_OPERATION_MODE],),
- DeviceType.OVEN: (BINARY_SENSOR_DESC[ThinQProperty.REMOTE_CONTROL_ENABLED],),
- DeviceType.REFRIGERATOR: (
- BINARY_SENSOR_DESC[ThinQProperty.DOOR_STATE],
- BINARY_SENSOR_DESC[ThinQProperty.ECO_FRIENDLY_MODE],
- BINARY_SENSOR_DESC[ThinQProperty.POWER_SAVE_ENABLED],
- BINARY_SENSOR_DESC[ThinQProperty.SABBATH_MODE],
- ),
- DeviceType.KIMCHI_REFRIGERATOR: (
- BINARY_SENSOR_DESC[ThinQProperty.ONE_TOUCH_FILTER],
- ),
- DeviceType.STYLER: (BINARY_SENSOR_DESC[ThinQProperty.REMOTE_CONTROL_ENABLED],),
- DeviceType.WASHCOMBO_MAIN: (
- BINARY_SENSOR_DESC[ThinQProperty.REMOTE_CONTROL_ENABLED],
- ),
- DeviceType.WASHCOMBO_MINI: (
- BINARY_SENSOR_DESC[ThinQProperty.REMOTE_CONTROL_ENABLED],
- ),
- DeviceType.WASHER: (BINARY_SENSOR_DESC[ThinQProperty.REMOTE_CONTROL_ENABLED],),
- DeviceType.WASHTOWER_DRYER: (
- BINARY_SENSOR_DESC[ThinQProperty.REMOTE_CONTROL_ENABLED],
- ),
- DeviceType.WASHTOWER: (BINARY_SENSOR_DESC[ThinQProperty.REMOTE_CONTROL_ENABLED],),
- DeviceType.WASHTOWER_WASHER: (
- BINARY_SENSOR_DESC[ThinQProperty.REMOTE_CONTROL_ENABLED],
- ),
- DeviceType.WATER_HEATER: (
- BINARY_SENSOR_DESC[ThinQProperty.WATER_HEATER_OPERATION_MODE],
- ),
- DeviceType.WINE_CELLAR: (BINARY_SENSOR_DESC[ThinQProperty.SABBATH_MODE],),
-}
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: ThinqConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up an entry for binary sensor platform."""
- entities: list[ThinQBinarySensorEntity] = []
- for coordinator in entry.runtime_data.coordinators.values():
- if (
- descriptions := DEVICE_TYPE_BINARY_SENSOR_MAP.get(
- coordinator.api.device.device_type
- )
- ) is not None:
- for description in descriptions:
- entities.extend(
- ThinQBinarySensorEntity(coordinator, description, property_id)
- for property_id in coordinator.api.get_active_idx(
- description.key, ActiveMode.READ_ONLY
- )
- )
-
- if entities:
- async_add_entities(entities)
-
-
-class ThinQBinarySensorEntity(ThinQEntity, BinarySensorEntity):
- """Represent a thinq binary sensor platform."""
-
- entity_description: ThinQBinarySensorEntityDescription
-
- def _update_status(self) -> None:
- """Update status itself."""
- super()._update_status()
-
- if (key := self.entity_description.on_key) is not None:
- self._attr_is_on = self.data.value == key
- else:
- self._attr_is_on = self.data.is_on
-
- _LOGGER.debug(
- "[%s:%s] update status: %s -> %s",
- self.coordinator.device_name,
- self.property_id,
- self.data.value,
- self.is_on,
- )
diff --git a/homeassistant/components/lg_thinq/climate.py b/homeassistant/components/lg_thinq/climate.py
deleted file mode 100644
index 9ead57ab7b0..00000000000
--- a/homeassistant/components/lg_thinq/climate.py
+++ /dev/null
@@ -1,334 +0,0 @@
-"""Support for climate entities."""
-
-from __future__ import annotations
-
-from dataclasses import dataclass
-import logging
-from typing import Any
-
-from thinqconnect import DeviceType
-from thinqconnect.integration import ExtendedProperty
-
-from homeassistant.components.climate import (
- ATTR_TARGET_TEMP_HIGH,
- ATTR_TARGET_TEMP_LOW,
- FAN_OFF,
- ClimateEntity,
- ClimateEntityDescription,
- ClimateEntityFeature,
- HVACMode,
-)
-from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.temperature import display_temp
-
-from . import ThinqConfigEntry
-from .coordinator import DeviceDataUpdateCoordinator
-from .entity import ThinQEntity
-
-
-@dataclass(frozen=True, kw_only=True)
-class ThinQClimateEntityDescription(ClimateEntityDescription):
- """Describes ThinQ climate entity."""
-
- min_temp: float | None = None
- max_temp: float | None = None
- step: float | None = None
-
-
-DEVIE_TYPE_CLIMATE_MAP: dict[DeviceType, tuple[ThinQClimateEntityDescription, ...]] = {
- DeviceType.AIR_CONDITIONER: (
- ThinQClimateEntityDescription(
- key=ExtendedProperty.CLIMATE_AIR_CONDITIONER,
- name=None,
- translation_key=ExtendedProperty.CLIMATE_AIR_CONDITIONER,
- ),
- ),
- DeviceType.SYSTEM_BOILER: (
- ThinQClimateEntityDescription(
- key=ExtendedProperty.CLIMATE_SYSTEM_BOILER,
- name=None,
- min_temp=16,
- max_temp=30,
- step=1,
- ),
- ),
-}
-
-STR_TO_HVAC: dict[str, HVACMode] = {
- "air_dry": HVACMode.DRY,
- "auto": HVACMode.AUTO,
- "cool": HVACMode.COOL,
- "fan": HVACMode.FAN_ONLY,
- "heat": HVACMode.HEAT,
-}
-
-HVAC_TO_STR: dict[HVACMode, str] = {
- HVACMode.AUTO: "auto",
- HVACMode.COOL: "cool",
- HVACMode.DRY: "air_dry",
- HVACMode.FAN_ONLY: "fan",
- HVACMode.HEAT: "heat",
-}
-
-THINQ_PRESET_MODE: list[str] = ["air_clean", "aroma", "energy_saving"]
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: ThinqConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up an entry for climate platform."""
- entities: list[ThinQClimateEntity] = []
- for coordinator in entry.runtime_data.coordinators.values():
- if (
- descriptions := DEVIE_TYPE_CLIMATE_MAP.get(
- coordinator.api.device.device_type
- )
- ) is not None:
- for description in descriptions:
- entities.extend(
- ThinQClimateEntity(coordinator, description, property_id)
- for property_id in coordinator.api.get_active_idx(description.key)
- )
-
- if entities:
- async_add_entities(entities)
-
-
-class ThinQClimateEntity(ThinQEntity, ClimateEntity):
- """Represent a thinq climate platform."""
-
- entity_description: ThinQClimateEntityDescription
-
- def __init__(
- self,
- coordinator: DeviceDataUpdateCoordinator,
- entity_description: ThinQClimateEntityDescription,
- property_id: str,
- ) -> None:
- """Initialize a climate entity."""
- super().__init__(coordinator, entity_description, property_id)
-
- self._attr_supported_features = (
- ClimateEntityFeature.TARGET_TEMPERATURE
- | ClimateEntityFeature.TURN_ON
- | ClimateEntityFeature.TURN_OFF
- )
- self._attr_hvac_modes = [HVACMode.OFF]
- self._attr_hvac_mode = HVACMode.OFF
- self._attr_preset_modes = []
- self._attr_temperature_unit = UnitOfTemperature.CELSIUS
- self._requested_hvac_mode: str | None = None
-
- # Set up HVAC modes.
- for mode in self.data.hvac_modes:
- if mode in STR_TO_HVAC:
- self._attr_hvac_modes.append(STR_TO_HVAC[mode])
- elif mode in THINQ_PRESET_MODE:
- self._attr_preset_modes.append(mode)
- self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
-
- # Set up fan modes.
- self._attr_fan_modes = self.data.fan_modes
- if self.fan_modes:
- self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
-
- # Supports target temperature range.
- if self.data.support_temperature_range:
- self._attr_supported_features |= (
- ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
- )
-
- def _update_status(self) -> None:
- """Update status itself."""
- super()._update_status()
-
- # Update fan, hvac and preset mode.
- if self.data.is_on:
- if self.supported_features & ClimateEntityFeature.FAN_MODE:
- self._attr_fan_mode = self.data.fan_mode
-
- hvac_mode = self._requested_hvac_mode or self.data.hvac_mode
- if hvac_mode in STR_TO_HVAC:
- self._attr_hvac_mode = STR_TO_HVAC.get(hvac_mode)
- self._attr_preset_mode = None
- elif hvac_mode in THINQ_PRESET_MODE:
- self._attr_preset_mode = hvac_mode
- else:
- if self.supported_features & ClimateEntityFeature.FAN_MODE:
- self._attr_fan_mode = FAN_OFF
-
- self._attr_hvac_mode = HVACMode.OFF
- self._attr_preset_mode = None
-
- self.reset_requested_hvac_mode()
- self._attr_current_humidity = self.data.humidity
- self._attr_current_temperature = self.data.current_temp
-
- if (max_temp := self.entity_description.max_temp) is not None or (
- max_temp := self.data.max
- ) is not None:
- self._attr_max_temp = max_temp
- if (min_temp := self.entity_description.min_temp) is not None or (
- min_temp := self.data.min
- ) is not None:
- self._attr_min_temp = min_temp
- if (step := self.entity_description.step) is not None or (
- step := self.data.step
- ) is not None:
- self._attr_target_temperature_step = step
-
- # Update target temperatures.
- if (
- self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
- and self.hvac_mode == HVACMode.AUTO
- ):
- self._attr_target_temperature = None
- self._attr_target_temperature_high = self.data.target_temp_high
- self._attr_target_temperature_low = self.data.target_temp_low
- else:
- self._attr_target_temperature = self.data.target_temp
- self._attr_target_temperature_high = None
- self._attr_target_temperature_low = None
-
- _LOGGER.debug(
- "[%s:%s] update status: %s/%s -> %s/%s, hvac:%s, unit:%s, step:%s",
- self.coordinator.device_name,
- self.property_id,
- self.data.current_temp,
- self.data.target_temp,
- self.current_temperature,
- self.target_temperature,
- self.hvac_mode,
- self.temperature_unit,
- self.target_temperature_step,
- )
-
- def reset_requested_hvac_mode(self) -> None:
- """Cancel request to set hvac mode."""
- self._requested_hvac_mode = None
-
- async def async_turn_on(self) -> None:
- """Turn the entity on."""
- _LOGGER.debug(
- "[%s:%s] async_turn_on", self.coordinator.device_name, self.property_id
- )
- await self.async_call_api(self.coordinator.api.async_turn_on(self.property_id))
-
- async def async_turn_off(self) -> None:
- """Turn the entity off."""
- _LOGGER.debug(
- "[%s:%s] async_turn_off", self.coordinator.device_name, self.property_id
- )
- await self.async_call_api(self.coordinator.api.async_turn_off(self.property_id))
-
- async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
- """Set new target hvac mode."""
- if hvac_mode == HVACMode.OFF:
- await self.async_turn_off()
- return
-
- # If device is off, turn on first.
- if not self.data.is_on:
- await self.async_turn_on()
-
- # When we request hvac mode while turning on the device, the previously set
- # hvac mode is displayed first and then switches to the requested hvac mode.
- # To prevent this, set the requested hvac mode here so that it will be set
- # immediately on the next update.
- self._requested_hvac_mode = HVAC_TO_STR.get(hvac_mode)
-
- _LOGGER.debug(
- "[%s:%s] async_set_hvac_mode: %s",
- self.coordinator.device_name,
- self.property_id,
- hvac_mode,
- )
- await self.async_call_api(
- self.coordinator.api.async_set_hvac_mode(
- self.property_id, self._requested_hvac_mode
- ),
- self.reset_requested_hvac_mode,
- )
-
- async def async_set_preset_mode(self, preset_mode: str) -> None:
- """Set new preset mode."""
- _LOGGER.debug(
- "[%s:%s] async_set_preset_mode: %s",
- self.coordinator.device_name,
- self.property_id,
- preset_mode,
- )
- await self.async_call_api(
- self.coordinator.api.async_set_hvac_mode(self.property_id, preset_mode)
- )
-
- async def async_set_fan_mode(self, fan_mode: str) -> None:
- """Set new target fan mode."""
- _LOGGER.debug(
- "[%s:%s] async_set_fan_mode: %s",
- self.coordinator.device_name,
- self.property_id,
- fan_mode,
- )
- await self.async_call_api(
- self.coordinator.api.async_set_fan_mode(self.property_id, fan_mode)
- )
-
- def _round_by_step(self, temperature: float) -> float:
- """Round the value by step."""
- if (
- target_temp := display_temp(
- self.coordinator.hass,
- temperature,
- self.coordinator.hass.config.units.temperature_unit,
- self.target_temperature_step or 1,
- )
- ) is not None:
- return target_temp
-
- return temperature
-
- async def async_set_temperature(self, **kwargs: Any) -> None:
- """Set new target temperature."""
- _LOGGER.debug(
- "[%s:%s] async_set_temperature: %s",
- self.coordinator.device_name,
- self.property_id,
- kwargs,
- )
-
- if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
- if (
- target_temp := self._round_by_step(temperature)
- ) != self.target_temperature:
- await self.async_call_api(
- self.coordinator.api.async_set_target_temperature(
- self.property_id, target_temp
- )
- )
-
- if (temperature_low := kwargs.get(ATTR_TARGET_TEMP_LOW)) is not None:
- if (
- target_temp_low := self._round_by_step(temperature_low)
- ) != self.target_temperature_low:
- await self.async_call_api(
- self.coordinator.api.async_set_target_temperature_low(
- self.property_id, target_temp_low
- )
- )
-
- if (temperature_high := kwargs.get(ATTR_TARGET_TEMP_HIGH)) is not None:
- if (
- target_temp_high := self._round_by_step(temperature_high)
- ) != self.target_temperature_high:
- await self.async_call_api(
- self.coordinator.api.async_set_target_temperature_high(
- self.property_id, target_temp_high
- )
- )
diff --git a/homeassistant/components/lg_thinq/config_flow.py b/homeassistant/components/lg_thinq/config_flow.py
deleted file mode 100644
index cdb41916688..00000000000
--- a/homeassistant/components/lg_thinq/config_flow.py
+++ /dev/null
@@ -1,103 +0,0 @@
-"""Config flow for LG ThinQ."""
-
-from __future__ import annotations
-
-import logging
-from typing import Any
-import uuid
-
-from thinqconnect import ThinQApi, ThinQAPIException
-from thinqconnect.country import Country
-import voluptuous as vol
-
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_ACCESS_TOKEN, CONF_COUNTRY
-from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.selector import CountrySelector, CountrySelectorConfig
-
-from .const import (
- CLIENT_PREFIX,
- CONF_CONNECT_CLIENT_ID,
- DEFAULT_COUNTRY,
- DOMAIN,
- THINQ_DEFAULT_NAME,
- THINQ_PAT_URL,
-)
-
-SUPPORTED_COUNTRIES = [country.value for country in Country]
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class ThinQFlowHandler(ConfigFlow, domain=DOMAIN):
- """Handle a config flow."""
-
- VERSION = 1
-
- def _get_default_country_code(self) -> str:
- """Get the default country code based on config."""
- country = self.hass.config.country
- if country is not None and country in SUPPORTED_COUNTRIES:
- return country
-
- return DEFAULT_COUNTRY
-
- async def _validate_and_create_entry(
- self, access_token: str, country_code: str
- ) -> ConfigFlowResult:
- """Create an entry for the flow."""
- connect_client_id = f"{CLIENT_PREFIX}-{uuid.uuid4()!s}"
-
- # To verify PAT, create an api to retrieve the device list.
- await ThinQApi(
- session=async_get_clientsession(self.hass),
- access_token=access_token,
- country_code=country_code,
- client_id=connect_client_id,
- ).async_get_device_list()
-
- # If verification is success, create entry.
- return self.async_create_entry(
- title=THINQ_DEFAULT_NAME,
- data={
- CONF_ACCESS_TOKEN: access_token,
- CONF_CONNECT_CLIENT_ID: connect_client_id,
- CONF_COUNTRY: country_code,
- },
- )
-
- async def async_step_user(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle a flow initiated by the user."""
- errors: dict[str, str] = {}
-
- if user_input is not None:
- access_token = user_input[CONF_ACCESS_TOKEN]
- country_code = user_input[CONF_COUNTRY]
-
- # Check if PAT is already configured.
- await self.async_set_unique_id(access_token)
- self._abort_if_unique_id_configured()
-
- try:
- return await self._validate_and_create_entry(access_token, country_code)
- except ThinQAPIException:
- errors["base"] = "token_unauthorized"
-
- return self.async_show_form(
- step_id="user",
- data_schema=vol.Schema(
- {
- vol.Required(CONF_ACCESS_TOKEN): cv.string,
- vol.Required(
- CONF_COUNTRY, default=self._get_default_country_code()
- ): CountrySelector(
- CountrySelectorConfig(countries=SUPPORTED_COUNTRIES)
- ),
- }
- ),
- description_placeholders={"pat_url": THINQ_PAT_URL},
- errors=errors,
- )
diff --git a/homeassistant/components/lg_thinq/const.py b/homeassistant/components/lg_thinq/const.py
deleted file mode 100644
index a65dee715db..00000000000
--- a/homeassistant/components/lg_thinq/const.py
+++ /dev/null
@@ -1,20 +0,0 @@
-"""Constants for LG ThinQ."""
-
-from datetime import timedelta
-from typing import Final
-
-# Config flow
-DOMAIN = "lg_thinq"
-COMPANY = "LGE"
-DEFAULT_COUNTRY: Final = "US"
-THINQ_DEFAULT_NAME: Final = "LG ThinQ"
-THINQ_PAT_URL: Final = "https://connect-pat.lgthinq.com"
-CLIENT_PREFIX: Final = "home-assistant"
-CONF_CONNECT_CLIENT_ID: Final = "connect_client_id"
-
-# MQTT
-MQTT_SUBSCRIPTION_INTERVAL: Final = timedelta(days=1)
-
-# MQTT: Message types
-DEVICE_PUSH_MESSAGE: Final = "DEVICE_PUSH"
-DEVICE_STATUS_MESSAGE: Final = "DEVICE_STATUS"
diff --git a/homeassistant/components/lg_thinq/coordinator.py b/homeassistant/components/lg_thinq/coordinator.py
deleted file mode 100644
index 0ba859b1228..00000000000
--- a/homeassistant/components/lg_thinq/coordinator.py
+++ /dev/null
@@ -1,81 +0,0 @@
-"""DataUpdateCoordinator for the LG ThinQ device."""
-
-from __future__ import annotations
-
-import logging
-from typing import Any
-
-from thinqconnect import ThinQAPIException
-from thinqconnect.integration import HABridge
-
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-
-from .const import DOMAIN
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
- """LG Device's Data Update Coordinator."""
-
- def __init__(self, hass: HomeAssistant, ha_bridge: HABridge) -> None:
- """Initialize data coordinator."""
- super().__init__(
- hass,
- _LOGGER,
- name=f"{DOMAIN}_{ha_bridge.device.device_id}",
- )
-
- self.data = {}
- self.api = ha_bridge
- self.device_id = ha_bridge.device.device_id
- self.sub_id = ha_bridge.sub_id
-
- alias = ha_bridge.device.alias
-
- # The device name is usually set to 'alias'.
- # But, if the sub_id exists, it will be set to 'alias {sub_id}'.
- # e.g. alias='MyWashTower', sub_id='dryer' then 'MyWashTower dryer'.
- self.device_name = f"{alias} {self.sub_id}" if self.sub_id else alias
-
- # The unique id is usually set to 'device_id'.
- # But, if the sub_id exists, it will be set to 'device_id_{sub_id}'.
- # e.g. device_id='TQSXXXX', sub_id='dryer' then 'TQSXXXX_dryer'.
- self.unique_id = (
- f"{self.device_id}_{self.sub_id}" if self.sub_id else self.device_id
- )
-
- async def _async_update_data(self) -> dict[str, Any]:
- """Request to the server to update the status from full response data."""
- try:
- return await self.api.fetch_data()
- except ThinQAPIException as e:
- raise UpdateFailed(e) from e
-
- def refresh_status(self) -> None:
- """Refresh current status."""
- self.async_set_updated_data(self.data)
-
- def handle_update_status(self, status: dict[str, Any]) -> None:
- """Handle the status received from the mqtt connection."""
- data = self.api.update_status(status)
- if data is not None:
- self.async_set_updated_data(data)
-
- def handle_notification_message(self, message: str | None) -> None:
- """Handle the status received from the mqtt connection."""
- data = self.api.update_notification(message)
- if data is not None:
- self.async_set_updated_data(data)
-
-
-async def async_setup_device_coordinator(
- hass: HomeAssistant, ha_bridge: HABridge
-) -> DeviceDataUpdateCoordinator:
- """Create DeviceDataUpdateCoordinator and device_api per device."""
- coordinator = DeviceDataUpdateCoordinator(hass, ha_bridge)
- await coordinator.async_refresh()
-
- _LOGGER.debug("Setup device's coordinator: %s", coordinator.device_name)
- return coordinator
diff --git a/homeassistant/components/lg_thinq/entity.py b/homeassistant/components/lg_thinq/entity.py
deleted file mode 100644
index f31b535dcaf..00000000000
--- a/homeassistant/components/lg_thinq/entity.py
+++ /dev/null
@@ -1,114 +0,0 @@
-"""Base class for ThinQ entities."""
-
-from __future__ import annotations
-
-from collections.abc import Callable, Coroutine
-import logging
-from typing import Any
-
-from thinqconnect import ThinQAPIException
-from thinqconnect.devices.const import Location
-from thinqconnect.integration import PropertyState
-
-from homeassistant.const import UnitOfTemperature
-from homeassistant.core import callback
-from homeassistant.exceptions import ServiceValidationError
-from homeassistant.helpers import device_registry as dr
-from homeassistant.helpers.entity import EntityDescription
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from .const import COMPANY, DOMAIN
-from .coordinator import DeviceDataUpdateCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-EMPTY_STATE = PropertyState()
-
-UNIT_CONVERSION_MAP: dict[str, str] = {
- "F": UnitOfTemperature.FAHRENHEIT,
- "C": UnitOfTemperature.CELSIUS,
-}
-
-
-class ThinQEntity(CoordinatorEntity[DeviceDataUpdateCoordinator]):
- """The base implementation of all lg thinq entities."""
-
- _attr_has_entity_name = True
-
- def __init__(
- self,
- coordinator: DeviceDataUpdateCoordinator,
- entity_description: EntityDescription,
- property_id: str,
- ) -> None:
- """Initialize an entity."""
- super().__init__(coordinator)
-
- self.entity_description = entity_description
- self.property_id = property_id
- self.location = self.coordinator.api.get_location_for_idx(self.property_id)
-
- self._attr_device_info = dr.DeviceInfo(
- identifiers={(DOMAIN, coordinator.unique_id)},
- manufacturer=COMPANY,
- model=coordinator.api.device.model_name,
- name=coordinator.device_name,
- )
- self._attr_unique_id = f"{coordinator.unique_id}_{self.property_id}"
- if self.location is not None and self.location not in (
- Location.MAIN,
- Location.OVEN,
- coordinator.sub_id,
- ):
- self._attr_translation_placeholders = {"location": self.location}
- self._attr_translation_key = (
- f"{entity_description.translation_key}_for_location"
- )
-
- @property
- def data(self) -> PropertyState:
- """Return the state data of entity."""
- return self.coordinator.data.get(self.property_id, EMPTY_STATE)
-
- def _get_unit_of_measurement(self, unit: str | None) -> str | None:
- """Convert thinq unit string to HA unit string."""
- if unit is None:
- return None
-
- return UNIT_CONVERSION_MAP.get(unit)
-
- def _update_status(self) -> None:
- """Update status itself.
-
- All inherited classes can update their own status in here.
- """
-
- @callback
- def _handle_coordinator_update(self) -> None:
- """Handle updated data from the coordinator."""
- self._update_status()
- self.async_write_ha_state()
-
- async def async_added_to_hass(self) -> None:
- """Call when entity is added to hass."""
- await super().async_added_to_hass()
- self._handle_coordinator_update()
-
- async def async_call_api(
- self,
- target: Coroutine[Any, Any, Any],
- on_fail_method: Callable[[], None] | None = None,
- ) -> None:
- """Call the given api and handle exception."""
- try:
- await target
- except ThinQAPIException as exc:
- if on_fail_method:
- on_fail_method()
- raise ServiceValidationError(
- exc.message, translation_domain=DOMAIN, translation_key=exc.code
- ) from exc
- except ValueError as exc:
- if on_fail_method:
- on_fail_method()
- raise ServiceValidationError(exc) from exc
diff --git a/homeassistant/components/lg_thinq/event.py b/homeassistant/components/lg_thinq/event.py
deleted file mode 100644
index b963cba37cc..00000000000
--- a/homeassistant/components/lg_thinq/event.py
+++ /dev/null
@@ -1,115 +0,0 @@
-"""Support for event entity."""
-
-from __future__ import annotations
-
-import logging
-
-from thinqconnect import DeviceType
-from thinqconnect.integration import ActiveMode, ThinQPropertyEx
-
-from homeassistant.components.event import EventEntity, EventEntityDescription
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import ThinqConfigEntry
-from .coordinator import DeviceDataUpdateCoordinator
-from .entity import ThinQEntity
-
-NOTIFICATION_EVENT_DESC = EventEntityDescription(
- key=ThinQPropertyEx.NOTIFICATION,
- translation_key=ThinQPropertyEx.NOTIFICATION,
-)
-ERROR_EVENT_DESC = EventEntityDescription(
- key=ThinQPropertyEx.ERROR,
- translation_key=ThinQPropertyEx.ERROR,
-)
-ALL_EVENTS: tuple[EventEntityDescription, ...] = (
- ERROR_EVENT_DESC,
- NOTIFICATION_EVENT_DESC,
-)
-DEVICE_TYPE_EVENT_MAP: dict[DeviceType, tuple[EventEntityDescription, ...]] = {
- DeviceType.AIR_CONDITIONER: (NOTIFICATION_EVENT_DESC,),
- DeviceType.AIR_PURIFIER_FAN: (NOTIFICATION_EVENT_DESC,),
- DeviceType.AIR_PURIFIER: (NOTIFICATION_EVENT_DESC,),
- DeviceType.DEHUMIDIFIER: (NOTIFICATION_EVENT_DESC,),
- DeviceType.DISH_WASHER: ALL_EVENTS,
- DeviceType.DRYER: ALL_EVENTS,
- DeviceType.HUMIDIFIER: (NOTIFICATION_EVENT_DESC,),
- DeviceType.KIMCHI_REFRIGERATOR: (NOTIFICATION_EVENT_DESC,),
- DeviceType.MICROWAVE_OVEN: (NOTIFICATION_EVENT_DESC,),
- DeviceType.OVEN: (NOTIFICATION_EVENT_DESC,),
- DeviceType.REFRIGERATOR: (NOTIFICATION_EVENT_DESC,),
- DeviceType.ROBOT_CLEANER: ALL_EVENTS,
- DeviceType.STICK_CLEANER: (NOTIFICATION_EVENT_DESC,),
- DeviceType.STYLER: ALL_EVENTS,
- DeviceType.WASHCOMBO_MAIN: ALL_EVENTS,
- DeviceType.WASHCOMBO_MINI: ALL_EVENTS,
- DeviceType.WASHER: ALL_EVENTS,
- DeviceType.WASHTOWER_DRYER: ALL_EVENTS,
- DeviceType.WASHTOWER: ALL_EVENTS,
- DeviceType.WASHTOWER_WASHER: ALL_EVENTS,
- DeviceType.WINE_CELLAR: (NOTIFICATION_EVENT_DESC,),
-}
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: ThinqConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up an entry for event platform."""
- entities: list[ThinQEventEntity] = []
- for coordinator in entry.runtime_data.coordinators.values():
- if (
- descriptions := DEVICE_TYPE_EVENT_MAP.get(
- coordinator.api.device.device_type
- )
- ) is not None:
- for description in descriptions:
- entities.extend(
- ThinQEventEntity(coordinator, description, property_id)
- for property_id in coordinator.api.get_active_idx(
- description.key, ActiveMode.READ_ONLY
- )
- )
-
- if entities:
- async_add_entities(entities)
-
-
-class ThinQEventEntity(ThinQEntity, EventEntity):
- """Represent an thinq event platform."""
-
- def __init__(
- self,
- coordinator: DeviceDataUpdateCoordinator,
- entity_description: EventEntityDescription,
- property_id: str,
- ) -> None:
- """Initialize an event platform."""
- super().__init__(coordinator, entity_description, property_id)
-
- # For event types.
- self._attr_event_types = self.data.options
-
- def _update_status(self) -> None:
- """Update status itself."""
- super()._update_status()
-
- _LOGGER.debug(
- "[%s:%s] update status: %s, event_types=%s",
- self.coordinator.device_name,
- self.property_id,
- self.data.value,
- self.event_types,
- )
- # Handle an event.
- if (value := self.data.value) is not None and value in self.event_types:
- self._async_handle_update(value)
-
- def _async_handle_update(self, value: str) -> None:
- """Handle the event."""
- self._trigger_event(value)
- self.async_write_ha_state()
diff --git a/homeassistant/components/lg_thinq/fan.py b/homeassistant/components/lg_thinq/fan.py
deleted file mode 100644
index edcadf2598a..00000000000
--- a/homeassistant/components/lg_thinq/fan.py
+++ /dev/null
@@ -1,153 +0,0 @@
-"""Support for fan entities."""
-
-from __future__ import annotations
-
-import logging
-from typing import Any
-
-from thinqconnect import DeviceType
-from thinqconnect.integration import ExtendedProperty
-
-from homeassistant.components.fan import (
- FanEntity,
- FanEntityDescription,
- FanEntityFeature,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.util.percentage import (
- ordered_list_item_to_percentage,
- percentage_to_ordered_list_item,
-)
-
-from . import ThinqConfigEntry
-from .coordinator import DeviceDataUpdateCoordinator
-from .entity import ThinQEntity
-
-DEVICE_TYPE_FAN_MAP: dict[DeviceType, tuple[FanEntityDescription, ...]] = {
- DeviceType.CEILING_FAN: (
- FanEntityDescription(
- key=ExtendedProperty.FAN,
- name=None,
- ),
- ),
-}
-
-FOUR_STEP_SPEEDS = ["low", "mid", "high", "turbo"]
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: ThinqConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up an entry for fan platform."""
- entities: list[ThinQFanEntity] = []
- for coordinator in entry.runtime_data.coordinators.values():
- if (
- descriptions := DEVICE_TYPE_FAN_MAP.get(coordinator.api.device.device_type)
- ) is not None:
- for description in descriptions:
- entities.extend(
- ThinQFanEntity(coordinator, description, property_id)
- for property_id in coordinator.api.get_active_idx(description.key)
- )
-
- if entities:
- async_add_entities(entities)
-
-
-class ThinQFanEntity(ThinQEntity, FanEntity):
- """Represent a thinq fan platform."""
-
- def __init__(
- self,
- coordinator: DeviceDataUpdateCoordinator,
- entity_description: FanEntityDescription,
- property_id: str,
- ) -> None:
- """Initialize fan platform."""
- super().__init__(coordinator, entity_description, property_id)
-
- self._ordered_named_fan_speeds = []
- self._attr_supported_features = (
- FanEntityFeature.SET_SPEED
- | FanEntityFeature.TURN_ON
- | FanEntityFeature.TURN_OFF
- )
- if (fan_modes := self.data.fan_modes) is not None:
- self._attr_speed_count = len(fan_modes)
- if self.speed_count == 4:
- self._ordered_named_fan_speeds = FOUR_STEP_SPEEDS
-
- def _update_status(self) -> None:
- """Update status itself."""
- super()._update_status()
-
- # Update power on state.
- self._attr_is_on = self.data.is_on
-
- # Update fan speed.
- if (
- self.data.is_on
- and (mode := self.data.fan_mode) in self._ordered_named_fan_speeds
- ):
- self._attr_percentage = ordered_list_item_to_percentage(
- self._ordered_named_fan_speeds, mode
- )
- else:
- self._attr_percentage = 0
-
- _LOGGER.debug(
- "[%s:%s] update status: %s -> %s (percentage=%s)",
- self.coordinator.device_name,
- self.property_id,
- self.data.is_on,
- self.is_on,
- self.percentage,
- )
-
- async def async_set_percentage(self, percentage: int) -> None:
- """Set the speed percentage of the fan."""
- if percentage == 0:
- await self.async_turn_off()
- return
- try:
- value = percentage_to_ordered_list_item(
- self._ordered_named_fan_speeds, percentage
- )
- except ValueError:
- _LOGGER.exception("Failed to async_set_percentage")
- return
-
- _LOGGER.debug(
- "[%s:%s] async_set_percentage. percentage=%s, value=%s",
- self.coordinator.device_name,
- self.property_id,
- percentage,
- value,
- )
- await self.async_call_api(
- self.coordinator.api.async_set_fan_mode(self.property_id, value)
- )
-
- async def async_turn_on(
- self,
- percentage: int | None = None,
- preset_mode: str | None = None,
- **kwargs: Any,
- ) -> None:
- """Turn on the fan."""
- _LOGGER.debug(
- "[%s:%s] async_turn_on", self.coordinator.device_name, self.property_id
- )
- await self.async_call_api(self.coordinator.api.async_turn_on(self.property_id))
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn the fan off."""
- _LOGGER.debug(
- "[%s:%s] async_turn_off", self.coordinator.device_name, self.property_id
- )
- await self.async_call_api(self.coordinator.api.async_turn_off(self.property_id))
diff --git a/homeassistant/components/lg_thinq/icons.json b/homeassistant/components/lg_thinq/icons.json
deleted file mode 100644
index 87cf04e0c1a..00000000000
--- a/homeassistant/components/lg_thinq/icons.json
+++ /dev/null
@@ -1,407 +0,0 @@
-{
- "entity": {
- "switch": {
- "auto_mode": {
- "default": "mdi:cogs"
- },
- "express_mode": {
- "default": "mdi:snowflake-variant"
- },
- "hot_water_mode": {
- "default": "mdi:list-status"
- },
- "humidity_warm_mode": {
- "default": "mdi:heat-wave"
- },
- "hygiene_dry_mode": {
- "default": "mdi:format-list-bulleted"
- },
- "mood_lamp_state": {
- "default": "mdi:lamp"
- },
- "operation_power": {
- "default": "mdi:power"
- },
- "optimal_humidity": {
- "default": "mdi:water-percent"
- },
- "power_save_enabled": {
- "default": "mdi:hydro-power"
- },
- "rapid_freeze": {
- "default": "mdi:snowflake"
- },
- "sleep_mode": {
- "default": "mdi:format-list-bulleted"
- },
- "uv_nano": {
- "default": "mdi:air-filter"
- },
- "warm_mode": {
- "default": "mdi:heat-wave"
- }
- },
- "binary_sensor": {
- "eco_friendly_mode": {
- "default": "mdi:sprout"
- },
- "power_save_enabled": {
- "default": "mdi:meter-electric"
- },
- "remote_control_enabled": {
- "default": "mdi:remote"
- },
- "remote_control_enabled_for_location": {
- "default": "mdi:remote"
- },
- "rinse_refill": {
- "default": "mdi:tune-vertical-variant"
- },
- "sabbath_mode": {
- "default": "mdi:food-off-outline"
- },
- "machine_clean_reminder": {
- "default": "mdi:tune-vertical-variant"
- },
- "signal_level": {
- "default": "mdi:tune-vertical-variant"
- },
- "clean_light_reminder": {
- "default": "mdi:tune-vertical-variant"
- },
- "operation_mode": {
- "default": "mdi:power"
- },
- "one_touch_filter": {
- "default": "mdi:air-filter"
- }
- },
- "climate": {
- "climate_air_conditioner": {
- "state_attributes": {
- "fan_mode": {
- "state": {
- "slow": "mdi:fan-chevron-down",
- "low": "mdi:fan-speed-1",
- "mid": "mdi:fan-speed-2",
- "high": "mdi:fan-speed-3",
- "power": "mdi:fan-chevron-up",
- "auto": "mdi:fan-auto"
- }
- }
- }
- }
- },
- "event": {
- "error": {
- "default": "mdi:alert-circle-outline"
- },
- "notification": {
- "default": "mdi:message-badge-outline"
- }
- },
- "number": {
- "target_temperature": {
- "default": "mdi:thermometer"
- },
- "target_temperature_for_location": {
- "default": "mdi:thermometer"
- },
- "light_status": {
- "default": "mdi:television-ambient-light"
- },
- "fan_speed": {
- "default": "mdi:wind-power-outline"
- },
- "lamp_brightness": {
- "default": "mdi:alarm-light-outline"
- },
- "wind_temperature": {
- "default": "mdi:thermometer"
- },
- "relative_hour_to_start": {
- "default": "mdi:timer-edit-outline"
- },
- "relative_hour_to_start_for_location": {
- "default": "mdi:timer-edit-outline"
- },
- "relative_hour_to_start_wm": {
- "default": "mdi:timer-edit-outline"
- },
- "relative_hour_to_start_wm_for_location": {
- "default": "mdi:timer-edit-outline"
- },
- "relative_hour_to_stop": {
- "default": "mdi:timer-edit-outline"
- },
- "relative_hour_to_stop_for_location": {
- "default": "mdi:timer-edit-outline"
- },
- "relative_hour_to_stop_wm": {
- "default": "mdi:timer-edit-outline"
- },
- "relative_hour_to_stop_wm_for_location": {
- "default": "mdi:timer-edit-outline"
- },
- "sleep_timer_relative_hour_to_stop": {
- "default": "mdi:bed-clock"
- },
- "sleep_timer_relative_hour_to_stop_for_location": {
- "default": "mdi:bed-clock"
- }
- },
- "select": {
- "wind_strength": {
- "default": "mdi:wind-power-outline"
- },
- "monitoring_enabled": {
- "default": "mdi:monitor-eye"
- },
- "current_job_mode": {
- "default": "mdi:format-list-bulleted"
- },
- "operation_mode": {
- "default": "mdi:gesture-tap-button"
- },
- "operation_mode_for_location": {
- "default": "mdi:gesture-tap-button"
- },
- "air_clean_operation_mode": {
- "default": "mdi:air-filter"
- },
- "cook_mode": {
- "default": "mdi:chef-hat"
- },
- "cook_mode_for_location": {
- "default": "mdi:chef-hat"
- },
- "light_brightness": {
- "default": "mdi:list-status"
- },
- "wind_angle": {
- "default": "mdi:rotate-360"
- },
- "display_light": {
- "default": "mdi:brightness-6"
- },
- "fresh_air_filter": {
- "default": "mdi:air-filter"
- },
- "hygiene_dry_mode": {
- "default": "mdi:format-list-bulleted"
- }
- },
- "sensor": {
- "odor_level": {
- "default": "mdi:scent"
- },
- "current_temperature": {
- "default": "mdi:thermometer"
- },
- "temperature": {
- "default": "mdi:thermometer"
- },
- "total_pollution_level": {
- "default": "mdi:air-filter"
- },
- "monitoring_enabled": {
- "default": "mdi:monitor-eye"
- },
- "growth_mode": {
- "default": "mdi:sprout-outline"
- },
- "growth_mode_for_location": {
- "default": "mdi:sprout-outline"
- },
- "wind_volume": {
- "default": "mdi:wind-power-outline"
- },
- "wind_volume_for_location": {
- "default": "mdi:wind-power-outline"
- },
- "brightness": {
- "default": "mdi:tune-vertical-variant"
- },
- "brightness_for_location": {
- "default": "mdi:tune-vertical-variant"
- },
- "duration": {
- "default": "mdi:tune-vertical-variant"
- },
- "duration_for_location": {
- "default": "mdi:tune-vertical-variant"
- },
- "day_target_temperature": {
- "default": "mdi:thermometer"
- },
- "day_target_temperature_for_location": {
- "default": "mdi:thermometer"
- },
- "night_target_temperature": {
- "default": "mdi:thermometer"
- },
- "night_target_temperature_for_location": {
- "default": "mdi:thermometer"
- },
- "temperature_state": {
- "default": "mdi:thermometer"
- },
- "temperature_state_for_location": {
- "default": "mdi:thermometer"
- },
- "current_state": {
- "default": "mdi:list-status"
- },
- "current_state_for_location": {
- "default": "mdi:list-status"
- },
- "fresh_air_filter": {
- "default": "mdi:air-filter"
- },
- "filter_lifetime": {
- "default": "mdi:air-filter"
- },
- "used_time": {
- "default": "mdi:air-filter"
- },
- "current_job_mode": {
- "default": "mdi:dots-circle"
- },
- "current_job_mode_stick_cleaner": {
- "default": "mdi:dots-circle"
- },
- "personalization_mode": {
- "default": "mdi:dots-circle"
- },
- "current_dish_washing_course": {
- "default": "mdi:format-list-checks"
- },
- "rinse_level": {
- "default": "mdi:tune-vertical-variant"
- },
- "softening_level": {
- "default": "mdi:tune-vertical-variant"
- },
- "cock_state": {
- "default": "mdi:air-filter"
- },
- "sterilizing_state": {
- "default": "mdi:water-alert-outline"
- },
- "water_type": {
- "default": "mdi:water"
- },
- "target_temperature": {
- "default": "mdi:thermometer"
- },
- "target_temperature_for_location": {
- "default": "mdi:thermometer"
- },
- "elapsed_day_state": {
- "default": "mdi:calendar-range-outline"
- },
- "elapsed_day_total": {
- "default": "mdi:calendar-range-outline"
- },
- "recipe_name": {
- "default": "mdi:information-box-outline"
- },
- "wort_info": {
- "default": "mdi:information-box-outline"
- },
- "yeast_info": {
- "default": "mdi:information-box-outline"
- },
- "hop_oil_info": {
- "default": "mdi:information-box-outline"
- },
- "flavor_info": {
- "default": "mdi:information-box-outline"
- },
- "beer_remain": {
- "default": "mdi:glass-mug-variant"
- },
- "battery_level": {
- "default": "mdi:battery-medium"
- },
- "relative_to_start": {
- "default": "mdi:clock-time-three-outline"
- },
- "relative_to_start_for_location": {
- "default": "mdi:clock-time-three-outline"
- },
- "relative_to_start_wm": {
- "default": "mdi:clock-time-three-outline"
- },
- "relative_to_start_wm_for_location": {
- "default": "mdi:clock-time-three-outline"
- },
- "relative_to_stop": {
- "default": "mdi:clock-time-three-outline"
- },
- "relative_to_stop_for_location": {
- "default": "mdi:clock-time-three-outline"
- },
- "relative_to_stop_wm": {
- "default": "mdi:clock-time-three-outline"
- },
- "relative_to_stop_wm_for_location": {
- "default": "mdi:clock-time-three-outline"
- },
- "sleep_timer_relative_to_stop": {
- "default": "mdi:bed-clock"
- },
- "sleep_timer_relative_to_stop_for_location": {
- "default": "mdi:bed-clock"
- },
- "absolute_to_start": {
- "default": "mdi:clock-time-three-outline"
- },
- "absolute_to_start_for_location": {
- "default": "mdi:clock-time-three-outline"
- },
- "absolute_to_stop": {
- "default": "mdi:clock-time-three-outline"
- },
- "absolute_to_stop_for_location": {
- "default": "mdi:clock-time-three-outline"
- },
- "remain": {
- "default": "mdi:timer-sand"
- },
- "remain_for_location": {
- "default": "mdi:timer-sand"
- },
- "running": {
- "default": "mdi:timer-play-outline"
- },
- "running_for_location": {
- "default": "mdi:timer-play-outline"
- },
- "total": {
- "default": "mdi:timer-play-outline"
- },
- "total_for_location": {
- "default": "mdi:timer-play-outline"
- },
- "target": {
- "default": "mdi:clock-time-three-outline"
- },
- "target_for_location": {
- "default": "mdi:clock-time-three-outline"
- },
- "light_start": {
- "default": "mdi:clock-time-three-outline"
- },
- "light_start_for_location": {
- "default": "mdi:clock-time-three-outline"
- },
- "power_level": {
- "default": "mdi:radiator"
- },
- "power_level_for_location": {
- "default": "mdi:radiator"
- }
- }
- }
-}
diff --git a/homeassistant/components/lg_thinq/manifest.json b/homeassistant/components/lg_thinq/manifest.json
deleted file mode 100644
index 665a5a9e179..00000000000
--- a/homeassistant/components/lg_thinq/manifest.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "domain": "lg_thinq",
- "name": "LG ThinQ",
- "codeowners": ["@LG-ThinQ-Integration"],
- "config_flow": true,
- "dependencies": [],
- "documentation": "https://www.home-assistant.io/integrations/lg_thinq/",
- "iot_class": "cloud_push",
- "loggers": ["thinqconnect"],
- "requirements": ["thinqconnect==1.0.0"]
-}
diff --git a/homeassistant/components/lg_thinq/mqtt.py b/homeassistant/components/lg_thinq/mqtt.py
deleted file mode 100644
index 30d1302e458..00000000000
--- a/homeassistant/components/lg_thinq/mqtt.py
+++ /dev/null
@@ -1,186 +0,0 @@
-"""Support for LG ThinQ Connect API."""
-
-from __future__ import annotations
-
-import asyncio
-from datetime import datetime
-import json
-import logging
-from typing import Any
-
-from thinqconnect import (
- DeviceType,
- ThinQApi,
- ThinQAPIErrorCodes,
- ThinQAPIException,
- ThinQMQTTClient,
-)
-
-from homeassistant.core import Event, HomeAssistant
-
-from .const import DEVICE_PUSH_MESSAGE, DEVICE_STATUS_MESSAGE
-from .coordinator import DeviceDataUpdateCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class ThinQMQTT:
- """A class that implements MQTT connection."""
-
- def __init__(
- self,
- hass: HomeAssistant,
- thinq_api: ThinQApi,
- client_id: str,
- coordinators: dict[str, DeviceDataUpdateCoordinator],
- ) -> None:
- """Initialize a mqtt."""
- self.hass = hass
- self.thinq_api = thinq_api
- self.client_id = client_id
- self.coordinators = coordinators
- self.client: ThinQMQTTClient | None = None
-
- async def async_connect(self) -> bool:
- """Create a mqtt client and then try to connect."""
- try:
- self.client = await ThinQMQTTClient(
- self.thinq_api, self.client_id, self.on_message_received
- )
- if self.client is None:
- return False
-
- # Connect to server and create certificate.
- return await self.client.async_prepare_mqtt()
- except (ThinQAPIException, TypeError, ValueError):
- _LOGGER.exception("Failed to connect")
- return False
-
- async def async_disconnect(self, event: Event | None = None) -> None:
- """Unregister client and disconnects handlers."""
- await self.async_end_subscribes()
-
- if self.client is not None:
- try:
- await self.client.async_disconnect()
- except (ThinQAPIException, TypeError, ValueError):
- _LOGGER.exception("Failed to disconnect")
-
- def _get_failed_device_count(
- self, results: list[dict | BaseException | None]
- ) -> int:
- """Check if there exists errors while performing tasks and then return count."""
- # Note that result code '1207' means 'Already subscribed push'
- # and is not actually fail.
- return sum(
- isinstance(result, (TypeError, ValueError))
- or (
- isinstance(result, ThinQAPIException)
- and result.code != ThinQAPIErrorCodes.ALREADY_SUBSCRIBED_PUSH
- )
- for result in results
- )
-
- async def async_refresh_subscribe(self, now: datetime | None = None) -> None:
- """Update event subscribes."""
- _LOGGER.debug("async_refresh_subscribe: now=%s", now)
-
- tasks = [
- self.hass.async_create_task(
- self.thinq_api.async_post_event_subscribe(coordinator.device_id)
- )
- for coordinator in self.coordinators.values()
- ]
- if tasks:
- results = await asyncio.gather(*tasks, return_exceptions=True)
- if (count := self._get_failed_device_count(results)) > 0:
- _LOGGER.error("Failed to refresh subscription on %s devices", count)
-
- async def async_start_subscribes(self) -> None:
- """Start push/event subscribes."""
- _LOGGER.debug("async_start_subscribes")
-
- if self.client is None:
- _LOGGER.error("Failed to start subscription: No client")
- return
-
- tasks = [
- self.hass.async_create_task(
- self.thinq_api.async_post_push_subscribe(coordinator.device_id)
- )
- for coordinator in self.coordinators.values()
- ]
- tasks.extend(
- self.hass.async_create_task(
- self.thinq_api.async_post_event_subscribe(coordinator.device_id)
- )
- for coordinator in self.coordinators.values()
- )
- if tasks:
- results = await asyncio.gather(*tasks, return_exceptions=True)
- if (count := self._get_failed_device_count(results)) > 0:
- _LOGGER.error("Failed to start subscription on %s devices", count)
-
- await self.client.async_connect_mqtt()
-
- async def async_end_subscribes(self) -> None:
- """Start push/event unsubscribes."""
- _LOGGER.debug("async_end_subscribes")
-
- tasks = [
- self.hass.async_create_task(
- self.thinq_api.async_delete_push_subscribe(coordinator.device_id)
- )
- for coordinator in self.coordinators.values()
- ]
- tasks.extend(
- self.hass.async_create_task(
- self.thinq_api.async_delete_event_subscribe(coordinator.device_id)
- )
- for coordinator in self.coordinators.values()
- )
- if tasks:
- results = await asyncio.gather(*tasks, return_exceptions=True)
- if (count := self._get_failed_device_count(results)) > 0:
- _LOGGER.error("Failed to end subscription on %s devices", count)
-
- def on_message_received(
- self,
- topic: str,
- payload: bytes,
- dup: bool,
- qos: Any,
- retain: bool,
- **kwargs: dict,
- ) -> None:
- """Handle the received message that matching the topic."""
- decoded = payload.decode()
- try:
- message = json.loads(decoded)
- except ValueError:
- _LOGGER.error("Failed to parse message: payload=%s", decoded)
- return
-
- asyncio.run_coroutine_threadsafe(
- self.async_handle_device_event(message), self.hass.loop
- ).result()
-
- async def async_handle_device_event(self, message: dict) -> None:
- """Handle received mqtt message."""
- _LOGGER.debug("async_handle_device_event: message=%s", message)
- unique_id = (
- f"{message["deviceId"]}_{list(message["report"].keys())[0]}"
- if message["deviceType"] == DeviceType.WASHTOWER
- else message["deviceId"]
- )
- coordinator = self.coordinators.get(unique_id)
- if coordinator is None:
- _LOGGER.error("Failed to handle device event: No device")
- return
-
- push_type = message.get("pushType")
-
- if push_type == DEVICE_STATUS_MESSAGE:
- coordinator.handle_update_status(message.get("report", {}))
- elif push_type == DEVICE_PUSH_MESSAGE:
- coordinator.handle_notification_message(message.get("pushCode"))
diff --git a/homeassistant/components/lg_thinq/number.py b/homeassistant/components/lg_thinq/number.py
deleted file mode 100644
index 634c1a8fe84..00000000000
--- a/homeassistant/components/lg_thinq/number.py
+++ /dev/null
@@ -1,224 +0,0 @@
-"""Support for number entities."""
-
-from __future__ import annotations
-
-import logging
-
-from thinqconnect import DeviceType
-from thinqconnect.devices.const import Property as ThinQProperty
-from thinqconnect.integration import ActiveMode, TimerProperty
-
-from homeassistant.components.number import (
- NumberDeviceClass,
- NumberEntity,
- NumberEntityDescription,
- NumberMode,
-)
-from homeassistant.const import PERCENTAGE, UnitOfTemperature, UnitOfTime
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import ThinqConfigEntry
-from .entity import ThinQEntity
-
-NUMBER_DESC: dict[ThinQProperty, NumberEntityDescription] = {
- ThinQProperty.FAN_SPEED: NumberEntityDescription(
- key=ThinQProperty.FAN_SPEED,
- translation_key=ThinQProperty.FAN_SPEED,
- ),
- ThinQProperty.LAMP_BRIGHTNESS: NumberEntityDescription(
- key=ThinQProperty.LAMP_BRIGHTNESS,
- translation_key=ThinQProperty.LAMP_BRIGHTNESS,
- ),
- ThinQProperty.LIGHT_STATUS: NumberEntityDescription(
- key=ThinQProperty.LIGHT_STATUS,
- native_unit_of_measurement=PERCENTAGE,
- translation_key=ThinQProperty.LIGHT_STATUS,
- ),
- ThinQProperty.TARGET_HUMIDITY: NumberEntityDescription(
- key=ThinQProperty.TARGET_HUMIDITY,
- device_class=NumberDeviceClass.HUMIDITY,
- native_unit_of_measurement=PERCENTAGE,
- translation_key=ThinQProperty.TARGET_HUMIDITY,
- ),
- ThinQProperty.TARGET_TEMPERATURE: NumberEntityDescription(
- key=ThinQProperty.TARGET_TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- translation_key=ThinQProperty.TARGET_TEMPERATURE,
- ),
- ThinQProperty.WIND_TEMPERATURE: NumberEntityDescription(
- key=ThinQProperty.WIND_TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- translation_key=ThinQProperty.WIND_TEMPERATURE,
- ),
-}
-TIMER_NUMBER_DESC: dict[ThinQProperty, NumberEntityDescription] = {
- ThinQProperty.RELATIVE_HOUR_TO_START: NumberEntityDescription(
- key=ThinQProperty.RELATIVE_HOUR_TO_START,
- native_unit_of_measurement=UnitOfTime.HOURS,
- translation_key=ThinQProperty.RELATIVE_HOUR_TO_START,
- ),
- TimerProperty.RELATIVE_HOUR_TO_START_WM: NumberEntityDescription(
- key=ThinQProperty.RELATIVE_HOUR_TO_START,
- native_min_value=0,
- native_unit_of_measurement=UnitOfTime.HOURS,
- translation_key=TimerProperty.RELATIVE_HOUR_TO_START_WM,
- ),
- ThinQProperty.RELATIVE_HOUR_TO_STOP: NumberEntityDescription(
- key=ThinQProperty.RELATIVE_HOUR_TO_STOP,
- native_unit_of_measurement=UnitOfTime.HOURS,
- translation_key=ThinQProperty.RELATIVE_HOUR_TO_STOP,
- ),
- TimerProperty.RELATIVE_HOUR_TO_STOP_WM: NumberEntityDescription(
- key=ThinQProperty.RELATIVE_HOUR_TO_STOP,
- native_min_value=0,
- native_unit_of_measurement=UnitOfTime.HOURS,
- translation_key=TimerProperty.RELATIVE_HOUR_TO_STOP_WM,
- ),
- ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP: NumberEntityDescription(
- key=ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP,
- native_unit_of_measurement=UnitOfTime.HOURS,
- translation_key=ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP,
- ),
-}
-WASHER_NUMBERS: tuple[NumberEntityDescription, ...] = (
- TIMER_NUMBER_DESC[TimerProperty.RELATIVE_HOUR_TO_START_WM],
- TIMER_NUMBER_DESC[TimerProperty.RELATIVE_HOUR_TO_STOP_WM],
-)
-
-DEVICE_TYPE_NUMBER_MAP: dict[DeviceType, tuple[NumberEntityDescription, ...]] = {
- DeviceType.AIR_CONDITIONER: (
- TIMER_NUMBER_DESC[ThinQProperty.RELATIVE_HOUR_TO_START],
- TIMER_NUMBER_DESC[ThinQProperty.RELATIVE_HOUR_TO_STOP],
- TIMER_NUMBER_DESC[ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP],
- ),
- DeviceType.AIR_PURIFIER_FAN: (
- NUMBER_DESC[ThinQProperty.WIND_TEMPERATURE],
- TIMER_NUMBER_DESC[ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP],
- ),
- DeviceType.DRYER: WASHER_NUMBERS,
- DeviceType.HOOD: (
- NUMBER_DESC[ThinQProperty.LAMP_BRIGHTNESS],
- NUMBER_DESC[ThinQProperty.FAN_SPEED],
- ),
- DeviceType.HUMIDIFIER: (
- NUMBER_DESC[ThinQProperty.TARGET_HUMIDITY],
- TIMER_NUMBER_DESC[ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP],
- ),
- DeviceType.MICROWAVE_OVEN: (
- NUMBER_DESC[ThinQProperty.LAMP_BRIGHTNESS],
- NUMBER_DESC[ThinQProperty.FAN_SPEED],
- ),
- DeviceType.OVEN: (NUMBER_DESC[ThinQProperty.TARGET_TEMPERATURE],),
- DeviceType.REFRIGERATOR: (NUMBER_DESC[ThinQProperty.TARGET_TEMPERATURE],),
- DeviceType.STYLER: (TIMER_NUMBER_DESC[TimerProperty.RELATIVE_HOUR_TO_STOP_WM],),
- DeviceType.WASHCOMBO_MAIN: WASHER_NUMBERS,
- DeviceType.WASHCOMBO_MINI: WASHER_NUMBERS,
- DeviceType.WASHER: WASHER_NUMBERS,
- DeviceType.WASHTOWER_DRYER: WASHER_NUMBERS,
- DeviceType.WASHTOWER: WASHER_NUMBERS,
- DeviceType.WASHTOWER_WASHER: WASHER_NUMBERS,
- DeviceType.WATER_HEATER: (
- NumberEntityDescription(
- key=ThinQProperty.TARGET_TEMPERATURE,
- native_max_value=60,
- native_min_value=35,
- native_step=1,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- translation_key=ThinQProperty.TARGET_TEMPERATURE,
- ),
- ),
- DeviceType.WINE_CELLAR: (
- NUMBER_DESC[ThinQProperty.LIGHT_STATUS],
- NUMBER_DESC[ThinQProperty.TARGET_TEMPERATURE],
- ),
-}
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: ThinqConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up an entry for number platform."""
- entities: list[ThinQNumberEntity] = []
- for coordinator in entry.runtime_data.coordinators.values():
- if (
- descriptions := DEVICE_TYPE_NUMBER_MAP.get(
- coordinator.api.device.device_type
- )
- ) is not None:
- for description in descriptions:
- entities.extend(
- ThinQNumberEntity(coordinator, description, property_id)
- for property_id in coordinator.api.get_active_idx(
- description.key, ActiveMode.READ_WRITE
- )
- )
-
- if entities:
- async_add_entities(entities)
-
-
-class ThinQNumberEntity(ThinQEntity, NumberEntity):
- """Represent a thinq number platform."""
-
- _attr_mode = NumberMode.BOX
-
- def _update_status(self) -> None:
- """Update status itself."""
- super()._update_status()
-
- self._attr_native_value = self.data.value
-
- # Update unit.
- if (
- unit_of_measurement := self._get_unit_of_measurement(self.data.unit)
- ) is not None:
- self._attr_native_unit_of_measurement = unit_of_measurement
-
- # Undate range.
- if (
- self.entity_description.native_min_value is None
- and (min_value := self.data.min) is not None
- ):
- self._attr_native_min_value = min_value
-
- if (
- self.entity_description.native_max_value is None
- and (max_value := self.data.max) is not None
- ):
- self._attr_native_max_value = max_value
-
- if (
- self.entity_description.native_step is None
- and (step := self.data.step) is not None
- ):
- self._attr_native_step = step
-
- _LOGGER.debug(
- "[%s:%s] update status: %s -> %s, unit:%s, min:%s, max:%s, step:%s",
- self.coordinator.device_name,
- self.property_id,
- self.data.value,
- self.native_value,
- self.native_unit_of_measurement,
- self.native_min_value,
- self.native_max_value,
- self.native_step,
- )
-
- async def async_set_native_value(self, value: float) -> None:
- """Change to new number value."""
- if self.step.is_integer():
- value = int(value)
- _LOGGER.debug(
- "[%s:%s] async_set_native_value: %s",
- self.coordinator.device_name,
- self.property_id,
- value,
- )
-
- await self.async_call_api(self.coordinator.api.post(self.property_id, value))
diff --git a/homeassistant/components/lg_thinq/select.py b/homeassistant/components/lg_thinq/select.py
deleted file mode 100644
index e555d616ca3..00000000000
--- a/homeassistant/components/lg_thinq/select.py
+++ /dev/null
@@ -1,207 +0,0 @@
-"""Support for select entities."""
-
-from __future__ import annotations
-
-import logging
-
-from thinqconnect import DeviceType
-from thinqconnect.devices.const import Property as ThinQProperty
-from thinqconnect.integration import ActiveMode
-
-from homeassistant.components.select import SelectEntity, SelectEntityDescription
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import ThinqConfigEntry
-from .coordinator import DeviceDataUpdateCoordinator
-from .entity import ThinQEntity
-
-SELECT_DESC: dict[ThinQProperty, SelectEntityDescription] = {
- ThinQProperty.MONITORING_ENABLED: SelectEntityDescription(
- key=ThinQProperty.MONITORING_ENABLED,
- translation_key=ThinQProperty.MONITORING_ENABLED,
- ),
- ThinQProperty.COOK_MODE: SelectEntityDescription(
- key=ThinQProperty.COOK_MODE,
- translation_key=ThinQProperty.COOK_MODE,
- ),
- ThinQProperty.DISPLAY_LIGHT: SelectEntityDescription(
- key=ThinQProperty.DISPLAY_LIGHT,
- translation_key=ThinQProperty.DISPLAY_LIGHT,
- ),
- ThinQProperty.CURRENT_JOB_MODE: SelectEntityDescription(
- key=ThinQProperty.CURRENT_JOB_MODE,
- translation_key=ThinQProperty.CURRENT_JOB_MODE,
- ),
- ThinQProperty.FRESH_AIR_FILTER: SelectEntityDescription(
- key=ThinQProperty.FRESH_AIR_FILTER,
- translation_key=ThinQProperty.FRESH_AIR_FILTER,
- ),
-}
-AIR_FLOW_SELECT_DESC: dict[ThinQProperty, SelectEntityDescription] = {
- ThinQProperty.WIND_STRENGTH: SelectEntityDescription(
- key=ThinQProperty.WIND_STRENGTH,
- translation_key=ThinQProperty.WIND_STRENGTH,
- ),
- ThinQProperty.WIND_ANGLE: SelectEntityDescription(
- key=ThinQProperty.WIND_ANGLE,
- translation_key=ThinQProperty.WIND_ANGLE,
- ),
-}
-OPERATION_SELECT_DESC: dict[ThinQProperty, SelectEntityDescription] = {
- ThinQProperty.AIR_CLEAN_OPERATION_MODE: SelectEntityDescription(
- key=ThinQProperty.AIR_CLEAN_OPERATION_MODE,
- translation_key="air_clean_operation_mode",
- ),
- ThinQProperty.DISH_WASHER_OPERATION_MODE: SelectEntityDescription(
- key=ThinQProperty.DISH_WASHER_OPERATION_MODE,
- translation_key="operation_mode",
- ),
- ThinQProperty.DRYER_OPERATION_MODE: SelectEntityDescription(
- key=ThinQProperty.DRYER_OPERATION_MODE,
- translation_key="operation_mode",
- ),
- ThinQProperty.HYGIENE_DRY_MODE: SelectEntityDescription(
- key=ThinQProperty.HYGIENE_DRY_MODE,
- translation_key=ThinQProperty.HYGIENE_DRY_MODE,
- ),
- ThinQProperty.LIGHT_BRIGHTNESS: SelectEntityDescription(
- key=ThinQProperty.LIGHT_BRIGHTNESS,
- translation_key=ThinQProperty.LIGHT_BRIGHTNESS,
- ),
- ThinQProperty.OVEN_OPERATION_MODE: SelectEntityDescription(
- key=ThinQProperty.OVEN_OPERATION_MODE,
- translation_key="operation_mode",
- ),
- ThinQProperty.STYLER_OPERATION_MODE: SelectEntityDescription(
- key=ThinQProperty.STYLER_OPERATION_MODE,
- translation_key="operation_mode",
- ),
- ThinQProperty.WASHER_OPERATION_MODE: SelectEntityDescription(
- key=ThinQProperty.WASHER_OPERATION_MODE,
- translation_key="operation_mode",
- ),
-}
-
-DEVICE_TYPE_SELECT_MAP: dict[DeviceType, tuple[SelectEntityDescription, ...]] = {
- DeviceType.AIR_CONDITIONER: (
- SELECT_DESC[ThinQProperty.MONITORING_ENABLED],
- OPERATION_SELECT_DESC[ThinQProperty.AIR_CLEAN_OPERATION_MODE],
- ),
- DeviceType.AIR_PURIFIER_FAN: (
- AIR_FLOW_SELECT_DESC[ThinQProperty.WIND_STRENGTH],
- AIR_FLOW_SELECT_DESC[ThinQProperty.WIND_ANGLE],
- SELECT_DESC[ThinQProperty.DISPLAY_LIGHT],
- SELECT_DESC[ThinQProperty.CURRENT_JOB_MODE],
- ),
- DeviceType.AIR_PURIFIER: (
- AIR_FLOW_SELECT_DESC[ThinQProperty.WIND_STRENGTH],
- SELECT_DESC[ThinQProperty.CURRENT_JOB_MODE],
- ),
- DeviceType.DEHUMIDIFIER: (AIR_FLOW_SELECT_DESC[ThinQProperty.WIND_STRENGTH],),
- DeviceType.DISH_WASHER: (
- OPERATION_SELECT_DESC[ThinQProperty.DISH_WASHER_OPERATION_MODE],
- ),
- DeviceType.DRYER: (OPERATION_SELECT_DESC[ThinQProperty.DRYER_OPERATION_MODE],),
- DeviceType.HUMIDIFIER: (
- AIR_FLOW_SELECT_DESC[ThinQProperty.WIND_STRENGTH],
- SELECT_DESC[ThinQProperty.DISPLAY_LIGHT],
- SELECT_DESC[ThinQProperty.CURRENT_JOB_MODE],
- OPERATION_SELECT_DESC[ThinQProperty.HYGIENE_DRY_MODE],
- ),
- DeviceType.OVEN: (
- SELECT_DESC[ThinQProperty.COOK_MODE],
- OPERATION_SELECT_DESC[ThinQProperty.OVEN_OPERATION_MODE],
- ),
- DeviceType.REFRIGERATOR: (SELECT_DESC[ThinQProperty.FRESH_AIR_FILTER],),
- DeviceType.STYLER: (OPERATION_SELECT_DESC[ThinQProperty.STYLER_OPERATION_MODE],),
- DeviceType.WASHCOMBO_MAIN: (
- OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE],
- ),
- DeviceType.WASHCOMBO_MINI: (
- OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE],
- ),
- DeviceType.WASHER: (OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE],),
- DeviceType.WASHTOWER_DRYER: (
- OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE],
- ),
- DeviceType.WASHTOWER: (
- OPERATION_SELECT_DESC[ThinQProperty.DRYER_OPERATION_MODE],
- OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE],
- ),
- DeviceType.WASHTOWER_WASHER: (
- OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE],
- ),
- DeviceType.WATER_HEATER: (SELECT_DESC[ThinQProperty.CURRENT_JOB_MODE],),
- DeviceType.WINE_CELLAR: (OPERATION_SELECT_DESC[ThinQProperty.LIGHT_BRIGHTNESS],),
-}
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: ThinqConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up an entry for select platform."""
- entities: list[ThinQSelectEntity] = []
- for coordinator in entry.runtime_data.coordinators.values():
- if (
- descriptions := DEVICE_TYPE_SELECT_MAP.get(
- coordinator.api.device.device_type
- )
- ) is not None:
- for description in descriptions:
- entities.extend(
- ThinQSelectEntity(coordinator, description, property_id)
- for property_id in coordinator.api.get_active_idx(
- description.key, ActiveMode.WRITABLE
- )
- )
-
- if entities:
- async_add_entities(entities)
-
-
-class ThinQSelectEntity(ThinQEntity, SelectEntity):
- """Represent a thinq select platform."""
-
- def __init__(
- self,
- coordinator: DeviceDataUpdateCoordinator,
- entity_description: SelectEntityDescription,
- property_id: str,
- ) -> None:
- """Initialize a select entity."""
- super().__init__(coordinator, entity_description, property_id)
-
- self._attr_options = self.data.options if self.data.options is not None else []
-
- def _update_status(self) -> None:
- """Update status itself."""
- super()._update_status()
-
- if self.data.value:
- self._attr_current_option = str(self.data.value)
- else:
- self._attr_current_option = None
-
- _LOGGER.debug(
- "[%s:%s] update status: %s -> %s, options:%s",
- self.coordinator.device_name,
- self.property_id,
- self.data.value,
- self.current_option,
- self.options,
- )
-
- async def async_select_option(self, option: str) -> None:
- """Change the selected option."""
- _LOGGER.debug(
- "[%s:%s] async_select_option: %s",
- self.coordinator.device_name,
- self.property_id,
- option,
- )
- await self.async_call_api(self.coordinator.api.post(self.property_id, option))
diff --git a/homeassistant/components/lg_thinq/sensor.py b/homeassistant/components/lg_thinq/sensor.py
deleted file mode 100644
index 99b4df8176e..00000000000
--- a/homeassistant/components/lg_thinq/sensor.py
+++ /dev/null
@@ -1,447 +0,0 @@
-"""Support for sensor entities."""
-
-from __future__ import annotations
-
-import logging
-
-from thinqconnect import DeviceType
-from thinqconnect.devices.const import Property as ThinQProperty
-from thinqconnect.integration import ActiveMode, ThinQPropertyEx, TimerProperty
-
-from homeassistant.components.sensor import (
- SensorDeviceClass,
- SensorEntity,
- SensorEntityDescription,
- SensorStateClass,
-)
-from homeassistant.const import (
- CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
- PERCENTAGE,
- UnitOfTemperature,
- UnitOfTime,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import ThinqConfigEntry
-from .coordinator import DeviceDataUpdateCoordinator
-from .entity import ThinQEntity
-
-AIR_QUALITY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.PM1: SensorEntityDescription(
- key=ThinQProperty.PM1,
- device_class=SensorDeviceClass.PM1,
- native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
- state_class=SensorStateClass.MEASUREMENT,
- ),
- ThinQProperty.PM2: SensorEntityDescription(
- key=ThinQProperty.PM2,
- device_class=SensorDeviceClass.PM25,
- native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
- state_class=SensorStateClass.MEASUREMENT,
- ),
- ThinQProperty.PM10: SensorEntityDescription(
- key=ThinQProperty.PM10,
- device_class=SensorDeviceClass.PM10,
- native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
- state_class=SensorStateClass.MEASUREMENT,
- ),
- ThinQProperty.HUMIDITY: SensorEntityDescription(
- key=ThinQProperty.HUMIDITY,
- device_class=SensorDeviceClass.HUMIDITY,
- native_unit_of_measurement=PERCENTAGE,
- state_class=SensorStateClass.MEASUREMENT,
- ),
- ThinQProperty.MONITORING_ENABLED: SensorEntityDescription(
- key=ThinQProperty.MONITORING_ENABLED,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.MONITORING_ENABLED,
- ),
- ThinQProperty.TEMPERATURE: SensorEntityDescription(
- key=ThinQProperty.TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- state_class=SensorStateClass.MEASUREMENT,
- translation_key=ThinQProperty.TEMPERATURE,
- ),
- ThinQProperty.ODOR_LEVEL: SensorEntityDescription(
- key=ThinQProperty.ODOR_LEVEL,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.ODOR_LEVEL,
- ),
- ThinQProperty.TOTAL_POLLUTION_LEVEL: SensorEntityDescription(
- key=ThinQProperty.TOTAL_POLLUTION_LEVEL,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.TOTAL_POLLUTION_LEVEL,
- ),
-}
-BATTERY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.BATTERY_PERCENT: SensorEntityDescription(
- key=ThinQProperty.BATTERY_PERCENT,
- translation_key=ThinQProperty.BATTERY_LEVEL,
- ),
-}
-DISH_WASHING_COURSE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.CURRENT_DISH_WASHING_COURSE: SensorEntityDescription(
- key=ThinQProperty.CURRENT_DISH_WASHING_COURSE,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.CURRENT_DISH_WASHING_COURSE,
- )
-}
-FILTER_INFO_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.FILTER_LIFETIME: SensorEntityDescription(
- key=ThinQProperty.FILTER_LIFETIME,
- native_unit_of_measurement=UnitOfTime.HOURS,
- translation_key=ThinQProperty.FILTER_LIFETIME,
- ),
-}
-HUMIDITY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.CURRENT_HUMIDITY: SensorEntityDescription(
- key=ThinQProperty.CURRENT_HUMIDITY,
- device_class=SensorDeviceClass.HUMIDITY,
- native_unit_of_measurement=PERCENTAGE,
- state_class=SensorStateClass.MEASUREMENT,
- )
-}
-JOB_MODE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.CURRENT_JOB_MODE: SensorEntityDescription(
- key=ThinQProperty.CURRENT_JOB_MODE,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.CURRENT_JOB_MODE,
- ),
- ThinQPropertyEx.CURRENT_JOB_MODE_STICK_CLEANER: SensorEntityDescription(
- key=ThinQProperty.CURRENT_JOB_MODE,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQPropertyEx.CURRENT_JOB_MODE_STICK_CLEANER,
- ),
- ThinQProperty.PERSONALIZATION_MODE: SensorEntityDescription(
- key=ThinQProperty.PERSONALIZATION_MODE,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.PERSONALIZATION_MODE,
- ),
-}
-LIGHT_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.BRIGHTNESS: SensorEntityDescription(
- key=ThinQProperty.BRIGHTNESS,
- translation_key=ThinQProperty.BRIGHTNESS,
- ),
- ThinQProperty.DURATION: SensorEntityDescription(
- key=ThinQProperty.DURATION,
- native_unit_of_measurement=UnitOfTime.HOURS,
- translation_key=ThinQProperty.DURATION,
- ),
-}
-POWER_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.POWER_LEVEL: SensorEntityDescription(
- key=ThinQProperty.POWER_LEVEL,
- translation_key=ThinQProperty.POWER_LEVEL,
- )
-}
-PREFERENCE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.RINSE_LEVEL: SensorEntityDescription(
- key=ThinQProperty.RINSE_LEVEL,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.RINSE_LEVEL,
- ),
- ThinQProperty.SOFTENING_LEVEL: SensorEntityDescription(
- key=ThinQProperty.SOFTENING_LEVEL,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.SOFTENING_LEVEL,
- ),
-}
-RECIPE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.RECIPE_NAME: SensorEntityDescription(
- key=ThinQProperty.RECIPE_NAME,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.RECIPE_NAME,
- ),
- ThinQProperty.WORT_INFO: SensorEntityDescription(
- key=ThinQProperty.WORT_INFO,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.WORT_INFO,
- ),
- ThinQProperty.YEAST_INFO: SensorEntityDescription(
- key=ThinQProperty.YEAST_INFO,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.YEAST_INFO,
- ),
- ThinQProperty.HOP_OIL_INFO: SensorEntityDescription(
- key=ThinQProperty.HOP_OIL_INFO,
- translation_key=ThinQProperty.HOP_OIL_INFO,
- ),
- ThinQProperty.FLAVOR_INFO: SensorEntityDescription(
- key=ThinQProperty.FLAVOR_INFO,
- translation_key=ThinQProperty.FLAVOR_INFO,
- ),
- ThinQProperty.BEER_REMAIN: SensorEntityDescription(
- key=ThinQProperty.BEER_REMAIN,
- native_unit_of_measurement=PERCENTAGE,
- translation_key=ThinQProperty.BEER_REMAIN,
- ),
-}
-REFRIGERATION_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.FRESH_AIR_FILTER: SensorEntityDescription(
- key=ThinQProperty.FRESH_AIR_FILTER,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.FRESH_AIR_FILTER,
- ),
-}
-RUN_STATE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.CURRENT_STATE: SensorEntityDescription(
- key=ThinQProperty.CURRENT_STATE,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.CURRENT_STATE,
- ),
- ThinQProperty.COCK_STATE: SensorEntityDescription(
- key=ThinQProperty.COCK_STATE,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.COCK_STATE,
- ),
- ThinQProperty.STERILIZING_STATE: SensorEntityDescription(
- key=ThinQProperty.STERILIZING_STATE,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.STERILIZING_STATE,
- ),
- ThinQProperty.GROWTH_MODE: SensorEntityDescription(
- key=ThinQProperty.GROWTH_MODE,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.GROWTH_MODE,
- ),
- ThinQProperty.WIND_VOLUME: SensorEntityDescription(
- key=ThinQProperty.WIND_VOLUME,
- device_class=SensorDeviceClass.WIND_SPEED,
- translation_key=ThinQProperty.WIND_VOLUME,
- ),
-}
-TEMPERATURE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.TARGET_TEMPERATURE: SensorEntityDescription(
- key=ThinQProperty.TARGET_TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- translation_key=ThinQProperty.TARGET_TEMPERATURE,
- ),
- ThinQProperty.DAY_TARGET_TEMPERATURE: SensorEntityDescription(
- key=ThinQProperty.DAY_TARGET_TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- state_class=SensorStateClass.MEASUREMENT,
- translation_key=ThinQProperty.DAY_TARGET_TEMPERATURE,
- ),
- ThinQProperty.NIGHT_TARGET_TEMPERATURE: SensorEntityDescription(
- key=ThinQProperty.NIGHT_TARGET_TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- state_class=SensorStateClass.MEASUREMENT,
- translation_key=ThinQProperty.NIGHT_TARGET_TEMPERATURE,
- ),
- ThinQProperty.TEMPERATURE_STATE: SensorEntityDescription(
- key=ThinQProperty.TEMPERATURE_STATE,
- device_class=SensorDeviceClass.ENUM,
- translation_key=ThinQProperty.TEMPERATURE_STATE,
- ),
- ThinQProperty.CURRENT_TEMPERATURE: SensorEntityDescription(
- key=ThinQProperty.CURRENT_TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- state_class=SensorStateClass.MEASUREMENT,
- translation_key=ThinQProperty.CURRENT_TEMPERATURE,
- ),
-}
-WATER_FILTER_INFO_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.USED_TIME: SensorEntityDescription(
- key=ThinQProperty.USED_TIME,
- native_unit_of_measurement=UnitOfTime.MONTHS,
- translation_key=ThinQProperty.USED_TIME,
- ),
-}
-WATER_INFO_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
- ThinQProperty.WATER_TYPE: SensorEntityDescription(
- key=ThinQProperty.WATER_TYPE,
- translation_key=ThinQProperty.WATER_TYPE,
- ),
-}
-
-WASHER_SENSORS: tuple[SensorEntityDescription, ...] = (
- RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
-)
-DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = {
- DeviceType.AIR_CONDITIONER: (
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL],
- FILTER_INFO_SENSOR_DESC[ThinQProperty.FILTER_LIFETIME],
- ),
- DeviceType.AIR_PURIFIER_FAN: (
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.TEMPERATURE],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL],
- ),
- DeviceType.AIR_PURIFIER: (
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL],
- JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE],
- JOB_MODE_SENSOR_DESC[ThinQProperty.PERSONALIZATION_MODE],
- ),
- DeviceType.COOKTOP: (
- RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
- POWER_SENSOR_DESC[ThinQProperty.POWER_LEVEL],
- ),
- DeviceType.DEHUMIDIFIER: (
- JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE],
- HUMIDITY_SENSOR_DESC[ThinQProperty.CURRENT_HUMIDITY],
- ),
- DeviceType.DISH_WASHER: (
- DISH_WASHING_COURSE_SENSOR_DESC[ThinQProperty.CURRENT_DISH_WASHING_COURSE],
- PREFERENCE_SENSOR_DESC[ThinQProperty.RINSE_LEVEL],
- PREFERENCE_SENSOR_DESC[ThinQProperty.SOFTENING_LEVEL],
- RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
- ),
- DeviceType.DRYER: WASHER_SENSORS,
- DeviceType.HOME_BREW: (
- RECIPE_SENSOR_DESC[ThinQProperty.RECIPE_NAME],
- RECIPE_SENSOR_DESC[ThinQProperty.WORT_INFO],
- RECIPE_SENSOR_DESC[ThinQProperty.YEAST_INFO],
- RECIPE_SENSOR_DESC[ThinQProperty.HOP_OIL_INFO],
- RECIPE_SENSOR_DESC[ThinQProperty.FLAVOR_INFO],
- RECIPE_SENSOR_DESC[ThinQProperty.BEER_REMAIN],
- RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
- ),
- DeviceType.HUMIDIFIER: (
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.TEMPERATURE],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED],
- AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL],
- ),
- DeviceType.KIMCHI_REFRIGERATOR: (
- REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER],
- SensorEntityDescription(
- key=ThinQProperty.TARGET_TEMPERATURE,
- translation_key=ThinQProperty.TARGET_TEMPERATURE,
- ),
- ),
- DeviceType.MICROWAVE_OVEN: (RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],),
- DeviceType.OVEN: (
- RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
- TEMPERATURE_SENSOR_DESC[ThinQProperty.TARGET_TEMPERATURE],
- ),
- DeviceType.PLANT_CULTIVATOR: (
- LIGHT_SENSOR_DESC[ThinQProperty.BRIGHTNESS],
- LIGHT_SENSOR_DESC[ThinQProperty.DURATION],
- RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
- RUN_STATE_SENSOR_DESC[ThinQProperty.GROWTH_MODE],
- RUN_STATE_SENSOR_DESC[ThinQProperty.WIND_VOLUME],
- TEMPERATURE_SENSOR_DESC[ThinQProperty.DAY_TARGET_TEMPERATURE],
- TEMPERATURE_SENSOR_DESC[ThinQProperty.NIGHT_TARGET_TEMPERATURE],
- TEMPERATURE_SENSOR_DESC[ThinQProperty.TEMPERATURE_STATE],
- ),
- DeviceType.REFRIGERATOR: (
- REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER],
- WATER_FILTER_INFO_SENSOR_DESC[ThinQProperty.USED_TIME],
- ),
- DeviceType.ROBOT_CLEANER: (
- RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
- JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE],
- ),
- DeviceType.STICK_CLEANER: (
- BATTERY_SENSOR_DESC[ThinQProperty.BATTERY_PERCENT],
- JOB_MODE_SENSOR_DESC[ThinQPropertyEx.CURRENT_JOB_MODE_STICK_CLEANER],
- RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
- ),
- DeviceType.STYLER: WASHER_SENSORS,
- DeviceType.WASHCOMBO_MAIN: WASHER_SENSORS,
- DeviceType.WASHCOMBO_MINI: WASHER_SENSORS,
- DeviceType.WASHER: WASHER_SENSORS,
- DeviceType.WASHTOWER_DRYER: WASHER_SENSORS,
- DeviceType.WASHTOWER: WASHER_SENSORS,
- DeviceType.WASHTOWER_WASHER: WASHER_SENSORS,
- DeviceType.WATER_HEATER: (
- TEMPERATURE_SENSOR_DESC[ThinQProperty.CURRENT_TEMPERATURE],
- ),
- DeviceType.WATER_PURIFIER: (
- RUN_STATE_SENSOR_DESC[ThinQProperty.COCK_STATE],
- RUN_STATE_SENSOR_DESC[ThinQProperty.STERILIZING_STATE],
- WATER_INFO_SENSOR_DESC[ThinQProperty.WATER_TYPE],
- ),
-}
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: ThinqConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up an entry for sensor platform."""
- entities: list[ThinQSensorEntity] = []
- for coordinator in entry.runtime_data.coordinators.values():
- if (
- descriptions := DEVICE_TYPE_SENSOR_MAP.get(
- coordinator.api.device.device_type
- )
- ) is not None:
- for description in descriptions:
- entities.extend(
- ThinQSensorEntity(coordinator, description, property_id)
- for property_id in coordinator.api.get_active_idx(
- description.key,
- (
- ActiveMode.READABLE
- if (
- coordinator.api.device.device_type == DeviceType.COOKTOP
- or isinstance(description.key, TimerProperty)
- )
- else ActiveMode.READ_ONLY
- ),
- )
- )
-
- if entities:
- async_add_entities(entities)
-
-
-class ThinQSensorEntity(ThinQEntity, SensorEntity):
- """Represent a thinq sensor platform."""
-
- def __init__(
- self,
- coordinator: DeviceDataUpdateCoordinator,
- entity_description: SensorEntityDescription,
- property_id: str,
- ) -> None:
- """Initialize a sensor entity."""
- super().__init__(coordinator, entity_description, property_id)
-
- if entity_description.device_class == SensorDeviceClass.ENUM:
- self._attr_options = self.data.options
-
- def _update_status(self) -> None:
- """Update status itself."""
- super()._update_status()
-
- self._attr_native_value = self.data.value
-
- if (data_unit := self._get_unit_of_measurement(self.data.unit)) is not None:
- # For different from description's unit
- self._attr_native_unit_of_measurement = data_unit
-
- _LOGGER.debug(
- "[%s:%s] update status: %s -> %s, options:%s, unit:%s",
- self.coordinator.device_name,
- self.property_id,
- self.data.value,
- self.native_value,
- self.options,
- self.native_unit_of_measurement,
- )
diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json
deleted file mode 100644
index 277e3db3df0..00000000000
--- a/homeassistant/components/lg_thinq/strings.json
+++ /dev/null
@@ -1,992 +0,0 @@
-{
- "config": {
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
- },
- "error": {
- "token_unauthorized": "The token is invalid or unauthorized."
- },
- "step": {
- "user": {
- "title": "Connect to ThinQ",
- "description": "Please enter a ThinQ [PAT(Personal Access Token)]({pat_url}) created with your LG ThinQ account.",
- "data": {
- "access_token": "Personal Access Token",
- "country": "Country"
- }
- }
- }
- },
- "entity": {
- "switch": {
- "auto_mode": {
- "name": "Auto mode"
- },
- "express_mode": {
- "name": "Ice plus"
- },
- "hot_water_mode": {
- "name": "Hot water"
- },
- "humidity_warm_mode": {
- "name": "Warm mist"
- },
- "hygiene_dry_mode": {
- "name": "Drying mode"
- },
- "mood_lamp_state": {
- "name": "Mood light"
- },
- "operation_power": {
- "name": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]"
- },
- "optimal_humidity": {
- "name": "Ventilation"
- },
- "power_save_enabled": {
- "name": "Energy saving"
- },
- "rapid_freeze": {
- "name": "Quick freeze"
- },
- "sleep_mode": {
- "name": "Sleep mode"
- },
- "uv_nano": {
- "name": "UVnano"
- },
- "warm_mode": {
- "name": "Heating"
- }
- },
- "binary_sensor": {
- "eco_friendly_mode": {
- "name": "Eco friendly"
- },
- "power_save_enabled": {
- "name": "Power saving mode"
- },
- "remote_control_enabled": {
- "name": "Remote start"
- },
- "remote_control_enabled_for_location": {
- "name": "{location} remote start"
- },
- "rinse_refill": {
- "name": "Rinse refill needed"
- },
- "sabbath_mode": {
- "name": "Sabbath"
- },
- "machine_clean_reminder": {
- "name": "Machine clean reminder"
- },
- "signal_level": {
- "name": "Chime sound"
- },
- "clean_light_reminder": {
- "name": "Clean indicator light"
- },
- "operation_mode": {
- "name": "[%key:component::binary_sensor::entity_component::power::name%]"
- },
- "one_touch_filter": {
- "name": "Fresh air filter"
- }
- },
- "climate": {
- "climate_air_conditioner": {
- "state_attributes": {
- "fan_mode": {
- "state": {
- "slow": "Slow",
- "low": "Low",
- "mid": "Medium",
- "high": "High",
- "power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]",
- "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]"
- }
- },
- "preset_mode": {
- "state": {
- "air_clean": "Air purify",
- "aroma": "Aroma",
- "energy_saving": "Energy saving"
- }
- }
- }
- }
- },
- "event": {
- "error": {
- "name": "Error",
- "state_attributes": {
- "event_type": {
- "state": {
- "block_error": "Cleaning has stopped. Check for obstacles",
- "brush_error": "Moving brush has a problem",
- "bubble_error": "Bubble error",
- "child_lock_active_error": "Child lock",
- "cliff_error": "Fall prevention sensor has an error",
- "clutch_error": "Clutch error",
- "compressor_error": "Compressor error",
- "dispensing_error": "Dispensor error",
- "door_close_error": "Door closed error",
- "door_lock_error": "Door lock error",
- "door_open_error": "Door open",
- "door_sensor_error": "Door sensor error",
- "drainmotor_error": "Drain error",
- "dust_full_error": "Dust bin is full and needs to be emptied",
- "empty_water_alert_error": "Empty water",
- "fan_motor_error": "Fan lock error",
- "filter_clogging_error": "Filter error",
- "frozen_error": "Freezing detection error",
- "heater_circuit_error": "Heater circuit failure",
- "high_power_supply_error": "Power supply error",
- "high_temperature_detection_error": "High-temperature error",
- "inner_lid_open_error": "Lid open error",
- "ir_sensor_error": "IR sensor error",
- "le_error": "LE error",
- "le2_error": "LE2 error",
- "left_wheel_error": "Left wheel has a problem",
- "locked_motor_error": "Driver motor error",
- "mop_error": "Cannot operate properly without the mop attached",
- "motor_error": "Motor trouble",
- "motor_lock_error": "Motor lock error",
- "move_error": "The wheels are not touching the floor",
- "need_water_drain": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::empty_water_alert_error%]",
- "need_water_replenishment": "Fill water",
- "no_battery_error": "Robot cleaner's battery is low",
- "no_dust_bin_error": "Dust bin is not installed",
- "no_filter_error": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::filter_clogging_error%]",
- "out_of_balance_error": "Out of balance load",
- "overfill_error": "Overfill error",
- "part_malfunction_error": "AIE error",
- "power_code_connection_error": "Power cord connection error",
- "power_fail_error": "Power failure",
- "right_wheel_error": "Right wheel has a problem",
- "stack_error": "Stacking error",
- "steam_heat_error": "Steam heater error",
- "suction_blocked_error": "Suction motor is clogged",
- "temperature_sensor_error": "Thermistor error",
- "time_to_run_the_tub_clean_cycle_error": "Tub clean recommendation",
- "timeout_error": "Timeout error",
- "turbidity_sensor_error": "Turbidity sensor error",
- "unable_to_lock_error": "Door lock error",
- "unbalanced_load_error": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::out_of_balance_error%]",
- "unknown_error": "Product requires attention",
- "vibration_sensor_error": "Vibration sensor error",
- "water_drain_error": "Water drain error",
- "water_leakage_error": "Water leakage problem",
- "water_leaks_error": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::water_leakage_error%]",
- "water_level_sensor_error": "Water sensor error",
- "water_supply_error": "Water supply error"
- }
- }
- }
- },
- "notification": {
- "name": "Notification",
- "state_attributes": {
- "event_type": {
- "state": {
- "charging_is_complete": "Charging is completed",
- "cleaning_is_complete": "Cycle is finished",
- "cleaning_is_completed": "Cleaning is completed",
- "cleaning_is_failed": "Cleaning has failed",
- "cooking_is_complete": "Turned off",
- "door_is_open": "The door is open",
- "drying_failed": "An error has occurred in the dryer",
- "drying_is_complete": "Drying is completed",
- "error_during_cleaning": "Cleaning stopped due to an error",
- "error_during_washing": "An error has occurred in the washing machine",
- "error_has_occurred": "An error has occurred",
- "frozen_is_complete": "Ice plus is done",
- "homeguard_is_stopped": "Home guard has stopped",
- "lack_of_water": "There is no water in the water tank",
- "motion_is_detected": "Photograph is sent as movement is detected during home guard",
- "need_to_check_location": "Location check is required",
- "pollution_is_high": "Air status is rapidly becoming bad",
- "preheating_is_complete": "Preheating is done",
- "rinse_is_not_enough": "Add rinse aid for better drying performance",
- "salt_refill_is_needed": "Add salt for better softening performance",
- "scheduled_cleaning_starts": "Scheduled cleaning starts",
- "styling_is_complete": "Styling is completed",
- "time_to_change_filter": "It is time to replace the filter",
- "time_to_change_water_filter": "You need to replace water filter",
- "time_to_clean": "Need to selfcleaning",
- "time_to_clean_filter": "It is time to clean the filter",
- "timer_is_complete": "Timer has been completed",
- "washing_is_complete": "Washing is completed",
- "water_is_full": "Water is full",
- "water_leak_has_occurred": "The dishwasher has detected a water leak"
- }
- }
- }
- }
- },
- "number": {
- "target_temperature": {
- "name": "[%key:component::sensor::entity_component::temperature::name%]"
- },
- "target_temperature_for_location": {
- "name": "{location} temperature"
- },
- "light_status": {
- "name": "Light"
- },
- "fan_speed": {
- "name": "Fan"
- },
- "lamp_brightness": {
- "name": "[%key:component::lg_thinq::entity::number::light_status::name%]"
- },
- "wind_temperature": {
- "name": "Wind temperature"
- },
- "relative_hour_to_start": {
- "name": "Schedule turn-on"
- },
- "relative_hour_to_start_for_location": {
- "name": "{location} schedule turn-on"
- },
- "relative_hour_to_start_wm": {
- "name": "Delay starts in"
- },
- "relative_hour_to_start_wm_for_location": {
- "name": "{location} delay starts in"
- },
- "relative_hour_to_stop": {
- "name": "Schedule turn-off"
- },
- "relative_hour_to_stop_for_location": {
- "name": "{location} schedule turn-off"
- },
- "relative_hour_to_stop_wm": {
- "name": "Delay ends in"
- },
- "relative_hour_to_stop_wm_for_location": {
- "name": "{location} delay ends in"
- },
- "sleep_timer_relative_hour_to_stop": {
- "name": "Sleep timer"
- },
- "sleep_timer_relative_hour_to_stop_for_location": {
- "name": "{location} sleep timer"
- },
- "target_humidity": {
- "name": "Target humidity"
- }
- },
- "sensor": {
- "odor_level": {
- "name": "Odor",
- "state": {
- "invalid": "Invalid",
- "weak": "Weak",
- "normal": "Normal",
- "strong": "Strong",
- "very_strong": "Very strong"
- }
- },
- "current_temperature": {
- "name": "Current temperature"
- },
- "temperature": {
- "name": "Temperature"
- },
- "total_pollution_level": {
- "name": "Overall air quality",
- "state": {
- "invalid": "Invalid",
- "good": "Good",
- "normal": "Moderate",
- "bad": "Unhealthy",
- "very_bad": "Poor"
- }
- },
- "monitoring_enabled": {
- "name": "Air quality sensor",
- "state": {
- "on_working": "Turns on with product",
- "always": "Always on"
- }
- },
- "growth_mode": {
- "name": "Mode",
- "state": {
- "standard": "Auto",
- "ext_leaf": "Vegetables",
- "ext_herb": "Herbs",
- "ext_flower": "Flowers",
- "ext_expert": "Custom growing mode"
- }
- },
- "growth_mode_for_location": {
- "name": "{location} mode",
- "state": {
- "standard": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
- "ext_leaf": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::ext_leaf%]",
- "ext_herb": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::ext_herb%]",
- "ext_flower": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::ext_flower%]",
- "ext_expert": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::ext_expert%]"
- }
- },
- "wind_volume_for_location": {
- "name": "{location} wind speed"
- },
- "brightness": {
- "name": "Lighting intensity"
- },
- "brightness_for_location": {
- "name": "{location} lighting intensity"
- },
- "duration": {
- "name": "Lighting duration"
- },
- "duration_for_location": {
- "name": "{location} lighting duration"
- },
- "day_target_temperature": {
- "name": "Day growth temperature"
- },
- "day_target_temperature_for_location": {
- "name": "{location} day growth temperature"
- },
- "night_target_temperature": {
- "name": "Night growth temperature"
- },
- "night_target_temperature_for_location": {
- "name": "{location} night growth temperature"
- },
- "temperature_state": {
- "name": "[%key:component::sensor::entity_component::temperature::name%]",
- "state": {
- "high": "High",
- "normal": "Good",
- "low": "Low"
- }
- },
- "temperature_state_for_location": {
- "name": "[%key:component::lg_thinq::entity::number::target_temperature_for_location::name%]",
- "state": {
- "high": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::high%]",
- "normal": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::normal%]",
- "low": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::low%]"
- }
- },
- "current_state": {
- "name": "Current status",
- "state": {
- "add_drain": "Filling",
- "as_pop_up": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::unknown_error%]",
- "cancel": "Cancel",
- "carbonation": "Carbonation",
- "change_condition": "Settings Change",
- "charging": "Charging",
- "charging_complete": "Charging completed",
- "checking_turbidity": "Detecting soil level",
- "cleaning": "Cleaning",
- "cleaning_is_done": "Cleaning is done",
- "complete": "Done",
- "cook": "Cooking",
- "cook_complete": "[%key:component::lg_thinq::entity::sensor::current_state::state::complete%]",
- "cooking_in_progress": "[%key:component::lg_thinq::entity::sensor::current_state::state::cook%]",
- "cool_down": "Cool down",
- "cooling": "Cooling",
- "detecting": "Detecting",
- "detergent_amount": "Providing the info about the amount of detergent",
- "diagnosis": "Smart diagnosis is in progress",
- "dispensing": "Auto dispensing",
- "display_loadsize": "Load size",
- "done": "[%key:component::lg_thinq::entity::sensor::current_state::state::complete%]",
- "drying": "Drying",
- "during_aging": "Aging",
- "during_fermentation": "Fermentation",
- "end": "Finished",
- "end_cooling": "[%key:component::lg_thinq::entity::sensor::current_state::state::drying%]",
- "error": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::unknown_error%]",
- "extracting_capsule": "Capsule brewing",
- "extraction_mode": "Storing",
- "firmware": "Updating firmware",
- "fota": "Updating",
- "frozen_prevent_initial": "Freeze protection standby",
- "frozen_prevent_running": "Freeze protection in progress",
- "frozen_prevent_pause": "Freeze protection paused",
- "homing": "Moving",
- "initial": "[%key:common::state::standby%]",
- "initializing": "[%key:common::state::standby%]",
- "lock": "Control lock",
- "macrosector": "Remote is in use",
- "melting": "Wort dissolving",
- "monitoring_detecting": "HomeGuard is active",
- "monitoring_moving": "Going to the starting point",
- "monitoring_positioning": "Setting homeguard start point",
- "night_dry": "Night dry",
- "oven_setting": "Cooktop connected",
- "pause": "[%key:common::state::paused%]",
- "paused": "[%key:common::state::paused%]",
- "power_fail": "Power fail",
- "power_on": "[%key:common::state::on%]",
- "power_off": "[%key:common::state::off%]",
- "preference": "Setting",
- "preheat": "Preheating",
- "preheat_complete": "[%key:component::lg_thinq::entity::event::notification::state_attributes::event_type::state::preheating_is_complete%]",
- "preheating": "[%key:component::lg_thinq::entity::sensor::current_state::state::preheat%]",
- "preheating_is_done": "[%key:component::lg_thinq::entity::event::notification::state_attributes::event_type::state::preheating_is_complete%]",
- "prepareing_fermentation": "Preparing now",
- "presteam": "Ready to steam",
- "prewash": "Prewashing",
- "proofing": "Proofing",
- "refreshing": "Refreshing",
- "reservation": "[%key:component::lg_thinq::entity::sensor::current_state::state::macrosector%]",
- "reserved": "Delay set",
- "rinse_hold": "Waiting to rinse",
- "rinsing": "Rinsing",
- "running": "Running",
- "running_end": "Complete",
- "setdate": "[%key:component::lg_thinq::entity::sensor::current_state::state::macrosector%]",
- "shoes_module": "Drying shoes",
- "sleep": "In sleep mode",
- "smart_grid_run": "Running smart grid",
- "soaking": "Soak",
- "softening": "Softener",
- "spinning": "Spinning",
- "stay": "Refresh",
- "standby": "[%key:common::state::standby%]",
- "steam": "Refresh",
- "steam_softening": "Steam softening",
- "sterilize": "Sterilize",
- "temperature_stabilization": "Temperature adjusting",
- "working": "[%key:component::lg_thinq::entity::sensor::current_state::state::cleaning%]",
- "wrinkle_care": "Wrinkle care"
- }
- },
- "current_state_for_location": {
- "name": "{location} current status",
- "state": {
- "add_drain": "[%key:component::lg_thinq::entity::sensor::current_state::state::add_drain%]",
- "as_pop_up": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::unknown_error%]",
- "cancel": "[%key:component::lg_thinq::entity::sensor::current_state::state::cancel%]",
- "carbonation": "[%key:component::lg_thinq::entity::sensor::current_state::state::carbonation%]",
- "change_condition": "[%key:component::lg_thinq::entity::sensor::current_state::state::change_condition%]",
- "charging": "[%key:component::lg_thinq::entity::sensor::current_state::state::charging%]",
- "charging_complete": "[%key:component::lg_thinq::entity::sensor::current_state::state::charging_complete%]",
- "checking_turbidity": "[%key:component::lg_thinq::entity::sensor::current_state::state::checking_turbidity%]",
- "cleaning": "[%key:component::lg_thinq::entity::sensor::current_state::state::cleaning%]",
- "cleaning_is_done": "[%key:component::lg_thinq::entity::sensor::current_state::state::cleaning_is_done%]",
- "complete": "[%key:component::lg_thinq::entity::sensor::current_state::state::complete%]",
- "cook": "[%key:component::lg_thinq::entity::sensor::current_state::state::cook%]",
- "cook_complete": "[%key:component::lg_thinq::entity::sensor::current_state::state::complete%]",
- "cooking_in_progress": "[%key:component::lg_thinq::entity::sensor::current_state::state::cook%]",
- "cool_down": "[%key:component::lg_thinq::entity::sensor::current_state::state::cool_down%]",
- "cooling": "[%key:component::lg_thinq::entity::sensor::current_state::state::cooling%]",
- "detecting": "[%key:component::lg_thinq::entity::sensor::current_state::state::detecting%]",
- "detergent_amount": "[%key:component::lg_thinq::entity::sensor::current_state::state::detergent_amount%]",
- "diagnosis": "[%key:component::lg_thinq::entity::sensor::current_state::state::diagnosis%]",
- "dispensing": "[%key:component::lg_thinq::entity::sensor::current_state::state::dispensing%]",
- "display_loadsize": "[%key:component::lg_thinq::entity::sensor::current_state::state::display_loadsize%]",
- "done": "[%key:component::lg_thinq::entity::sensor::current_state::state::complete%]",
- "drying": "[%key:component::lg_thinq::entity::sensor::current_state::state::drying%]",
- "during_aging": "[%key:component::lg_thinq::entity::sensor::current_state::state::during_aging%]",
- "during_fermentation": "[%key:component::lg_thinq::entity::sensor::current_state::state::during_fermentation%]",
- "end": "[%key:component::lg_thinq::entity::sensor::current_state::state::end%]",
- "end_cooling": "[%key:component::lg_thinq::entity::sensor::current_state::state::drying%]",
- "error": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::unknown_error%]",
- "extracting_capsule": "[%key:component::lg_thinq::entity::sensor::current_state::state::extracting_capsule%]",
- "extraction_mode": "[%key:component::lg_thinq::entity::sensor::current_state::state::extraction_mode%]",
- "firmware": "[%key:component::lg_thinq::entity::sensor::current_state::state::firmware%]",
- "fota": "[%key:component::lg_thinq::entity::sensor::current_state::state::fota%]",
- "frozen_prevent_initial": "[%key:component::lg_thinq::entity::sensor::current_state::state::frozen_prevent_initial%]",
- "frozen_prevent_running": "[%key:component::lg_thinq::entity::sensor::current_state::state::frozen_prevent_running%]",
- "frozen_prevent_pause": "[%key:component::lg_thinq::entity::sensor::current_state::state::frozen_prevent_pause%]",
- "homing": "[%key:component::lg_thinq::entity::sensor::current_state::state::homing%]",
- "initial": "[%key:common::state::standby%]",
- "initializing": "[%key:common::state::standby%]",
- "lock": "[%key:component::lg_thinq::entity::sensor::current_state::state::lock%]",
- "macrosector": "[%key:component::lg_thinq::entity::sensor::current_state::state::macrosector%]",
- "melting": "[%key:component::lg_thinq::entity::sensor::current_state::state::melting%]",
- "monitoring_detecting": "[%key:component::lg_thinq::entity::sensor::current_state::state::monitoring_detecting%]",
- "monitoring_moving": "[%key:component::lg_thinq::entity::sensor::current_state::state::monitoring_moving%]",
- "monitoring_positioning": "[%key:component::lg_thinq::entity::sensor::current_state::state::monitoring_positioning%]",
- "night_dry": "[%key:component::lg_thinq::entity::sensor::current_state::state::night_dry%]",
- "oven_setting": "[%key:component::lg_thinq::entity::sensor::current_state::state::oven_setting%]",
- "pause": "[%key:common::state::paused%]",
- "paused": "[%key:common::state::paused%]",
- "power_fail": "[%key:component::lg_thinq::entity::sensor::current_state::state::power_fail%]",
- "power_on": "[%key:common::state::on%]",
- "power_off": "[%key:common::state::off%]",
- "preference": "[%key:component::lg_thinq::entity::sensor::current_state::state::preference%]",
- "preheat": "[%key:component::lg_thinq::entity::sensor::current_state::state::preheat%]",
- "preheat_complete": "[%key:component::lg_thinq::entity::event::notification::state_attributes::event_type::state::preheating_is_complete%]",
- "preheating": "[%key:component::lg_thinq::entity::sensor::current_state::state::preheat%]",
- "preheating_is_done": "[%key:component::lg_thinq::entity::event::notification::state_attributes::event_type::state::preheating_is_complete%]",
- "prepareing_fermentation": "[%key:component::lg_thinq::entity::sensor::current_state::state::prepareing_fermentation%]",
- "presteam": "[%key:component::lg_thinq::entity::sensor::current_state::state::presteam%]",
- "prewash": "[%key:component::lg_thinq::entity::sensor::current_state::state::prewash%]",
- "proofing": "[%key:component::lg_thinq::entity::sensor::current_state::state::proofing%]",
- "refreshing": "[%key:component::lg_thinq::entity::sensor::current_state::state::refreshing%]",
- "reservation": "[%key:component::lg_thinq::entity::sensor::current_state::state::macrosector%]",
- "reserved": "[%key:component::lg_thinq::entity::sensor::current_state::state::reserved%]",
- "rinse_hold": "[%key:component::lg_thinq::entity::sensor::current_state::state::rinse_hold%]",
- "rinsing": "[%key:component::lg_thinq::entity::sensor::current_state::state::rinsing%]",
- "running": "[%key:component::lg_thinq::entity::sensor::current_state::state::running%]",
- "running_end": "[%key:component::lg_thinq::entity::sensor::current_state::state::running_end%]",
- "setdate": "[%key:component::lg_thinq::entity::sensor::current_state::state::macrosector%]",
- "shoes_module": "[%key:component::lg_thinq::entity::sensor::current_state::state::shoes_module%]",
- "sleep": "[%key:component::lg_thinq::entity::sensor::current_state::state::sleep%]",
- "smart_grid_run": "[%key:component::lg_thinq::entity::sensor::current_state::state::smart_grid_run%]",
- "soaking": "[%key:component::lg_thinq::entity::sensor::current_state::state::soaking%]",
- "softening": "[%key:component::lg_thinq::entity::sensor::current_state::state::softening%]",
- "spinning": "[%key:component::lg_thinq::entity::sensor::current_state::state::spinning%]",
- "stay": "[%key:component::lg_thinq::entity::sensor::current_state::state::stay%]",
- "standby": "[%key:common::state::standby%]",
- "steam": "[%key:component::lg_thinq::entity::sensor::current_state::state::steam%]",
- "steam_softening": "[%key:component::lg_thinq::entity::sensor::current_state::state::steam_softening%]",
- "sterilize": "[%key:component::lg_thinq::entity::sensor::current_state::state::sterilize%]",
- "temperature_stabilization": "[%key:component::lg_thinq::entity::sensor::current_state::state::temperature_stabilization%]",
- "working": "[%key:component::lg_thinq::entity::sensor::current_state::state::cleaning%]",
- "wrinkle_care": "[%key:component::lg_thinq::entity::sensor::current_state::state::wrinkle_care%]"
- }
- },
- "fresh_air_filter": {
- "name": "[%key:component::lg_thinq::entity::binary_sensor::one_touch_filter::name%]",
- "state": {
- "off": "[%key:common::state::off%]",
- "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
- "power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]",
- "replace": "Replace filter",
- "smart_power": "Smart safe storage",
- "smart_off": "[%key:common::state::off%]",
- "smart_on": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::smart_power%]"
- }
- },
- "filter_lifetime": {
- "name": "Filter remaining"
- },
- "used_time": {
- "name": "Water filter used"
- },
- "current_job_mode": {
- "name": "Operating mode",
- "state": {
- "air_clean": "Purify",
- "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
- "clothes_dry": "Laundry",
- "edge": "Edge cleaning",
- "heat_pump": "Heat pump",
- "high": "Power",
- "intensive_dry": "Spot",
- "macro": "Custom mode",
- "mop": "Mop",
- "normal": "Normal",
- "off": "[%key:common::state::off%]",
- "quiet_humidity": "Silent",
- "rapid_humidity": "Jet",
- "sector_base": "Cell by cell",
- "select": "My space",
- "smart_humidity": "Smart",
- "spot": "Spiral spot mode",
- "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]",
- "vacation": "Vacation",
- "zigzag": "Zigzag"
- }
- },
- "current_job_mode_stick_cleaner": {
- "name": "Operating mode",
- "state": {
- "auto": "Low power",
- "high": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]",
- "mop": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::mop%]",
- "normal": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::normal%]",
- "off": "[%key:common::state::off%]",
- "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]"
- }
- },
- "personalization_mode": {
- "name": "Personal mode",
- "state": {
- "auto_inside": "[%key:component::lg_thinq::entity::switch::auto_mode::name%]",
- "sleep": "Sleep mode",
- "baby": "Baby care mode",
- "sick_house": "New Home mode",
- "auto_outside": "Interlocking mode",
- "pet": "Pet mode",
- "cooking": "Cooking mode",
- "smoke": "Smoke mode",
- "exercise": "Exercise mode",
- "others": "Others"
- }
- },
- "current_dish_washing_course": {
- "name": "Current cycle",
- "state": {
- "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
- "heavy": "Intensive",
- "delicate": "Delicate",
- "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]",
- "normal": "Normal",
- "rinse": "Rinse",
- "refresh": "Refresh",
- "express": "Express",
- "machine_clean": "Machine clean",
- "short_mode": "Short mode",
- "download_cycle": "Download cycle",
- "quick": "Quick",
- "steam": "Steam care",
- "spray": "Spray",
- "eco": "Eco"
- }
- },
- "rinse_level": {
- "name": "Rinse aid dispenser level",
- "state": {
- "rinselevel_0": "0",
- "rinselevel_1": "1",
- "rinselevel_2": "2",
- "rinselevel_3": "3",
- "rinselevel_4": "4"
- }
- },
- "softening_level": {
- "name": "Softening level",
- "state": {
- "softeninglevel_0": "[%key:component::lg_thinq::entity::sensor::rinse_level::state::rinselevel_0%]",
- "softeninglevel_1": "[%key:component::lg_thinq::entity::sensor::rinse_level::state::rinselevel_1%]",
- "softeninglevel_2": "[%key:component::lg_thinq::entity::sensor::rinse_level::state::rinselevel_2%]",
- "softeninglevel_3": "[%key:component::lg_thinq::entity::sensor::rinse_level::state::rinselevel_3%]",
- "softeninglevel_4": "[%key:component::lg_thinq::entity::sensor::rinse_level::state::rinselevel_4%]"
- }
- },
- "cock_state": {
- "name": "[%key:component::lg_thinq::entity::switch::uv_nano::name%]",
- "state": {
- "cleaning": "In progress",
- "normal": "[%key:common::state::standby%]"
- }
- },
- "sterilizing_state": {
- "name": "High-temp sterilization",
- "state": {
- "off": "[%key:common::state::off%]",
- "on": "Sterilizing",
- "cancel": "[%key:component::lg_thinq::entity::sensor::current_state::state::cancel%]"
- }
- },
- "water_type": {
- "name": "Type"
- },
- "target_temperature": {
- "name": "[%key:component::sensor::entity_component::temperature::name%]",
- "state": {
- "kimchi": "Kimchi",
- "off": "[%key:common::state::off%]",
- "freezer": "Freezer",
- "fridge": "Fridge",
- "storage": "Storage",
- "meat_fish": "Meat/Fish",
- "rice_grain": "Rice/Grain",
- "vegetable_fruit": "Vege/Fruit",
- "temperature_number": "Number"
- }
- },
- "target_temperature_for_location": {
- "name": "[%key:component::lg_thinq::entity::number::target_temperature_for_location::name%]",
- "state": {
- "kimchi": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::kimchi%]",
- "off": "[%key:common::state::off%]",
- "freezer": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::freezer%]",
- "fridge": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::fridge%]",
- "storage": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::storage%]",
- "meat_fish": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::meat_fish%]",
- "rice_grain": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::rice_grain%]",
- "vegetable_fruit": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::vegetable_fruit%]",
- "temperature_number": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::temperature_number%]"
- }
- },
- "elapsed_day_state": {
- "name": "Brewing period"
- },
- "elapsed_day_total": {
- "name": "Brewing duration"
- },
- "recipe_name": {
- "name": "Homebrew recipe",
- "state": {
- "ipa": "IPA",
- "pale_ale": "Pale ale",
- "stout": "Stout",
- "wheat": "Wheat",
- "pilsner": "Pilsner",
- "red_ale": "Red ale",
- "my_recipe": "My recipe"
- }
- },
- "wort_info": {
- "name": "Wort",
- "state": {
- "hoppy": "Hoppy",
- "deep_gold": "DeepGold",
- "wheat": "Wheat",
- "dark": "Dark"
- }
- },
- "yeast_info": {
- "name": "Yeast",
- "state": {
- "american_ale": "American ale",
- "english_ale": "English ale",
- "lager": "Lager",
- "weizen": "Weizen"
- }
- },
- "hop_oil_info": {
- "name": "Hops"
- },
- "flavor_info": {
- "name": "Flavor"
- },
- "beer_remain": {
- "name": "Recipe progress"
- },
- "battery_level": {
- "name": "Battery",
- "state": {
- "high": "Full",
- "mid": "Medium",
- "low": "Low",
- "warning": "Empty"
- }
- },
- "relative_to_start": {
- "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_start::name%]"
- },
- "relative_to_start_for_location": {
- "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_start_for_location::name%]"
- },
- "relative_to_start_wm": {
- "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_start_wm::name%]"
- },
- "relative_to_start_wm_for_location": {
- "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_start_wm_for_location::name%]"
- },
- "relative_to_stop": {
- "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_stop::name%]"
- },
- "relative_to_stop_for_location": {
- "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_stop_for_location::name%]"
- },
- "relative_to_stop_wm": {
- "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_stop_wm::name%]"
- },
- "relative_to_stop_wm_for_location": {
- "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_stop_wm_for_location::name%]"
- },
- "sleep_timer_relative_to_stop": {
- "name": "[%key:component::lg_thinq::entity::number::sleep_timer_relative_hour_to_stop::name%]"
- },
- "sleep_timer_relative_to_stop_for_location": {
- "name": "[%key:component::lg_thinq::entity::number::sleep_timer_relative_hour_to_stop_for_location::name%]"
- },
- "absolute_to_start": {
- "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_start::name%]"
- },
- "absolute_to_start_for_location": {
- "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_start_for_location::name%]"
- },
- "absolute_to_stop": {
- "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_stop::name%]"
- },
- "absolute_to_stop_for_location": {
- "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_stop_for_location::name%]"
- },
- "remain": {
- "name": "Remaining time"
- },
- "remain_for_location": {
- "name": "{location} remaining time"
- },
- "running": {
- "name": "Running time"
- },
- "running_for_location": {
- "name": "{location} running time"
- },
- "total": {
- "name": "Total time"
- },
- "total_for_location": {
- "name": "{location} total time"
- },
- "target": {
- "name": "Cook time"
- },
- "target_for_location": {
- "name": "{location} cook time"
- },
- "light_start": {
- "name": "Lights on time"
- },
- "light_start_for_location": {
- "name": "{location} lights on time"
- },
- "power_level": {
- "name": "Power level"
- },
- "power_level_for_location": {
- "name": "{location} power level"
- }
- },
- "select": {
- "wind_strength": {
- "name": "Speed",
- "state": {
- "slow": "[%key:component::lg_thinq::entity::climate::climate_air_conditioner::state_attributes::fan_mode::state::slow%]",
- "low": "Low",
- "mid": "Medium",
- "high": "High",
- "power": "Turbo",
- "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]",
- "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
- "wind_1": "Step 1",
- "wind_2": "Step 2",
- "wind_3": "Step 3",
- "wind_4": "Step 4",
- "wind_5": "Step 5",
- "wind_6": "Step 6",
- "wind_7": "Step 7",
- "wind_8": "Step 8",
- "wind_9": "Step 9",
- "wind_10": "Step 10"
- }
- },
- "monitoring_enabled": {
- "name": "[%key:component::lg_thinq::entity::sensor::monitoring_enabled::name%]",
- "state": {
- "on_working": "[%key:component::lg_thinq::entity::sensor::monitoring_enabled::state::on_working%]",
- "always": "[%key:component::lg_thinq::entity::sensor::monitoring_enabled::state::always%]"
- }
- },
- "current_job_mode": {
- "name": "Operating mode",
- "state": {
- "air_clean": "Purifying",
- "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
- "baby_care": "[%key:component::lg_thinq::entity::sensor::personalization_mode::state::baby%]",
- "circulator": "Booster",
- "clean": "Single",
- "direct_clean": "Direct mode",
- "dual_clean": "Dual",
- "fast": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]",
- "heat_pump": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::heat_pump%]",
- "humidify": "Mist",
- "humidify_and_air_clean": "Mist & purifying",
- "humidity": "Humid",
- "nature_clean": "Natural mode",
- "pet_clean": "[%key:component::lg_thinq::entity::sensor::personalization_mode::state::pet%]",
- "silent": "Silent",
- "sleep": "Sleep",
- "smart": "Smart mode",
- "space_clean": "Diffusion mode",
- "spot_clean": "Wide mode",
- "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]",
- "up_feature": "Additional mode",
- "vacation": "Vacation"
- }
- },
- "operation_mode": {
- "name": "Operation",
- "state": {
- "cancel": "[%key:component::lg_thinq::entity::sensor::current_state::state::cancel%]",
- "power_off": "Power off",
- "preheating": "Preheating",
- "start": "[%key:common::action::start%]",
- "stop": "[%key:common::action::stop%]",
- "wake_up": "Sleep mode off"
- }
- },
- "operation_mode_for_location": {
- "name": "{location} operation",
- "state": {
- "cancel": "[%key:component::lg_thinq::entity::sensor::current_state::state::cancel%]",
- "power_off": "[%key:component::lg_thinq::entity::select::operation_mode::state::power_off%]",
- "preheating": "[%key:component::lg_thinq::entity::select::operation_mode::state::preheating%]",
- "start": "[%key:common::action::start%]",
- "stop": "[%key:common::action::stop%]",
- "wake_up": "[%key:component::lg_thinq::entity::select::operation_mode::state::wake_up%]"
- }
- },
- "air_clean_operation_mode": {
- "name": "[%key:component::lg_thinq::entity::climate::climate_air_conditioner::state_attributes::preset_mode::state::air_clean%]",
- "state": {
- "start": "[%key:common::action::start%]",
- "stop": "[%key:common::action::stop%]"
- }
- },
- "cook_mode": {
- "name": "Cook mode",
- "state": {
- "bake": "Bake",
- "convection_bake": "Convection bake",
- "convection_roast": "Convection roast",
- "roast": "Roast",
- "crisp_convection": "Crisp convection"
- }
- },
- "cook_mode_for_location": {
- "name": "{location} cook mode",
- "state": {
- "bake": "[%key:component::lg_thinq::entity::select::cook_mode::state::bake%]",
- "convection_bake": "[%key:component::lg_thinq::entity::select::cook_mode::state::convection_bake%]",
- "convection_roast": "[%key:component::lg_thinq::entity::select::cook_mode::state::convection_roast%]",
- "roast": "[%key:component::lg_thinq::entity::select::cook_mode::state::roast%]",
- "crisp_convection": "[%key:component::lg_thinq::entity::select::cook_mode::state::crisp_convection%]"
- }
- },
- "light_brightness": {
- "name": "Light"
- },
- "wind_angle": {
- "name": "Rotation",
- "state": {
- "off": "[%key:common::state::off%]",
- "angle_45": "45°",
- "angle_60": "60°",
- "angle_90": "90°",
- "angle_140": "140°"
- }
- },
- "display_light": {
- "name": "Display brightness",
- "state": {
- "off": "[%key:common::state::off%]",
- "level_1": "Brightness 1",
- "level_2": "Brightness 2",
- "level_3": "Brightness 3"
- }
- },
- "fresh_air_filter": {
- "name": "[%key:component::lg_thinq::entity::binary_sensor::one_touch_filter::name%]",
- "state": {
- "off": "[%key:common::state::off%]",
- "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
- "power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]",
- "replace": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::replace%]",
- "smart_power": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::smart_power%]",
- "smart_off": "[%key:common::state::off%]",
- "smart_on": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::smart_power%]"
- }
- },
- "hygiene_dry_mode": {
- "name": "[%key:component::lg_thinq::entity::switch::hygiene_dry_mode::name%]",
- "state": {
- "off": "[%key:common::state::off%]",
- "fast": "Fast",
- "silent": "Silent",
- "normal": "[%key:component::lg_thinq::entity::sensor::current_dish_washing_course::state::delicate%]"
- }
- }
- }
- }
-}
diff --git a/homeassistant/components/lg_thinq/switch.py b/homeassistant/components/lg_thinq/switch.py
deleted file mode 100644
index 25fd7eb8b64..00000000000
--- a/homeassistant/components/lg_thinq/switch.py
+++ /dev/null
@@ -1,228 +0,0 @@
-"""Support for switch entities."""
-
-from __future__ import annotations
-
-from dataclasses import dataclass
-import logging
-from typing import Any
-
-from thinqconnect import DeviceType
-from thinqconnect.devices.const import Property as ThinQProperty
-from thinqconnect.integration import ActiveMode
-
-from homeassistant.components.switch import (
- SwitchDeviceClass,
- SwitchEntity,
- SwitchEntityDescription,
-)
-from homeassistant.const import EntityCategory
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import ThinqConfigEntry
-from .entity import ThinQEntity
-
-
-@dataclass(frozen=True, kw_only=True)
-class ThinQSwitchEntityDescription(SwitchEntityDescription):
- """Describes ThinQ switch entity."""
-
- on_key: str | None = None
- off_key: str | None = None
-
-
-DEVICE_TYPE_SWITCH_MAP: dict[DeviceType, tuple[ThinQSwitchEntityDescription, ...]] = {
- DeviceType.AIR_CONDITIONER: (
- ThinQSwitchEntityDescription(
- key=ThinQProperty.POWER_SAVE_ENABLED,
- translation_key=ThinQProperty.POWER_SAVE_ENABLED,
- on_key="true",
- off_key="false",
- entity_category=EntityCategory.CONFIG,
- ),
- ),
- DeviceType.AIR_PURIFIER_FAN: (
- ThinQSwitchEntityDescription(
- key=ThinQProperty.AIR_FAN_OPERATION_MODE, translation_key="operation_power"
- ),
- ThinQSwitchEntityDescription(
- key=ThinQProperty.UV_NANO,
- translation_key=ThinQProperty.UV_NANO,
- on_key="on",
- off_key="off",
- entity_category=EntityCategory.CONFIG,
- ),
- ThinQSwitchEntityDescription(
- key=ThinQProperty.WARM_MODE,
- translation_key=ThinQProperty.WARM_MODE,
- on_key="warm_on",
- off_key="warm_off",
- entity_category=EntityCategory.CONFIG,
- ),
- ),
- DeviceType.AIR_PURIFIER: (
- ThinQSwitchEntityDescription(
- key=ThinQProperty.AIR_PURIFIER_OPERATION_MODE,
- translation_key="operation_power",
- ),
- ),
- DeviceType.DEHUMIDIFIER: (
- ThinQSwitchEntityDescription(
- key=ThinQProperty.DEHUMIDIFIER_OPERATION_MODE,
- translation_key="operation_power",
- ),
- ),
- DeviceType.HUMIDIFIER: (
- ThinQSwitchEntityDescription(
- key=ThinQProperty.HUMIDIFIER_OPERATION_MODE,
- translation_key="operation_power",
- ),
- ThinQSwitchEntityDescription(
- key=ThinQProperty.WARM_MODE,
- translation_key="humidity_warm_mode",
- on_key="warm_on",
- off_key="warm_off",
- entity_category=EntityCategory.CONFIG,
- ),
- ThinQSwitchEntityDescription(
- key=ThinQProperty.MOOD_LAMP_STATE,
- translation_key=ThinQProperty.MOOD_LAMP_STATE,
- on_key="on",
- off_key="off",
- entity_category=EntityCategory.CONFIG,
- ),
- ThinQSwitchEntityDescription(
- key=ThinQProperty.AUTO_MODE,
- translation_key=ThinQProperty.AUTO_MODE,
- on_key="auto_on",
- off_key="auto_off",
- entity_category=EntityCategory.CONFIG,
- ),
- ThinQSwitchEntityDescription(
- key=ThinQProperty.SLEEP_MODE,
- translation_key=ThinQProperty.SLEEP_MODE,
- on_key="sleep_on",
- off_key="sleep_off",
- entity_category=EntityCategory.CONFIG,
- ),
- ),
- DeviceType.REFRIGERATOR: (
- ThinQSwitchEntityDescription(
- key=ThinQProperty.EXPRESS_MODE,
- translation_key=ThinQProperty.EXPRESS_MODE,
- on_key="true",
- off_key="false",
- entity_category=EntityCategory.CONFIG,
- ),
- ThinQSwitchEntityDescription(
- key=ThinQProperty.RAPID_FREEZE,
- translation_key=ThinQProperty.RAPID_FREEZE,
- on_key="true",
- off_key="false",
- entity_category=EntityCategory.CONFIG,
- ),
- ),
- DeviceType.SYSTEM_BOILER: (
- ThinQSwitchEntityDescription(
- key=ThinQProperty.HOT_WATER_MODE,
- translation_key=ThinQProperty.HOT_WATER_MODE,
- on_key="on",
- off_key="off",
- entity_category=EntityCategory.CONFIG,
- ),
- ),
- DeviceType.WINE_CELLAR: (
- ThinQSwitchEntityDescription(
- key=ThinQProperty.OPTIMAL_HUMIDITY,
- translation_key=ThinQProperty.OPTIMAL_HUMIDITY,
- on_key="on",
- off_key="off",
- entity_category=EntityCategory.CONFIG,
- ),
- ),
-}
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: ThinqConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up an entry for switch platform."""
- entities: list[ThinQSwitchEntity] = []
- for coordinator in entry.runtime_data.coordinators.values():
- if (
- descriptions := DEVICE_TYPE_SWITCH_MAP.get(
- coordinator.api.device.device_type
- )
- ) is not None:
- for description in descriptions:
- entities.extend(
- ThinQSwitchEntity(coordinator, description, property_id)
- for property_id in coordinator.api.get_active_idx(
- description.key, ActiveMode.READ_WRITE
- )
- )
-
- if entities:
- async_add_entities(entities)
-
-
-class ThinQSwitchEntity(ThinQEntity, SwitchEntity):
- """Represent a thinq switch platform."""
-
- entity_description: ThinQSwitchEntityDescription
- _attr_device_class = SwitchDeviceClass.SWITCH
-
- def _update_status(self) -> None:
- """Update status itself."""
- super()._update_status()
-
- if (key := self.entity_description.on_key) is not None:
- self._attr_is_on = self.data.value == key
- else:
- self._attr_is_on = self.data.is_on
-
- _LOGGER.debug(
- "[%s:%s] update status: %s -> %s",
- self.coordinator.device_name,
- self.property_id,
- self.data.is_on,
- self.is_on,
- )
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn on the switch."""
- _LOGGER.debug(
- "[%s:%s] async_turn_on id: %s",
- self.coordinator.device_name,
- self.name,
- self.property_id,
- )
- if (on_command := self.entity_description.on_key) is not None:
- await self.async_call_api(
- self.coordinator.api.post(self.property_id, on_command)
- )
- else:
- await self.async_call_api(
- self.coordinator.api.async_turn_on(self.property_id)
- )
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn off the switch."""
- _LOGGER.debug(
- "[%s:%s] async_turn_off id: %s",
- self.coordinator.device_name,
- self.name,
- self.property_id,
- )
- if (off_command := self.entity_description.off_key) is not None:
- await self.async_call_api(
- self.coordinator.api.post(self.property_id, off_command)
- )
- else:
- await self.async_call_api(
- self.coordinator.api.async_turn_off(self.property_id)
- )
diff --git a/homeassistant/components/lg_thinq/vacuum.py b/homeassistant/components/lg_thinq/vacuum.py
deleted file mode 100644
index 138b9ba55bf..00000000000
--- a/homeassistant/components/lg_thinq/vacuum.py
+++ /dev/null
@@ -1,172 +0,0 @@
-"""Support for vacuum entities."""
-
-from __future__ import annotations
-
-from enum import StrEnum
-import logging
-
-from thinqconnect import DeviceType
-from thinqconnect.integration import ExtendedProperty
-
-from homeassistant.components.vacuum import (
- STATE_CLEANING,
- STATE_DOCKED,
- STATE_ERROR,
- STATE_RETURNING,
- StateVacuumEntity,
- StateVacuumEntityDescription,
- VacuumEntityFeature,
-)
-from homeassistant.const import STATE_IDLE, STATE_PAUSED
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import ThinqConfigEntry
-from .entity import ThinQEntity
-
-DEVICE_TYPE_VACUUM_MAP: dict[DeviceType, tuple[StateVacuumEntityDescription, ...]] = {
- DeviceType.ROBOT_CLEANER: (
- StateVacuumEntityDescription(
- key=ExtendedProperty.VACUUM,
- name=None,
- ),
- ),
-}
-
-
-class State(StrEnum):
- """State of device."""
-
- HOMING = "homing"
- PAUSE = "pause"
- RESUME = "resume"
- SLEEP = "sleep"
- START = "start"
- WAKE_UP = "wake_up"
-
-
-ROBOT_STATUS_TO_HA = {
- "charging": STATE_DOCKED,
- "diagnosis": STATE_IDLE,
- "homing": STATE_RETURNING,
- "initializing": STATE_IDLE,
- "macrosector": STATE_IDLE,
- "monitoring_detecting": STATE_IDLE,
- "monitoring_moving": STATE_IDLE,
- "monitoring_positioning": STATE_IDLE,
- "pause": STATE_PAUSED,
- "reservation": STATE_IDLE,
- "setdate": STATE_IDLE,
- "sleep": STATE_IDLE,
- "standby": STATE_IDLE,
- "working": STATE_CLEANING,
- "error": STATE_ERROR,
-}
-ROBOT_BATT_TO_HA = {
- "moveless": 5,
- "dock_level": 5,
- "low": 30,
- "mid": 50,
- "high": 90,
- "full": 100,
- "over_charge": 100,
-}
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: ThinqConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up an entry for vacuum platform."""
- entities: list[ThinQStateVacuumEntity] = []
- for coordinator in entry.runtime_data.coordinators.values():
- if (
- descriptions := DEVICE_TYPE_VACUUM_MAP.get(
- coordinator.api.device.device_type
- )
- ) is not None:
- for description in descriptions:
- entities.extend(
- ThinQStateVacuumEntity(coordinator, description, property_id)
- for property_id in coordinator.api.get_active_idx(description.key)
- )
-
- if entities:
- async_add_entities(entities)
-
-
-class ThinQStateVacuumEntity(ThinQEntity, StateVacuumEntity):
- """Represent a thinq vacuum platform."""
-
- _attr_supported_features = (
- VacuumEntityFeature.SEND_COMMAND
- | VacuumEntityFeature.STATE
- | VacuumEntityFeature.BATTERY
- | VacuumEntityFeature.START
- | VacuumEntityFeature.PAUSE
- | VacuumEntityFeature.RETURN_HOME
- )
-
- def _update_status(self) -> None:
- """Update status itself."""
- super()._update_status()
-
- # Update state.
- self._attr_state = ROBOT_STATUS_TO_HA[self.data.current_state]
-
- # Update battery.
- if (level := self.data.battery) is not None:
- self._attr_battery_level = (
- level if isinstance(level, int) else ROBOT_BATT_TO_HA.get(level, 0)
- )
-
- _LOGGER.debug(
- "[%s:%s] update status: %s -> %s (battery_level=%s)",
- self.coordinator.device_name,
- self.property_id,
- self.data.current_state,
- self.state,
- self.battery_level,
- )
-
- async def async_start(self, **kwargs) -> None:
- """Start the device."""
- if self.data.current_state == State.SLEEP:
- value = State.WAKE_UP
- elif self._attr_state == STATE_PAUSED:
- value = State.RESUME
- else:
- value = State.START
-
- _LOGGER.debug(
- "[%s:%s] async_start", self.coordinator.device_name, self.property_id
- )
- await self.async_call_api(
- self.coordinator.api.async_set_clean_operation_mode(self.property_id, value)
- )
-
- async def async_pause(self, **kwargs) -> None:
- """Pause the device."""
- _LOGGER.debug(
- "[%s:%s] async_pause", self.coordinator.device_name, self.property_id
- )
- await self.async_call_api(
- self.coordinator.api.async_set_clean_operation_mode(
- self.property_id, State.PAUSE
- )
- )
-
- async def async_return_to_base(self, **kwargs) -> None:
- """Return device to dock."""
- _LOGGER.debug(
- "[%s:%s] async_return_to_base",
- self.coordinator.device_name,
- self.property_id,
- )
- await self.async_call_api(
- self.coordinator.api.async_set_clean_operation_mode(
- self.property_id, State.HOMING
- )
- )
diff --git a/homeassistant/components/lidarr/__init__.py b/homeassistant/components/lidarr/__init__.py
index a421a881b69..907c89eb737 100644
--- a/homeassistant/components/lidarr/__init__.py
+++ b/homeassistant/components/lidarr/__init__.py
@@ -16,7 +16,6 @@ from homeassistant.helpers.device_registry import DeviceEntryType
from .const import DEFAULT_NAME, DOMAIN
from .coordinator import (
- AlbumsDataUpdateCoordinator,
DiskSpaceDataUpdateCoordinator,
QueueDataUpdateCoordinator,
StatusDataUpdateCoordinator,
@@ -36,7 +35,6 @@ class LidarrData:
queue: QueueDataUpdateCoordinator
status: StatusDataUpdateCoordinator
wanted: WantedDataUpdateCoordinator
- albums: AlbumsDataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: LidarrConfigEntry) -> bool:
@@ -56,7 +54,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: LidarrConfigEntry) -> bo
queue=QueueDataUpdateCoordinator(hass, host_configuration, lidarr),
status=StatusDataUpdateCoordinator(hass, host_configuration, lidarr),
wanted=WantedDataUpdateCoordinator(hass, host_configuration, lidarr),
- albums=AlbumsDataUpdateCoordinator(hass, host_configuration, lidarr),
)
for field in fields(data):
coordinator = getattr(data, field.name)
diff --git a/homeassistant/components/lidarr/config_flow.py b/homeassistant/components/lidarr/config_flow.py
index dfbfff2cdfd..bc7a40c976e 100644
--- a/homeassistant/components/lidarr/config_flow.py
+++ b/homeassistant/components/lidarr/config_flow.py
@@ -10,11 +10,12 @@ from aiopyarr import exceptions
from aiopyarr.lidarr_client import LidarrClient
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from . import LidarrConfigEntry
from .const import DEFAULT_NAME, DOMAIN
@@ -23,10 +24,16 @@ class LidarrConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ def __init__(self) -> None:
+ """Initialize the flow."""
+ self.entry: LidarrConfigEntry | None = None
+
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -45,7 +52,10 @@ class LidarrConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a flow initiated by the user."""
errors = {}
- if user_input is not None:
+ if user_input is None:
+ user_input = dict(self.entry.data) if self.entry else None
+
+ else:
try:
if result := await validate_input(self.hass, user_input):
user_input[CONF_API_KEY] = result[1]
@@ -60,18 +70,17 @@ class LidarrConfigFlow(ConfigFlow, domain=DOMAIN):
except exceptions.ArrException:
errors = {"base": "unknown"}
if not errors:
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=user_input
+ if self.entry:
+ self.hass.config_entries.async_update_entry(
+ self.entry, data=user_input
)
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
+
+ return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=DEFAULT_NAME, data=user_input)
- if user_input is None:
- user_input = {}
- if self.source == SOURCE_REAUTH:
- user_input = dict(self._get_reauth_entry().data)
-
+ user_input = user_input or {}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
diff --git a/homeassistant/components/lidarr/coordinator.py b/homeassistant/components/lidarr/coordinator.py
index 1010f708748..2f18e4f0ebb 100644
--- a/homeassistant/components/lidarr/coordinator.py
+++ b/homeassistant/components/lidarr/coordinator.py
@@ -17,7 +17,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DEFAULT_MAX_RECORDS, DOMAIN, LOGGER
-T = TypeVar("T", bound=list[LidarrRootFolder] | LidarrQueue | str | LidarrAlbum | int)
+T = TypeVar("T", bound=list[LidarrRootFolder] | LidarrQueue | str | LidarrAlbum)
class LidarrDataUpdateCoordinator(DataUpdateCoordinator[T], Generic[T], ABC):
@@ -96,11 +96,3 @@ class WantedDataUpdateCoordinator(LidarrDataUpdateCoordinator[LidarrAlbum]):
LidarrAlbum,
await self.api_client.async_get_wanted(page_size=DEFAULT_MAX_RECORDS),
)
-
-
-class AlbumsDataUpdateCoordinator(LidarrDataUpdateCoordinator[int]):
- """Albums update coordinator."""
-
- async def _fetch_data(self) -> int:
- """Fetch the album data."""
- return len(cast(list[LidarrAlbum], await self.api_client.async_get_albums()))
diff --git a/homeassistant/components/lidarr/sensor.py b/homeassistant/components/lidarr/sensor.py
index b02361e65ca..e7ea1027ff0 100644
--- a/homeassistant/components/lidarr/sensor.py
+++ b/homeassistant/components/lidarr/sensor.py
@@ -85,7 +85,7 @@ SENSOR_TYPES: dict[str, LidarrSensorEntityDescription[Any]] = {
"queue": LidarrSensorEntityDescription[LidarrQueue](
key="queue",
translation_key="queue",
- native_unit_of_measurement="albums",
+ native_unit_of_measurement="Albums",
value_fn=lambda data, _: data.totalRecords,
state_class=SensorStateClass.TOTAL,
attributes_fn=lambda data: {i.title: queue_str(i) for i in data.records},
@@ -93,7 +93,7 @@ SENSOR_TYPES: dict[str, LidarrSensorEntityDescription[Any]] = {
"wanted": LidarrSensorEntityDescription[LidarrQueue](
key="wanted",
translation_key="wanted",
- native_unit_of_measurement="albums",
+ native_unit_of_measurement="Albums",
value_fn=lambda data, _: data.totalRecords,
state_class=SensorStateClass.TOTAL,
entity_registry_enabled_default=False,
@@ -101,14 +101,6 @@ SENSOR_TYPES: dict[str, LidarrSensorEntityDescription[Any]] = {
album.title: album.artist.artistName for album in data.records
},
),
- "albums": LidarrSensorEntityDescription[int](
- key="albums",
- translation_key="albums",
- native_unit_of_measurement="albums",
- value_fn=lambda data, _: data,
- state_class=SensorStateClass.TOTAL,
- entity_registry_enabled_default=False,
- ),
}
diff --git a/homeassistant/components/lidarr/strings.json b/homeassistant/components/lidarr/strings.json
index 68e9c395319..bbe4b19db25 100644
--- a/homeassistant/components/lidarr/strings.json
+++ b/homeassistant/components/lidarr/strings.json
@@ -39,9 +39,6 @@
},
"wanted": {
"name": "Wanted"
- },
- "albums": {
- "name": "Albums"
}
}
}
diff --git a/homeassistant/components/lifx/strings.json b/homeassistant/components/lifx/strings.json
index 19d86e57f09..68f9e31aabd 100644
--- a/homeassistant/components/lifx/strings.json
+++ b/homeassistant/components/lifx/strings.json
@@ -26,8 +26,7 @@
"abort": {
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
},
"entity": {
diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py
index 37ee6fe88fd..0bdabf26ff4 100644
--- a/homeassistant/components/light/__init__.py
+++ b/homeassistant/components/light/__init__.py
@@ -408,7 +408,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
def preprocess_data(data: dict[str, Any]) -> VolDictType:
"""Preprocess the service data."""
base: VolDictType = {
- entity_field: data.pop(entity_field) # type: ignore[arg-type]
+ entity_field: data.pop(entity_field)
for entity_field in cv.ENTITY_SERVICE_FIELDS
if entity_field in data
}
diff --git a/homeassistant/components/linear_garage_door/config_flow.py b/homeassistant/components/linear_garage_door/config_flow.py
index 2cfd0af6a8f..d1dda97c513 100644
--- a/homeassistant/components/linear_garage_door/config_flow.py
+++ b/homeassistant/components/linear_garage_door/config_flow.py
@@ -11,7 +11,7 @@ from linear_garage_door import Linear
from linear_garage_door.errors import InvalidLoginError
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -69,6 +69,7 @@ class LinearGarageDoorConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
self.data: dict[str, Sequence[Collection[str]]] = {}
+ self._reauth_entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -92,14 +93,14 @@ class LinearGarageDoorConfigFlow(ConfigFlow, domain=DOMAIN):
self.data = info
# Check if we are reauthenticating
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
- data_updates={
- CONF_EMAIL: self.data["email"],
- CONF_PASSWORD: self.data["password"],
- },
+ if self._reauth_entry is not None:
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry,
+ data=self._reauth_entry.data
+ | {"email": self.data["email"], "password": self.data["password"]},
)
+ await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return await self.async_step_site()
@@ -149,6 +150,9 @@ class LinearGarageDoorConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Reauth in case of a password change or other error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_user()
diff --git a/homeassistant/components/linkplay/__init__.py b/homeassistant/components/linkplay/__init__.py
index 918e52a755d..808f2f93ce2 100644
--- a/homeassistant/components/linkplay/__init__.py
+++ b/homeassistant/components/linkplay/__init__.py
@@ -4,7 +4,6 @@ from dataclasses import dataclass
from aiohttp import ClientSession
from linkplay.bridge import LinkPlayBridge
-from linkplay.controller import LinkPlayController
from linkplay.discovery import linkplay_factory_httpapi_bridge
from linkplay.exceptions import LinkPlayRequestException
@@ -13,7 +12,7 @@ from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
-from .const import CONTROLLER, CONTROLLER_KEY, DOMAIN, PLATFORMS
+from .const import PLATFORMS
from .utils import async_get_client_session
@@ -33,7 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: LinkPlayConfigEntry) ->
session: ClientSession = await async_get_client_session(hass)
bridge: LinkPlayBridge | None = None
- # try create a bridge
try:
bridge = await linkplay_factory_httpapi_bridge(entry.data[CONF_HOST], session)
except LinkPlayRequestException as exception:
@@ -41,19 +39,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: LinkPlayConfigEntry) ->
f"Failed to connect to LinkPlay device at {entry.data[CONF_HOST]}"
) from exception
- # setup the controller and discover multirooms
- controller: LinkPlayController | None = None
- hass.data.setdefault(DOMAIN, {})
- if CONTROLLER not in hass.data[DOMAIN]:
- controller = LinkPlayController(session)
- hass.data[DOMAIN][CONTROLLER_KEY] = controller
- else:
- controller = hass.data[DOMAIN][CONTROLLER_KEY]
-
- await controller.add_bridge(bridge)
- await controller.discover_multirooms()
-
- # forward to platforms
entry.runtime_data = LinkPlayData(bridge=bridge)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
diff --git a/homeassistant/components/linkplay/const.py b/homeassistant/components/linkplay/const.py
index a776365e38f..f531e311f46 100644
--- a/homeassistant/components/linkplay/const.py
+++ b/homeassistant/components/linkplay/const.py
@@ -1,12 +1,7 @@
"""LinkPlay constants."""
-from linkplay.controller import LinkPlayController
-
from homeassistant.const import Platform
-from homeassistant.util.hass_dict import HassKey
DOMAIN = "linkplay"
-CONTROLLER = "controller"
-CONTROLLER_KEY: HassKey[LinkPlayController] = HassKey(CONTROLLER)
PLATFORMS = [Platform.MEDIA_PLAYER]
DATA_SESSION = "session"
diff --git a/homeassistant/components/linkplay/diagnostics.py b/homeassistant/components/linkplay/diagnostics.py
deleted file mode 100644
index cfc1346aff4..00000000000
--- a/homeassistant/components/linkplay/diagnostics.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""Diagnostics support for Linkplay."""
-
-from __future__ import annotations
-
-from typing import Any
-
-from homeassistant.core import HomeAssistant
-
-from . import LinkPlayConfigEntry
-
-
-async def async_get_config_entry_diagnostics(
- hass: HomeAssistant, entry: LinkPlayConfigEntry
-) -> dict[str, Any]:
- """Return diagnostics for a config entry."""
- data = entry.runtime_data
- return {"device_info": data.bridge.to_dict()}
diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json
index e74d22b8207..dd1e08eda49 100644
--- a/homeassistant/components/linkplay/manifest.json
+++ b/homeassistant/components/linkplay/manifest.json
@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["linkplay"],
- "requirements": ["python-linkplay==0.0.20"],
+ "requirements": ["python-linkplay==0.0.15"],
"zeroconf": ["_linkplay._tcp.local."]
}
diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py
index c29c2978522..8654600ac73 100644
--- a/homeassistant/components/linkplay/media_player.py
+++ b/homeassistant/components/linkplay/media_player.py
@@ -8,8 +8,7 @@ from typing import Any, Concatenate
from linkplay.bridge import LinkPlayBridge
from linkplay.consts import EqualizerMode, LoopMode, PlayingMode, PlayingStatus
-from linkplay.controller import LinkPlayController, LinkPlayMultiroom
-from linkplay.exceptions import LinkPlayRequestException
+from linkplay.exceptions import LinkPlayException, LinkPlayRequestException
import voluptuous as vol
from homeassistant.components import media_source
@@ -23,20 +22,18 @@ from homeassistant.components.media_player import (
RepeatMode,
async_process_play_media_url,
)
-from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_platform,
- entity_registry as er,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utcnow
-from . import LinkPlayConfigEntry, LinkPlayData
-from .const import CONTROLLER_KEY, DOMAIN
+from . import LinkPlayConfigEntry
+from .const import DOMAIN
from .utils import MANUFACTURER_GENERIC, get_info_from_project
_LOGGER = logging.getLogger(__name__)
@@ -48,7 +45,6 @@ STATE_MAP: dict[PlayingStatus, MediaPlayerState] = {
}
SOURCE_MAP: dict[PlayingMode, str] = {
- PlayingMode.NETWORK: "Wifi",
PlayingMode.LINE_IN: "Line In",
PlayingMode.BLUETOOTH: "Bluetooth",
PlayingMode.OPTICAL: "Optical",
@@ -69,8 +65,6 @@ SOURCE_MAP: dict[PlayingMode, str] = {
PlayingMode.FM: "FM Radio",
PlayingMode.RCA: "RCA",
PlayingMode.UDISK: "USB",
- PlayingMode.SPOTIFY: "Spotify",
- PlayingMode.TIDAL: "Tidal",
PlayingMode.FOLLOWER: "Follower",
}
@@ -203,8 +197,9 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity):
try:
await self._bridge.player.update_status()
self._update_properties()
- except LinkPlayRequestException:
+ except LinkPlayException:
self._attr_available = False
+ raise
@exception_wrap
async def async_select_source(self, source: str) -> None:
@@ -293,82 +288,7 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity):
@exception_wrap
async def async_play_preset(self, preset_number: int) -> None:
"""Play preset number."""
- try:
- await self._bridge.player.play_preset(preset_number)
- except ValueError as err:
- raise HomeAssistantError(err) from err
-
- @exception_wrap
- async def async_media_seek(self, position: float) -> None:
- """Seek to a position."""
- await self._bridge.player.seek(round(position))
-
- @exception_wrap
- async def async_join_players(self, group_members: list[str]) -> None:
- """Join `group_members` as a player group with the current player."""
-
- controller: LinkPlayController = self.hass.data[DOMAIN][CONTROLLER_KEY]
- multiroom = self._bridge.multiroom
- if multiroom is None:
- multiroom = LinkPlayMultiroom(self._bridge)
-
- for group_member in group_members:
- bridge = self._get_linkplay_bridge(group_member)
- if bridge:
- await multiroom.add_follower(bridge)
-
- await controller.discover_multirooms()
-
- def _get_linkplay_bridge(self, entity_id: str) -> LinkPlayBridge:
- """Get linkplay bridge from entity_id."""
-
- entity_registry = er.async_get(self.hass)
-
- # Check for valid linkplay media_player entity
- entity_entry = entity_registry.async_get(entity_id)
-
- if (
- entity_entry is None
- or entity_entry.domain != Platform.MEDIA_PLAYER
- or entity_entry.platform != DOMAIN
- or entity_entry.config_entry_id is None
- ):
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="invalid_grouping_entity",
- translation_placeholders={"entity_id": entity_id},
- )
-
- config_entry = self.hass.config_entries.async_get_entry(
- entity_entry.config_entry_id
- )
- assert config_entry
-
- # Return bridge
- data: LinkPlayData = config_entry.runtime_data
- return data.bridge
-
- @property
- def group_members(self) -> list[str]:
- """List of players which are grouped together."""
- multiroom = self._bridge.multiroom
- if multiroom is not None:
- return [multiroom.leader.device.uuid] + [
- follower.device.uuid for follower in multiroom.followers
- ]
-
- return []
-
- @exception_wrap
- async def async_unjoin_player(self) -> None:
- """Remove this player from any group."""
- controller: LinkPlayController = self.hass.data[DOMAIN][CONTROLLER_KEY]
-
- multiroom = self._bridge.multiroom
- if multiroom is not None:
- await multiroom.remove_follower(self._bridge)
-
- await controller.discover_multirooms()
+ await self._bridge.player.play_preset(preset_number)
def _update_properties(self) -> None:
"""Update the properties of the media player."""
@@ -388,9 +308,9 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity):
)
self._attr_source = SOURCE_MAP.get(self._bridge.player.play_mode, "other")
- self._attr_media_position = self._bridge.player.current_position_in_seconds
+ self._attr_media_position = self._bridge.player.current_position / 1000
self._attr_media_position_updated_at = utcnow()
- self._attr_media_duration = self._bridge.player.total_length_in_seconds
+ self._attr_media_duration = self._bridge.player.total_length / 1000
self._attr_media_artist = self._bridge.player.artist
self._attr_media_title = self._bridge.player.title
self._attr_media_album_name = self._bridge.player.album
diff --git a/homeassistant/components/linkplay/services.yaml b/homeassistant/components/linkplay/services.yaml
index 0d7335a28c8..20bc47be7a7 100644
--- a/homeassistant/components/linkplay/services.yaml
+++ b/homeassistant/components/linkplay/services.yaml
@@ -11,4 +11,5 @@ play_preset:
selector:
number:
min: 1
+ max: 10
mode: box
diff --git a/homeassistant/components/linkplay/strings.json b/homeassistant/components/linkplay/strings.json
index f3495b293e0..12870816af7 100644
--- a/homeassistant/components/linkplay/strings.json
+++ b/homeassistant/components/linkplay/strings.json
@@ -34,10 +34,5 @@
}
}
}
- },
- "exceptions": {
- "invalid_grouping_entity": {
- "message": "Entity with id {entity_id} can't be added to the LinkPlay multiroom. Is the entity a LinkPlay mediaplayer?"
- }
}
}
diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py
index 9aa0b19c506..19ddf0122c4 100644
--- a/homeassistant/components/litejet/config_flow.py
+++ b/homeassistant/components/litejet/config_flow.py
@@ -24,6 +24,10 @@ from .const import CONF_DEFAULT_TRANSITION, DOMAIN
class LiteJetOptionsFlow(OptionsFlow):
"""Handle LiteJet options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize LiteJet options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -53,6 +57,9 @@ class LiteJetConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Create a LiteJet config entry based upon user input."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
errors = {}
if user_input is not None:
port = user_input[CONF_PORT]
@@ -80,4 +87,4 @@ class LiteJetConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> LiteJetOptionsFlow:
"""Get the options flow for this handler."""
- return LiteJetOptionsFlow()
+ return LiteJetOptionsFlow(config_entry)
diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json
index 1df907029a9..3cff83707f5 100644
--- a/homeassistant/components/litejet/manifest.json
+++ b/homeassistant/components/litejet/manifest.json
@@ -8,6 +8,5 @@
"iot_class": "local_push",
"loggers": ["pylitejet"],
"quality_scale": "platinum",
- "requirements": ["pylitejet==0.6.3"],
- "single_config_entry": true
+ "requirements": ["pylitejet==0.6.3"]
}
diff --git a/homeassistant/components/litejet/strings.json b/homeassistant/components/litejet/strings.json
index c55df54c931..398f1a1e5aa 100644
--- a/homeassistant/components/litejet/strings.json
+++ b/homeassistant/components/litejet/strings.json
@@ -9,6 +9,9 @@
}
}
},
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ },
"error": {
"open_failed": "Cannot open the specified serial port."
}
diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py
index 90f1fcba56d..633c6a5a5a2 100644
--- a/homeassistant/components/litterrobot/config_flow.py
+++ b/homeassistant/components/litterrobot/config_flow.py
@@ -43,11 +43,16 @@ class LitterRobotConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle user's reauth credentials."""
errors = {}
if user_input:
- user_input = user_input | {CONF_USERNAME: self.username}
- if not (error := await self._async_validate_input(user_input)):
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=user_input
- )
+ entry_id = self.context["entry_id"]
+ if entry := self.hass.config_entries.async_get_entry(entry_id):
+ user_input = user_input | {CONF_USERNAME: self.username}
+ if not (error := await self._async_validate_input(user_input)):
+ self.hass.config_entries.async_update_entry(
+ entry,
+ data=entry.data | user_input,
+ )
+ await self.hass.config_entries.async_reload(entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
errors["base"] = error
return self.async_show_form(
diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py
index f5553bf5d49..a1ed2ea600d 100644
--- a/homeassistant/components/litterrobot/vacuum.py
+++ b/homeassistant/components/litterrobot/vacuum.py
@@ -18,6 +18,7 @@ from homeassistant.components.vacuum import (
StateVacuumEntityDescription,
VacuumEntityFeature,
)
+from homeassistant.const import STATE_OFF
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -38,7 +39,7 @@ LITTER_BOX_STATUS_STATE_MAP = {
LitterBoxStatus.DRAWER_FULL_2: STATE_DOCKED,
LitterBoxStatus.READY: STATE_DOCKED,
LitterBoxStatus.CAT_SENSOR_INTERRUPTED: STATE_PAUSED,
- LitterBoxStatus.OFF: STATE_DOCKED,
+ LitterBoxStatus.OFF: STATE_OFF,
}
LITTER_BOX_ENTITY = StateVacuumEntityDescription(
diff --git a/homeassistant/components/local_calendar/strings.json b/homeassistant/components/local_calendar/strings.json
index 2b61fc9ab3e..387cfdcf092 100644
--- a/homeassistant/components/local_calendar/strings.json
+++ b/homeassistant/components/local_calendar/strings.json
@@ -13,9 +13,6 @@
"description": "You can import events in iCal format (.ics file)."
}
},
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
- },
"error": {
"invalid_ics_file": "Invalid .ics file"
}
diff --git a/homeassistant/components/local_file/__init__.py b/homeassistant/components/local_file/__init__.py
index 70144cd0704..4ad752bbc54 100644
--- a/homeassistant/components/local_file/__init__.py
+++ b/homeassistant/components/local_file/__init__.py
@@ -1,37 +1 @@
"""The local_file component."""
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_FILE_PATH, Platform
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryError
-
-from .const import DOMAIN
-from .util import check_file_path_access
-
-PLATFORMS = [Platform.CAMERA]
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
- """Set up Local file from a config entry."""
- file_path: str = entry.options[CONF_FILE_PATH]
- if not await hass.async_add_executor_job(check_file_path_access, file_path):
- raise ConfigEntryError(
- translation_domain=DOMAIN,
- translation_key="not_readable_path",
- translation_placeholders={"file_path": file_path},
- )
-
- await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
- entry.async_on_unload(entry.add_update_listener(update_listener))
-
- return True
-
-
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
- """Unload Local file config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
-
-
-async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
- """Handle options update."""
- await hass.config_entries.async_reload(entry.entry_id)
diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py
index db421bbce1d..74d887b613f 100644
--- a/homeassistant/components/local_file/camera.py
+++ b/homeassistant/components/local_file/camera.py
@@ -4,6 +4,7 @@ from __future__ import annotations
import logging
import mimetypes
+import os
import voluptuous as vol
@@ -11,21 +12,14 @@ from homeassistant.components.camera import (
PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA,
Camera,
)
-from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_FILE_PATH, CONF_NAME
-from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
-from homeassistant.exceptions import ServiceValidationError
-from homeassistant.helpers import (
- config_validation as cv,
- entity_platform,
- issue_registry as ir,
-)
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import PlatformNotReady, ServiceValidationError
+from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
-from homeassistant.util import slugify
-from .const import DEFAULT_NAME, DOMAIN, SERVICE_UPDATE_FILE_PATH
-from .util import check_file_path_access
+from .const import DEFAULT_NAME, SERVICE_UPDATE_FILE_PATH
_LOGGER = logging.getLogger(__name__)
@@ -37,31 +31,11 @@ PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend(
)
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the Camera for local file from a config entry."""
-
- platform = entity_platform.async_get_current_platform()
- platform.async_register_entity_service(
- SERVICE_UPDATE_FILE_PATH,
- {
- vol.Required(CONF_FILE_PATH): cv.string,
- },
- "update_file_path",
- )
-
- async_add_entities(
- [
- LocalFile(
- entry.options[CONF_NAME],
- entry.options[CONF_FILE_PATH],
- entry.entry_id,
- )
- ]
- )
+def check_file_path_access(file_path: str) -> bool:
+ """Check that filepath given is readable."""
+ if not os.access(file_path, os.R_OK):
+ return False
+ return True
async def async_setup_platform(
@@ -72,57 +46,29 @@ async def async_setup_platform(
) -> None:
"""Set up the Camera that works with local files."""
file_path: str = config[CONF_FILE_PATH]
- file_path_slug = slugify(file_path)
+
+ platform = entity_platform.async_get_current_platform()
+ platform.async_register_entity_service(
+ SERVICE_UPDATE_FILE_PATH,
+ {
+ vol.Required(CONF_FILE_PATH): cv.string,
+ },
+ "update_file_path",
+ )
if not await hass.async_add_executor_job(check_file_path_access, file_path):
- ir.async_create_issue(
- hass,
- DOMAIN,
- f"no_access_path_{file_path_slug}",
- breaks_in_ha_version="2025.5.0",
- is_fixable=False,
- learn_more_url="https://www.home-assistant.io/integrations/local_file/",
- severity=ir.IssueSeverity.WARNING,
- translation_key="no_access_path",
- translation_placeholders={
- "file_path": file_path_slug,
- },
- )
- return
+ raise PlatformNotReady(f"File path {file_path} is not readable")
- ir.async_create_issue(
- hass,
- HOMEASSISTANT_DOMAIN,
- f"deprecated_yaml_{DOMAIN}",
- breaks_in_ha_version="2025.5.0",
- is_fixable=False,
- issue_domain=DOMAIN,
- learn_more_url="https://www.home-assistant.io/integrations/local_file/",
- severity=ir.IssueSeverity.WARNING,
- translation_key="deprecated_yaml",
- translation_placeholders={
- "domain": DOMAIN,
- "integration_title": "Local file",
- },
- )
-
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_IMPORT},
- data=config,
- )
- )
+ async_add_entities([LocalFile(config[CONF_NAME], file_path)])
class LocalFile(Camera):
"""Representation of a local file camera."""
- def __init__(self, name: str, file_path: str, unique_id: str) -> None:
+ def __init__(self, name: str, file_path: str) -> None:
"""Initialize Local File Camera component."""
super().__init__()
self._attr_name = name
- self._attr_unique_id = unique_id
self._file_path = file_path
# Set content type of local file
content, _ = mimetypes.guess_type(file_path)
diff --git a/homeassistant/components/local_file/config_flow.py b/homeassistant/components/local_file/config_flow.py
deleted file mode 100644
index 36a41c03543..00000000000
--- a/homeassistant/components/local_file/config_flow.py
+++ /dev/null
@@ -1,77 +0,0 @@
-"""Config flow for Local file."""
-
-from __future__ import annotations
-
-from collections.abc import Mapping
-from typing import Any, cast
-
-import voluptuous as vol
-
-from homeassistant.const import CONF_FILE_PATH, CONF_NAME
-from homeassistant.helpers.schema_config_entry_flow import (
- SchemaCommonFlowHandler,
- SchemaConfigFlowHandler,
- SchemaFlowError,
- SchemaFlowFormStep,
-)
-from homeassistant.helpers.selector import TextSelector
-
-from .const import DEFAULT_NAME, DOMAIN
-from .util import check_file_path_access
-
-
-async def validate_options(
- handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
-) -> dict[str, Any]:
- """Validate options selected."""
- file_path: str = user_input[CONF_FILE_PATH]
- if not await handler.parent_handler.hass.async_add_executor_job(
- check_file_path_access, file_path
- ):
- raise SchemaFlowError("not_readable_path")
-
- handler.parent_handler._async_abort_entries_match( # noqa: SLF001
- {CONF_FILE_PATH: user_input[CONF_FILE_PATH]}
- )
-
- return user_input
-
-
-DATA_SCHEMA_OPTIONS = vol.Schema(
- {
- vol.Required(CONF_FILE_PATH): TextSelector(),
- }
-)
-DATA_SCHEMA_SETUP = vol.Schema(
- {
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
- }
-).extend(DATA_SCHEMA_OPTIONS.schema)
-
-CONFIG_FLOW = {
- "user": SchemaFlowFormStep(
- schema=DATA_SCHEMA_SETUP,
- validate_user_input=validate_options,
- ),
- "import": SchemaFlowFormStep(
- schema=DATA_SCHEMA_SETUP,
- validate_user_input=validate_options,
- ),
-}
-OPTIONS_FLOW = {
- "init": SchemaFlowFormStep(
- DATA_SCHEMA_OPTIONS,
- validate_user_input=validate_options,
- )
-}
-
-
-class LocalFileConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
- """Handle a config flow for Local file."""
-
- config_flow = CONFIG_FLOW
- options_flow = OPTIONS_FLOW
-
- def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
- """Return config entry title."""
- return cast(str, options[CONF_NAME])
diff --git a/homeassistant/components/local_file/manifest.json b/homeassistant/components/local_file/manifest.json
index 0e6e64d17e5..46268ff2a77 100644
--- a/homeassistant/components/local_file/manifest.json
+++ b/homeassistant/components/local_file/manifest.json
@@ -2,7 +2,6 @@
"domain": "local_file",
"name": "Local File",
"codeowners": [],
- "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_file",
"iot_class": "local_polling"
}
diff --git a/homeassistant/components/local_file/strings.json b/homeassistant/components/local_file/strings.json
index abf31a6f94e..801d85ce1e0 100644
--- a/homeassistant/components/local_file/strings.json
+++ b/homeassistant/components/local_file/strings.json
@@ -1,42 +1,4 @@
{
- "config": {
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
- },
- "error": {
- "not_readable_path": "The provided path to the file can not be read"
- },
- "step": {
- "user": {
- "data": {
- "name": "[%key:common::config_flow::data::name%]",
- "file_path": "File path"
- },
- "data_description": {
- "name": "Name for the created entity.",
- "file_path": "The full path to the image file to be displayed. Be sure the path of the file is in the allowed paths, you can read more about this in the documentation."
- }
- }
- }
- },
- "options": {
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
- },
- "error": {
- "not_readable_path": "[%key:component::local_file::config::error::not_readable_path%]"
- },
- "step": {
- "init": {
- "data": {
- "file_path": "[%key:component::local_file::config::step::user::data::file_path%]"
- },
- "data_description": {
- "file_path": "[%key:component::local_file::config::step::user::data_description::file_path%]"
- }
- }
- }
- },
"services": {
"update_file_path": {
"name": "Updates file path",
@@ -44,7 +6,7 @@
"fields": {
"file_path": {
"name": "File path",
- "description": "[%key:component::local_file::config::step::user::data_description::file_path%]"
+ "description": "The full path to the new image file to be displayed."
}
}
}
@@ -53,11 +15,5 @@
"file_path_not_accessible": {
"message": "Path {file_path} is not accessible"
}
- },
- "issues": {
- "no_access_path": {
- "title": "Incorrect file path",
- "description": "While trying to import your configuration the provided file path {file_path} could not be read.\nPlease update your configuration to a correct file path and restart to fix this issue."
- }
}
}
diff --git a/homeassistant/components/local_file/util.py b/homeassistant/components/local_file/util.py
deleted file mode 100644
index 9e25bb88678..00000000000
--- a/homeassistant/components/local_file/util.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""Utils for local file."""
-
-import os
-
-
-def check_file_path_access(file_path: str) -> bool:
- """Check that filepath given is readable."""
- if not os.access(file_path, os.R_OK):
- return False
- return True
diff --git a/homeassistant/components/local_ip/config_flow.py b/homeassistant/components/local_ip/config_flow.py
index 6bf9f865489..3a4612d84aa 100644
--- a/homeassistant/components/local_ip/config_flow.py
+++ b/homeassistant/components/local_ip/config_flow.py
@@ -16,6 +16,9 @@ class SimpleConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
if user_input is None:
return self.async_show_form(step_id="user")
diff --git a/homeassistant/components/local_ip/manifest.json b/homeassistant/components/local_ip/manifest.json
index 6a68ed59628..11d86ea0230 100644
--- a/homeassistant/components/local_ip/manifest.json
+++ b/homeassistant/components/local_ip/manifest.json
@@ -5,6 +5,5 @@
"config_flow": true,
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/local_ip",
- "iot_class": "local_polling",
- "single_config_entry": true
+ "iot_class": "local_polling"
}
diff --git a/homeassistant/components/local_ip/strings.json b/homeassistant/components/local_ip/strings.json
index 7f7508aa9b3..a4d9138d88e 100644
--- a/homeassistant/components/local_ip/strings.json
+++ b/homeassistant/components/local_ip/strings.json
@@ -6,6 +6,9 @@
"title": "[%key:component::local_ip::title%]",
"description": "[%key:common::config_flow::description::confirm_setup%]"
}
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}
diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py
index 2e2ffddac88..239a52ff7a1 100644
--- a/homeassistant/components/logbook/__init__.py
+++ b/homeassistant/components/logbook/__init__.py
@@ -55,7 +55,7 @@ CONFIG_SCHEMA = vol.Schema(
LOG_MESSAGE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_NAME): cv.string,
- vol.Required(ATTR_MESSAGE): cv.string,
+ vol.Required(ATTR_MESSAGE): cv.template,
vol.Optional(ATTR_DOMAIN): cv.slug,
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
}
@@ -112,6 +112,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
# away so we use the "logbook" domain
domain = DOMAIN
+ message = message.async_render(parse_result=False)
async_log_entry(hass, name, message, domain, entity_id, service.context)
frontend.async_register_built_in_panel(
diff --git a/homeassistant/components/london_underground/coordinator.py b/homeassistant/components/london_underground/coordinator.py
index 29d1e8e2f54..cf14ad14b43 100644
--- a/homeassistant/components/london_underground/coordinator.py
+++ b/homeassistant/components/london_underground/coordinator.py
@@ -24,7 +24,6 @@ class LondonTubeCoordinator(DataUpdateCoordinator[dict[str, dict[str, str]]]):
super().__init__(
hass,
_LOGGER,
- config_entry=None,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py
index 37f0f27d2d8..9079b056731 100644
--- a/homeassistant/components/luftdaten/__init__.py
+++ b/homeassistant/components/luftdaten/__init__.py
@@ -52,7 +52,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator: DataUpdateCoordinator[dict[str, Any]] = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"{DOMAIN}_{sensor_community.sensor_id}",
update_interval=DEFAULT_SCAN_INTERVAL,
update_method=async_update,
diff --git a/homeassistant/components/luftdaten/strings.json b/homeassistant/components/luftdaten/strings.json
index ea842f18ebd..b7d0a90b511 100644
--- a/homeassistant/components/luftdaten/strings.json
+++ b/homeassistant/components/luftdaten/strings.json
@@ -8,9 +8,6 @@
}
}
},
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
- },
"error": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"invalid_sensor": "Sensor not available or invalid",
diff --git a/homeassistant/components/lupusec/alarm_control_panel.py b/homeassistant/components/lupusec/alarm_control_panel.py
index 4b3d12ad743..73aba775a2a 100644
--- a/homeassistant/components/lupusec/alarm_control_panel.py
+++ b/homeassistant/components/lupusec/alarm_control_panel.py
@@ -9,9 +9,14 @@ import lupupy
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -59,16 +64,16 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity):
)
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the device."""
if self._device.is_standby:
- state = AlarmControlPanelState.DISARMED
+ state = STATE_ALARM_DISARMED
elif self._device.is_away:
- state = AlarmControlPanelState.ARMED_AWAY
+ state = STATE_ALARM_ARMED_AWAY
elif self._device.is_home:
- state = AlarmControlPanelState.ARMED_HOME
+ state = STATE_ALARM_ARMED_HOME
elif self._device.is_alarm_triggered:
- state = AlarmControlPanelState.TRIGGERED
+ state = STATE_ALARM_TRIGGERED
else:
state = None
return state
diff --git a/homeassistant/components/lutron/config_flow.py b/homeassistant/components/lutron/config_flow.py
index 6a48e0d4b67..e14d56fde57 100644
--- a/homeassistant/components/lutron/config_flow.py
+++ b/homeassistant/components/lutron/config_flow.py
@@ -26,6 +26,11 @@ class LutronConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""First step in the config flow."""
+
+ # Check if a configuration entry already exists
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
errors = {}
if user_input is not None:
diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json
index 82bdfad4774..d9432f77bba 100644
--- a/homeassistant/components/lutron/manifest.json
+++ b/homeassistant/components/lutron/manifest.json
@@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/lutron",
"iot_class": "local_polling",
"loggers": ["pylutron"],
- "requirements": ["pylutron==0.2.16"],
- "single_config_entry": true
+ "requirements": ["pylutron==0.2.15"]
}
diff --git a/homeassistant/components/lutron/scene.py b/homeassistant/components/lutron/scene.py
index 9e8070713a9..b66ca08a587 100644
--- a/homeassistant/components/lutron/scene.py
+++ b/homeassistant/components/lutron/scene.py
@@ -51,4 +51,4 @@ class LutronScene(LutronKeypad, Scene):
def activate(self, **kwargs: Any) -> None:
"""Activate the scene."""
- self._lutron_device.tap()
+ self._lutron_device.press()
diff --git a/homeassistant/components/lutron/strings.json b/homeassistant/components/lutron/strings.json
index b73e0bd15ed..770a453eb9e 100644
--- a/homeassistant/components/lutron/strings.json
+++ b/homeassistant/components/lutron/strings.json
@@ -17,6 +17,9 @@
"description": "Please enter the main repeater login information",
"title": "Main repeater setup"
}
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"entity": {
diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json
index e96778f0a31..776e771b9d3 100644
--- a/homeassistant/components/lutron_caseta/manifest.json
+++ b/homeassistant/components/lutron_caseta/manifest.json
@@ -11,12 +11,6 @@
"loggers": ["pylutron_caseta"],
"requirements": ["pylutron-caseta==0.21.1"],
"zeroconf": [
- {
- "type": "_lutron._tcp.local.",
- "properties": {
- "SYSTYPE": "hwqs*"
- }
- },
{
"type": "_lutron._tcp.local.",
"properties": {
diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py
index f99adf26999..b338605a6ea 100644
--- a/homeassistant/components/lyric/__init__.py
+++ b/homeassistant/components/lyric/__init__.py
@@ -95,7 +95,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator[Lyric](
hass,
_LOGGER,
- config_entry=entry,
# Name of the data. For logging purposes.
name="lyric_coordinator",
update_method=async_update_data,
diff --git a/homeassistant/components/lyric/api.py b/homeassistant/components/lyric/api.py
index 7399e013b96..c9a424bf8ab 100644
--- a/homeassistant/components/lyric/api.py
+++ b/homeassistant/components/lyric/api.py
@@ -36,7 +36,8 @@ class ConfigEntryLyricClient(LyricClient):
async def async_get_access_token(self):
"""Return a valid access token."""
- await self._oauth_session.async_ensure_token_valid()
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token["access_token"]
diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json
index 83c65359643..739ad7fad68 100644
--- a/homeassistant/components/lyric/strings.json
+++ b/homeassistant/components/lyric/strings.json
@@ -16,8 +16,7 @@
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
- "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
- "missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]"
+ "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
diff --git a/homeassistant/components/madvr/config_flow.py b/homeassistant/components/madvr/config_flow.py
index 60f7b8fc481..9151df1ef3c 100644
--- a/homeassistant/components/madvr/config_flow.py
+++ b/homeassistant/components/madvr/config_flow.py
@@ -44,9 +44,15 @@ class MadVRConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle reconfiguration of the device."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfiguration flow initialized by the user."""
- return await self._handle_config_step(user_input, step_id="reconfigure")
+ return await self._handle_config_step(user_input, step_id="reconfigure_confirm")
async def _handle_config_step(
self, user_input: dict[str, Any] | None = None, step_id: str = "user"
diff --git a/homeassistant/components/madvr/strings.json b/homeassistant/components/madvr/strings.json
index 06851efa2c8..9c7594c68d0 100644
--- a/homeassistant/components/madvr/strings.json
+++ b/homeassistant/components/madvr/strings.json
@@ -13,7 +13,7 @@
"port": "The port your madVR Envy is listening on. In 99% of cases, leave this as the default."
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"title": "Reconfigure madVR Envy",
"description": "Your device needs to be on in order to reconfigure the integation.",
"data": {
diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py
index 244f38e0902..c1910d0dfa1 100644
--- a/homeassistant/components/manual/alarm_control_panel.py
+++ b/homeassistant/components/manual/alarm_control_panel.py
@@ -11,7 +11,6 @@ from homeassistant.components.alarm_control_panel import (
PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA,
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
from homeassistant.const import (
@@ -22,6 +21,15 @@ from homeassistant.const import (
CONF_NAME,
CONF_TRIGGER_TIME,
CONF_UNIQUE_ID,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
@@ -39,16 +47,6 @@ CONF_ARMING_STATES = "arming_states"
CONF_CODE_TEMPLATE = "code_template"
CONF_CODE_ARM_REQUIRED = "code_arm_required"
-CONF_ALARM_ARMED_AWAY = "armed_away"
-CONF_ALARM_ARMED_CUSTOM_BYPASS = "armed_custom_bypass"
-CONF_ALARM_ARMED_HOME = "armed_home"
-CONF_ALARM_ARMED_NIGHT = "armed_night"
-CONF_ALARM_ARMED_VACATION = "armed_vacation"
-CONF_ALARM_ARMING = "arming"
-CONF_ALARM_DISARMED = "disarmed"
-CONF_ALARM_PENDING = "pending"
-CONF_ALARM_TRIGGERED = "triggered"
-
DEFAULT_ALARM_NAME = "HA Alarm"
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=60)
DEFAULT_ARMING_TIME = datetime.timedelta(seconds=60)
@@ -56,46 +54,39 @@ DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
DEFAULT_DISARM_AFTER_TRIGGER = False
SUPPORTED_STATES = [
- AlarmControlPanelState.DISARMED,
- AlarmControlPanelState.ARMED_AWAY,
- AlarmControlPanelState.ARMED_HOME,
- AlarmControlPanelState.ARMED_NIGHT,
- AlarmControlPanelState.ARMED_VACATION,
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
- AlarmControlPanelState.TRIGGERED,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_TRIGGERED,
]
SUPPORTED_PRETRIGGER_STATES = [
- state for state in SUPPORTED_STATES if state != AlarmControlPanelState.TRIGGERED
+ state for state in SUPPORTED_STATES if state != STATE_ALARM_TRIGGERED
]
SUPPORTED_ARMING_STATES = [
state
for state in SUPPORTED_STATES
- if state
- not in (
- AlarmControlPanelState.DISARMED,
- AlarmControlPanelState.TRIGGERED,
- )
+ if state not in (STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
]
SUPPORTED_ARMING_STATE_TO_FEATURE = {
- AlarmControlPanelState.ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY,
- AlarmControlPanelState.ARMED_HOME: AlarmControlPanelEntityFeature.ARM_HOME,
- AlarmControlPanelState.ARMED_NIGHT: AlarmControlPanelEntityFeature.ARM_NIGHT,
- AlarmControlPanelState.ARMED_VACATION: AlarmControlPanelEntityFeature.ARM_VACATION,
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY,
+ STATE_ALARM_ARMED_HOME: AlarmControlPanelEntityFeature.ARM_HOME,
+ STATE_ALARM_ARMED_NIGHT: AlarmControlPanelEntityFeature.ARM_NIGHT,
+ STATE_ALARM_ARMED_VACATION: AlarmControlPanelEntityFeature.ARM_VACATION,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
}
ATTR_PREVIOUS_STATE = "previous_state"
ATTR_NEXT_STATE = "next_state"
-def _state_validator(
- config: dict[AlarmControlPanelState | str, Any],
-) -> dict[str, Any]:
+def _state_validator(config: dict[str, Any]) -> dict[str, Any]:
"""Validate the state."""
- state: AlarmControlPanelState
for state in SUPPORTED_PRETRIGGER_STATES:
if CONF_DELAY_TIME not in config[state]:
config[state] = config[state] | {CONF_DELAY_TIME: config[CONF_DELAY_TIME]}
@@ -151,26 +142,26 @@ PLATFORM_SCHEMA = vol.Schema(
vol.Optional(
CONF_ARMING_STATES, default=SUPPORTED_ARMING_STATES
): vol.All(cv.ensure_list, [vol.In(SUPPORTED_ARMING_STATES)]),
- vol.Optional(CONF_ALARM_ARMED_AWAY, default={}): _state_schema(
- AlarmControlPanelState.ARMED_AWAY
+ vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema(
+ STATE_ALARM_ARMED_AWAY
),
- vol.Optional(CONF_ALARM_ARMED_HOME, default={}): _state_schema(
- AlarmControlPanelState.ARMED_HOME
+ vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema(
+ STATE_ALARM_ARMED_HOME
),
- vol.Optional(CONF_ALARM_ARMED_NIGHT, default={}): _state_schema(
- AlarmControlPanelState.ARMED_NIGHT
+ vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): _state_schema(
+ STATE_ALARM_ARMED_NIGHT
),
- vol.Optional(CONF_ALARM_ARMED_VACATION, default={}): _state_schema(
- AlarmControlPanelState.ARMED_VACATION
+ vol.Optional(STATE_ALARM_ARMED_VACATION, default={}): _state_schema(
+ STATE_ALARM_ARMED_VACATION
),
- vol.Optional(CONF_ALARM_ARMED_CUSTOM_BYPASS, default={}): _state_schema(
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS
+ vol.Optional(
+ STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}
+ ): _state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS),
+ vol.Optional(STATE_ALARM_DISARMED, default={}): _state_schema(
+ STATE_ALARM_DISARMED
),
- vol.Optional(CONF_ALARM_DISARMED, default={}): _state_schema(
- AlarmControlPanelState.DISARMED
- ),
- vol.Optional(CONF_ALARM_TRIGGERED, default={}): _state_schema(
- AlarmControlPanelState.TRIGGERED
+ vol.Optional(STATE_ALARM_TRIGGERED, default={}): _state_schema(
+ STATE_ALARM_TRIGGERED
),
},
),
@@ -226,25 +217,25 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
config: dict[str, Any],
) -> None:
"""Init the manual alarm panel."""
- self._state: AlarmControlPanelState = AlarmControlPanelState.DISARMED
+ self._state = STATE_ALARM_DISARMED
self._hass = hass
self._attr_name = name
self._attr_unique_id = unique_id
self._code = code_template or code or None
self._attr_code_arm_required = code_arm_required
self._disarm_after_trigger = disarm_after_trigger
- self._previous_state: AlarmControlPanelState = self._state
+ self._previous_state = self._state
self._state_ts: datetime.datetime = dt_util.utcnow()
- self._delay_time_by_state: dict[AlarmControlPanelState, Any] = {
+ self._delay_time_by_state = {
state: config[state][CONF_DELAY_TIME]
for state in SUPPORTED_PRETRIGGER_STATES
}
- self._trigger_time_by_state: dict[AlarmControlPanelState, Any] = {
+ self._trigger_time_by_state = {
state: config[state][CONF_TRIGGER_TIME]
for state in SUPPORTED_PRETRIGGER_STATES
}
- self._arming_time_by_state: dict[AlarmControlPanelState, Any] = {
+ self._arming_time_by_state = {
state: config[state][CONF_ARMING_TIME] for state in SUPPORTED_ARMING_STATES
}
@@ -255,11 +246,11 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
]
@property
- def alarm_state(self) -> AlarmControlPanelState:
+ def state(self) -> str:
"""Return the state of the device."""
- if self._state == AlarmControlPanelState.TRIGGERED:
+ if self._state == STATE_ALARM_TRIGGERED:
if self._within_pending_time(self._state):
- return AlarmControlPanelState.PENDING
+ return STATE_ALARM_PENDING
trigger_time: datetime.timedelta = self._trigger_time_by_state[
self._previous_state
]
@@ -267,42 +258,39 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
self._state_ts + self._pending_time(self._state) + trigger_time
) < dt_util.utcnow():
if self._disarm_after_trigger:
- return AlarmControlPanelState.DISARMED
+ return STATE_ALARM_DISARMED
self._state = self._previous_state
return self._state
if self._state in SUPPORTED_ARMING_STATES and self._within_arming_time(
self._state
):
- return AlarmControlPanelState.ARMING
+ return STATE_ALARM_ARMING
return self._state
@property
- def _active_state(self) -> AlarmControlPanelState:
+ def _active_state(self) -> str:
"""Get the current state."""
- if self.state in (
- AlarmControlPanelState.PENDING,
- AlarmControlPanelState.ARMING,
- ):
+ if self.state in (STATE_ALARM_PENDING, STATE_ALARM_ARMING):
return self._previous_state
return self._state
- def _arming_time(self, state: AlarmControlPanelState) -> datetime.timedelta:
+ def _arming_time(self, state: str) -> datetime.timedelta:
"""Get the arming time."""
arming_time: datetime.timedelta = self._arming_time_by_state[state]
return arming_time
- def _pending_time(self, state: AlarmControlPanelState) -> datetime.timedelta:
+ def _pending_time(self, state: str) -> datetime.timedelta:
"""Get the pending time."""
delay_time: datetime.timedelta = self._delay_time_by_state[self._previous_state]
return delay_time
- def _within_arming_time(self, state: AlarmControlPanelState) -> bool:
+ def _within_arming_time(self, state: str) -> bool:
"""Get if the action is in the arming time window."""
return self._state_ts + self._arming_time(state) > dt_util.utcnow()
- def _within_pending_time(self, state: AlarmControlPanelState) -> bool:
+ def _within_pending_time(self, state: str) -> bool:
"""Get if the action is in the pending time window."""
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
@@ -317,35 +305,35 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
- self._async_validate_code(code, AlarmControlPanelState.DISARMED)
- self._state = AlarmControlPanelState.DISARMED
+ self._async_validate_code(code, STATE_ALARM_DISARMED)
+ self._state = STATE_ALARM_DISARMED
self._state_ts = dt_util.utcnow()
self.async_write_ha_state()
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
- self._async_validate_code(code, AlarmControlPanelState.ARMED_HOME)
- self._async_update_state(AlarmControlPanelState.ARMED_HOME)
+ self._async_validate_code(code, STATE_ALARM_ARMED_HOME)
+ self._async_update_state(STATE_ALARM_ARMED_HOME)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
- self._async_validate_code(code, AlarmControlPanelState.ARMED_AWAY)
- self._async_update_state(AlarmControlPanelState.ARMED_AWAY)
+ self._async_validate_code(code, STATE_ALARM_ARMED_AWAY)
+ self._async_update_state(STATE_ALARM_ARMED_AWAY)
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
- self._async_validate_code(code, AlarmControlPanelState.ARMED_NIGHT)
- self._async_update_state(AlarmControlPanelState.ARMED_NIGHT)
+ self._async_validate_code(code, STATE_ALARM_ARMED_NIGHT)
+ self._async_update_state(STATE_ALARM_ARMED_NIGHT)
async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
"""Send arm vacation command."""
- self._async_validate_code(code, AlarmControlPanelState.ARMED_VACATION)
- self._async_update_state(AlarmControlPanelState.ARMED_VACATION)
+ self._async_validate_code(code, STATE_ALARM_ARMED_VACATION)
+ self._async_update_state(STATE_ALARM_ARMED_VACATION)
async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
"""Send arm custom bypass command."""
- self._async_validate_code(code, AlarmControlPanelState.ARMED_CUSTOM_BYPASS)
- self._async_update_state(AlarmControlPanelState.ARMED_CUSTOM_BYPASS)
+ self._async_validate_code(code, STATE_ALARM_ARMED_CUSTOM_BYPASS)
+ self._async_update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS)
async def async_alarm_trigger(self, code: str | None = None) -> None:
"""Send alarm trigger command.
@@ -355,9 +343,9 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
"""
if not self._trigger_time_by_state[self._active_state]:
return
- self._async_update_state(AlarmControlPanelState.TRIGGERED)
+ self._async_update_state(STATE_ALARM_TRIGGERED)
- def _async_update_state(self, state: AlarmControlPanelState) -> None:
+ def _async_update_state(self, state: str) -> None:
"""Update the state."""
if self._state == state:
return
@@ -370,7 +358,7 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
def _async_set_state_update_events(self) -> None:
state = self._state
- if state == AlarmControlPanelState.TRIGGERED:
+ if state == STATE_ALARM_TRIGGERED:
pending_time = self._pending_time(state)
async_track_point_in_time(
self._hass, self.async_scheduled_update, self._state_ts + pending_time
@@ -394,7 +382,7 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
def _async_validate_code(self, code: str | None, state: str) -> None:
"""Validate given code."""
if (
- state != AlarmControlPanelState.DISARMED and not self.code_arm_required
+ state != STATE_ALARM_DISARMED and not self.code_arm_required
) or self._code is None:
return
@@ -417,13 +405,10 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
- if self.state in (
- AlarmControlPanelState.PENDING,
- AlarmControlPanelState.ARMING,
- ):
+ if self.state in (STATE_ALARM_PENDING, STATE_ALARM_ARMING):
prev_state: str | None = self._previous_state
state: str | None = self._state
- elif self.state == AlarmControlPanelState.TRIGGERED:
+ elif self.state == STATE_ALARM_TRIGGERED:
prev_state = self._previous_state
state = None
else:
@@ -444,9 +429,9 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
if next_state := state.attributes.get(ATTR_NEXT_STATE):
# If in arming or pending state we record the transition,
# not the current state
- self._state = AlarmControlPanelState(next_state)
+ self._state = next_state
else:
- self._state = AlarmControlPanelState(state.state)
+ self._state = state.state
if prev_state := state.attributes.get(ATTR_PREVIOUS_STATE):
self._previous_state = prev_state
diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py
index 768690e8ec5..8d447bbc8ac 100644
--- a/homeassistant/components/manual_mqtt/alarm_control_panel.py
+++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py
@@ -12,7 +12,6 @@ from homeassistant.components import mqtt
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
from homeassistant.const import (
@@ -23,6 +22,14 @@ from homeassistant.const import (
CONF_PENDING_TIME,
CONF_PLATFORM,
CONF_TRIGGER_TIME,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
)
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -47,15 +54,6 @@ CONF_PAYLOAD_ARM_NIGHT = "payload_arm_night"
CONF_PAYLOAD_ARM_VACATION = "payload_arm_vacation"
CONF_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass"
-CONF_ALARM_ARMED_AWAY = "armed_away"
-CONF_ALARM_ARMED_CUSTOM_BYPASS = "armed_custom_bypass"
-CONF_ALARM_ARMED_HOME = "armed_home"
-CONF_ALARM_ARMED_NIGHT = "armed_night"
-CONF_ALARM_ARMED_VACATION = "armed_vacation"
-CONF_ALARM_DISARMED = "disarmed"
-CONF_ALARM_PENDING = "pending"
-CONF_ALARM_TRIGGERED = "triggered"
-
DEFAULT_ALARM_NAME = "HA Alarm"
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
@@ -69,21 +67,21 @@ DEFAULT_ARM_CUSTOM_BYPASS = "ARM_CUSTOM_BYPASS"
DEFAULT_DISARM = "DISARM"
SUPPORTED_STATES = [
- AlarmControlPanelState.DISARMED,
- AlarmControlPanelState.ARMED_AWAY,
- AlarmControlPanelState.ARMED_HOME,
- AlarmControlPanelState.ARMED_NIGHT,
- AlarmControlPanelState.ARMED_VACATION,
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
- AlarmControlPanelState.TRIGGERED,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_TRIGGERED,
]
SUPPORTED_PRETRIGGER_STATES = [
- state for state in SUPPORTED_STATES if state != AlarmControlPanelState.TRIGGERED
+ state for state in SUPPORTED_STATES if state != STATE_ALARM_TRIGGERED
]
SUPPORTED_PENDING_STATES = [
- state for state in SUPPORTED_STATES if state != AlarmControlPanelState.DISARMED
+ state for state in SUPPORTED_STATES if state != STATE_ALARM_DISARMED
]
ATTR_PRE_PENDING_STATE = "pre_pending_state"
@@ -145,26 +143,26 @@ PLATFORM_SCHEMA = vol.Schema(
vol.Optional(
CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER
): cv.boolean,
- vol.Optional(CONF_ALARM_ARMED_AWAY, default={}): _state_schema(
- AlarmControlPanelState.ARMED_AWAY
+ vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema(
+ STATE_ALARM_ARMED_AWAY
),
- vol.Optional(CONF_ALARM_ARMED_HOME, default={}): _state_schema(
- AlarmControlPanelState.ARMED_HOME
+ vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema(
+ STATE_ALARM_ARMED_HOME
),
- vol.Optional(CONF_ALARM_ARMED_NIGHT, default={}): _state_schema(
- AlarmControlPanelState.ARMED_NIGHT
+ vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): _state_schema(
+ STATE_ALARM_ARMED_NIGHT
),
- vol.Optional(CONF_ALARM_ARMED_VACATION, default={}): _state_schema(
- AlarmControlPanelState.ARMED_VACATION
+ vol.Optional(STATE_ALARM_ARMED_VACATION, default={}): _state_schema(
+ STATE_ALARM_ARMED_VACATION
),
- vol.Optional(CONF_ALARM_ARMED_CUSTOM_BYPASS, default={}): _state_schema(
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS
+ vol.Optional(
+ STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}
+ ): _state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS),
+ vol.Optional(STATE_ALARM_DISARMED, default={}): _state_schema(
+ STATE_ALARM_DISARMED
),
- vol.Optional(CONF_ALARM_DISARMED, default={}): _state_schema(
- AlarmControlPanelState.DISARMED
- ),
- vol.Optional(CONF_ALARM_TRIGGERED, default={}): _state_schema(
- AlarmControlPanelState.TRIGGERED
+ vol.Optional(STATE_ALARM_TRIGGERED, default={}): _state_schema(
+ STATE_ALARM_TRIGGERED
),
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
@@ -270,7 +268,7 @@ class ManualMQTTAlarm(AlarmControlPanelEntity):
config,
):
"""Init the manual MQTT alarm panel."""
- self._state = AlarmControlPanelState.DISARMED
+ self._state = STATE_ALARM_DISARMED
self._hass = hass
self._attr_name = name
if code_template:
@@ -306,38 +304,38 @@ class ManualMQTTAlarm(AlarmControlPanelEntity):
self._payload_arm_custom_bypass = payload_arm_custom_bypass
@property
- def alarm_state(self) -> AlarmControlPanelState:
+ def state(self) -> str:
"""Return the state of the device."""
- if self._state == AlarmControlPanelState.TRIGGERED:
+ if self._state == STATE_ALARM_TRIGGERED:
if self._within_pending_time(self._state):
- return AlarmControlPanelState.PENDING
+ return STATE_ALARM_PENDING
trigger_time = self._trigger_time_by_state[self._previous_state]
if (
self._state_ts + self._pending_time(self._state) + trigger_time
) < dt_util.utcnow():
if self._disarm_after_trigger:
- return AlarmControlPanelState.DISARMED
+ return STATE_ALARM_DISARMED
self._state = self._previous_state
return self._state
if self._state in SUPPORTED_PENDING_STATES and self._within_pending_time(
self._state
):
- return AlarmControlPanelState.PENDING
+ return STATE_ALARM_PENDING
return self._state
@property
def _active_state(self):
"""Get the current state."""
- if self.state == AlarmControlPanelState.PENDING:
+ if self.state == STATE_ALARM_PENDING:
return self._previous_state
return self._state
def _pending_time(self, state):
"""Get the pending time."""
pending_time = self._pending_time_by_state[state]
- if state == AlarmControlPanelState.TRIGGERED:
+ if state == STATE_ALARM_TRIGGERED:
pending_time += self._delay_time_by_state[self._previous_state]
return pending_time
@@ -356,35 +354,35 @@ class ManualMQTTAlarm(AlarmControlPanelEntity):
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
- self._async_validate_code(code, AlarmControlPanelState.DISARMED)
- self._state = AlarmControlPanelState.DISARMED
+ self._async_validate_code(code, STATE_ALARM_DISARMED)
+ self._state = STATE_ALARM_DISARMED
self._state_ts = dt_util.utcnow()
self.async_write_ha_state()
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
- self._async_validate_code(code, AlarmControlPanelState.ARMED_HOME)
- self._async_update_state(AlarmControlPanelState.ARMED_HOME)
+ self._async_validate_code(code, STATE_ALARM_ARMED_HOME)
+ self._async_update_state(STATE_ALARM_ARMED_HOME)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
- self._async_validate_code(code, AlarmControlPanelState.ARMED_AWAY)
- self._async_update_state(AlarmControlPanelState.ARMED_AWAY)
+ self._async_validate_code(code, STATE_ALARM_ARMED_AWAY)
+ self._async_update_state(STATE_ALARM_ARMED_AWAY)
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
- self._async_validate_code(code, AlarmControlPanelState.ARMED_NIGHT)
- self._async_update_state(AlarmControlPanelState.ARMED_NIGHT)
+ self._async_validate_code(code, STATE_ALARM_ARMED_NIGHT)
+ self._async_update_state(STATE_ALARM_ARMED_NIGHT)
async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
"""Send arm vacation command."""
- self._async_validate_code(code, AlarmControlPanelState.ARMED_VACATION)
- self._async_update_state(AlarmControlPanelState.ARMED_VACATION)
+ self._async_validate_code(code, STATE_ALARM_ARMED_VACATION)
+ self._async_update_state(STATE_ALARM_ARMED_VACATION)
async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
"""Send arm custom bypass command."""
- self._async_validate_code(code, AlarmControlPanelState.ARMED_CUSTOM_BYPASS)
- self._async_update_state(AlarmControlPanelState.ARMED_CUSTOM_BYPASS)
+ self._async_validate_code(code, STATE_ALARM_ARMED_CUSTOM_BYPASS)
+ self._async_update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS)
async def async_alarm_trigger(self, code: str | None = None) -> None:
"""Send alarm trigger command.
@@ -394,7 +392,7 @@ class ManualMQTTAlarm(AlarmControlPanelEntity):
"""
if not self._trigger_time_by_state[self._active_state]:
return
- self._async_update_state(AlarmControlPanelState.TRIGGERED)
+ self._async_update_state(STATE_ALARM_TRIGGERED)
def _async_update_state(self, state: str) -> None:
"""Update the state."""
@@ -407,7 +405,7 @@ class ManualMQTTAlarm(AlarmControlPanelEntity):
self.async_write_ha_state()
pending_time = self._pending_time(state)
- if state == AlarmControlPanelState.TRIGGERED:
+ if state == STATE_ALARM_TRIGGERED:
async_track_point_in_time(
self._hass, self.async_scheduled_update, self._state_ts + pending_time
)
@@ -426,7 +424,7 @@ class ManualMQTTAlarm(AlarmControlPanelEntity):
def _async_validate_code(self, code, state):
"""Validate given code."""
if (
- state != AlarmControlPanelState.DISARMED and not self.code_arm_required
+ state != STATE_ALARM_DISARMED and not self.code_arm_required
) or self._code is None:
return
@@ -445,7 +443,7 @@ class ManualMQTTAlarm(AlarmControlPanelEntity):
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
- if self.state != AlarmControlPanelState.PENDING:
+ if self.state != STATE_ALARM_PENDING:
return {}
return {
ATTR_PRE_PENDING_STATE: self._previous_state,
diff --git a/homeassistant/components/map/__init__.py b/homeassistant/components/map/__init__.py
new file mode 100644
index 00000000000..25095e92b93
--- /dev/null
+++ b/homeassistant/components/map/__init__.py
@@ -0,0 +1,53 @@
+"""Support for showing device locations."""
+
+from homeassistant.components import onboarding
+from homeassistant.components.lovelace import _create_map_dashboard
+from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
+from homeassistant.helpers.storage import Store
+from homeassistant.helpers.typing import ConfigType
+
+DOMAIN = "map"
+
+CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
+
+STORAGE_KEY = DOMAIN
+STORAGE_VERSION_MAJOR = 1
+
+
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+ """Create a map panel."""
+
+ if DOMAIN in config:
+ async_create_issue(
+ hass,
+ HOMEASSISTANT_DOMAIN,
+ f"deprecated_yaml_{DOMAIN}",
+ breaks_in_ha_version="2024.10.0",
+ is_fixable=False,
+ is_persistent=False,
+ issue_domain=DOMAIN,
+ severity=IssueSeverity.WARNING,
+ translation_key="deprecated_yaml",
+ translation_placeholders={
+ "domain": DOMAIN,
+ "integration_title": "map",
+ },
+ )
+
+ store: Store[dict[str, bool]] = Store(
+ hass,
+ STORAGE_VERSION_MAJOR,
+ STORAGE_KEY,
+ )
+ data = await store.async_load()
+ if data:
+ return True
+
+ if onboarding.async_is_onboarded(hass):
+ await _create_map_dashboard(hass)
+
+ await store.async_save({"migrated": True})
+
+ return True
diff --git a/homeassistant/components/map/manifest.json b/homeassistant/components/map/manifest.json
new file mode 100644
index 00000000000..6a0333c862a
--- /dev/null
+++ b/homeassistant/components/map/manifest.json
@@ -0,0 +1,9 @@
+{
+ "domain": "map",
+ "name": "Map",
+ "codeowners": [],
+ "dependencies": ["frontend", "lovelace"],
+ "documentation": "https://www.home-assistant.io/integrations/map",
+ "integration_type": "system",
+ "quality_scale": "internal"
+}
diff --git a/homeassistant/components/mastodon/config_flow.py b/homeassistant/components/mastodon/config_flow.py
index 7c0985570f7..5e1af5fae92 100644
--- a/homeassistant/components/mastodon/config_flow.py
+++ b/homeassistant/components/mastodon/config_flow.py
@@ -6,7 +6,6 @@ from typing import Any
from mastodon.Mastodon import MastodonNetworkError, MastodonUnauthorizedError
import voluptuous as vol
-from yarl import URL
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
@@ -43,11 +42,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
)
-def base_url_from_url(url: str) -> str:
- """Return the base url from a url."""
- return str(URL(url).origin())
-
-
class MastodonConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
@@ -111,8 +105,6 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a flow initialized by the user."""
errors: dict[str, str] | None = None
if user_input:
- user_input[CONF_BASE_URL] = base_url_from_url(user_input[CONF_BASE_URL])
-
instance, account, errors = await self.hass.async_add_executor_job(
self.check_connection,
user_input[CONF_BASE_URL],
@@ -138,7 +130,7 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN):
LOGGER.debug("Importing Mastodon from configuration.yaml")
- base_url = base_url_from_url(str(import_data.get(CONF_BASE_URL, DEFAULT_URL)))
+ base_url = str(import_data.get(CONF_BASE_URL, DEFAULT_URL))
client_id = str(import_data.get(CONF_CLIENT_ID))
client_secret = str(import_data.get(CONF_CLIENT_SECRET))
access_token = str(import_data.get(CONF_ACCESS_TOKEN))
diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json
index 43c151c7c23..520bd0550cc 100644
--- a/homeassistant/components/matrix/manifest.json
+++ b/homeassistant/components/matrix/manifest.json
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/matrix",
"iot_class": "cloud_push",
"loggers": ["matrix_client"],
- "requirements": ["matrix-nio==0.25.2", "Pillow==11.0.0"]
+ "requirements": ["matrix-nio==0.25.2", "Pillow==10.4.0"]
}
diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py
index cdbe1e36245..f41fa3baaba 100644
--- a/homeassistant/components/matter/climate.py
+++ b/homeassistant/components/matter/climate.py
@@ -188,7 +188,6 @@ class MatterClimate(MatterEntity, ClimateEntity):
_attr_hvac_mode: HVACMode = HVACMode.OFF
_feature_map: int | None = None
_enable_turn_on_off_backwards_compatibility = False
- _platform_translation_key = "thermostat"
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
@@ -428,7 +427,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.CLIMATE,
entity_description=ClimateEntityDescription(
key="MatterThermostat",
- name=None,
+ translation_key="thermostat",
),
entity_class=MatterClimate,
required_attributes=(clusters.Thermostat.Attributes.LocalTemperature,),
diff --git a/homeassistant/components/matter/config_flow.py b/homeassistant/components/matter/config_flow.py
index 6f7505eb61f..ae71b7a1711 100644
--- a/homeassistant/components/matter/config_flow.py
+++ b/homeassistant/components/matter/config_flow.py
@@ -14,6 +14,8 @@ from homeassistant.components.hassio import (
AddonInfo,
AddonManager,
AddonState,
+ HassioServiceInfo,
+ is_hassio,
)
from homeassistant.components.onboarding import async_is_onboarded
from homeassistant.components.zeroconf import ZeroconfServiceInfo
@@ -23,8 +25,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import aiohttp_client
-from homeassistant.helpers.hassio import is_hassio
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from .addon import get_addon_manager
from .const import (
diff --git a/homeassistant/components/matter/cover.py b/homeassistant/components/matter/cover.py
index ba9c3afbdee..c32b7bc9e1a 100644
--- a/homeassistant/components/matter/cover.py
+++ b/homeassistant/components/matter/cover.py
@@ -201,8 +201,7 @@ DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.COVER,
entity_description=CoverEntityDescription(
- key="MatterCover",
- name=None,
+ key="MatterCover", translation_key="cover"
),
entity_class=MatterCover,
required_attributes=(
@@ -217,7 +216,7 @@ DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.COVER,
entity_description=CoverEntityDescription(
- key="MatterCoverPositionAwareLift", name=None
+ key="MatterCoverPositionAwareLift", translation_key="cover"
),
entity_class=MatterCover,
required_attributes=(
@@ -232,7 +231,7 @@ DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.COVER,
entity_description=CoverEntityDescription(
- key="MatterCoverPositionAwareTilt", name=None
+ key="MatterCoverPositionAwareTilt", translation_key="cover"
),
entity_class=MatterCover,
required_attributes=(
@@ -247,7 +246,7 @@ DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.COVER,
entity_description=CoverEntityDescription(
- key="MatterCoverPositionAwareLiftAndTilt", name=None
+ key="MatterCoverPositionAwareLiftAndTilt", translation_key="cover"
),
entity_class=MatterCover,
required_attributes=(
diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py
index 5b07f9a069f..342522787ab 100644
--- a/homeassistant/components/matter/discovery.py
+++ b/homeassistant/components/matter/discovery.py
@@ -24,7 +24,6 @@ from .select import DISCOVERY_SCHEMAS as SELECT_SCHEMAS
from .sensor import DISCOVERY_SCHEMAS as SENSOR_SCHEMAS
from .switch import DISCOVERY_SCHEMAS as SWITCH_SCHEMAS
from .update import DISCOVERY_SCHEMAS as UPDATE_SCHEMAS
-from .vacuum import DISCOVERY_SCHEMAS as VACUUM_SCHEMAS
from .valve import DISCOVERY_SCHEMAS as VALVE_SCHEMAS
DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = {
@@ -41,7 +40,6 @@ DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = {
Platform.SENSOR: SENSOR_SCHEMAS,
Platform.SWITCH: SWITCH_SCHEMAS,
Platform.UPDATE: UPDATE_SCHEMAS,
- Platform.VACUUM: VACUUM_SCHEMAS,
Platform.VALVE: VALVE_SCHEMAS,
}
SUPPORTED_PLATFORMS = tuple(DISCOVERY_SCHEMAS)
diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py
index 7c378fe465e..1a454bb7357 100644
--- a/homeassistant/components/matter/entity.py
+++ b/homeassistant/components/matter/entity.py
@@ -45,7 +45,6 @@ class MatterEntity(Entity):
_attr_has_entity_name = True
_attr_should_poll = False
_name_postfix: str | None = None
- _platform_translation_key: str | None = None
def __init__(
self,
@@ -84,8 +83,6 @@ class MatterEntity(Entity):
and ep.has_attribute(None, entity_info.primary_attribute)
):
self._name_postfix = str(self._endpoint.endpoint_id)
- if self._platform_translation_key and not self.translation_key:
- self._attr_translation_key = self._platform_translation_key
# prefer the label attribute for the entity name
# Matter has a way for users and/or vendors to specify a name for an endpoint
diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py
index 51c2fb0c882..458a57538eb 100644
--- a/homeassistant/components/matter/fan.py
+++ b/homeassistant/components/matter/fan.py
@@ -60,7 +60,6 @@ class MatterFan(MatterEntity, FanEntity):
_last_known_percentage: int = 0
_enable_turn_on_off_backwards_compatibility = False
_feature_map: int | None = None
- _platform_translation_key = "fan"
async def async_turn_on(
self,
@@ -330,8 +329,7 @@ DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.FAN,
entity_description=FanEntityDescription(
- key="MatterFan",
- name=None,
+ key="MatterFan", name=None, translation_key="fan"
),
entity_class=MatterFan,
# FanEntityFeature
diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py
index 6d184bcc01f..72d06f4b9f1 100644
--- a/homeassistant/components/matter/light.py
+++ b/homeassistant/components/matter/light.py
@@ -89,7 +89,6 @@ class MatterLight(MatterEntity, LightEntity):
_supports_color = False
_supports_color_temperature = False
_transitions_disabled = False
- _platform_translation_key = "light"
async def _set_xy_color(
self, xy_color: tuple[float, float], transition: float = 0.0
@@ -444,8 +443,7 @@ DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.LIGHT,
entity_description=LightEntityDescription(
- key="MatterLight",
- name=None,
+ key="MatterLight", translation_key="light"
),
entity_class=MatterLight,
required_attributes=(clusters.OnOff.Attributes.OnOff,),
@@ -472,8 +470,7 @@ DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.LIGHT,
entity_description=LightEntityDescription(
- key="MatterHSColorLightFallback",
- name=None,
+ key="MatterHSColorLightFallback", translation_key="light"
),
entity_class=MatterLight,
required_attributes=(
@@ -493,8 +490,7 @@ DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.LIGHT,
entity_description=LightEntityDescription(
- key="MatterXYColorLightFallback",
- name=None,
+ key="MatterXYColorLightFallback", translation_key="light"
),
entity_class=MatterLight,
required_attributes=(
@@ -514,8 +510,7 @@ DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.LIGHT,
entity_description=LightEntityDescription(
- key="MatterColorTemperatureLightFallback",
- name=None,
+ key="MatterColorTemperatureLightFallback", translation_key="light"
),
entity_class=MatterLight,
required_attributes=(
diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py
index c5e10554fe7..8adaecd67ad 100644
--- a/homeassistant/components/matter/lock.py
+++ b/homeassistant/components/matter/lock.py
@@ -40,7 +40,6 @@ class MatterLock(MatterEntity, LockEntity):
_feature_map: int | None = None
_optimistic_timer: asyncio.TimerHandle | None = None
- _platform_translation_key = "lock"
@property
def code_format(self) -> str | None:
@@ -201,8 +200,7 @@ DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.LOCK,
entity_description=LockEntityDescription(
- key="MatterLock",
- name=None,
+ key="MatterLock", translation_key="lock"
),
entity_class=MatterLock,
required_attributes=(clusters.DoorLock.Attributes.LockState,),
diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json
index 4573fe17401..295b0a23735 100644
--- a/homeassistant/components/matter/manifest.json
+++ b/homeassistant/components/matter/manifest.json
@@ -1,7 +1,6 @@
{
"domain": "matter",
"name": "Matter (BETA)",
- "after_dependencies": ["hassio"],
"codeowners": ["@home-assistant/matter"],
"config_flow": true,
"dependencies": ["websocket_api"],
diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py
index 1a2fc36c014..1bba18b2c5b 100644
--- a/homeassistant/components/matter/select.py
+++ b/homeassistant/components/matter/select.py
@@ -162,11 +162,23 @@ DISCOVERY_SCHEMAS = [
clusters.RefrigeratorAndTemperatureControlledCabinetMode.Attributes.SupportedModes,
),
),
+ MatterDiscoverySchema(
+ platform=Platform.SELECT,
+ entity_description=MatterSelectEntityDescription(
+ key="MatterRvcRunMode",
+ translation_key="mode",
+ ),
+ entity_class=MatterModeSelectEntity,
+ required_attributes=(
+ clusters.RvcRunMode.Attributes.CurrentMode,
+ clusters.RvcRunMode.Attributes.SupportedModes,
+ ),
+ ),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="MatterRvcCleanMode",
- translation_key="clean_mode",
+ translation_key="mode",
),
entity_class=MatterModeSelectEntity,
required_attributes=(
diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json
index 69fa68765b3..b4ef5b79340 100644
--- a/homeassistant/components/matter/strings.json
+++ b/homeassistant/components/matter/strings.json
@@ -36,7 +36,6 @@
"addon_start_failed": "Failed to start the Matter Server add-on.",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"not_matter_addon": "Discovered add-on is not the official Matter Server add-on.",
"reconfiguration_successful": "Successfully reconfigured the Matter integration."
},
@@ -174,9 +173,6 @@
}
},
"select": {
- "clean_mode": {
- "name": "Clean mode"
- },
"mode": {
"name": "Mode"
},
@@ -255,11 +251,6 @@
"name": "Power"
}
},
- "vacuum": {
- "vacuum": {
- "name": "[%key:component::vacuum::title%]"
- }
- },
"valve": {
"valve": {
"name": "[%key:component::valve::title%]"
diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py
index 75269de953c..953897fdaa6 100644
--- a/homeassistant/components/matter/switch.py
+++ b/homeassistant/components/matter/switch.py
@@ -35,8 +35,6 @@ async def async_setup_entry(
class MatterSwitch(MatterEntity, SwitchEntity):
"""Representation of a Matter switch."""
- _platform_translation_key = "switch"
-
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn switch on."""
await self.matter_client.send_device_command(
@@ -68,7 +66,7 @@ DISCOVERY_SCHEMAS = [
entity_description=SwitchEntityDescription(
key="MatterPlug",
device_class=SwitchDeviceClass.OUTLET,
- name=None,
+ translation_key="switch",
),
entity_class=MatterSwitch,
required_attributes=(clusters.OnOff.Attributes.OnOff,),
@@ -108,7 +106,7 @@ DISCOVERY_SCHEMAS = [
entity_description=SwitchEntityDescription(
key="MatterSwitch",
device_class=SwitchDeviceClass.OUTLET,
- name=None,
+ translation_key="switch",
),
entity_class=MatterSwitch,
required_attributes=(clusters.OnOff.Attributes.OnOff,),
diff --git a/homeassistant/components/matter/update.py b/homeassistant/components/matter/update.py
index f31dd7b3aa3..736664e0101 100644
--- a/homeassistant/components/matter/update.py
+++ b/homeassistant/components/matter/update.py
@@ -100,23 +100,21 @@ class MatterUpdate(MatterEntity, UpdateEntity):
== clusters.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum.kIdle
):
self._attr_in_progress = False
- self._attr_update_percentage = None
return
update_progress: int = self.get_matter_attribute_value(
clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress
)
- self._attr_in_progress = True
if (
update_state
== clusters.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum.kDownloading
and update_progress is not None
and update_progress > 0
):
- self._attr_update_percentage = update_progress
+ self._attr_in_progress = update_progress
else:
- self._attr_update_percentage = None
+ self._attr_in_progress = True
async def async_update(self) -> None:
"""Call when the entity needs to be updated."""
diff --git a/homeassistant/components/matter/vacuum.py b/homeassistant/components/matter/vacuum.py
deleted file mode 100644
index 2ecd7128df6..00000000000
--- a/homeassistant/components/matter/vacuum.py
+++ /dev/null
@@ -1,226 +0,0 @@
-"""Matter vacuum platform."""
-
-from __future__ import annotations
-
-from enum import IntEnum
-from typing import TYPE_CHECKING, Any
-
-from chip.clusters import Objects as clusters
-from matter_server.client.models import device_types
-
-from homeassistant.components.vacuum import (
- STATE_CLEANING,
- STATE_DOCKED,
- STATE_ERROR,
- STATE_RETURNING,
- StateVacuumEntity,
- StateVacuumEntityDescription,
- VacuumEntityFeature,
-)
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import STATE_IDLE, Platform
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from .entity import MatterEntity
-from .helpers import get_matter
-from .models import MatterDiscoverySchema
-
-
-class OperationalState(IntEnum):
- """Operational State of the vacuum cleaner.
-
- Combination of generic OperationalState and RvcOperationalState.
- """
-
- NO_ERROR = 0x00
- UNABLE_TO_START_OR_RESUME = 0x01
- UNABLE_TO_COMPLETE_OPERATION = 0x02
- COMMAND_INVALID_IN_STATE = 0x03
- SEEKING_CHARGER = 0x40
- CHARGING = 0x41
- DOCKED = 0x42
-
-
-class ModeTag(IntEnum):
- """Enum with available ModeTag values."""
-
- IDLE = 0x4000 # 16384 decimal
- CLEANING = 0x4001 # 16385 decimal
- MAPPING = 0x4002 # 16386 decimal
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up Matter vacuum platform from Config Entry."""
- matter = get_matter(hass)
- matter.register_platform_handler(Platform.VACUUM, async_add_entities)
-
-
-class MatterVacuum(MatterEntity, StateVacuumEntity):
- """Representation of a Matter Vacuum cleaner entity."""
-
- _last_accepted_commands: list[int] | None = None
- _supported_run_modes: (
- dict[int, clusters.RvcCleanMode.Structs.ModeOptionStruct] | None
- ) = None
- entity_description: StateVacuumEntityDescription
- _platform_translation_key = "vacuum"
-
- async def async_stop(self, **kwargs: Any) -> None:
- """Stop the vacuum cleaner."""
- await self._send_device_command(clusters.OperationalState.Commands.Stop())
-
- async def async_return_to_base(self, **kwargs: Any) -> None:
- """Set the vacuum cleaner to return to the dock."""
- await self._send_device_command(clusters.RvcOperationalState.Commands.GoHome())
-
- async def async_locate(self, **kwargs: Any) -> None:
- """Locate the vacuum cleaner."""
- await self._send_device_command(clusters.Identify.Commands.Identify())
-
- async def async_start(self) -> None:
- """Start or resume the cleaning task."""
- if TYPE_CHECKING:
- assert self._last_accepted_commands is not None
- if (
- clusters.RvcOperationalState.Commands.Resume.command_id
- in self._last_accepted_commands
- ):
- await self._send_device_command(
- clusters.RvcOperationalState.Commands.Resume()
- )
- else:
- await self._send_device_command(clusters.OperationalState.Commands.Start())
-
- async def async_pause(self) -> None:
- """Pause the cleaning task."""
- await self._send_device_command(clusters.OperationalState.Commands.Pause())
-
- async def _send_device_command(
- self,
- command: clusters.ClusterCommand,
- ) -> None:
- """Send a command to the device."""
- await self.matter_client.send_device_command(
- node_id=self._endpoint.node.node_id,
- endpoint_id=self._endpoint.endpoint_id,
- command=command,
- )
-
- @callback
- def _update_from_device(self) -> None:
- """Update from device."""
- self._calculate_features()
- # optional battery level
- if VacuumEntityFeature.BATTERY & self._attr_supported_features:
- self._attr_battery_level = self.get_matter_attribute_value(
- clusters.PowerSource.Attributes.BatPercentRemaining
- )
- # derive state from the run mode + operational state
- run_mode_raw: int = self.get_matter_attribute_value(
- clusters.RvcRunMode.Attributes.CurrentMode
- )
- operational_state: int = self.get_matter_attribute_value(
- clusters.RvcOperationalState.Attributes.OperationalState
- )
- state: str | None = None
- if TYPE_CHECKING:
- assert self._supported_run_modes is not None
- if operational_state in (OperationalState.CHARGING, OperationalState.DOCKED):
- state = STATE_DOCKED
- elif operational_state == OperationalState.SEEKING_CHARGER:
- state = STATE_RETURNING
- elif operational_state in (
- OperationalState.UNABLE_TO_COMPLETE_OPERATION,
- OperationalState.UNABLE_TO_START_OR_RESUME,
- ):
- state = STATE_ERROR
- elif (run_mode := self._supported_run_modes.get(run_mode_raw)) is not None:
- tags = {x.value for x in run_mode.modeTags}
- if ModeTag.CLEANING in tags:
- state = STATE_CLEANING
- elif ModeTag.IDLE in tags:
- state = STATE_IDLE
- self._attr_state = state
-
- @callback
- def _calculate_features(self) -> None:
- """Calculate features for HA Vacuum platform."""
- accepted_operational_commands: list[int] = self.get_matter_attribute_value(
- clusters.RvcOperationalState.Attributes.AcceptedCommandList
- )
- # in principle the feature set should not change, except for the accepted commands
- if self._last_accepted_commands == accepted_operational_commands:
- return
- self._last_accepted_commands = accepted_operational_commands
- supported_features: VacuumEntityFeature = VacuumEntityFeature(0)
- supported_features |= VacuumEntityFeature.STATE
- # optional battery attribute = battery feature
- if self.get_matter_attribute_value(
- clusters.PowerSource.Attributes.BatPercentRemaining
- ):
- supported_features |= VacuumEntityFeature.BATTERY
- # optional identify cluster = locate feature (value must be not None or 0)
- if self.get_matter_attribute_value(clusters.Identify.Attributes.IdentifyType):
- supported_features |= VacuumEntityFeature.LOCATE
- # create a map of supported run modes
- run_modes: list[clusters.RvcCleanMode.Structs.ModeOptionStruct] = (
- self.get_matter_attribute_value(
- clusters.RvcRunMode.Attributes.SupportedModes
- )
- )
- self._supported_run_modes = {mode.mode: mode for mode in run_modes}
- # map operational state commands to vacuum features
- if (
- clusters.RvcOperationalState.Commands.Pause.command_id
- in accepted_operational_commands
- ):
- supported_features |= VacuumEntityFeature.PAUSE
- if (
- clusters.OperationalState.Commands.Stop.command_id
- in accepted_operational_commands
- ):
- supported_features |= VacuumEntityFeature.STOP
- if (
- clusters.OperationalState.Commands.Start.command_id
- in accepted_operational_commands
- ):
- # note that start has been replaced by resume in rev2 of the spec
- supported_features |= VacuumEntityFeature.START
- if (
- clusters.RvcOperationalState.Commands.Resume.command_id
- in accepted_operational_commands
- ):
- supported_features |= VacuumEntityFeature.START
- if (
- clusters.RvcOperationalState.Commands.GoHome.command_id
- in accepted_operational_commands
- ):
- supported_features |= VacuumEntityFeature.RETURN_HOME
-
- self._attr_supported_features = supported_features
-
-
-# Discovery schema(s) to map Matter Attributes to HA entities
-DISCOVERY_SCHEMAS = [
- MatterDiscoverySchema(
- platform=Platform.VACUUM,
- entity_description=StateVacuumEntityDescription(
- key="MatterVacuumCleaner", name=None
- ),
- entity_class=MatterVacuum,
- required_attributes=(
- clusters.RvcRunMode.Attributes.CurrentMode,
- clusters.RvcOperationalState.Attributes.CurrentPhase,
- ),
- optional_attributes=(
- clusters.RvcCleanMode.Attributes.CurrentMode,
- clusters.PowerSource.Attributes.BatPercentRemaining,
- ),
- device_type=(device_types.RoboticVacuumCleaner,),
- ),
-]
diff --git a/homeassistant/components/matter/valve.py b/homeassistant/components/matter/valve.py
index ccb4e89da17..f2e212246ca 100644
--- a/homeassistant/components/matter/valve.py
+++ b/homeassistant/components/matter/valve.py
@@ -40,7 +40,6 @@ class MatterValve(MatterEntity, ValveEntity):
_feature_map: int | None = None
entity_description: ValveEntityDescription
- _platform_translation_key = "valve"
async def send_device_command(
self,
@@ -140,7 +139,7 @@ DISCOVERY_SCHEMAS = [
entity_description=ValveEntityDescription(
key="MatterValve",
device_class=ValveDeviceClass.WATER,
- name=None,
+ translation_key="valve",
),
entity_class=MatterValve,
required_attributes=(
diff --git a/homeassistant/components/mealie/config_flow.py b/homeassistant/components/mealie/config_flow.py
index 2f90ceaf97a..b1ce6f7147b 100644
--- a/homeassistant/components/mealie/config_flow.py
+++ b/homeassistant/components/mealie/config_flow.py
@@ -116,6 +116,12 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the integration."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle reconfiguration confirmation."""
errors: dict[str, str] = {}
if user_input:
self.host = user_input[CONF_HOST]
@@ -135,7 +141,7 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
},
)
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=USER_SCHEMA,
errors=errors,
)
diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json
index b59399815ea..72f2d769dd2 100644
--- a/homeassistant/components/mealie/strings.json
+++ b/homeassistant/components/mealie/strings.json
@@ -17,7 +17,7 @@
"api_token": "[%key:common::config_flow::data::api_token%]"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"description": "Please reconfigure with Mealie.",
"data": {
"host": "[%key:common::config_flow::data::url%]",
diff --git a/homeassistant/components/meater/__init__.py b/homeassistant/components/meater/__init__.py
index 50eff40c0e8..08ca32029cb 100644
--- a/homeassistant/components/meater/__init__.py
+++ b/homeassistant/components/meater/__init__.py
@@ -64,7 +64,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
# Name of the data. For logging purposes.
name="meater_api",
update_method=async_update_data,
diff --git a/homeassistant/components/meater/strings.json b/homeassistant/components/meater/strings.json
index 20dd2919026..279841bb147 100644
--- a/homeassistant/components/meater/strings.json
+++ b/homeassistant/components/meater/strings.json
@@ -19,8 +19,7 @@
}
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
diff --git a/homeassistant/components/medcom_ble/__init__.py b/homeassistant/components/medcom_ble/__init__.py
index 8603e1b9ce5..36357746b95 100644
--- a/homeassistant/components/medcom_ble/__init__.py
+++ b/homeassistant/components/medcom_ble/__init__.py
@@ -53,7 +53,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=DOMAIN,
update_method=_async_update_method,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json
index ebfa79d7190..635ab5f6d40 100644
--- a/homeassistant/components/media_extractor/manifest.json
+++ b/homeassistant/components/media_extractor/manifest.json
@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
- "requirements": ["yt-dlp[default]==2024.11.04"],
+ "requirements": ["yt-dlp==2024.09.27"],
"single_config_entry": true
}
diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py
index 3ea8f581245..604f9b7cc88 100644
--- a/homeassistant/components/media_source/__init__.py
+++ b/homeassistant/components/media_source/__init__.py
@@ -18,7 +18,7 @@ from homeassistant.components.media_player import (
from homeassistant.components.websocket_api import ActiveConnection
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.frame import report_usage
+from homeassistant.helpers.frame import report
from homeassistant.helpers.integration_platform import (
async_process_integration_platforms,
)
@@ -156,7 +156,7 @@ async def async_resolve_media(
raise Unresolvable("Media Source not loaded")
if target_media_player is UNDEFINED:
- report_usage(
+ report(
"calls media_source.async_resolve_media without passing an entity_id",
exclude_integrations={DOMAIN},
)
diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py
index b604ee5016e..8e981986dd7 100644
--- a/homeassistant/components/melcloud/config_flow.py
+++ b/homeassistant/components/melcloud/config_flow.py
@@ -142,6 +142,12 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle a reconfiguration flow initialized by the user."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfiguration flow initialized by the user."""
errors: dict[str, str] = {}
@@ -184,7 +190,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json
index 19ef0b76aad..968f9cf4e50 100644
--- a/homeassistant/components/melcloud/strings.json
+++ b/homeassistant/components/melcloud/strings.json
@@ -17,7 +17,7 @@
"password": "[%key:common::config_flow::data::password%]"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"title": "Reconfigure your MelCloud",
"description": "Reconfigure the entry to obtain a new token, for your account: `{username}`.",
"data": {
@@ -36,9 +36,7 @@
"abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"already_configured": "MELCloud integration already configured for this email. Access token has been refreshed.",
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
+ "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
}
},
"services": {
diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py
index 62964d22bb1..84a44682413 100644
--- a/homeassistant/components/met/config_flow.py
+++ b/homeassistant/components/met/config_flow.py
@@ -11,6 +11,7 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import (
CONF_ELEVATION,
@@ -142,12 +143,12 @@ class MetConfigFlowHandler(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
- ) -> MetOptionsFlowHandler:
+ ) -> OptionsFlow:
"""Get the options flow for Met."""
- return MetOptionsFlowHandler()
+ return MetOptionsFlowHandler(config_entry)
-class MetOptionsFlowHandler(OptionsFlow):
+class MetOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Options flow for Met component."""
async def async_step_init(
@@ -158,13 +159,13 @@ class MetOptionsFlowHandler(OptionsFlow):
if user_input is not None:
# Update config entry with data from user input
self.hass.config_entries.async_update_entry(
- self.config_entry, data=user_input
+ self._config_entry, data=user_input
)
return self.async_create_entry(
- title=self.config_entry.title, data=user_input
+ title=self._config_entry.title, data=user_input
)
return self.async_show_form(
step_id="init",
- data_schema=_get_data_schema(self.hass, config_entry=self.config_entry),
+ data_schema=_get_data_schema(self.hass, config_entry=self._config_entry),
)
diff --git a/homeassistant/components/met_eireann/__init__.py b/homeassistant/components/met_eireann/__init__.py
index ab2695cbd11..7d0e6401bd6 100644
--- a/homeassistant/components/met_eireann/__init__.py
+++ b/homeassistant/components/met_eireann/__init__.py
@@ -46,7 +46,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=config_entry,
name=DOMAIN,
update_method=_async_update_data,
update_interval=UPDATE_INTERVAL,
diff --git a/homeassistant/components/met_eireann/strings.json b/homeassistant/components/met_eireann/strings.json
index d8c2918e6d3..984f46d71d6 100644
--- a/homeassistant/components/met_eireann/strings.json
+++ b/homeassistant/components/met_eireann/strings.json
@@ -12,9 +12,6 @@
}
}
},
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
- },
"error": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py
index 1d4f8293c5e..ddba982934c 100644
--- a/homeassistant/components/meteo_france/__init__.py
+++ b/homeassistant/components/meteo_france/__init__.py
@@ -75,15 +75,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not coordinator_forecast.last_update_success:
raise ConfigEntryNotReady
- # Check rain forecast.
- coordinator_rain = DataUpdateCoordinator(
- hass,
- _LOGGER,
- name=f"Météo-France rain for city {entry.title}",
- update_method=_async_update_data_rain,
- update_interval=SCAN_INTERVAL_RAIN,
- )
- await coordinator_rain.async_config_entry_first_refresh()
+ # Check if rain forecast is available.
+ if coordinator_forecast.data.position.get("rain_product_available") == 1:
+ coordinator_rain = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=f"Météo-France rain for city {entry.title}",
+ update_method=_async_update_data_rain,
+ update_interval=SCAN_INTERVAL_RAIN,
+ )
+ await coordinator_rain.async_refresh()
+
+ if not coordinator_rain.last_update_success:
+ raise ConfigEntryNotReady
+ else:
+ _LOGGER.warning(
+ "1 hour rain forecast not available. %s is not in covered zone",
+ entry.title,
+ )
department = coordinator_forecast.data.position.get("dept")
_LOGGER.debug(
diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py
index 8c2fb41c634..f81d60c3d00 100644
--- a/homeassistant/components/meteoclimatic/__init__.py
+++ b/homeassistant/components/meteoclimatic/__init__.py
@@ -32,7 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"Meteoclimatic weather for {entry.title} ({station_code})",
update_method=async_update_data,
update_interval=SCAN_INTERVAL,
diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py
index 1d516bbc4f5..18fc121d5d3 100644
--- a/homeassistant/components/metoffice/__init__.py
+++ b/homeassistant/components/metoffice/__init__.py
@@ -109,7 +109,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
metoffice_hourly_coordinator = TimestampDataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"MetOffice Hourly Coordinator for {site_name}",
update_method=async_update_3hourly,
update_interval=DEFAULT_SCAN_INTERVAL,
@@ -118,7 +117,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
metoffice_daily_coordinator = TimestampDataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"MetOffice Daily Coordinator for {site_name}",
update_method=async_update_daily,
update_interval=DEFAULT_SCAN_INTERVAL,
diff --git a/homeassistant/components/microbees/config_flow.py b/homeassistant/components/microbees/config_flow.py
index 92fa40b24f0..4d0f5b4474b 100644
--- a/homeassistant/components/microbees/config_flow.py
+++ b/homeassistant/components/microbees/config_flow.py
@@ -6,7 +6,8 @@ from typing import Any
from microBeesPy import MicroBees, MicroBeesException
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant import config_entries
+from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
@@ -19,6 +20,7 @@ class OAuth2FlowHandler(
"""Handle a config flow for microBees."""
DOMAIN = DOMAIN
+ reauth_entry: config_entries.ConfigEntry | None = None
@property
def logger(self) -> logging.Logger:
@@ -47,21 +49,26 @@ class OAuth2FlowHandler(
self.logger.exception("Unexpected error")
return self.async_abort(reason="unknown")
- await self.async_set_unique_id(current_user.id)
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
+ await self.async_set_unique_id(current_user.id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=current_user.username,
data=data,
)
-
- self._abort_if_unique_id_mismatch(reason="wrong_account")
- return self.async_update_reload_and_abort(self._get_reauth_entry(), data=data)
+ if self.reauth_entry.unique_id == current_user.id:
+ self.hass.config_entries.async_update_entry(self.reauth_entry, data=data)
+ await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+ return self.async_abort(reason="wrong_account")
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
diff --git a/homeassistant/components/microbees/strings.json b/homeassistant/components/microbees/strings.json
index 8635753a564..49d42af83d3 100644
--- a/homeassistant/components/microbees/strings.json
+++ b/homeassistant/components/microbees/strings.json
@@ -21,7 +21,6 @@
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"wrong_account": "You can only reauthenticate this entry with the same microBees account."
},
diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py
index bca394f0d38..6035565acf1 100644
--- a/homeassistant/components/mikrotik/config_flow.py
+++ b/homeassistant/components/mikrotik/config_flow.py
@@ -39,6 +39,7 @@ class MikrotikFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Mikrotik config flow."""
VERSION = 1
+ _reauth_entry: ConfigEntry | None
@staticmethod
@callback
@@ -46,7 +47,7 @@ class MikrotikFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> MikrotikOptionsFlowHandler:
"""Get the options flow for this handler."""
- return MikrotikOptionsFlowHandler()
+ return MikrotikOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -86,6 +87,9 @@ class MikrotikFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -93,10 +97,9 @@ class MikrotikFlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
errors = {}
-
- reauth_entry = self._get_reauth_entry()
+ assert self._reauth_entry
if user_input is not None:
- user_input = {**reauth_entry.data, **user_input}
+ user_input = {**self._reauth_entry.data, **user_input}
try:
await self.hass.async_add_executor_job(get_api, user_input)
except CannotConnect:
@@ -105,10 +108,17 @@ class MikrotikFlowHandler(ConfigFlow, domain=DOMAIN):
errors[CONF_PASSWORD] = "invalid_auth"
if not errors:
- return self.async_update_reload_and_abort(reauth_entry, data=user_input)
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry,
+ data=user_input,
+ )
+ await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
- description_placeholders={CONF_USERNAME: reauth_entry.data[CONF_USERNAME]},
+ description_placeholders={
+ CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME]
+ },
step_id="reauth_confirm",
data_schema=vol.Schema(
{
@@ -122,6 +132,10 @@ class MikrotikFlowHandler(ConfigFlow, domain=DOMAIN):
class MikrotikOptionsFlowHandler(OptionsFlow):
"""Handle Mikrotik options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize Mikrotik options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json
index 6316eb72096..16e7bf552ba 100644
--- a/homeassistant/components/mill/manifest.json
+++ b/homeassistant/components/mill/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/mill",
"iot_class": "local_polling",
"loggers": ["mill", "mill_local"],
- "requirements": ["millheater==0.12.2", "mill-local==0.3.0"]
+ "requirements": ["millheater==0.11.8", "mill-local==0.3.0"]
}
diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py
index 57a9632a6ff..8a301ea4225 100644
--- a/homeassistant/components/minio/__init__.py
+++ b/homeassistant/components/minio/__init__.py
@@ -73,11 +73,11 @@ CONFIG_SCHEMA = vol.Schema(
)
BUCKET_KEY_SCHEMA = vol.Schema(
- {vol.Required(ATTR_BUCKET): cv.string, vol.Required(ATTR_KEY): cv.string}
+ {vol.Required(ATTR_BUCKET): cv.template, vol.Required(ATTR_KEY): cv.template}
)
BUCKET_KEY_FILE_SCHEMA = BUCKET_KEY_SCHEMA.extend(
- {vol.Required(ATTR_FILE_PATH): cv.string}
+ {vol.Required(ATTR_FILE_PATH): cv.template}
)
@@ -125,11 +125,15 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
get_minio_endpoint(host, port), access_key, secret_key, secure
)
+ def _render_service_value(service, key):
+ value = service.data[key]
+ return value.async_render(parse_result=False)
+
def put_file(service: ServiceCall) -> None:
"""Upload file service."""
- bucket = service.data[ATTR_BUCKET]
- key = service.data[ATTR_KEY]
- file_path = service.data[ATTR_FILE_PATH]
+ bucket = _render_service_value(service, ATTR_BUCKET)
+ key = _render_service_value(service, ATTR_KEY)
+ file_path = _render_service_value(service, ATTR_FILE_PATH)
if not hass.config.is_allowed_path(file_path):
raise ValueError(f"Invalid file_path {file_path}")
@@ -138,9 +142,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
def get_file(service: ServiceCall) -> None:
"""Download file service."""
- bucket = service.data[ATTR_BUCKET]
- key = service.data[ATTR_KEY]
- file_path = service.data[ATTR_FILE_PATH]
+ bucket = _render_service_value(service, ATTR_BUCKET)
+ key = _render_service_value(service, ATTR_KEY)
+ file_path = _render_service_value(service, ATTR_FILE_PATH)
if not hass.config.is_allowed_path(file_path):
raise ValueError(f"Invalid file_path {file_path}")
@@ -149,8 +153,8 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
def remove_file(service: ServiceCall) -> None:
"""Delete file service."""
- bucket = service.data[ATTR_BUCKET]
- key = service.data[ATTR_KEY]
+ bucket = _render_service_value(service, ATTR_BUCKET)
+ key = _render_service_value(service, ATTR_KEY)
minio_client.remove_object(bucket, key)
diff --git a/homeassistant/components/mjpeg/config_flow.py b/homeassistant/components/mjpeg/config_flow.py
index e0150f8c461..84267936788 100644
--- a/homeassistant/components/mjpeg/config_flow.py
+++ b/homeassistant/components/mjpeg/config_flow.py
@@ -141,7 +141,7 @@ class MJPEGFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> MJPEGOptionsFlowHandler:
"""Get the options flow for this handler."""
- return MJPEGOptionsFlowHandler()
+ return MJPEGOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -183,6 +183,10 @@ class MJPEGFlowHandler(ConfigFlow, domain=DOMAIN):
class MJPEGOptionsFlowHandler(OptionsFlow):
"""Handle MJPEG IP Camera options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize MJPEG IP Camera options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py
index 9fadca31b50..80893e0cbfa 100644
--- a/homeassistant/components/mobile_app/__init__.py
+++ b/homeassistant/components/mobile_app/__init__.py
@@ -4,7 +4,6 @@ from contextlib import suppress
from functools import partial
from typing import Any
-from homeassistant.auth import EVENT_USER_REMOVED
from homeassistant.components import cloud, intent, notify as hass_notify
from homeassistant.components.webhook import (
async_register as webhook_register,
@@ -12,7 +11,7 @@ from homeassistant.components.webhook import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_DEVICE_ID, CONF_WEBHOOK_ID, Platform
-from homeassistant.core import Event, HomeAssistant
+from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
@@ -37,7 +36,6 @@ from .const import (
ATTR_MODEL,
ATTR_OS_VERSION,
CONF_CLOUDHOOK_URL,
- CONF_USER_ID,
DATA_CONFIG_ENTRIES,
DATA_DELETED_IDS,
DATA_DEVICES,
@@ -92,15 +90,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
websocket_api.async_setup_commands(hass)
- async def _handle_user_removed(event: Event) -> None:
- """Remove an entry when the user is removed."""
- user_id = event.data["user_id"]
- for entry in hass.config_entries.async_entries(DOMAIN):
- if entry.data[CONF_USER_ID] == user_id:
- await hass.config_entries.async_remove(entry.entry_id)
-
- hass.bus.async_listen(EVENT_USER_REMOVED, _handle_user_removed)
-
return True
diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py
index 48f8c726836..64a9e71b3fc 100644
--- a/homeassistant/components/modbus/__init__.py
+++ b/homeassistant/components/modbus/__init__.py
@@ -87,6 +87,7 @@ from .const import (
CONF_HVAC_MODE_VALUES,
CONF_HVAC_ONOFF_REGISTER,
CONF_INPUT_TYPE,
+ CONF_LAZY_ERROR,
CONF_MAX_TEMP,
CONF_MAX_VALUE,
CONF_MIN_TEMP,
@@ -95,6 +96,7 @@ from .const import (
CONF_NAN_VALUE,
CONF_PARITY,
CONF_PRECISION,
+ CONF_RETRIES,
CONF_SCALE,
CONF_SLAVE_COUNT,
CONF_STATE_CLOSED,
@@ -160,6 +162,7 @@ BASE_COMPONENT_SCHEMA = vol.Schema(
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.positive_int,
+ vol.Optional(CONF_LAZY_ERROR): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
@@ -231,10 +234,8 @@ BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
CALL_TYPE_X_REGISTER_HOLDINGS,
]
),
- vol.Optional(CONF_STATE_OFF): vol.All(
- cv.ensure_list, [cv.positive_int]
- ),
- vol.Optional(CONF_STATE_ON): vol.All(cv.ensure_list, [cv.positive_int]),
+ vol.Optional(CONF_STATE_OFF): cv.positive_int,
+ vol.Optional(CONF_STATE_ON): cv.positive_int,
vol.Optional(CONF_DELAY, default=0): cv.positive_int,
}
),
@@ -392,6 +393,7 @@ MODBUS_SCHEMA = vol.Schema(
vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string,
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
vol.Optional(CONF_DELAY, default=0): cv.positive_int,
+ vol.Optional(CONF_RETRIES): cv.positive_int,
vol.Optional(CONF_MSG_WAIT): cv.positive_int,
vol.Optional(CONF_BINARY_SENSORS): vol.All(
cv.ensure_list, [BINARY_SENSOR_SCHEMA]
diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py
index b50d21faf42..54ee49ed6a2 100644
--- a/homeassistant/components/modbus/binary_sensor.py
+++ b/homeassistant/components/modbus/binary_sensor.py
@@ -90,7 +90,6 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity):
self._coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=None,
name=name,
)
diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py
index 7a1a4121a93..02f5d99c72c 100644
--- a/homeassistant/components/modbus/const.py
+++ b/homeassistant/components/modbus/const.py
@@ -20,6 +20,7 @@ CONF_DATA_TYPE = "data_type"
CONF_DEVICE_ADDRESS = "device_address"
CONF_FANS = "fans"
CONF_INPUT_TYPE = "input_type"
+CONF_LAZY_ERROR = "lazy_error_count"
CONF_MAX_TEMP = "max_temp"
CONF_MAX_VALUE = "max_value"
CONF_MIN_TEMP = "min_temp"
@@ -27,6 +28,7 @@ CONF_MIN_VALUE = "min_value"
CONF_MSG_WAIT = "message_wait_milliseconds"
CONF_NAN_VALUE = "nan_value"
CONF_PARITY = "parity"
+CONF_RETRIES = "retries"
CONF_PRECISION = "precision"
CONF_SCALE = "scale"
CONF_SLAVE_COUNT = "slave_count"
diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py
index 90833516e59..9f0e862f283 100644
--- a/homeassistant/components/modbus/entity.py
+++ b/homeassistant/components/modbus/entity.py
@@ -297,10 +297,8 @@ class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity):
self._verify_type = convert[
config[CONF_VERIFY].get(CONF_INPUT_TYPE, config[CONF_WRITE_TYPE])
][0]
- self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, [self.command_on])
- self._state_off = config[CONF_VERIFY].get(
- CONF_STATE_OFF, [self._command_off]
- )
+ self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, self.command_on)
+ self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off)
else:
self._verify_active = False
@@ -365,9 +363,9 @@ class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity):
self._attr_is_on = bool(result.bits[0] & 1)
else:
value = int(result.registers[0])
- if value in self._state_on:
+ if value == self._state_on:
self._attr_is_on = True
- elif value in self._state_off:
+ elif value == self._state_off:
self._attr_is_on = False
elif value is not None:
_LOGGER.error(
diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py
index d85b4e0e67f..cc70a783234 100644
--- a/homeassistant/components/modbus/modbus.py
+++ b/homeassistant/components/modbus/modbus.py
@@ -34,6 +34,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.typing import ConfigType
@@ -61,9 +62,11 @@ from .const import (
PLATFORMS,
RTUOVERTCP,
SERIAL,
+ SERVICE_RESTART,
SERVICE_STOP,
SERVICE_WRITE_COIL,
SERVICE_WRITE_REGISTER,
+ SIGNAL_START_ENTITY,
SIGNAL_STOP_ENTITY,
TCP,
UDP,
@@ -230,12 +233,34 @@ async def async_modbus_setup(
hub = hub_collect[service.data[ATTR_HUB]]
await hub.async_close()
- hass.services.async_register(
- DOMAIN,
- SERVICE_STOP,
- async_stop_hub,
- schema=vol.Schema({vol.Required(ATTR_HUB): cv.string}),
- )
+ async def async_restart_hub(service: ServiceCall) -> None:
+ """Restart Modbus hub."""
+ async_create_issue(
+ hass,
+ DOMAIN,
+ "deprecated_restart",
+ breaks_in_ha_version="2024.11.0",
+ is_fixable=False,
+ severity=IssueSeverity.WARNING,
+ translation_key="deprecated_restart",
+ )
+ _LOGGER.warning(
+ "`modbus.restart` is deprecated and will be removed in version 2024.11"
+ )
+ async_dispatcher_send(hass, SIGNAL_START_ENTITY)
+ hub = hub_collect[service.data[ATTR_HUB]]
+ await hub.async_restart()
+
+ for x_service in (
+ (SERVICE_STOP, async_stop_hub),
+ (SERVICE_RESTART, async_restart_hub),
+ ):
+ hass.services.async_register(
+ DOMAIN,
+ x_service[0],
+ x_service[1],
+ schema=vol.Schema({vol.Required(ATTR_HUB): cv.string}),
+ )
return True
@@ -316,7 +341,7 @@ class ModbusHub:
self._log_error(err, error_state=False)
return
message = f"modbus {self.name} communication open"
- _LOGGER.info(message)
+ _LOGGER.warning(message)
async def async_setup(self) -> bool:
"""Set up pymodbus client."""
@@ -368,7 +393,7 @@ class ModbusHub:
del self._client
self._client = None
message = f"modbus {self.name} communication closed"
- _LOGGER.info(message)
+ _LOGGER.warning(message)
async def low_level_pb_call(
self, slave: int | None, address: int, value: int | list[int], use_call: str
diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py
index d5a16c95cc4..4b4fd5bd51a 100644
--- a/homeassistant/components/modbus/sensor.py
+++ b/homeassistant/components/modbus/sensor.py
@@ -91,7 +91,6 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
self._coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=None,
name=name,
)
diff --git a/homeassistant/components/modbus/strings.json b/homeassistant/components/modbus/strings.json
index 7b55022645e..c0d702a9b89 100644
--- a/homeassistant/components/modbus/strings.json
+++ b/homeassistant/components/modbus/strings.json
@@ -97,6 +97,10 @@
"no_entities": {
"title": "Modbus {sub_1} contain no entities, entry not loaded.",
"description": "Please add at least one entity to Modbus {sub_1} in your configuration.yaml file and restart Home Assistant to fix this issue."
+ },
+ "deprecated_restart": {
+ "title": "modbus.restart is being removed",
+ "description": "Please use reload yaml via the developer tools in the UI instead of via the `modbus.restart` action."
}
}
}
diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py
index f8f1a7450eb..e1120094d01 100644
--- a/homeassistant/components/modbus/validators.py
+++ b/homeassistant/components/modbus/validators.py
@@ -27,6 +27,8 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
from .const import (
CONF_DATA_TYPE,
CONF_FAN_MODE_VALUES,
+ CONF_LAZY_ERROR,
+ CONF_RETRIES,
CONF_SLAVE_COUNT,
CONF_SWAP,
CONF_SWAP_BYTE,
@@ -282,6 +284,27 @@ def validate_modbus(
hub_name_inx: int,
) -> bool:
"""Validate modbus entries."""
+ if CONF_RETRIES in hub:
+ async_create_issue(
+ hass,
+ DOMAIN,
+ "deprecated_retries",
+ breaks_in_ha_version="2024.7.0",
+ is_fixable=False,
+ severity=IssueSeverity.WARNING,
+ translation_key="deprecated_retries",
+ translation_placeholders={
+ "config_key": "retries",
+ "integration": DOMAIN,
+ "url": "https://www.home-assistant.io/integrations/modbus",
+ },
+ )
+ _LOGGER.warning(
+ "`retries`: is deprecated and will be removed in version 2024.7"
+ )
+ else:
+ hub[CONF_RETRIES] = 3
+
host: str = (
hub[CONF_PORT]
if hub[CONF_TYPE] == SERIAL
@@ -330,6 +353,24 @@ def validate_entity(
ent_addr: set[str],
) -> bool:
"""Validate entity."""
+ if CONF_LAZY_ERROR in entity:
+ async_create_issue(
+ hass,
+ DOMAIN,
+ "removed_lazy_error_count",
+ breaks_in_ha_version="2024.7.0",
+ is_fixable=False,
+ severity=IssueSeverity.WARNING,
+ translation_key="removed_lazy_error_count",
+ translation_placeholders={
+ "config_key": "lazy_error_count",
+ "integration": DOMAIN,
+ "url": "https://www.home-assistant.io/integrations/modbus",
+ },
+ )
+ _LOGGER.warning(
+ "`lazy_error_count`: is deprecated and will be removed in version 2024.7"
+ )
name = f"{component}.{entity[CONF_NAME]}"
scan_interval = entity.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
if 0 < scan_interval < 5:
diff --git a/homeassistant/components/modern_forms/config_flow.py b/homeassistant/components/modern_forms/config_flow.py
index 33e814efb51..dee08736234 100644
--- a/homeassistant/components/modern_forms/config_flow.py
+++ b/homeassistant/components/modern_forms/config_flow.py
@@ -9,13 +9,11 @@ import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_HOST, CONF_MAC
+from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
-USER_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
-
class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a ModernForms config flow."""
@@ -57,21 +55,17 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None, prepare: bool = False
) -> ConfigFlowResult:
"""Config flow handler for ModernForms."""
+ source = self.context["source"]
+
# Request user input, unless we are preparing discovery flow
if user_input is None:
user_input = {}
if not prepare:
- if self.source == SOURCE_ZEROCONF:
- return self.async_show_form(
- step_id="zeroconf_confirm",
- description_placeholders={"name": self.name},
- )
- return self.async_show_form(
- step_id="user",
- data_schema=USER_SCHEMA,
- )
+ if source == SOURCE_ZEROCONF:
+ return self._show_confirm_dialog()
+ return self._show_setup_form()
- if self.source == SOURCE_ZEROCONF:
+ if source == SOURCE_ZEROCONF:
user_input[CONF_HOST] = self.host
user_input[CONF_MAC] = self.mac
@@ -81,21 +75,18 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN):
try:
device = await device.update()
except ModernFormsConnectionError:
- if self.source == SOURCE_ZEROCONF:
+ if source == SOURCE_ZEROCONF:
return self.async_abort(reason="cannot_connect")
- return self.async_show_form(
- step_id="user",
- data_schema=USER_SCHEMA,
- errors={"base": "cannot_connect"},
- )
+ return self._show_setup_form({"base": "cannot_connect"})
user_input[CONF_MAC] = device.info.mac_address
+ user_input[CONF_NAME] = device.info.device_name
# Check if already configured
await self.async_set_unique_id(user_input[CONF_MAC])
self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]})
title = device.info.device_name
- if self.source == SOURCE_ZEROCONF:
+ if source == SOURCE_ZEROCONF:
title = self.name
if prepare:
@@ -105,3 +96,19 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN):
title=title,
data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]},
)
+
+ def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult:
+ """Show the setup form to the user."""
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
+ errors=errors or {},
+ )
+
+ def _show_confirm_dialog(self, errors: dict | None = None) -> ConfigFlowResult:
+ """Show the confirm dialog to the user."""
+ return self.async_show_form(
+ step_id="zeroconf_confirm",
+ description_placeholders={"name": self.name},
+ errors=errors or {},
+ )
diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py
index 262d13ad3af..eb4c0bf7284 100644
--- a/homeassistant/components/mold_indicator/sensor.py
+++ b/homeassistant/components/mold_indicator/sensor.py
@@ -22,7 +22,6 @@ from homeassistant.const import (
CONF_NAME,
CONF_UNIQUE_ID,
PERCENTAGE,
- STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfTemperature,
)
@@ -38,7 +37,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device import async_device_info_to_link_from_entity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
-from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from homeassistant.util.unit_conversion import TemperatureConverter
from homeassistant.util.unit_system import METRIC_SYSTEM
@@ -151,6 +150,7 @@ class MoldIndicator(SensorEntity):
unique_id: str | None,
) -> None:
"""Initialize the sensor."""
+ self._state: str | None = None
self._attr_name = name
self._attr_unique_id = unique_id
self._indoor_temp_sensor = indoor_temp_sensor
@@ -272,7 +272,7 @@ class MoldIndicator(SensorEntity):
# re-calculate dewpoint and mold indicator
self._calc_dewpoint()
self._calc_moldindicator()
- if self._attr_native_value is None:
+ if self._state is None:
self._attr_available = False
else:
self._attr_available = True
@@ -311,7 +311,7 @@ class MoldIndicator(SensorEntity):
_LOGGER.debug("Updating temp sensor with value %s", state.state)
# Return an error if the sensor change its state to Unknown.
- if state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
+ if state.state == STATE_UNKNOWN:
_LOGGER.error(
"Unable to parse temperature sensor %s with state: %s",
state.entity_id,
@@ -319,6 +319,8 @@ class MoldIndicator(SensorEntity):
)
return None
+ unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+
if (temp := util.convert(state.state, float)) is None:
_LOGGER.error(
"Unable to parse temperature sensor %s with state: %s",
@@ -328,10 +330,12 @@ class MoldIndicator(SensorEntity):
return None
# convert to celsius if necessary
- if (
- unit := state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
- ) in UnitOfTemperature:
- return TemperatureConverter.convert(temp, unit, UnitOfTemperature.CELSIUS)
+ if unit == UnitOfTemperature.FAHRENHEIT:
+ return TemperatureConverter.convert(
+ temp, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS
+ )
+ if unit == UnitOfTemperature.CELSIUS:
+ return temp
_LOGGER.error(
"Temp sensor %s has unsupported unit: %s (allowed: %s, %s)",
state.entity_id,
@@ -348,7 +352,7 @@ class MoldIndicator(SensorEntity):
_LOGGER.debug("Updating humidity sensor with value %s", state.state)
# Return an error if the sensor change its state to Unknown.
- if state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
+ if state.state == STATE_UNKNOWN:
_LOGGER.error(
"Unable to parse humidity sensor %s, state: %s",
state.entity_id,
@@ -366,18 +370,19 @@ class MoldIndicator(SensorEntity):
if (unit := state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) != PERCENTAGE:
_LOGGER.error(
- "Humidity sensor %s has unsupported unit: %s (allowed: %s)",
+ "Humidity sensor %s has unsupported unit: %s %s",
state.entity_id,
unit,
- PERCENTAGE,
+ " (allowed: %)",
)
return None
if hum > 100 or hum < 0:
_LOGGER.error(
- "Humidity sensor %s is out of range: %s (allowed: 0-100)",
+ "Humidity sensor %s is out of range: %s %s",
state.entity_id,
hum,
+ "(allowed: 0-100%)",
)
return None
@@ -396,7 +401,7 @@ class MoldIndicator(SensorEntity):
# re-calculate dewpoint and mold indicator
self._calc_dewpoint()
self._calc_moldindicator()
- if self._attr_native_value is None:
+ if self._state is None:
self._attr_available = False
self._dewpoint = None
self._crit_temp = None
@@ -432,7 +437,7 @@ class MoldIndicator(SensorEntity):
self._dewpoint,
self._calib_factor,
)
- self._attr_native_value = None
+ self._state = None
self._attr_available = False
self._crit_temp = None
return
@@ -463,13 +468,18 @@ class MoldIndicator(SensorEntity):
# check bounds and format
if crit_humidity > 100:
- self._attr_native_value = "100"
+ self._state = "100"
elif crit_humidity < 0:
- self._attr_native_value = "0"
+ self._state = "0"
else:
- self._attr_native_value = f"{int(crit_humidity):d}"
+ self._state = f"{int(crit_humidity):d}"
- _LOGGER.debug("Mold indicator humidity: %s", self.native_value)
+ _LOGGER.debug("Mold indicator humidity: %s", self._state)
+
+ @property
+ def native_value(self) -> StateType:
+ """Return the state of the entity."""
+ return self._state
@property
def extra_state_attributes(self) -> dict[str, Any]:
diff --git a/homeassistant/components/mold_indicator/strings.json b/homeassistant/components/mold_indicator/strings.json
index e19fed690b2..03c6a05546f 100644
--- a/homeassistant/components/mold_indicator/strings.json
+++ b/homeassistant/components/mold_indicator/strings.json
@@ -1,5 +1,4 @@
{
- "title": "Mold Indicator",
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py
index b2619623a07..cac673e38c1 100644
--- a/homeassistant/components/monoprice/config_flow.py
+++ b/homeassistant/components/monoprice/config_flow.py
@@ -108,7 +108,7 @@ class MonoPriceConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> MonopriceOptionsFlowHandler:
"""Define the config flow to handle options."""
- return MonopriceOptionsFlowHandler()
+ return MonopriceOptionsFlowHandler(config_entry)
@callback
@@ -126,6 +126,10 @@ def _key_for_source(index, source, previous_sources):
class MonopriceOptionsFlowHandler(OptionsFlow):
"""Handle a Monoprice options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize."""
+ self.config_entry = config_entry
+
@callback
def _previous_sources(self):
if CONF_SOURCES in self.config_entry.options:
diff --git a/homeassistant/components/monzo/api.py b/homeassistant/components/monzo/api.py
index 5216232199c..6862564d343 100644
--- a/homeassistant/components/monzo/api.py
+++ b/homeassistant/components/monzo/api.py
@@ -20,6 +20,7 @@ class AuthenticatedMonzoAPI(AbstractMonzoApi):
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
- await self._oauth_session.async_ensure_token_valid()
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
return str(self._oauth_session.token["access_token"])
diff --git a/homeassistant/components/monzo/config_flow.py b/homeassistant/components/monzo/config_flow.py
index 9f005c6aaa4..2eb51b4d305 100644
--- a/homeassistant/components/monzo/config_flow.py
+++ b/homeassistant/components/monzo/config_flow.py
@@ -8,7 +8,7 @@ from typing import Any
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.const import CONF_TOKEN
from homeassistant.helpers import config_entry_oauth2_flow
@@ -23,6 +23,7 @@ class MonzoFlowHandler(
DOMAIN = DOMAIN
oauth_data: dict[str, Any]
+ reauth_entry: ConfigEntry | None = None
@property
def logger(self) -> logging.Logger:
@@ -34,11 +35,10 @@ class MonzoFlowHandler(
) -> ConfigFlowResult:
"""Wait for the user to confirm in-app approval."""
if user_input is not None:
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
return self.async_create_entry(title=DOMAIN, data=self.oauth_data)
return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
- data_updates=self.oauth_data,
+ self.reauth_entry, data={**self.reauth_entry.data, **self.oauth_data}
)
data_schema = vol.Schema({vol.Required("confirm"): bool})
@@ -51,11 +51,11 @@ class MonzoFlowHandler(
"""Create an entry for the flow."""
self.oauth_data = data
user_id = data[CONF_TOKEN]["user_id"]
- await self.async_set_unique_id(user_id)
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
+ await self.async_set_unique_id(user_id)
self._abort_if_unique_id_configured()
- else:
- self._abort_if_unique_id_mismatch(reason="wrong_account")
+ elif self.reauth_entry.unique_id != user_id:
+ return self.async_abort(reason="wrong_account")
return await self.async_step_await_approval_confirmation()
@@ -63,6 +63,9 @@ class MonzoFlowHandler(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
diff --git a/homeassistant/components/monzo/manifest.json b/homeassistant/components/monzo/manifest.json
index 7038cecd7ea..d9d17eb8abc 100644
--- a/homeassistant/components/monzo/manifest.json
+++ b/homeassistant/components/monzo/manifest.json
@@ -6,5 +6,5 @@
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/monzo",
"iot_class": "cloud_polling",
- "requirements": ["monzopy==1.4.2"]
+ "requirements": ["monzopy==1.3.2"]
}
diff --git a/homeassistant/components/mopeka/config_flow.py b/homeassistant/components/mopeka/config_flow.py
index 2e35ff4283f..72e9386a47f 100644
--- a/homeassistant/components/mopeka/config_flow.py
+++ b/homeassistant/components/mopeka/config_flow.py
@@ -58,7 +58,7 @@ class MopekaConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: config_entries.ConfigEntry,
) -> MopekaOptionsFlow:
"""Return the options flow for this handler."""
- return MopekaOptionsFlow()
+ return MopekaOptionsFlow(config_entry)
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
@@ -139,6 +139,10 @@ class MopekaConfigFlow(ConfigFlow, domain=DOMAIN):
class MopekaOptionsFlow(config_entries.OptionsFlow):
"""Handle options for the Mopeka component."""
+ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py
index e961880375c..131299314a2 100644
--- a/homeassistant/components/motion_blinds/config_flow.py
+++ b/homeassistant/components/motion_blinds/config_flow.py
@@ -38,6 +38,10 @@ CONFIG_SCHEMA = vol.Schema(
class OptionsFlowHandler(OptionsFlow):
"""Options for the component."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Init object."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -79,7 +83,7 @@ class MotionBlindsFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def async_step_dhcp(
self, discovery_info: dhcp.DhcpServiceInfo
diff --git a/homeassistant/components/motionblinds_ble/config_flow.py b/homeassistant/components/motionblinds_ble/config_flow.py
index d99096d3a09..cda673b13ac 100644
--- a/homeassistant/components/motionblinds_ble/config_flow.py
+++ b/homeassistant/components/motionblinds_ble/config_flow.py
@@ -187,12 +187,16 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Create the options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Handle an options flow for Motionblinds BLE."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py
index 3e4ad53d200..e24b844c4a2 100644
--- a/homeassistant/components/motioneye/__init__.py
+++ b/homeassistant/components/motioneye/__init__.py
@@ -322,7 +322,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=DOMAIN,
update_method=async_update_data,
update_interval=DEFAULT_SCAN_INTERVAL,
diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py
index 80a6449a22d..8107ca760cb 100644
--- a/homeassistant/components/motioneye/config_flow.py
+++ b/homeassistant/components/motioneye/config_flow.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from collections.abc import Mapping
-from typing import Any
+from typing import Any, cast
from motioneye_client.client import (
MotionEyeClientConnectionError,
@@ -12,6 +12,7 @@ from motioneye_client.client import (
)
import voluptuous as vol
+from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.config_entries import (
SOURCE_REAUTH,
ConfigEntry,
@@ -19,11 +20,10 @@ from homeassistant.config_entries import (
ConfigFlowResult,
OptionsFlow,
)
-from homeassistant.const import CONF_URL, CONF_WEBHOOK_ID
+from homeassistant.const import CONF_SOURCE, CONF_URL, CONF_WEBHOOK_ID
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from homeassistant.helpers.typing import VolDictType
from . import create_motioneye_client
@@ -53,7 +53,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
def _get_form(
- user_input: Mapping[str, Any], errors: dict[str, str] | None = None
+ user_input: dict[str, Any], errors: dict[str, str] | None = None
) -> ConfigFlowResult:
"""Show the form to the user."""
url_schema: VolDictType = {}
@@ -89,10 +89,16 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
+ reauth_entry = None
+ if self.context.get("entry_id"):
+ reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
+
if user_input is None:
- if self.source == SOURCE_REAUTH:
- return _get_form(self._get_reauth_entry().data)
- return _get_form({})
+ return _get_form(
+ cast(dict[str, Any], reauth_entry.data) if reauth_entry else {}
+ )
if self._hassio_discovery:
# In case of Supervisor discovery, use pushed URL
@@ -129,13 +135,16 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN):
if errors:
return _get_form(user_input, errors)
- if self.source == SOURCE_REAUTH:
- reauth_entry = self._get_reauth_entry()
+ if self.context.get(CONF_SOURCE) == SOURCE_REAUTH and reauth_entry is not None:
# Persist the same webhook id across reauths.
if CONF_WEBHOOK_ID in reauth_entry.data:
user_input[CONF_WEBHOOK_ID] = reauth_entry.data[CONF_WEBHOOK_ID]
-
- return self.async_update_reload_and_abort(reauth_entry, data=user_input)
+ self.hass.config_entries.async_update_entry(reauth_entry, data=user_input)
+ # Need to manually reload, as the listener won't have been
+ # installed because the initial load did not succeed (the reauth
+ # flow will not be initiated if the load succeeds).
+ await self.hass.config_entries.async_reload(reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
# Search for duplicates: there isn't a useful unique_id, but
# at least prevent entries with the same motionEye URL.
@@ -179,16 +188,18 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> MotionEyeOptionsFlow:
+ def async_get_options_flow(config_entry: ConfigEntry) -> MotionEyeOptionsFlow:
"""Get the Hyperion Options flow."""
- return MotionEyeOptionsFlow()
+ return MotionEyeOptionsFlow(config_entry)
class MotionEyeOptionsFlow(OptionsFlow):
"""motionEye options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize a motionEye options flow."""
+ self._config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -199,14 +210,14 @@ class MotionEyeOptionsFlow(OptionsFlow):
schema: dict[vol.Marker, type] = {
vol.Required(
CONF_WEBHOOK_SET,
- default=self.config_entry.options.get(
+ default=self._config_entry.options.get(
CONF_WEBHOOK_SET,
DEFAULT_WEBHOOK_SET,
),
): bool,
vol.Required(
CONF_WEBHOOK_SET_OVERWRITE,
- default=self.config_entry.options.get(
+ default=self._config_entry.options.get(
CONF_WEBHOOK_SET_OVERWRITE,
DEFAULT_WEBHOOK_SET_OVERWRITE,
),
@@ -217,9 +228,9 @@ class MotionEyeOptionsFlow(OptionsFlow):
# The input URL is not validated as being a URL, to allow for the possibility
# the template input won't be a valid URL until after it's rendered
description: dict[str, str] | None = None
- if CONF_STREAM_URL_TEMPLATE in self.config_entry.options:
+ if CONF_STREAM_URL_TEMPLATE in self._config_entry.options:
description = {
- "suggested_value": self.config_entry.options[
+ "suggested_value": self._config_entry.options[
CONF_STREAM_URL_TEMPLATE
]
}
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 907b1a1dd11..86eeca2017c 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -76,8 +76,8 @@ from .const import ( # noqa: F401
DEFAULT_QOS,
DEFAULT_RETAIN,
DOMAIN,
- ENTITY_PLATFORMS,
MQTT_CONNECTION_STATE,
+ RELOADABLE_PLATFORMS,
TEMPLATE_ERRORS,
)
from .models import ( # noqa: F401
@@ -438,7 +438,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
for entity in list(mqtt_platform.entities.values())
if getattr(entity, "_discovery_data", None) is None
and mqtt_platform.config_entry
- and mqtt_platform.domain in ENTITY_PLATFORMS
+ and mqtt_platform.domain in RELOADABLE_PLATFORMS
]
await asyncio.gather(*tasks)
diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py
index 65e24d5d780..3c1d0abdb66 100644
--- a/homeassistant/components/mqtt/abbreviations.py
+++ b/homeassistant/components/mqtt/abbreviations.py
@@ -30,7 +30,6 @@ ABBREVIATIONS = {
"cmd_on_tpl": "command_on_template",
"cmd_t": "command_topic",
"cmd_tpl": "command_template",
- "cmps": "components",
"cod_arm_req": "code_arm_required",
"cod_dis_req": "code_disarm_required",
"cod_form": "code_format",
@@ -46,7 +45,6 @@ ABBREVIATIONS = {
"dir_cmd_tpl": "direction_command_template",
"dir_stat_t": "direction_state_topic",
"dir_val_tpl": "direction_value_template",
- "dsp_prc": "display_precision",
"dock_cmd_t": "dock_command_topic",
"dock_cmd_tpl": "dock_command_template",
"e": "encoding",
@@ -94,7 +92,6 @@ ABBREVIATIONS = {
"min_mirs": "min_mireds",
"max_temp": "max_temp",
"min_temp": "min_temp",
- "migr_discvry": "migrate_discovery",
"mode": "mode",
"mode_cmd_tpl": "mode_command_template",
"mode_cmd_t": "mode_command_topic",
@@ -112,7 +109,6 @@ ABBREVIATIONS = {
"osc_cmd_tpl": "oscillation_command_template",
"osc_stat_t": "oscillation_state_topic",
"osc_val_tpl": "oscillation_value_template",
- "p": "platform",
"pause_cmd_t": "pause_command_topic",
"pause_mw_cmd_tpl": "pause_command_template",
"pct_cmd_t": "percentage_command_topic",
diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py
index 76bac8540a4..7f14c65ffb0 100644
--- a/homeassistant/components/mqtt/alarm_control_panel.py
+++ b/homeassistant/components/mqtt/alarm_control_panel.py
@@ -7,12 +7,23 @@ import logging
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
-from homeassistant.components.alarm_control_panel import (
- AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
-)
+from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_CODE, CONF_NAME, CONF_VALUE_TEMPLATE
+from homeassistant.const import (
+ CONF_CODE,
+ CONF_NAME,
+ CONF_VALUE_TEMPLATE,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_DISARMING,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -171,30 +182,29 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):
)
return
if payload == PAYLOAD_NONE:
- self._attr_alarm_state = None
+ self._attr_state = None
return
if payload not in (
- AlarmControlPanelState.DISARMED,
- AlarmControlPanelState.ARMED_HOME,
- AlarmControlPanelState.ARMED_AWAY,
- AlarmControlPanelState.ARMED_NIGHT,
- AlarmControlPanelState.ARMED_VACATION,
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
- AlarmControlPanelState.PENDING,
- AlarmControlPanelState.ARMING,
- AlarmControlPanelState.DISARMING,
- AlarmControlPanelState.TRIGGERED,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMING,
+ STATE_ALARM_TRIGGERED,
):
_LOGGER.warning("Received unexpected payload: %s", msg.payload)
return
- assert isinstance(payload, str)
- self._attr_alarm_state = AlarmControlPanelState(payload)
+ self._attr_state = str(payload)
@callback
def _prepare_subscribe_topics(self) -> None:
"""(Re)Subscribe to topics."""
self.add_subscription(
- CONF_STATE_TOPIC, self._state_message_received, {"_attr_alarm_state"}
+ CONF_STATE_TOPIC, self._state_message_received, {"_attr_state"}
)
async def _subscribe_topics(self) -> None:
diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py
index a626e0e5b28..4fa8b7db02a 100644
--- a/homeassistant/components/mqtt/client.py
+++ b/homeassistant/components/mqtt/client.py
@@ -376,9 +376,7 @@ class MQTT:
self._simple_subscriptions: defaultdict[str, set[Subscription]] = defaultdict(
set
)
- # To ensure the wildcard subscriptions order is preserved, we use a dict
- # with `None` values instead of a set.
- self._wildcard_subscriptions: dict[Subscription, None] = {}
+ self._wildcard_subscriptions: set[Subscription] = set()
# _retained_topics prevents a Subscription from receiving a
# retained message more than once per topic. This prevents flooding
# already active subscribers when new subscribers subscribe to a topic
@@ -756,7 +754,7 @@ class MQTT:
if subscription.is_simple_match:
self._simple_subscriptions[subscription.topic].add(subscription)
else:
- self._wildcard_subscriptions[subscription] = None
+ self._wildcard_subscriptions.add(subscription)
@callback
def _async_untrack_subscription(self, subscription: Subscription) -> None:
@@ -774,7 +772,7 @@ class MQTT:
if not simple_subscriptions[topic]:
del simple_subscriptions[topic]
else:
- del self._wildcard_subscriptions[subscription]
+ self._wildcard_subscriptions.remove(subscription)
except (KeyError, ValueError) as exc:
raise HomeAssistantError("Can't remove subscription twice") from exc
diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py
index 6e6b44cd4b8..ad41c35e51a 100644
--- a/homeassistant/components/mqtt/config_flow.py
+++ b/homeassistant/components/mqtt/config_flow.py
@@ -16,7 +16,13 @@ from cryptography.x509 import load_pem_x509_certificate
import voluptuous as vol
from homeassistant.components.file_upload import process_uploaded_file
-from homeassistant.components.hassio import AddonError, AddonManager, AddonState
+from homeassistant.components.hassio import (
+ AddonError,
+ AddonManager,
+ AddonState,
+ HassioServiceInfo,
+ is_hassio,
+)
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
@@ -36,7 +42,6 @@ from homeassistant.const import (
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.selector import (
BooleanSelector,
@@ -53,7 +58,6 @@ from homeassistant.helpers.selector import (
TextSelectorConfig,
TextSelectorType,
)
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
from .addon import get_addon_manager
@@ -206,6 +210,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ entry: ConfigEntry | None
_hassio_discovery: dict[str, Any] | None = None
_addon_manager: AddonManager
@@ -220,7 +225,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> MQTTOptionsFlowHandler:
"""Get the options flow for this handler."""
- return MQTTOptionsFlowHandler()
+ return MQTTOptionsFlowHandler(config_entry)
async def _async_install_addon(self) -> None:
"""Install the Mosquitto Mqtt broker add-on."""
@@ -338,6 +343,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
if is_hassio(self.hass):
# Offer to set up broker add-on if supervisor is available
self._addon_manager = get_addon_manager(self.hass)
@@ -393,6 +401,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication with MQTT broker."""
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
if is_hassio(self.hass):
# Check if entry setup matches the add-on discovery config
addon_manager = get_addon_manager(self.hass)
@@ -431,18 +440,18 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
"""Confirm re-authentication with MQTT broker."""
errors: dict[str, str] = {}
- reauth_entry = self._get_reauth_entry()
+ assert self.entry is not None
if user_input:
substituted_used_data = update_password_from_user_input(
- reauth_entry.data.get(CONF_PASSWORD), user_input
+ self.entry.data.get(CONF_PASSWORD), user_input
)
- new_entry_data = {**reauth_entry.data, **substituted_used_data}
+ new_entry_data = {**self.entry.data, **substituted_used_data}
if await self.hass.async_add_executor_job(
try_connection,
new_entry_data,
):
return self.async_update_reload_and_abort(
- reauth_entry, data=new_entry_data
+ self.entry, data=new_entry_data
)
errors["base"] = "invalid_auth"
@@ -450,7 +459,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
schema = self.add_suggested_values_to_schema(
REAUTH_SCHEMA,
{
- CONF_USERNAME: reauth_entry.data.get(CONF_USERNAME),
+ CONF_USERNAME: self.entry.data.get(CONF_USERNAME),
CONF_PASSWORD: PWD_NOT_CHANGED,
},
)
@@ -543,9 +552,11 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
class MQTTOptionsFlowHandler(OptionsFlow):
"""Handle MQTT options."""
- def __init__(self) -> None:
+ def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize MQTT options flow."""
+ self.config_entry = config_entry
self.broker_config: dict[str, str | int] = {}
+ self.options = config_entry.options
async def async_step_init(self, user_input: None = None) -> ConfigFlowResult:
"""Manage the MQTT options."""
diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py
index 9f1c55a54e0..1e1011cc381 100644
--- a/homeassistant/components/mqtt/const.py
+++ b/homeassistant/components/mqtt/const.py
@@ -61,7 +61,6 @@ CONF_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic"
CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template"
CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic"
CONF_ENABLED_BY_DEFAULT = "enabled_by_default"
-CONF_ENTITY_PICTURE = "entity_picture"
CONF_MODE_COMMAND_TEMPLATE = "mode_command_template"
CONF_MODE_COMMAND_TOPIC = "mode_command_topic"
CONF_MODE_LIST = "modes"
@@ -90,7 +89,6 @@ CONF_TEMP_MIN = "min_temp"
CONF_CERTIFICATE = "certificate"
CONF_CLIENT_KEY = "client_key"
CONF_CLIENT_CERT = "client_cert"
-CONF_COMPONENTS = "components"
CONF_TLS_INSECURE = "tls_insecure"
# Device and integration info options
@@ -160,7 +158,7 @@ MQTT_CONNECTION_STATE = "mqtt_connection_state"
PAYLOAD_EMPTY_JSON = "{}"
PAYLOAD_NONE = "None"
-ENTITY_PLATFORMS = [
+RELOADABLE_PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.BUTTON,
@@ -191,7 +189,7 @@ ENTITY_PLATFORMS = [
TEMPLATE_ERRORS = (jinja2.TemplateError, TemplateError, TypeError, ValueError)
-SUPPORTED_COMPONENTS = (
+SUPPORTED_COMPONENTS = {
"alarm_control_panel",
"binary_sensor",
"button",
@@ -220,4 +218,4 @@ SUPPORTED_COMPONENTS = (
"vacuum",
"valve",
"water_heater",
-)
+}
diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py
index a5ddb3ef4e6..af27615e2c0 100644
--- a/homeassistant/components/mqtt/discovery.py
+++ b/homeassistant/components/mqtt/discovery.py
@@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio
from collections import deque
-from dataclasses import dataclass
import functools
from itertools import chain
import logging
@@ -12,22 +11,15 @@ import re
import time
from typing import TYPE_CHECKING, Any
-import voluptuous as vol
-
-from homeassistant.config_entries import (
- SOURCE_MQTT,
- ConfigEntry,
- signal_discovered_config_entry_removed,
-)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_PLATFORM
from homeassistant.core import HassJobType, HomeAssistant, callback
-from homeassistant.helpers import discovery_flow
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
-from homeassistant.helpers.service_info.mqtt import MqttServiceInfo, ReceivePayloadType
+from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
from homeassistant.helpers.typing import DiscoveryInfoType
from homeassistant.loader import async_get_mqtt
from homeassistant.util.json import json_loads_object
@@ -40,14 +32,13 @@ from .const import (
ATTR_DISCOVERY_PAYLOAD,
ATTR_DISCOVERY_TOPIC,
CONF_AVAILABILITY,
- CONF_COMPONENTS,
CONF_ORIGIN,
CONF_TOPIC,
DOMAIN,
SUPPORTED_COMPONENTS,
)
-from .models import DATA_MQTT, MqttComponentConfig, MqttOriginInfo, ReceiveMessage
-from .schemas import DEVICE_DISCOVERY_SCHEMA, MQTT_ORIGIN_INFO_SCHEMA, SHARED_OPTIONS
+from .models import DATA_MQTT, MqttOriginInfo, ReceiveMessage
+from .schemas import MQTT_ORIGIN_INFO_SCHEMA
from .util import async_forward_entry_setup_and_setup_discovery
ABBREVIATIONS_SET = set(ABBREVIATIONS)
@@ -73,47 +64,13 @@ MQTT_DISCOVERY_DONE: SignalTypeFormat[Any] = SignalTypeFormat(
TOPIC_BASE = "~"
-CONF_MIGRATE_DISCOVERY = "migrate_discovery"
-
-MIGRATE_DISCOVERY_SCHEMA = vol.Schema(
- {vol.Optional(CONF_MIGRATE_DISCOVERY): True},
-)
-
class MQTTDiscoveryPayload(dict[str, Any]):
"""Class to hold and MQTT discovery payload and discovery data."""
- device_discovery: bool = False
- migrate_discovery: bool = False
discovery_data: DiscoveryInfoType
-@dataclass(frozen=True)
-class MQTTIntegrationDiscoveryConfig:
- """Class to hold an integration discovery playload."""
-
- integration: str
- msg: ReceiveMessage
-
-
-@callback
-def _async_process_discovery_migration(payload: MQTTDiscoveryPayload) -> bool:
- """Process a discovery migration request in the discovery payload."""
- # Allow abbreviation
- if migr_discvry := (payload.pop("migr_discvry", None)):
- payload[CONF_MIGRATE_DISCOVERY] = migr_discvry
- if CONF_MIGRATE_DISCOVERY in payload:
- try:
- MIGRATE_DISCOVERY_SCHEMA(payload)
- except vol.Invalid as exc:
- _LOGGER.warning(exc)
- return False
- payload.migrate_discovery = True
- payload.clear()
- return True
- return False
-
-
def clear_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) -> None:
"""Clear entry from already discovered list."""
hass.data[DATA_MQTT].discovery_already_discovered.discard(discovery_hash)
@@ -125,51 +82,36 @@ def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) ->
@callback
-def get_origin_log_string(
- discovery_payload: MQTTDiscoveryPayload, *, include_url: bool
-) -> str:
- """Get the origin information from a discovery payload for logging."""
+def async_log_discovery_origin_info(
+ message: str, discovery_payload: MQTTDiscoveryPayload, level: int = logging.INFO
+) -> None:
+ """Log information about the discovery and origin."""
+ if not _LOGGER.isEnabledFor(level):
+ # bail early if logging is disabled
+ return
if CONF_ORIGIN not in discovery_payload:
- return ""
+ _LOGGER.log(level, message)
+ return
origin_info: MqttOriginInfo = discovery_payload[CONF_ORIGIN]
sw_version_log = ""
if sw_version := origin_info.get("sw_version"):
sw_version_log = f", version: {sw_version}"
support_url_log = ""
- if include_url and (support_url := get_origin_support_url(discovery_payload)):
+ if support_url := origin_info.get("support_url"):
support_url_log = f", support URL: {support_url}"
- return f" from external application {origin_info["name"]}{sw_version_log}{support_url_log}"
-
-
-@callback
-def get_origin_support_url(discovery_payload: MQTTDiscoveryPayload) -> str | None:
- """Get the origin information support URL from a discovery payload."""
- if CONF_ORIGIN not in discovery_payload:
- return ""
- origin_info: MqttOriginInfo = discovery_payload[CONF_ORIGIN]
- return origin_info.get("support_url")
-
-
-@callback
-def async_log_discovery_origin_info(
- message: str, discovery_payload: MQTTDiscoveryPayload, level: int = logging.INFO
-) -> None:
- """Log information about the discovery and origin."""
- # We only log origin info once per device discovery
- if not _LOGGER.isEnabledFor(level):
- # bail out early if logging is disabled
- return
_LOGGER.log(
level,
- "%s%s",
+ "%s from external application %s%s%s",
message,
- get_origin_log_string(discovery_payload, include_url=True),
+ origin_info["name"],
+ sw_version_log,
+ support_url_log,
)
@callback
def _replace_abbreviations(
- payload: dict[str, Any] | str,
+ payload: Any | dict[str, Any],
abbreviations: dict[str, str],
abbreviations_set: set[str],
) -> None:
@@ -181,20 +123,11 @@ def _replace_abbreviations(
@callback
-def _replace_all_abbreviations(
- discovery_payload: dict[str, Any], component_only: bool = False
-) -> None:
+def _replace_all_abbreviations(discovery_payload: Any | dict[str, Any]) -> None:
"""Replace all abbreviations in an MQTT discovery payload."""
_replace_abbreviations(discovery_payload, ABBREVIATIONS, ABBREVIATIONS_SET)
- if CONF_AVAILABILITY in discovery_payload:
- for availability_conf in cv.ensure_list(discovery_payload[CONF_AVAILABILITY]):
- _replace_abbreviations(availability_conf, ABBREVIATIONS, ABBREVIATIONS_SET)
-
- if component_only:
- return
-
if CONF_ORIGIN in discovery_payload:
_replace_abbreviations(
discovery_payload[CONF_ORIGIN],
@@ -209,15 +142,13 @@ def _replace_all_abbreviations(
DEVICE_ABBREVIATIONS_SET,
)
- if CONF_COMPONENTS in discovery_payload:
- if not isinstance(discovery_payload[CONF_COMPONENTS], dict):
- return
- for comp_conf in discovery_payload[CONF_COMPONENTS].values():
- _replace_all_abbreviations(comp_conf, component_only=True)
+ if CONF_AVAILABILITY in discovery_payload:
+ for availability_conf in cv.ensure_list(discovery_payload[CONF_AVAILABILITY]):
+ _replace_abbreviations(availability_conf, ABBREVIATIONS, ABBREVIATIONS_SET)
@callback
-def _replace_topic_base(discovery_payload: MQTTDiscoveryPayload) -> None:
+def _replace_topic_base(discovery_payload: dict[str, Any]) -> None:
"""Replace topic base in MQTT discovery data."""
base = discovery_payload.pop(TOPIC_BASE)
for key, value in discovery_payload.items():
@@ -237,79 +168,6 @@ def _replace_topic_base(discovery_payload: MQTTDiscoveryPayload) -> None:
availability_conf[CONF_TOPIC] = f"{topic[:-1]}{base}"
-@callback
-def _generate_device_config(
- hass: HomeAssistant,
- object_id: str,
- node_id: str | None,
- migrate_discovery: bool = False,
-) -> MQTTDiscoveryPayload:
- """Generate a cleanup or discovery migration message on device cleanup.
-
- If an empty payload, or a migrate discovery request is received for a device,
- we forward an empty payload for all previously discovered components.
- """
- mqtt_data = hass.data[DATA_MQTT]
- device_node_id: str = f"{node_id} {object_id}" if node_id else object_id
- config = MQTTDiscoveryPayload({CONF_DEVICE: {}, CONF_COMPONENTS: {}})
- config.migrate_discovery = migrate_discovery
- comp_config = config[CONF_COMPONENTS]
- for platform, discover_id in mqtt_data.discovery_already_discovered:
- ids = discover_id.split(" ")
- component_node_id = ids.pop(0)
- component_object_id = " ".join(ids)
- if not ids:
- continue
- if device_node_id == component_node_id:
- comp_config[component_object_id] = {CONF_PLATFORM: platform}
-
- return config if comp_config else MQTTDiscoveryPayload({})
-
-
-@callback
-def _parse_device_payload(
- hass: HomeAssistant,
- payload: ReceivePayloadType,
- object_id: str,
- node_id: str | None,
-) -> MQTTDiscoveryPayload:
- """Parse a device discovery payload.
-
- The device discovery payload is translated info the config payloads for every single
- component inside the device based configuration.
- An empty payload is translated in a cleanup, which forwards an empty payload to all
- removed components.
- """
- device_payload = MQTTDiscoveryPayload()
- if payload == "":
- if not (device_payload := _generate_device_config(hass, object_id, node_id)):
- _LOGGER.warning(
- "No device components to cleanup for %s, node_id '%s'",
- object_id,
- node_id,
- )
- return device_payload
- try:
- device_payload = MQTTDiscoveryPayload(json_loads_object(payload))
- except ValueError:
- _LOGGER.warning("Unable to parse JSON %s: '%s'", object_id, payload)
- return device_payload
- if _async_process_discovery_migration(device_payload):
- return _generate_device_config(hass, object_id, node_id, migrate_discovery=True)
- _replace_all_abbreviations(device_payload)
- try:
- DEVICE_DISCOVERY_SCHEMA(device_payload)
- except vol.Invalid as exc:
- _LOGGER.warning(
- "Invalid MQTT device discovery payload for %s, %s: '%s'",
- object_id,
- exc,
- payload,
- )
- return MQTTDiscoveryPayload({})
- return device_payload
-
-
@callback
def _valid_origin_info(discovery_payload: MQTTDiscoveryPayload) -> bool:
"""Parse and validate origin info from a single component discovery payload."""
@@ -327,37 +185,13 @@ def _valid_origin_info(discovery_payload: MQTTDiscoveryPayload) -> bool:
return True
-@callback
-def _merge_common_device_options(
- component_config: MQTTDiscoveryPayload, device_config: dict[str, Any]
-) -> None:
- """Merge common device options with the component config options.
-
- Common options are:
- CONF_AVAILABILITY,
- CONF_AVAILABILITY_MODE,
- CONF_AVAILABILITY_TEMPLATE,
- CONF_AVAILABILITY_TOPIC,
- CONF_COMMAND_TOPIC,
- CONF_PAYLOAD_AVAILABLE,
- CONF_PAYLOAD_NOT_AVAILABLE,
- CONF_STATE_TOPIC,
- Common options in the body of the device based config are inherited into
- the component. Unless the option is explicitly specified at component level,
- in that case the option at component level will override the common option.
- """
- for option in SHARED_OPTIONS:
- if option in device_config and option not in component_config:
- component_config[option] = device_config.get(option)
-
-
async def async_start( # noqa: C901
hass: HomeAssistant, discovery_topic: str, config_entry: ConfigEntry
) -> None:
"""Start MQTT Discovery."""
mqtt_data = hass.data[DATA_MQTT]
platform_setup_lock: dict[str, asyncio.Lock] = {}
- integration_discovery_messages: dict[str, MQTTIntegrationDiscoveryConfig] = {}
+ integration_discovery_messages: dict[str, int] = {}
@callback
def _async_add_component(discovery_payload: MQTTDiscoveryPayload) -> None:
@@ -395,7 +229,8 @@ async def async_start( # noqa: C901
_LOGGER.warning(
(
"Received message on illegal discovery topic '%s'. The topic"
- " contains non allowed characters. For more information see "
+ " contains "
+ "not allowed characters. For more information see "
"https://www.home-assistant.io/integrations/mqtt/#discovery-topic"
),
topic,
@@ -404,118 +239,51 @@ async def async_start( # noqa: C901
component, node_id, object_id = match.groups()
- discovered_components: list[MqttComponentConfig] = []
- if component == CONF_DEVICE:
- # Process device based discovery message and regenerate
- # cleanup config for the all the components that are being removed.
- # This is done when a component in the device config is omitted and detected
- # as being removed, or when the device config update payload is empty.
- # In that case this will regenerate a cleanup message for all every already
- # discovered components that were linked to the initial device discovery.
- device_discovery_payload = _parse_device_payload(
- hass, payload, object_id, node_id
- )
- if not device_discovery_payload:
- return
- device_config: dict[str, Any]
- origin_config: dict[str, Any] | None
- component_configs: dict[str, dict[str, Any]]
- device_config = device_discovery_payload[CONF_DEVICE]
- origin_config = device_discovery_payload.get(CONF_ORIGIN)
- component_configs = device_discovery_payload[CONF_COMPONENTS]
- for component_id, config in component_configs.items():
- component = config.pop(CONF_PLATFORM)
- # The object_id in the device discovery topic is the unique identifier.
- # It is used as node_id for the components it contains.
- component_node_id = object_id
- # The component_id in the discovery playload is used as object_id
- # If we have an additional node_id in the discovery topic,
- # we extend the component_id with it.
- component_object_id = (
- f"{node_id} {component_id}" if node_id else component_id
- )
- # We add wrapper to the discovery payload with the discovery data.
- # If the dict is empty after removing the platform, the payload is
- # assumed to remove the existing config and we do not want to add
- # device or orig or shared availability attributes.
- if discovery_payload := MQTTDiscoveryPayload(config):
- discovery_payload[CONF_DEVICE] = device_config
- discovery_payload[CONF_ORIGIN] = origin_config
- # Only assign shared config options
- # when they are not set at entity level
- _merge_common_device_options(
- discovery_payload, device_discovery_payload
- )
- discovery_payload.device_discovery = True
- discovery_payload.migrate_discovery = (
- device_discovery_payload.migrate_discovery
- )
- discovered_components.append(
- MqttComponentConfig(
- component,
- component_object_id,
- component_node_id,
- discovery_payload,
- )
- )
- _LOGGER.debug(
- "Process device discovery payload %s", device_discovery_payload
- )
- device_discovery_id = f"{node_id} {object_id}" if node_id else object_id
- message = f"Processing device discovery for '{device_discovery_id}'"
- async_log_discovery_origin_info(
- message, MQTTDiscoveryPayload(device_discovery_payload)
- )
-
- else:
- # Process component based discovery message
+ if payload:
try:
- discovery_payload = MQTTDiscoveryPayload(
- json_loads_object(payload) if payload else {}
- )
+ discovery_payload = MQTTDiscoveryPayload(json_loads_object(payload))
except ValueError:
_LOGGER.warning("Unable to parse JSON %s: '%s'", object_id, payload)
return
- if not _async_process_discovery_migration(discovery_payload):
- _replace_all_abbreviations(discovery_payload)
- if not _valid_origin_info(discovery_payload):
- return
- discovered_components.append(
- MqttComponentConfig(component, object_id, node_id, discovery_payload)
- )
-
- discovery_pending_discovered = mqtt_data.discovery_pending_discovered
- for component_config in discovered_components:
- component = component_config.component
- node_id = component_config.node_id
- object_id = component_config.object_id
- discovery_payload = component_config.discovery_payload
-
+ _replace_all_abbreviations(discovery_payload)
+ if not _valid_origin_info(discovery_payload):
+ return
if TOPIC_BASE in discovery_payload:
_replace_topic_base(discovery_payload)
+ else:
+ discovery_payload = MQTTDiscoveryPayload({})
- # If present, the node_id will be included in the discovery_id.
- discovery_id = f"{node_id} {object_id}" if node_id else object_id
- discovery_hash = (component, discovery_id)
+ # If present, the node_id will be included in the discovered object id
+ discovery_id = f"{node_id} {object_id}" if node_id else object_id
+ discovery_hash = (component, discovery_id)
+ if discovery_payload:
# Attach MQTT topic to the payload, used for debug prints
- discovery_payload.discovery_data = {
+ setattr(
+ discovery_payload,
+ "__configuration_source__",
+ f"MQTT (topic: '{topic}')",
+ )
+ discovery_data = {
ATTR_DISCOVERY_HASH: discovery_hash,
ATTR_DISCOVERY_PAYLOAD: discovery_payload,
ATTR_DISCOVERY_TOPIC: topic,
}
+ setattr(discovery_payload, "discovery_data", discovery_data)
- if discovery_hash in discovery_pending_discovered:
- pending = discovery_pending_discovered[discovery_hash]["pending"]
- pending.appendleft(discovery_payload)
- _LOGGER.debug(
- "Component has already been discovered: %s %s, queuing update",
- component,
- discovery_id,
- )
- return
+ discovery_payload[CONF_PLATFORM] = "mqtt"
- async_process_discovery_payload(component, discovery_id, discovery_payload)
+ if discovery_hash in mqtt_data.discovery_pending_discovered:
+ pending = mqtt_data.discovery_pending_discovered[discovery_hash]["pending"]
+ pending.appendleft(discovery_payload)
+ _LOGGER.debug(
+ "Component has already been discovered: %s %s, queuing update",
+ component,
+ discovery_id,
+ )
+ return
+
+ async_process_discovery_payload(component, discovery_id, discovery_payload)
@callback
def async_process_discovery_payload(
@@ -523,7 +291,7 @@ async def async_start( # noqa: C901
) -> None:
"""Process the payload of a new discovery."""
- _LOGGER.debug("Process component discovery payload %s", payload)
+ _LOGGER.debug("Process discovery payload %s", payload)
discovery_hash = (component, discovery_id)
already_discovered = discovery_hash in mqtt_data.discovery_already_discovered
@@ -580,8 +348,6 @@ async def async_start( # noqa: C901
0,
job_type=HassJobType.Callback,
)
- # Subscribe first for platform discovery wildcard topics first,
- # and then subscribe device discovery wildcard topics.
for topic in chain(
(
f"{discovery_topic}/{component}/+/config"
@@ -591,10 +357,6 @@ async def async_start( # noqa: C901
f"{discovery_topic}/{component}/+/+/config"
for component in SUPPORTED_COMPONENTS
),
- (
- f"{discovery_topic}/device/+/config",
- f"{discovery_topic}/device/+/+/config",
- ),
)
]
@@ -602,39 +364,13 @@ async def async_start( # noqa: C901
mqtt_integrations = await async_get_mqtt(hass)
integration_unsubscribe = mqtt_data.integration_unsubscribe
- async def _async_handle_config_entry_removed(entry: ConfigEntry) -> None:
- """Handle integration config entry changes."""
- for discovery_key in entry.discovery_keys[DOMAIN]:
- if (
- discovery_key.version != 1
- or not isinstance(discovery_key.key, str)
- or discovery_key.key not in integration_discovery_messages
- ):
- continue
- topic = discovery_key.key
- discovery_message = integration_discovery_messages[topic]
- del integration_discovery_messages[topic]
- _LOGGER.debug("Rediscover service on topic %s", topic)
- # Initiate re-discovery
- await async_integration_message_received(
- discovery_message.integration, discovery_message.msg
- )
-
- mqtt_data.discovery_unsubscribe.append(
- async_dispatcher_connect(
- hass,
- signal_discovered_config_entry_removed(DOMAIN),
- _async_handle_config_entry_removed,
- )
- )
-
async def async_integration_message_received(
integration: str, msg: ReceiveMessage
) -> None:
"""Process the received message."""
if (
msg.topic in integration_discovery_messages
- and integration_discovery_messages[msg.topic].msg.payload == msg.payload
+ and integration_discovery_messages[msg.topic] == hash(msg.payload)
):
_LOGGER.debug(
"Ignoring already processed discovery message for '%s' on topic %s: %s",
@@ -657,23 +393,14 @@ async def async_start( # noqa: C901
subscribed_topic=msg.subscribed_topic,
timestamp=msg.timestamp,
)
- discovery_key = discovery_flow.DiscoveryKey(
- domain=DOMAIN, key=msg.topic, version=1
- )
- discovery_flow.async_create_flow(
- hass,
- integration,
- {"source": SOURCE_MQTT},
- data,
- discovery_key=discovery_key,
+ await hass.config_entries.flow.async_init(
+ integration, context={"source": DOMAIN}, data=data
)
if msg.payload:
# Update the last discovered config message
- integration_discovery_messages[msg.topic] = (
- MQTTIntegrationDiscoveryConfig(integration=integration, msg=msg)
- )
+ integration_discovery_messages[msg.topic] = hash(msg.payload)
elif msg.topic in integration_discovery_messages:
- # Cleanup cache if discovery payload is empty
+ # Cleanup hash if discovery payload is empty
del integration_discovery_messages[msg.topic]
integration_unsubscribe.update(
diff --git a/homeassistant/components/mqtt/entity.py b/homeassistant/components/mqtt/entity.py
index 46b2c9e1d42..5845dae12e2 100644
--- a/homeassistant/components/mqtt/entity.py
+++ b/homeassistant/components/mqtt/entity.py
@@ -76,7 +76,6 @@ from .const import (
CONF_CONNECTIONS,
CONF_ENABLED_BY_DEFAULT,
CONF_ENCODING,
- CONF_ENTITY_PICTURE,
CONF_HW_VERSION,
CONF_IDENTIFIERS,
CONF_JSON_ATTRS_TEMPLATE,
@@ -104,8 +103,6 @@ from .discovery import (
MQTT_DISCOVERY_UPDATED,
MQTTDiscoveryPayload,
clear_discovery_hash,
- get_origin_log_string,
- get_origin_support_url,
set_discovery_hash,
)
from .models import (
@@ -593,7 +590,6 @@ async def cleanup_device_registry(
entity_registry = er.async_get(hass)
if (
device_id
- and device_id not in device_registry.deleted_devices
and config_entry_id
and not er.async_entries_for_device(
entity_registry, device_id, include_disabled_entities=False
@@ -675,7 +671,6 @@ class MqttDiscoveryDeviceUpdateMixin(ABC):
self._config_entry = config_entry
self._config_entry_id = config_entry.entry_id
self._skip_device_removal: bool = False
- self._migrate_discovery: str | None = None
discovery_hash = get_discovery_hash(discovery_data)
self._remove_discovery_updated = async_dispatcher_connect(
@@ -708,95 +703,12 @@ class MqttDiscoveryDeviceUpdateMixin(ABC):
) -> None:
"""Handle discovery update."""
discovery_hash = get_discovery_hash(self._discovery_data)
- # Start discovery migration or rollback if migrate_discovery flag is set
- # and the discovery topic is valid and not yet migrating
- if (
- discovery_payload.migrate_discovery
- and self._migrate_discovery is None
- and self._discovery_data[ATTR_DISCOVERY_TOPIC]
- == discovery_payload.discovery_data[ATTR_DISCOVERY_TOPIC]
- ):
- self._migrate_discovery = self._discovery_data[ATTR_DISCOVERY_TOPIC]
- discovery_hash = self._discovery_data[ATTR_DISCOVERY_HASH]
- origin_info = get_origin_log_string(
- self._discovery_data[ATTR_DISCOVERY_PAYLOAD], include_url=False
- )
- action = "Rollback" if discovery_payload.device_discovery else "Migration"
- schema_type = "platform" if discovery_payload.device_discovery else "device"
- _LOGGER.info(
- "%s to MQTT %s discovery schema started for %s '%s'"
- "%s on topic %s. To complete %s, publish a %s discovery "
- "message with %s '%s'. After completed %s, "
- "publish an empty (retained) payload to %s",
- action,
- schema_type,
- discovery_hash[0],
- discovery_hash[1],
- origin_info,
- self._migrate_discovery,
- action.lower(),
- schema_type,
- discovery_hash[0],
- discovery_hash[1],
- action.lower(),
- self._migrate_discovery,
- )
-
- # Cleanup platform resources
- await self.async_tear_down()
- # Unregister and clean discovery
- stop_discovery_updates(
- self.hass, self._discovery_data, self._remove_discovery_updated
- )
- send_discovery_done(self.hass, self._discovery_data)
- return
-
_LOGGER.debug(
"Got update for %s with hash: %s '%s'",
self.log_name,
discovery_hash,
discovery_payload,
)
- new_discovery_topic = discovery_payload.discovery_data[ATTR_DISCOVERY_TOPIC]
-
- # Abort early if an update is not received via the registered discovery topic.
- # This can happen if a device and single component discovery payload
- # share the same discovery ID.
- if self._discovery_data[ATTR_DISCOVERY_TOPIC] != new_discovery_topic:
- # Prevent illegal updates
- old_origin_info = get_origin_log_string(
- self._discovery_data[ATTR_DISCOVERY_PAYLOAD], include_url=False
- )
- new_origin_info = get_origin_log_string(
- discovery_payload.discovery_data[ATTR_DISCOVERY_PAYLOAD],
- include_url=False,
- )
- new_origin_support_url = get_origin_support_url(
- discovery_payload.discovery_data[ATTR_DISCOVERY_PAYLOAD]
- )
- if new_origin_support_url:
- get_support = f"for support visit {new_origin_support_url}"
- else:
- get_support = (
- "for documentation on migration to device schema or rollback to "
- "discovery schema, visit https://www.home-assistant.io/integrations/"
- "mqtt/#migration-from-single-component-to-device-based-discovery"
- )
- _LOGGER.warning(
- "Received a conflicting MQTT discovery message for %s '%s' which was "
- "previously discovered on topic %s%s; the conflicting discovery "
- "message was received on topic %s%s; %s",
- discovery_hash[0],
- discovery_hash[1],
- self._discovery_data[ATTR_DISCOVERY_TOPIC],
- old_origin_info,
- new_discovery_topic,
- new_origin_info,
- get_support,
- )
- send_discovery_done(self.hass, self._discovery_data)
- return
-
if (
discovery_payload
and discovery_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]
@@ -893,7 +805,6 @@ class MqttDiscoveryUpdateMixin(Entity):
mqtt_data = hass.data[DATA_MQTT]
self._registry_hooks = mqtt_data.discovery_registry_hooks
discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH]
- self._migrate_discovery: str | None = None
if discovery_hash in self._registry_hooks:
self._registry_hooks.pop(discovery_hash)()
@@ -951,12 +862,7 @@ class MqttDiscoveryUpdateMixin(Entity):
if TYPE_CHECKING:
assert self._discovery_data
self._cleanup_discovery_on_remove()
- if self._migrate_discovery is None:
- # Unload and cleanup registry
- await self._async_remove_state_and_registry_entry()
- else:
- # Only unload the entity
- await self.async_remove(force_remove=True)
+ await self._async_remove_state_and_registry_entry()
send_discovery_done(self.hass, self._discovery_data)
@callback
@@ -971,102 +877,18 @@ class MqttDiscoveryUpdateMixin(Entity):
"""
if TYPE_CHECKING:
assert self._discovery_data
- discovery_hash = get_discovery_hash(self._discovery_data)
- # Start discovery migration or rollback if migrate_discovery flag is set
- # and the discovery topic is valid and not yet migrating
- if (
- payload.migrate_discovery
- and self._migrate_discovery is None
- and self._discovery_data[ATTR_DISCOVERY_TOPIC]
- == payload.discovery_data[ATTR_DISCOVERY_TOPIC]
- ):
- if self.unique_id is None or self.device_info is None:
- _LOGGER.error(
- "Discovery migration is not possible for "
- "for entity %s on topic %s. A unique_id "
- "and device context is required, got unique_id: %s, device: %s",
- self.entity_id,
- self._discovery_data[ATTR_DISCOVERY_TOPIC],
- self.unique_id,
- self.device_info,
- )
- send_discovery_done(self.hass, self._discovery_data)
- return
-
- self._migrate_discovery = self._discovery_data[ATTR_DISCOVERY_TOPIC]
- discovery_hash = self._discovery_data[ATTR_DISCOVERY_HASH]
- origin_info = get_origin_log_string(
- self._discovery_data[ATTR_DISCOVERY_PAYLOAD], include_url=False
- )
- action = "Rollback" if payload.device_discovery else "Migration"
- schema_type = "platform" if payload.device_discovery else "device"
- _LOGGER.info(
- "%s to MQTT %s discovery schema started for entity %s"
- "%s on topic %s. To complete %s, publish a %s discovery "
- "message with %s entity '%s'. After completed %s, "
- "publish an empty (retained) payload to %s",
- action,
- schema_type,
- self.entity_id,
- origin_info,
- self._migrate_discovery,
- action.lower(),
- schema_type,
- discovery_hash[0],
- discovery_hash[1],
- action.lower(),
- self._migrate_discovery,
- )
- old_payload = self._discovery_data[ATTR_DISCOVERY_PAYLOAD]
+ discovery_hash: tuple[str, str] = self._discovery_data[ATTR_DISCOVERY_HASH]
_LOGGER.debug(
"Got update for entity with hash: %s '%s'",
discovery_hash,
payload,
)
- new_discovery_topic = payload.discovery_data[ATTR_DISCOVERY_TOPIC]
- # Abort early if an update is not received via the registered discovery topic.
- # This can happen if a device and single component discovery payload
- # share the same discovery ID.
- if self._discovery_data[ATTR_DISCOVERY_TOPIC] != new_discovery_topic:
- # Prevent illegal updates
- old_origin_info = get_origin_log_string(
- self._discovery_data[ATTR_DISCOVERY_PAYLOAD], include_url=False
- )
- new_origin_info = get_origin_log_string(
- payload.discovery_data[ATTR_DISCOVERY_PAYLOAD], include_url=False
- )
- new_origin_support_url = get_origin_support_url(
- payload.discovery_data[ATTR_DISCOVERY_PAYLOAD]
- )
- if new_origin_support_url:
- get_support = f"for support visit {new_origin_support_url}"
- else:
- get_support = (
- "for documentation on migration to device schema or rollback to "
- "discovery schema, visit https://www.home-assistant.io/integrations/"
- "mqtt/#migration-from-single-component-to-device-based-discovery"
- )
- _LOGGER.warning(
- "Received a conflicting MQTT discovery message for entity %s; the "
- "entity was previously discovered on topic %s%s; the conflicting "
- "discovery message was received on topic %s%s; %s",
- self.entity_id,
- self._discovery_data[ATTR_DISCOVERY_TOPIC],
- old_origin_info,
- new_discovery_topic,
- new_origin_info,
- get_support,
- )
- send_discovery_done(self.hass, self._discovery_data)
- return
-
+ old_payload: DiscoveryInfoType
+ old_payload = self._discovery_data[ATTR_DISCOVERY_PAYLOAD]
debug_info.update_entity_discovery_data(self.hass, payload, self.entity_id)
if not payload:
# Empty payload: Remove component
- if self._migrate_discovery is None:
- _LOGGER.info("Removing component: %s", self.entity_id)
- else:
- _LOGGER.info("Unloading component: %s", self.entity_id)
+ _LOGGER.info("Removing component: %s", self.entity_id)
self.hass.async_create_task(
self._async_process_discovery_update_and_remove()
)
@@ -1389,7 +1211,6 @@ class MqttEntity(
config.get(CONF_ENABLED_BY_DEFAULT)
)
self._attr_icon = config.get(CONF_ICON)
- self._attr_entity_picture = config.get(CONF_ENTITY_PICTURE)
# Set the entity name if needed
self._set_entity_name(config)
diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json
index 25e98c01aaf..34370c82507 100644
--- a/homeassistant/components/mqtt/manifest.json
+++ b/homeassistant/components/mqtt/manifest.json
@@ -1,13 +1,11 @@
{
"domain": "mqtt",
"name": "MQTT",
- "after_dependencies": ["hassio"],
"codeowners": ["@emontnemery", "@jbouwh", "@bdraco"],
"config_flow": true,
"dependencies": ["file_upload", "http"],
"documentation": "https://www.home-assistant.io/integrations/mqtt",
"iot_class": "local_push",
"quality_scale": "platinum",
- "requirements": ["paho-mqtt==1.6.1"],
- "single_config_entry": true
+ "requirements": ["paho-mqtt==1.6.1"]
}
diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py
index 34c1f304944..f7abbc29464 100644
--- a/homeassistant/components/mqtt/models.py
+++ b/homeassistant/components/mqtt/models.py
@@ -410,15 +410,5 @@ class MqttData:
tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict)
-@dataclass(slots=True)
-class MqttComponentConfig:
- """(component, object_id, node_id, discovery_payload)."""
-
- component: str
- object_id: str
- node_id: str | None
- discovery_payload: MQTTDiscoveryPayload
-
-
DATA_MQTT: HassKey[MqttData] = HassKey("mqtt")
DATA_MQTT_AVAILABLE: HassKey[asyncio.Future[bool]] = HassKey("mqtt_client_available")
diff --git a/homeassistant/components/mqtt/schemas.py b/homeassistant/components/mqtt/schemas.py
index 5e942c24738..67c6b447709 100644
--- a/homeassistant/components/mqtt/schemas.py
+++ b/homeassistant/components/mqtt/schemas.py
@@ -2,8 +2,6 @@
from __future__ import annotations
-from typing import Any
-
import voluptuous as vol
from homeassistant.const import (
@@ -13,7 +11,6 @@ from homeassistant.const import (
CONF_MODEL,
CONF_MODEL_ID,
CONF_NAME,
- CONF_PLATFORM,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
)
@@ -28,14 +25,10 @@ from .const import (
CONF_AVAILABILITY_MODE,
CONF_AVAILABILITY_TEMPLATE,
CONF_AVAILABILITY_TOPIC,
- CONF_COMMAND_TOPIC,
- CONF_COMPONENTS,
CONF_CONFIGURATION_URL,
CONF_CONNECTIONS,
CONF_DEPRECATED_VIA_HUB,
CONF_ENABLED_BY_DEFAULT,
- CONF_ENCODING,
- CONF_ENTITY_PICTURE,
CONF_HW_VERSION,
CONF_IDENTIFIERS,
CONF_JSON_ATTRS_TEMPLATE,
@@ -45,9 +38,7 @@ from .const import (
CONF_ORIGIN,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
- CONF_QOS,
CONF_SERIAL_NUMBER,
- CONF_STATE_TOPIC,
CONF_SUGGESTED_AREA,
CONF_SUPPORT_URL,
CONF_SW_VERSION,
@@ -55,34 +46,10 @@ from .const import (
CONF_VIA_DEVICE,
DEFAULT_PAYLOAD_AVAILABLE,
DEFAULT_PAYLOAD_NOT_AVAILABLE,
- ENTITY_PLATFORMS,
- SUPPORTED_COMPONENTS,
)
-from .util import valid_publish_topic, valid_qos_schema, valid_subscribe_topic
+from .util import valid_subscribe_topic
-# Device discovery options that are also available at entity component level
-SHARED_OPTIONS = [
- CONF_AVAILABILITY,
- CONF_AVAILABILITY_MODE,
- CONF_AVAILABILITY_TEMPLATE,
- CONF_AVAILABILITY_TOPIC,
- CONF_COMMAND_TOPIC,
- CONF_PAYLOAD_AVAILABLE,
- CONF_PAYLOAD_NOT_AVAILABLE,
- CONF_STATE_TOPIC,
-]
-
-MQTT_ORIGIN_INFO_SCHEMA = vol.All(
- vol.Schema(
- {
- vol.Required(CONF_NAME): cv.string,
- vol.Optional(CONF_SW_VERSION): cv.string,
- vol.Optional(CONF_SUPPORT_URL): cv.configuration_url,
- }
- ),
-)
-
-_MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema(
+MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema(
{
vol.Exclusive(CONF_AVAILABILITY_TOPIC, "availability"): valid_subscribe_topic,
vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template,
@@ -95,7 +62,7 @@ _MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema(
}
)
-_MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema(
+MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema(
{
vol.Optional(CONF_AVAILABILITY_MODE, default=AVAILABILITY_LATEST): vol.All(
cv.string, vol.In(AVAILABILITY_MODES)
@@ -119,8 +86,8 @@ _MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema(
}
)
-_MQTT_AVAILABILITY_SCHEMA = _MQTT_AVAILABILITY_SINGLE_SCHEMA.extend(
- _MQTT_AVAILABILITY_LIST_SCHEMA.schema
+MQTT_AVAILABILITY_SCHEMA = MQTT_AVAILABILITY_SINGLE_SCHEMA.extend(
+ MQTT_AVAILABILITY_LIST_SCHEMA.schema
)
@@ -170,10 +137,9 @@ MQTT_ORIGIN_INFO_SCHEMA = vol.All(
),
)
-MQTT_ENTITY_COMMON_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
+MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- vol.Optional(CONF_ENTITY_PICTURE): cv.url,
vol.Optional(CONF_ORIGIN): MQTT_ORIGIN_INFO_SCHEMA,
vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
@@ -184,35 +150,3 @@ MQTT_ENTITY_COMMON_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
-
-_UNIQUE_ID_SCHEMA = vol.Schema(
- {vol.Required(CONF_UNIQUE_ID): cv.string},
-).extend({}, extra=True)
-
-
-def check_unique_id(config: dict[str, Any]) -> dict[str, Any]:
- """Check if a unique ID is set in case an entity platform is configured."""
- platform = config[CONF_PLATFORM]
- if platform in ENTITY_PLATFORMS and len(config.keys()) > 1:
- _UNIQUE_ID_SCHEMA(config)
- return config
-
-
-_COMPONENT_CONFIG_SCHEMA = vol.All(
- vol.Schema(
- {vol.Required(CONF_PLATFORM): vol.In(SUPPORTED_COMPONENTS)},
- ).extend({}, extra=True),
- check_unique_id,
-)
-
-DEVICE_DISCOVERY_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
- {
- vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
- vol.Required(CONF_COMPONENTS): vol.Schema({str: _COMPONENT_CONFIG_SCHEMA}),
- vol.Required(CONF_ORIGIN): MQTT_ORIGIN_INFO_SCHEMA,
- vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
- vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
- vol.Optional(CONF_QOS): valid_qos_schema,
- vol.Optional(CONF_ENCODING): cv.string,
- }
-)
diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py
index 8878ff63127..f7bb9f75dd1 100644
--- a/homeassistant/components/mqtt/update.py
+++ b/homeassistant/components/mqtt/update.py
@@ -3,7 +3,7 @@
from __future__ import annotations
import logging
-from typing import Any
+from typing import Any, TypedDict, cast
import voluptuous as vol
@@ -34,7 +34,7 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "MQTT Update"
-CONF_DISPLAY_PRECISION = "display_precision"
+CONF_ENTITY_PICTURE = "entity_picture"
CONF_LATEST_VERSION_TEMPLATE = "latest_version_template"
CONF_LATEST_VERSION_TOPIC = "latest_version_topic"
CONF_PAYLOAD_INSTALL = "payload_install"
@@ -47,7 +47,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
{
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
- vol.Optional(CONF_DISPLAY_PRECISION, default=0): cv.positive_int,
+ vol.Optional(CONF_ENTITY_PICTURE): cv.string,
vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template,
vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_NAME): vol.Any(cv.string, None),
@@ -63,18 +63,15 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
DISCOVERY_SCHEMA = vol.All(PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA))
-MQTT_JSON_UPDATE_SCHEMA = vol.Schema(
- {
- vol.Optional("installed_version"): cv.string,
- vol.Optional("latest_version"): cv.string,
- vol.Optional("title"): cv.string,
- vol.Optional("release_summary"): cv.string,
- vol.Optional("release_url"): cv.url,
- vol.Optional("entity_picture"): cv.url,
- vol.Optional("in_progress"): cv.boolean,
- vol.Optional("update_percentage"): vol.Any(vol.Range(min=0, max=100), None),
- }
-)
+class _MqttUpdatePayloadType(TypedDict, total=False):
+ """Presentation of supported JSON payload to process state updates."""
+
+ installed_version: str
+ latest_version: str
+ title: str
+ release_summary: str
+ release_url: str
+ entity_picture: str
async def async_setup_entry(
@@ -99,12 +96,13 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
_default_name = DEFAULT_NAME
_entity_id_format = update.ENTITY_ID_FORMAT
+ _entity_picture: str | None
@property
def entity_picture(self) -> str | None:
"""Return the entity picture to use in the frontend."""
- if self._attr_entity_picture is not None:
- return self._attr_entity_picture
+ if self._entity_picture is not None:
+ return self._entity_picture
return super().entity_picture
@@ -116,10 +114,10 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
def _setup_from_config(self, config: ConfigType) -> None:
"""(Re)Setup the entity."""
self._attr_device_class = self._config.get(CONF_DEVICE_CLASS)
- self._attr_display_precision = self._config[CONF_DISPLAY_PRECISION]
self._attr_release_summary = self._config.get(CONF_RELEASE_SUMMARY)
self._attr_release_url = self._config.get(CONF_RELEASE_URL)
self._attr_title = self._config.get(CONF_TITLE)
+ self._entity_picture: str | None = self._config.get(CONF_ENTITY_PICTURE)
self._templates = {
CONF_VALUE_TEMPLATE: MqttValueTemplate(
config.get(CONF_VALUE_TEMPLATE),
@@ -144,7 +142,7 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
)
return
- json_payload: dict[str, Any] = {}
+ json_payload: _MqttUpdatePayloadType = {}
try:
rendered_json_payload = json_loads(payload)
if isinstance(rendered_json_payload, dict):
@@ -156,7 +154,7 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
rendered_json_payload,
msg.topic,
)
- json_payload = MQTT_JSON_UPDATE_SCHEMA(rendered_json_payload)
+ json_payload = cast(_MqttUpdatePayloadType, rendered_json_payload)
else:
_LOGGER.debug(
(
@@ -167,27 +165,14 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
msg.topic,
)
json_payload = {"installed_version": str(payload)}
- except vol.MultipleInvalid as exc:
- _LOGGER.warning(
- (
- "Schema violation after processing payload '%s'"
- " on topic '%s' for entity '%s': %s"
- ),
- payload,
- msg.topic,
- self.entity_id,
- exc,
- )
- return
except JSON_DECODE_EXCEPTIONS:
_LOGGER.debug(
(
"No valid (JSON) payload detected after processing payload '%s'"
- " on topic '%s' for entity '%s'"
+ " on topic %s"
),
payload,
msg.topic,
- self.entity_id,
)
json_payload["installed_version"] = str(payload)
@@ -207,14 +192,7 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
self._attr_release_url = json_payload["release_url"]
if "entity_picture" in json_payload:
- self._attr_entity_picture = json_payload["entity_picture"]
-
- if "update_percentage" in json_payload:
- self._attr_update_percentage = json_payload["update_percentage"]
- self._attr_in_progress = self._attr_update_percentage is not None
-
- if "in_progress" in json_payload:
- self._attr_in_progress = json_payload["in_progress"]
+ self._entity_picture = json_payload["entity_picture"]
@callback
def _handle_latest_version_received(self, msg: ReceiveMessage) -> None:
@@ -231,14 +209,12 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
CONF_STATE_TOPIC,
self._handle_state_message_received,
{
- "_attr_entity_picture",
- "_attr_in_progress",
"_attr_installed_version",
"_attr_latest_version",
"_attr_title",
"_attr_release_summary",
"_attr_release_url",
- "_attr_update_percentage",
+ "_entity_picture",
},
)
self.add_subscription(
@@ -261,7 +237,7 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
@property
def supported_features(self) -> UpdateEntityFeature:
"""Return the list of supported features."""
- support = UpdateEntityFeature(UpdateEntityFeature.PROGRESS)
+ support = UpdateEntityFeature(0)
if self._config.get(CONF_COMMAND_TOPIC) is not None:
support |= UpdateEntityFeature.INSTALL
diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py
index f2f6f39c96f..b79b9b4aa6a 100644
--- a/homeassistant/components/mullvad/__init__.py
+++ b/homeassistant/components/mullvad/__init__.py
@@ -27,7 +27,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
logging.getLogger(__name__),
- config_entry=entry,
name=DOMAIN,
update_method=async_get_mullvad_api_data,
update_interval=timedelta(minutes=1),
diff --git a/homeassistant/components/music_assistant/__init__.py b/homeassistant/components/music_assistant/__init__.py
deleted file mode 100644
index 9f0fc1aad27..00000000000
--- a/homeassistant/components/music_assistant/__init__.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""Music Assistant (music-assistant.io) integration."""
-
-from __future__ import annotations
-
-import asyncio
-from dataclasses import dataclass
-from typing import TYPE_CHECKING
-
-from music_assistant_client import MusicAssistantClient
-from music_assistant_client.exceptions import CannotConnect, InvalidServerVersion
-from music_assistant_models.enums import EventType
-from music_assistant_models.errors import MusicAssistantError
-
-from homeassistant.config_entries import ConfigEntry, ConfigEntryState
-from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP, Platform
-from homeassistant.core import Event, HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers import device_registry as dr
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.issue_registry import (
- IssueSeverity,
- async_create_issue,
- async_delete_issue,
-)
-
-from .const import DOMAIN, LOGGER
-
-if TYPE_CHECKING:
- from music_assistant_models.event import MassEvent
-
-type MusicAssistantConfigEntry = ConfigEntry[MusicAssistantEntryData]
-
-PLATFORMS = [Platform.MEDIA_PLAYER]
-
-CONNECT_TIMEOUT = 10
-LISTEN_READY_TIMEOUT = 30
-
-
-@dataclass
-class MusicAssistantEntryData:
- """Hold Mass data for the config entry."""
-
- mass: MusicAssistantClient
- listen_task: asyncio.Task
-
-
-async def async_setup_entry(
- hass: HomeAssistant, entry: MusicAssistantConfigEntry
-) -> bool:
- """Set up from a config entry."""
- http_session = async_get_clientsession(hass, verify_ssl=False)
- mass_url = entry.data[CONF_URL]
- mass = MusicAssistantClient(mass_url, http_session)
-
- try:
- async with asyncio.timeout(CONNECT_TIMEOUT):
- await mass.connect()
- except (TimeoutError, CannotConnect) as err:
- raise ConfigEntryNotReady(
- f"Failed to connect to music assistant server {mass_url}"
- ) from err
- except InvalidServerVersion as err:
- async_create_issue(
- hass,
- DOMAIN,
- "invalid_server_version",
- is_fixable=False,
- severity=IssueSeverity.ERROR,
- translation_key="invalid_server_version",
- )
- raise ConfigEntryNotReady(f"Invalid server version: {err}") from err
- except MusicAssistantError as err:
- LOGGER.exception("Failed to connect to music assistant server", exc_info=err)
- raise ConfigEntryNotReady(
- f"Unknown error connecting to the Music Assistant server {mass_url}"
- ) from err
-
- async_delete_issue(hass, DOMAIN, "invalid_server_version")
-
- async def on_hass_stop(event: Event) -> None:
- """Handle incoming stop event from Home Assistant."""
- await mass.disconnect()
-
- entry.async_on_unload(
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
- )
-
- # launch the music assistant client listen task in the background
- # use the init_ready event to wait until initialization is done
- init_ready = asyncio.Event()
- listen_task = asyncio.create_task(_client_listen(hass, entry, mass, init_ready))
-
- try:
- async with asyncio.timeout(LISTEN_READY_TIMEOUT):
- await init_ready.wait()
- except TimeoutError as err:
- listen_task.cancel()
- raise ConfigEntryNotReady("Music Assistant client not ready") from err
-
- entry.runtime_data = MusicAssistantEntryData(mass, listen_task)
-
- # If the listen task is already failed, we need to raise ConfigEntryNotReady
- if listen_task.done() and (listen_error := listen_task.exception()) is not None:
- await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
- try:
- await mass.disconnect()
- finally:
- raise ConfigEntryNotReady(listen_error) from listen_error
-
- # initialize platforms
- await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
-
- # register listener for removed players
- async def handle_player_removed(event: MassEvent) -> None:
- """Handle Mass Player Removed event."""
- if event.object_id is None:
- return
- dev_reg = dr.async_get(hass)
- if hass_device := dev_reg.async_get_device({(DOMAIN, event.object_id)}):
- dev_reg.async_update_device(
- hass_device.id, remove_config_entry_id=entry.entry_id
- )
-
- entry.async_on_unload(
- mass.subscribe(handle_player_removed, EventType.PLAYER_REMOVED)
- )
-
- return True
-
-
-async def _client_listen(
- hass: HomeAssistant,
- entry: ConfigEntry,
- mass: MusicAssistantClient,
- init_ready: asyncio.Event,
-) -> None:
- """Listen with the client."""
- try:
- await mass.start_listening(init_ready)
- except MusicAssistantError as err:
- if entry.state != ConfigEntryState.LOADED:
- raise
- LOGGER.error("Failed to listen: %s", err)
- except Exception as err: # pylint: disable=broad-except
- # We need to guard against unknown exceptions to not crash this task.
- if entry.state != ConfigEntryState.LOADED:
- raise
- LOGGER.exception("Unexpected exception: %s", err)
-
- if not hass.is_stopping:
- LOGGER.debug("Disconnected from server. Reloading integration")
- hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
-
-
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
- """Unload a config entry."""
- unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
-
- if unload_ok:
- mass_entry_data: MusicAssistantEntryData = entry.runtime_data
- mass_entry_data.listen_task.cancel()
- await mass_entry_data.mass.disconnect()
-
- return unload_ok
diff --git a/homeassistant/components/music_assistant/config_flow.py b/homeassistant/components/music_assistant/config_flow.py
deleted file mode 100644
index fc50a2d654b..00000000000
--- a/homeassistant/components/music_assistant/config_flow.py
+++ /dev/null
@@ -1,137 +0,0 @@
-"""Config flow for MusicAssistant integration."""
-
-from __future__ import annotations
-
-from typing import TYPE_CHECKING, Any
-
-from music_assistant_client import MusicAssistantClient
-from music_assistant_client.exceptions import (
- CannotConnect,
- InvalidServerVersion,
- MusicAssistantClientException,
-)
-from music_assistant_models.api import ServerInfoMessage
-import voluptuous as vol
-
-from homeassistant.components import zeroconf
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_URL
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers import aiohttp_client
-
-from .const import DOMAIN, LOGGER
-
-DEFAULT_URL = "http://mass.local:8095"
-DEFAULT_TITLE = "Music Assistant"
-
-
-def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema:
- """Return a schema for the manual step."""
- default_url = user_input.get(CONF_URL, DEFAULT_URL)
- return vol.Schema(
- {
- vol.Required(CONF_URL, default=default_url): str,
- }
- )
-
-
-async def get_server_info(hass: HomeAssistant, url: str) -> ServerInfoMessage:
- """Validate the user input allows us to connect."""
- async with MusicAssistantClient(
- url, aiohttp_client.async_get_clientsession(hass)
- ) as client:
- if TYPE_CHECKING:
- assert client.server_info is not None
- return client.server_info
-
-
-class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN):
- """Handle a config flow for MusicAssistant."""
-
- VERSION = 1
-
- def __init__(self) -> None:
- """Set up flow instance."""
- self.server_info: ServerInfoMessage | None = None
-
- async def async_step_user(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle a manual configuration."""
- errors: dict[str, str] = {}
- if user_input is not None:
- try:
- self.server_info = await get_server_info(
- self.hass, user_input[CONF_URL]
- )
- await self.async_set_unique_id(
- self.server_info.server_id, raise_on_progress=False
- )
- self._abort_if_unique_id_configured(
- updates={CONF_URL: self.server_info.base_url},
- reload_on_update=True,
- )
- except CannotConnect:
- errors["base"] = "cannot_connect"
- except InvalidServerVersion:
- errors["base"] = "invalid_server_version"
- except MusicAssistantClientException:
- LOGGER.exception("Unexpected exception")
- errors["base"] = "unknown"
- else:
- return self.async_create_entry(
- title=DEFAULT_TITLE,
- data={
- CONF_URL: self.server_info.base_url,
- },
- )
-
- return self.async_show_form(
- step_id="user", data_schema=get_manual_schema(user_input), errors=errors
- )
-
- return self.async_show_form(step_id="user", data_schema=get_manual_schema({}))
-
- async def async_step_zeroconf(
- self, discovery_info: zeroconf.ZeroconfServiceInfo
- ) -> ConfigFlowResult:
- """Handle a discovered Mass server.
-
- This flow is triggered by the Zeroconf component. It will check if the
- host is already configured and delegate to the import step if not.
- """
- # abort if discovery info is not what we expect
- if "server_id" not in discovery_info.properties:
- return self.async_abort(reason="missing_server_id")
- # abort if we already have exactly this server_id
- # reload the integration if the host got updated
- self.server_info = ServerInfoMessage.from_dict(discovery_info.properties)
- await self.async_set_unique_id(self.server_info.server_id)
- self._abort_if_unique_id_configured(
- updates={CONF_URL: self.server_info.base_url},
- reload_on_update=True,
- )
- try:
- await get_server_info(self.hass, self.server_info.base_url)
- except CannotConnect:
- return self.async_abort(reason="cannot_connect")
- return await self.async_step_discovery_confirm()
-
- async def async_step_discovery_confirm(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle user-confirmation of discovered server."""
- if TYPE_CHECKING:
- assert self.server_info is not None
- if user_input is not None:
- return self.async_create_entry(
- title=DEFAULT_TITLE,
- data={
- CONF_URL: self.server_info.base_url,
- },
- )
- self._set_confirm_only()
- return self.async_show_form(
- step_id="discovery_confirm",
- description_placeholders={"url": self.server_info.base_url},
- )
diff --git a/homeassistant/components/music_assistant/const.py b/homeassistant/components/music_assistant/const.py
deleted file mode 100644
index 6512f58b96c..00000000000
--- a/homeassistant/components/music_assistant/const.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""Constants for Music Assistant Component."""
-
-import logging
-
-DOMAIN = "music_assistant"
-DOMAIN_EVENT = f"{DOMAIN}_event"
-
-DEFAULT_NAME = "Music Assistant"
-
-ATTR_IS_GROUP = "is_group"
-ATTR_GROUP_MEMBERS = "group_members"
-ATTR_GROUP_PARENTS = "group_parents"
-
-ATTR_MASS_PLAYER_TYPE = "mass_player_type"
-ATTR_ACTIVE_QUEUE = "active_queue"
-ATTR_STREAM_TITLE = "stream_title"
-
-LOGGER = logging.getLogger(__package__)
diff --git a/homeassistant/components/music_assistant/entity.py b/homeassistant/components/music_assistant/entity.py
deleted file mode 100644
index f5b6d92b0cf..00000000000
--- a/homeassistant/components/music_assistant/entity.py
+++ /dev/null
@@ -1,86 +0,0 @@
-"""Base entity model."""
-
-from __future__ import annotations
-
-from typing import TYPE_CHECKING
-
-from music_assistant_models.enums import EventType
-from music_assistant_models.event import MassEvent
-from music_assistant_models.player import Player
-
-from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.entity import Entity
-
-from .const import DOMAIN
-
-if TYPE_CHECKING:
- from music_assistant_client import MusicAssistantClient
-
-
-class MusicAssistantEntity(Entity):
- """Base Entity from Music Assistant Player."""
-
- _attr_has_entity_name = True
- _attr_should_poll = False
-
- def __init__(self, mass: MusicAssistantClient, player_id: str) -> None:
- """Initialize MediaPlayer entity."""
- self.mass = mass
- self.player_id = player_id
- provider = self.mass.get_provider(self.player.provider)
- if TYPE_CHECKING:
- assert provider is not None
- self._attr_device_info = DeviceInfo(
- identifiers={(DOMAIN, player_id)},
- manufacturer=self.player.device_info.manufacturer or provider.name,
- model=self.player.device_info.model or self.player.name,
- name=self.player.display_name,
- configuration_url=f"{mass.server_url}/#/settings/editplayer/{player_id}",
- )
-
- async def async_added_to_hass(self) -> None:
- """Register callbacks."""
- await self.async_on_update()
- self.async_on_remove(
- self.mass.subscribe(
- self.__on_mass_update, EventType.PLAYER_UPDATED, self.player_id
- )
- )
- self.async_on_remove(
- self.mass.subscribe(
- self.__on_mass_update,
- EventType.QUEUE_UPDATED,
- )
- )
-
- @property
- def player(self) -> Player:
- """Return the Mass Player attached to this HA entity."""
- return self.mass.players[self.player_id]
-
- @property
- def unique_id(self) -> str | None:
- """Return unique id for entity."""
- _base = self.player_id
- if hasattr(self, "entity_description"):
- return f"{_base}_{self.entity_description.key}"
- return _base
-
- @property
- def available(self) -> bool:
- """Return availability of entity."""
- return self.player.available and bool(self.mass.connection.connected)
-
- async def __on_mass_update(self, event: MassEvent) -> None:
- """Call when we receive an event from MusicAssistant."""
- if event.event == EventType.QUEUE_UPDATED and event.object_id not in (
- self.player.active_source,
- self.player.active_group,
- self.player.player_id,
- ):
- return
- await self.async_on_update()
- self.async_write_ha_state()
-
- async def async_on_update(self) -> None:
- """Handle player updates."""
diff --git a/homeassistant/components/music_assistant/manifest.json b/homeassistant/components/music_assistant/manifest.json
deleted file mode 100644
index 65e6652407f..00000000000
--- a/homeassistant/components/music_assistant/manifest.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "domain": "music_assistant",
- "name": "Music Assistant",
- "after_dependencies": ["media_source", "media_player"],
- "codeowners": ["@music-assistant"],
- "config_flow": true,
- "documentation": "https://www.home-assistant.io/integrations/music_assistant",
- "iot_class": "local_push",
- "loggers": ["music_assistant"],
- "requirements": ["music-assistant-client==1.0.5"],
- "zeroconf": ["_mass._tcp.local."]
-}
diff --git a/homeassistant/components/music_assistant/media_player.py b/homeassistant/components/music_assistant/media_player.py
deleted file mode 100644
index f0f3675ee32..00000000000
--- a/homeassistant/components/music_assistant/media_player.py
+++ /dev/null
@@ -1,557 +0,0 @@
-"""MediaPlayer platform for Music Assistant integration."""
-
-from __future__ import annotations
-
-import asyncio
-from collections.abc import Awaitable, Callable, Coroutine, Mapping
-from contextlib import suppress
-import functools
-import os
-from typing import TYPE_CHECKING, Any
-
-from music_assistant_models.enums import (
- EventType,
- MediaType,
- PlayerFeature,
- QueueOption,
- RepeatMode as MassRepeatMode,
-)
-from music_assistant_models.errors import MediaNotFoundError, MusicAssistantError
-from music_assistant_models.event import MassEvent
-from music_assistant_models.media_items import ItemMapping, MediaItemType, Track
-
-from homeassistant.components import media_source
-from homeassistant.components.media_player import (
- ATTR_MEDIA_EXTRA,
- BrowseMedia,
- MediaPlayerDeviceClass,
- MediaPlayerEnqueue,
- MediaPlayerEntity,
- MediaPlayerEntityFeature,
- MediaPlayerState,
- MediaType as HAMediaType,
- RepeatMode,
- async_process_play_media_url,
-)
-from homeassistant.const import STATE_OFF
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import entity_registry as er
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.util.dt import utc_from_timestamp
-
-from . import MusicAssistantConfigEntry
-from .const import ATTR_ACTIVE_QUEUE, ATTR_MASS_PLAYER_TYPE, DOMAIN
-from .entity import MusicAssistantEntity
-
-if TYPE_CHECKING:
- from music_assistant_client import MusicAssistantClient
- from music_assistant_models.player import Player
- from music_assistant_models.player_queue import PlayerQueue
-
-SUPPORTED_FEATURES = (
- MediaPlayerEntityFeature.PAUSE
- | MediaPlayerEntityFeature.VOLUME_SET
- | MediaPlayerEntityFeature.STOP
- | MediaPlayerEntityFeature.PREVIOUS_TRACK
- | MediaPlayerEntityFeature.NEXT_TRACK
- | MediaPlayerEntityFeature.SHUFFLE_SET
- | MediaPlayerEntityFeature.REPEAT_SET
- | MediaPlayerEntityFeature.TURN_ON
- | MediaPlayerEntityFeature.TURN_OFF
- | MediaPlayerEntityFeature.PLAY
- | MediaPlayerEntityFeature.PLAY_MEDIA
- | MediaPlayerEntityFeature.VOLUME_STEP
- | MediaPlayerEntityFeature.CLEAR_PLAYLIST
- | MediaPlayerEntityFeature.BROWSE_MEDIA
- | MediaPlayerEntityFeature.MEDIA_ENQUEUE
- | MediaPlayerEntityFeature.MEDIA_ANNOUNCE
- | MediaPlayerEntityFeature.SEEK
-)
-
-QUEUE_OPTION_MAP = {
- # map from HA enqueue options to MA enqueue options
- # which are the same but just in case
- MediaPlayerEnqueue.ADD: QueueOption.ADD,
- MediaPlayerEnqueue.NEXT: QueueOption.NEXT,
- MediaPlayerEnqueue.PLAY: QueueOption.PLAY,
- MediaPlayerEnqueue.REPLACE: QueueOption.REPLACE,
-}
-
-ATTR_RADIO_MODE = "radio_mode"
-ATTR_MEDIA_ID = "media_id"
-ATTR_MEDIA_TYPE = "media_type"
-ATTR_ARTIST = "artist"
-ATTR_ALBUM = "album"
-ATTR_URL = "url"
-ATTR_USE_PRE_ANNOUNCE = "use_pre_announce"
-ATTR_ANNOUNCE_VOLUME = "announce_volume"
-ATTR_SOURCE_PLAYER = "source_player"
-ATTR_AUTO_PLAY = "auto_play"
-
-
-def catch_musicassistant_error[_R, **P](
- func: Callable[..., Awaitable[_R]],
-) -> Callable[..., Coroutine[Any, Any, _R | None]]:
- """Check and log commands to players."""
-
- @functools.wraps(func)
- async def wrapper(
- self: MusicAssistantPlayer, *args: P.args, **kwargs: P.kwargs
- ) -> _R | None:
- """Catch Music Assistant errors and convert to Home Assistant error."""
- try:
- return await func(self, *args, **kwargs)
- except MusicAssistantError as err:
- error_msg = str(err) or err.__class__.__name__
- raise HomeAssistantError(error_msg) from err
-
- return wrapper
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: MusicAssistantConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up Music Assistant MediaPlayer(s) from Config Entry."""
- mass = entry.runtime_data.mass
- added_ids = set()
-
- async def handle_player_added(event: MassEvent) -> None:
- """Handle Mass Player Added event."""
- if TYPE_CHECKING:
- assert event.object_id is not None
- if event.object_id in added_ids:
- return
- added_ids.add(event.object_id)
- async_add_entities([MusicAssistantPlayer(mass, event.object_id)])
-
- # register listener for new players
- entry.async_on_unload(mass.subscribe(handle_player_added, EventType.PLAYER_ADDED))
- mass_players = []
- # add all current players
- for player in mass.players:
- added_ids.add(player.player_id)
- mass_players.append(MusicAssistantPlayer(mass, player.player_id))
-
- async_add_entities(mass_players)
-
-
-class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
- """Representation of MediaPlayerEntity from Music Assistant Player."""
-
- _attr_name = None
- _attr_media_image_remotely_accessible = True
- _attr_media_content_type = HAMediaType.MUSIC
-
- def __init__(self, mass: MusicAssistantClient, player_id: str) -> None:
- """Initialize MediaPlayer entity."""
- super().__init__(mass, player_id)
- self._attr_icon = self.player.icon.replace("mdi-", "mdi:")
- self._attr_supported_features = SUPPORTED_FEATURES
- if PlayerFeature.SYNC in self.player.supported_features:
- self._attr_supported_features |= MediaPlayerEntityFeature.GROUPING
- self._attr_device_class = MediaPlayerDeviceClass.SPEAKER
- self._prev_time: float = 0
-
- async def async_added_to_hass(self) -> None:
- """Register callbacks."""
- await super().async_added_to_hass()
-
- # we subscribe to player queue time update but we only
- # accept a state change on big time jumps (e.g. seeking)
- async def queue_time_updated(event: MassEvent) -> None:
- if event.object_id != self.player.active_source:
- return
- if abs((self._prev_time or 0) - event.data) > 5:
- await self.async_on_update()
- self.async_write_ha_state()
- self._prev_time = event.data
-
- self.async_on_remove(
- self.mass.subscribe(
- queue_time_updated,
- EventType.QUEUE_TIME_UPDATED,
- )
- )
-
- @property
- def active_queue(self) -> PlayerQueue | None:
- """Return the active queue for this player (if any)."""
- if not self.player.active_source:
- return None
- return self.mass.player_queues.get(self.player.active_source)
-
- @property
- def extra_state_attributes(self) -> Mapping[str, Any]:
- """Return additional state attributes."""
- return {
- ATTR_MASS_PLAYER_TYPE: self.player.type.value,
- ATTR_ACTIVE_QUEUE: (
- self.active_queue.queue_id if self.active_queue else None
- ),
- }
-
- async def async_on_update(self) -> None:
- """Handle player updates."""
- if not self.available:
- return
- player = self.player
- active_queue = self.active_queue
- # update generic attributes
- if player.powered and active_queue is not None:
- self._attr_state = MediaPlayerState(active_queue.state.value)
- if player.powered and player.state is not None:
- self._attr_state = MediaPlayerState(player.state.value)
- else:
- self._attr_state = MediaPlayerState(STATE_OFF)
- group_members_entity_ids: list[str] = []
- if player.group_childs:
- # translate MA group_childs to HA group_members as entity id's
- entity_registry = er.async_get(self.hass)
- group_members_entity_ids = [
- entity_id
- for child_id in player.group_childs
- if (
- entity_id := entity_registry.async_get_entity_id(
- self.platform.domain, DOMAIN, child_id
- )
- )
- ]
- self._attr_group_members = group_members_entity_ids
- self._attr_volume_level = (
- player.volume_level / 100 if player.volume_level is not None else None
- )
- self._attr_is_volume_muted = player.volume_muted
- self._update_media_attributes(player, active_queue)
- self._update_media_image_url(player, active_queue)
-
- @catch_musicassistant_error
- async def async_media_play(self) -> None:
- """Send play command to device."""
- await self.mass.players.player_command_play(self.player_id)
-
- @catch_musicassistant_error
- async def async_media_pause(self) -> None:
- """Send pause command to device."""
- await self.mass.players.player_command_pause(self.player_id)
-
- @catch_musicassistant_error
- async def async_media_stop(self) -> None:
- """Send stop command to device."""
- await self.mass.players.player_command_stop(self.player_id)
-
- @catch_musicassistant_error
- async def async_media_next_track(self) -> None:
- """Send next track command to device."""
- await self.mass.players.player_command_next_track(self.player_id)
-
- @catch_musicassistant_error
- async def async_media_previous_track(self) -> None:
- """Send previous track command to device."""
- await self.mass.players.player_command_previous_track(self.player_id)
-
- @catch_musicassistant_error
- async def async_media_seek(self, position: float) -> None:
- """Send seek command."""
- position = int(position)
- await self.mass.players.player_command_seek(self.player_id, position)
-
- @catch_musicassistant_error
- async def async_mute_volume(self, mute: bool) -> None:
- """Mute the volume."""
- await self.mass.players.player_command_volume_mute(self.player_id, mute)
-
- @catch_musicassistant_error
- async def async_set_volume_level(self, volume: float) -> None:
- """Send new volume_level to device."""
- volume = int(volume * 100)
- await self.mass.players.player_command_volume_set(self.player_id, volume)
-
- @catch_musicassistant_error
- async def async_volume_up(self) -> None:
- """Send new volume_level to device."""
- await self.mass.players.player_command_volume_up(self.player_id)
-
- @catch_musicassistant_error
- async def async_volume_down(self) -> None:
- """Send new volume_level to device."""
- await self.mass.players.player_command_volume_down(self.player_id)
-
- @catch_musicassistant_error
- async def async_turn_on(self) -> None:
- """Turn on device."""
- await self.mass.players.player_command_power(self.player_id, True)
-
- @catch_musicassistant_error
- async def async_turn_off(self) -> None:
- """Turn off device."""
- await self.mass.players.player_command_power(self.player_id, False)
-
- @catch_musicassistant_error
- async def async_set_shuffle(self, shuffle: bool) -> None:
- """Set shuffle state."""
- if not self.active_queue:
- return
- await self.mass.player_queues.queue_command_shuffle(
- self.active_queue.queue_id, shuffle
- )
-
- @catch_musicassistant_error
- async def async_set_repeat(self, repeat: RepeatMode) -> None:
- """Set repeat state."""
- if not self.active_queue:
- return
- await self.mass.player_queues.queue_command_repeat(
- self.active_queue.queue_id, MassRepeatMode(repeat)
- )
-
- @catch_musicassistant_error
- async def async_clear_playlist(self) -> None:
- """Clear players playlist."""
- if TYPE_CHECKING:
- assert self.player.active_source is not None
- if queue := self.mass.player_queues.get(self.player.active_source):
- await self.mass.player_queues.queue_command_clear(queue.queue_id)
-
- @catch_musicassistant_error
- async def async_play_media(
- self,
- media_type: MediaType | str,
- media_id: str,
- enqueue: MediaPlayerEnqueue | None = None,
- announce: bool | None = None,
- **kwargs: Any,
- ) -> None:
- """Send the play_media command to the media player."""
- if media_source.is_media_source_id(media_id):
- # Handle media_source
- sourced_media = await media_source.async_resolve_media(
- self.hass, media_id, self.entity_id
- )
- media_id = sourced_media.url
- media_id = async_process_play_media_url(self.hass, media_id)
-
- if announce:
- await self._async_handle_play_announcement(
- media_id,
- use_pre_announce=kwargs[ATTR_MEDIA_EXTRA].get("use_pre_announce"),
- announce_volume=kwargs[ATTR_MEDIA_EXTRA].get("announce_volume"),
- )
- return
-
- # forward to our advanced play_media handler
- await self._async_handle_play_media(
- media_id=[media_id],
- enqueue=enqueue,
- media_type=media_type,
- radio_mode=kwargs[ATTR_MEDIA_EXTRA].get(ATTR_RADIO_MODE),
- )
-
- @catch_musicassistant_error
- async def async_join_players(self, group_members: list[str]) -> None:
- """Join `group_members` as a player group with the current player."""
- player_ids: list[str] = []
- for child_entity_id in group_members:
- # resolve HA entity_id to MA player_id
- if (hass_state := self.hass.states.get(child_entity_id)) is None:
- continue
- if (mass_player_id := hass_state.attributes.get("mass_player_id")) is None:
- continue
- player_ids.append(mass_player_id)
- await self.mass.players.player_command_sync_many(self.player_id, player_ids)
-
- @catch_musicassistant_error
- async def async_unjoin_player(self) -> None:
- """Remove this player from any group."""
- await self.mass.players.player_command_unsync(self.player_id)
-
- @catch_musicassistant_error
- async def _async_handle_play_media(
- self,
- media_id: list[str],
- enqueue: MediaPlayerEnqueue | QueueOption | None = None,
- radio_mode: bool | None = None,
- media_type: str | None = None,
- ) -> None:
- """Send the play_media command to the media player."""
- media_uris: list[str] = []
- item: MediaItemType | ItemMapping | None = None
- # work out (all) uri(s) to play
- for media_id_str in media_id:
- # URL or URI string
- if "://" in media_id_str:
- media_uris.append(media_id_str)
- continue
- # try content id as library id
- if media_type and media_id_str.isnumeric():
- with suppress(MediaNotFoundError):
- item = await self.mass.music.get_item(
- MediaType(media_type), media_id_str, "library"
- )
- if isinstance(item, MediaItemType | ItemMapping) and item.uri:
- media_uris.append(item.uri)
- continue
- # try local accessible filename
- elif await asyncio.to_thread(os.path.isfile, media_id_str):
- media_uris.append(media_id_str)
- continue
-
- if not media_uris:
- raise HomeAssistantError(
- f"Could not resolve {media_id} to playable media item"
- )
-
- # determine active queue to send the play request to
- if TYPE_CHECKING:
- assert self.player.active_source is not None
- if queue := self.mass.player_queues.get(self.player.active_source):
- queue_id = queue.queue_id
- else:
- queue_id = self.player_id
-
- await self.mass.player_queues.play_media(
- queue_id,
- media=media_uris,
- option=self._convert_queueoption_to_media_player_enqueue(enqueue),
- radio_mode=radio_mode if radio_mode else False,
- )
-
- @catch_musicassistant_error
- async def _async_handle_play_announcement(
- self,
- url: str,
- use_pre_announce: bool | None = None,
- announce_volume: int | None = None,
- ) -> None:
- """Send the play_announcement command to the media player."""
- await self.mass.players.play_announcement(
- self.player_id, url, use_pre_announce, announce_volume
- )
-
- async def async_browse_media(
- self,
- media_content_type: MediaType | str | None = None,
- media_content_id: str | None = None,
- ) -> BrowseMedia:
- """Implement the websocket media browsing helper."""
- return await media_source.async_browse_media(
- self.hass,
- media_content_id,
- content_filter=lambda item: item.media_content_type.startswith("audio/"),
- )
-
- def _update_media_image_url(
- self, player: Player, queue: PlayerQueue | None
- ) -> None:
- """Update image URL for the active queue item."""
- if queue is None or queue.current_item is None:
- self._attr_media_image_url = None
- return
- if image_url := self.mass.get_media_item_image_url(queue.current_item):
- self._attr_media_image_remotely_accessible = (
- self.mass.server_url not in image_url
- )
- self._attr_media_image_url = image_url
- return
- self._attr_media_image_url = None
-
- def _update_media_attributes(
- self, player: Player, queue: PlayerQueue | None
- ) -> None:
- """Update media attributes for the active queue item."""
- # pylint: disable=too-many-statements
- self._attr_media_artist = None
- self._attr_media_album_artist = None
- self._attr_media_album_name = None
- self._attr_media_title = None
- self._attr_media_content_id = None
- self._attr_media_duration = None
- self._attr_media_position = None
- self._attr_media_position_updated_at = None
-
- if queue is None and player.current_media:
- # player has some external source active
- self._attr_media_content_id = player.current_media.uri
- self._attr_app_id = player.active_source
- self._attr_media_title = player.current_media.title
- self._attr_media_artist = player.current_media.artist
- self._attr_media_album_name = player.current_media.album
- self._attr_media_duration = player.current_media.duration
- # shuffle and repeat are not (yet) supported for external sources
- self._attr_shuffle = None
- self._attr_repeat = None
- if TYPE_CHECKING:
- assert player.elapsed_time is not None
- self._attr_media_position = int(player.elapsed_time)
- self._attr_media_position_updated_at = (
- utc_from_timestamp(player.elapsed_time_last_updated)
- if player.elapsed_time_last_updated
- else None
- )
- if TYPE_CHECKING:
- assert player.elapsed_time is not None
- self._prev_time = player.elapsed_time
- return
-
- if queue is None:
- # player has no MA queue active
- self._attr_source = player.active_source
- self._attr_app_id = player.active_source
- return
-
- # player has an MA queue active (either its own queue or some group queue)
- self._attr_app_id = DOMAIN
- self._attr_shuffle = queue.shuffle_enabled
- self._attr_repeat = queue.repeat_mode.value
- if not (cur_item := queue.current_item):
- # queue is empty
- return
-
- self._attr_media_content_id = queue.current_item.uri
- self._attr_media_duration = queue.current_item.duration
- self._attr_media_position = int(queue.elapsed_time)
- self._attr_media_position_updated_at = utc_from_timestamp(
- queue.elapsed_time_last_updated
- )
- self._prev_time = queue.elapsed_time
-
- # handle stream title (radio station icy metadata)
- if (stream_details := cur_item.streamdetails) and stream_details.stream_title:
- self._attr_media_album_name = cur_item.name
- if " - " in stream_details.stream_title:
- stream_title_parts = stream_details.stream_title.split(" - ", 1)
- self._attr_media_title = stream_title_parts[1]
- self._attr_media_artist = stream_title_parts[0]
- else:
- self._attr_media_title = stream_details.stream_title
- return
-
- if not (media_item := cur_item.media_item):
- # queue is not playing a regular media item (edge case?!)
- self._attr_media_title = cur_item.name
- return
-
- # queue is playing regular media item
- self._attr_media_title = media_item.name
- # for tracks we can extract more info
- if media_item.media_type == MediaType.TRACK:
- if TYPE_CHECKING:
- assert isinstance(media_item, Track)
- self._attr_media_artist = media_item.artist_str
- if media_item.version:
- self._attr_media_title += f" ({media_item.version})"
- if media_item.album:
- self._attr_media_album_name = media_item.album.name
- self._attr_media_album_artist = getattr(
- media_item.album, "artist_str", None
- )
-
- def _convert_queueoption_to_media_player_enqueue(
- self, queue_option: MediaPlayerEnqueue | QueueOption | None
- ) -> QueueOption | None:
- """Convert a QueueOption to a MediaPlayerEnqueue."""
- if isinstance(queue_option, MediaPlayerEnqueue):
- queue_option = QUEUE_OPTION_MAP.get(queue_option)
- return queue_option
diff --git a/homeassistant/components/music_assistant/strings.json b/homeassistant/components/music_assistant/strings.json
deleted file mode 100644
index f15b0b1b306..00000000000
--- a/homeassistant/components/music_assistant/strings.json
+++ /dev/null
@@ -1,51 +0,0 @@
-{
- "config": {
- "step": {
- "init": {
- "data": {
- "url": "URL of the Music Assistant server"
- }
- },
- "manual": {
- "title": "Manually add Music Assistant Server",
- "description": "Enter the URL to your already running Music Assistant Server. If you do not have the Music Assistant Server running, you should install it first.",
- "data": {
- "url": "URL of the Music Assistant server"
- }
- },
- "discovery_confirm": {
- "description": "Do you want to add the Music Assistant Server `{url}` to Home Assistant?",
- "title": "Discovered Music Assistant Server"
- }
- },
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "invalid_server_version": "The Music Assistant server is not the correct version",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- },
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "already_in_progress": "Configuration flow is already in progress",
- "reconfiguration_successful": "Successfully reconfigured the Music Assistant integration.",
- "cannot_connect": "Failed to connect",
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
- }
- },
- "issues": {
- "invalid_server_version": {
- "title": "The Music Assistant server is not the correct version",
- "description": "Check if there are updates available for the Music Assistant Server and/or integration."
- }
- },
- "selector": {
- "enqueue": {
- "options": {
- "play": "Play",
- "next": "Play next",
- "add": "Add to queue",
- "replace": "Play now and clear queue",
- "replace_next": "Play next and clear queue"
- }
- }
- }
-}
diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py
index d5d2e3414d5..75eefaf6784 100644
--- a/homeassistant/components/mutesync/__init__.py
+++ b/homeassistant/components/mutesync/__init__.py
@@ -45,7 +45,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
update_coordinator.DataUpdateCoordinator(
hass,
logging.getLogger(__name__),
- config_entry=entry,
name=DOMAIN,
update_interval=UPDATE_INTERVAL_NOT_IN_MEETING,
update_method=update_data,
diff --git a/homeassistant/components/myuplink/api.py b/homeassistant/components/myuplink/api.py
index 32e0ea70193..89a5d0c19b0 100644
--- a/homeassistant/components/myuplink/api.py
+++ b/homeassistant/components/myuplink/api.py
@@ -26,6 +26,7 @@ class AsyncConfigEntryAuth(AbstractAuth):
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
- await self._oauth_session.async_ensure_token_valid()
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
return cast(str, self._oauth_session.token["access_token"])
diff --git a/homeassistant/components/myuplink/binary_sensor.py b/homeassistant/components/myuplink/binary_sensor.py
index 953859986d0..1478ed9c8b0 100644
--- a/homeassistant/components/myuplink/binary_sensor.py
+++ b/homeassistant/components/myuplink/binary_sensor.py
@@ -12,17 +12,10 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MyUplinkConfigEntry, MyUplinkDataCoordinator
-from .const import F_SERIES
from .entity import MyUplinkEntity, MyUplinkSystemEntity
-from .helpers import find_matching_platform, transform_model_series
+from .helpers import find_matching_platform
CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, BinarySensorEntityDescription]] = {
- F_SERIES: {
- "43161": BinarySensorEntityDescription(
- key="elect_add",
- translation_key="elect_add",
- ),
- },
"NIBEF": {
"43161": BinarySensorEntityDescription(
key="elect_add",
@@ -51,7 +44,6 @@ def get_description(device_point: DevicePoint) -> BinarySensorEntityDescription
2. Default to None
"""
prefix, _, _ = device_point.category.partition(" ")
- prefix = transform_model_series(prefix)
return CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(device_point.parameter_id)
diff --git a/homeassistant/components/myuplink/config_flow.py b/homeassistant/components/myuplink/config_flow.py
index 554347cfd19..fe31dcc6183 100644
--- a/homeassistant/components/myuplink/config_flow.py
+++ b/homeassistant/components/myuplink/config_flow.py
@@ -4,7 +4,7 @@ from collections.abc import Mapping
import logging
from typing import Any
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN, OAUTH2_SCOPES
@@ -17,6 +17,8 @@ class OAuth2FlowHandler(
DOMAIN = DOMAIN
+ config_entry_reauth: ConfigEntry | None = None
+
@property
def logger(self) -> logging.Logger:
"""Return logger."""
@@ -31,6 +33,9 @@ class OAuth2FlowHandler(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.config_entry_reauth = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -46,8 +51,9 @@ class OAuth2FlowHandler(
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
"""Create or update the config entry."""
- if self.source == SOURCE_REAUTH:
+ if self.config_entry_reauth:
return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=data
+ self.config_entry_reauth,
+ data=data,
)
return await super().async_oauth_create_entry(data)
diff --git a/homeassistant/components/myuplink/const.py b/homeassistant/components/myuplink/const.py
index 6fd354a21ec..3541a8078c3 100644
--- a/homeassistant/components/myuplink/const.py
+++ b/homeassistant/components/myuplink/const.py
@@ -6,5 +6,3 @@ API_ENDPOINT = "https://api.myuplink.com"
OAUTH2_AUTHORIZE = "https://api.myuplink.com/oauth/authorize"
OAUTH2_TOKEN = "https://api.myuplink.com/oauth/token"
OAUTH2_SCOPES = ["WRITESYSTEM", "READSYSTEM", "offline_access"]
-
-F_SERIES = "f-series"
diff --git a/homeassistant/components/myuplink/helpers.py b/homeassistant/components/myuplink/helpers.py
index de5486d8dea..ac3d2a2d7fa 100644
--- a/homeassistant/components/myuplink/helpers.py
+++ b/homeassistant/components/myuplink/helpers.py
@@ -6,8 +6,6 @@ from homeassistant.components.number import NumberEntityDescription
from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.const import Platform
-from .const import F_SERIES
-
def find_matching_platform(
device_point: DevicePoint,
@@ -38,93 +36,17 @@ def find_matching_platform(
return Platform.SENSOR
-WEEKDAYS = (
- "monday",
- "tuesday",
- "wednesday",
- "thursday",
- "friday",
- "saturday",
- "sunday",
-)
-
-PARAMETER_ID_TO_EXCLUDE_F730 = (
- "40940",
- "47007",
- "47015",
- "47020",
- "47021",
- "47022",
- "47023",
- "47024",
- "47025",
- "47026",
- "47027",
- "47028",
- "47032",
- "47050",
- "47051",
- "47206",
- "47209",
- "47271",
- "47272",
- "47273",
- "47274",
- "47375",
- "47376",
- "47538",
- "47539",
- "47635",
- "47669",
- "47703",
- "47737",
- "47771",
- "47772",
- "47805",
- "47806",
- "47839",
- "47840",
- "47907",
- "47941",
- "47975",
- "48009",
- "48072",
- "48442",
- "49909",
- "50113",
-)
-
-PARAMETER_ID_TO_INCLUDE_SMO20 = (
- "40940",
- "47011",
- "47015",
- "47028",
- "47032",
- "50004",
-)
-
-
def skip_entity(model: str, device_point: DevicePoint) -> bool:
"""Check if entity should be skipped for this device model."""
if model == "SMO 20":
- if (
- len(device_point.smart_home_categories) > 0
- or device_point.parameter_id in PARAMETER_ID_TO_INCLUDE_SMO20
+ if len(device_point.smart_home_categories) > 0 or device_point.parameter_id in (
+ "40940",
+ "47011",
+ "47015",
+ "47028",
+ "47032",
+ "50004",
):
return False
return True
- if model.lower().startswith("f"):
- # Entity names containing weekdays are used for advanced scheduling in the
- # heat pump and should not be exposed in the integration
- if any(d in device_point.parameter_name.lower() for d in WEEKDAYS):
- return True
- if device_point.parameter_id in PARAMETER_ID_TO_EXCLUDE_F730:
- return True
return False
-
-
-def transform_model_series(prefix: str) -> str:
- """Remap all F-series models."""
- if prefix.lower().startswith("f"):
- return F_SERIES
- return prefix
diff --git a/homeassistant/components/myuplink/number.py b/homeassistant/components/myuplink/number.py
index b05ab5d46c9..7c63a8ec8a2 100644
--- a/homeassistant/components/myuplink/number.py
+++ b/homeassistant/components/myuplink/number.py
@@ -10,9 +10,8 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MyUplinkConfigEntry, MyUplinkDataCoordinator
-from .const import F_SERIES
from .entity import MyUplinkEntity
-from .helpers import find_matching_platform, skip_entity, transform_model_series
+from .helpers import find_matching_platform, skip_entity
DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, NumberEntityDescription] = {
"DM": NumberEntityDescription(
@@ -23,13 +22,6 @@ DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, NumberEntityDescription] = {
}
CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, NumberEntityDescription]] = {
- F_SERIES: {
- "40940": NumberEntityDescription(
- key="degree_minutes",
- translation_key="degree_minutes",
- native_unit_of_measurement="DM",
- ),
- },
"NIBEF": {
"40940": NumberEntityDescription(
key="degree_minutes",
@@ -49,7 +41,6 @@ def get_description(device_point: DevicePoint) -> NumberEntityDescription | None
3. Default to None
"""
prefix, _, _ = device_point.category.partition(" ")
- prefix = transform_model_series(prefix)
description = CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(
device_point.parameter_id
)
diff --git a/homeassistant/components/myuplink/sensor.py b/homeassistant/components/myuplink/sensor.py
index ef827fc1fb1..e7c8054e304 100644
--- a/homeassistant/components/myuplink/sensor.py
+++ b/homeassistant/components/myuplink/sensor.py
@@ -25,9 +25,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import MyUplinkConfigEntry, MyUplinkDataCoordinator
-from .const import F_SERIES
from .entity import MyUplinkEntity
-from .helpers import find_matching_platform, skip_entity, transform_model_series
+from .helpers import find_matching_platform, skip_entity
DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, SensorEntityDescription] = {
"°C": SensorEntityDescription(
@@ -140,32 +139,6 @@ DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, SensorEntityDescription] = {
MARKER_FOR_UNKNOWN_VALUE = -32768
CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, SensorEntityDescription]] = {
- F_SERIES: {
- "43108": SensorEntityDescription(
- key="fan_mode",
- translation_key="fan_mode",
- ),
- "43427": SensorEntityDescription(
- key="status_compressor",
- translation_key="status_compressor",
- device_class=SensorDeviceClass.ENUM,
- ),
- "49993": SensorEntityDescription(
- key="elect_add",
- translation_key="elect_add",
- device_class=SensorDeviceClass.ENUM,
- ),
- "49994": SensorEntityDescription(
- key="priority",
- translation_key="priority",
- device_class=SensorDeviceClass.ENUM,
- ),
- "50095": SensorEntityDescription(
- key="status",
- translation_key="status",
- device_class=SensorDeviceClass.ENUM,
- ),
- },
"NIBEF": {
"43108": SensorEntityDescription(
key="fan_mode",
@@ -201,7 +174,6 @@ def get_description(device_point: DevicePoint) -> SensorEntityDescription | None
"""
description = None
prefix, _, _ = device_point.category.partition(" ")
- prefix = transform_model_series(prefix)
description = CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(
device_point.parameter_id
)
diff --git a/homeassistant/components/myuplink/strings.json b/homeassistant/components/myuplink/strings.json
index 9ec5c355d78..3351901b50b 100644
--- a/homeassistant/components/myuplink/strings.json
+++ b/homeassistant/components/myuplink/strings.json
@@ -34,11 +34,6 @@
"alarm": {
"name": "Alarm"
}
- },
- "sensor": {
- "status": {
- "name": "Status"
- }
}
}
}
diff --git a/homeassistant/components/myuplink/switch.py b/homeassistant/components/myuplink/switch.py
index 75ba6bd7819..1589701fcbc 100644
--- a/homeassistant/components/myuplink/switch.py
+++ b/homeassistant/components/myuplink/switch.py
@@ -12,21 +12,10 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MyUplinkConfigEntry, MyUplinkDataCoordinator
-from .const import F_SERIES
from .entity import MyUplinkEntity
-from .helpers import find_matching_platform, skip_entity, transform_model_series
+from .helpers import find_matching_platform, skip_entity
CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, SwitchEntityDescription]] = {
- F_SERIES: {
- "50004": SwitchEntityDescription(
- key="temporary_lux",
- translation_key="temporary_lux",
- ),
- "50005": SwitchEntityDescription(
- key="boost_ventilation",
- translation_key="boost_ventilation",
- ),
- },
"NIBEF": {
"50004": SwitchEntityDescription(
key="temporary_lux",
@@ -48,7 +37,6 @@ def get_description(device_point: DevicePoint) -> SwitchEntityDescription | None
2. Default to None
"""
prefix, _, _ = device_point.category.partition(" ")
- prefix = transform_model_series(prefix)
return CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(device_point.parameter_id)
diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py
index 494ce9fdac0..1b9a654e55e 100644
--- a/homeassistant/components/nam/config_flow.py
+++ b/homeassistant/components/nam/config_flow.py
@@ -220,11 +220,18 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle a reconfiguration flow initialized by the user."""
+ self.host = self._get_reconfigure_entry().data[CONF_HOST]
+
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfiguration flow initialized by the user."""
errors = {}
reconfigure_entry = self._get_reconfigure_entry()
- self.host = reconfigure_entry.data[CONF_HOST]
if user_input is not None:
try:
@@ -240,7 +247,7 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST, default=self.host): str,
diff --git a/homeassistant/components/nam/strings.json b/homeassistant/components/nam/strings.json
index 2caa4d8bd97..c4921ec52f9 100644
--- a/homeassistant/components/nam/strings.json
+++ b/homeassistant/components/nam/strings.json
@@ -28,7 +28,7 @@
"confirm_discovery": {
"description": "Do you want to set up Nettigo Air Monitor at {host}?"
},
- "reconfigure": {
+ "reconfigure_confirm": {
"description": "Update configuration for {device_name}.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py
index 27ef9a887fe..cc34e30eb59 100644
--- a/homeassistant/components/nanoleaf/config_flow.py
+++ b/homeassistant/components/nanoleaf/config_flow.py
@@ -11,7 +11,7 @@ from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable
import voluptuous as vol
from homeassistant.components import ssdp, zeroconf
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.json import save_json
@@ -34,6 +34,8 @@ USER_SCHEMA: Final = vol.Schema(
class NanoleafConfigFlow(ConfigFlow, domain=DOMAIN):
"""Nanoleaf config flow."""
+ reauth_entry: ConfigEntry | None = None
+
nanoleaf: Nanoleaf
# For discovery integration import
@@ -79,10 +81,14 @@ class NanoleafConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle Nanoleaf reauth flow if token is invalid."""
+ self.reauth_entry = cast(
+ ConfigEntry,
+ self.hass.config_entries.async_get_entry(self.context["entry_id"]),
+ )
self.nanoleaf = Nanoleaf(
async_get_clientsession(self.hass), entry_data[CONF_HOST]
)
- self.context["title_placeholders"] = {"name": self._get_reauth_entry().title}
+ self.context["title_placeholders"] = {"name": self.reauth_entry.title}
return await self.async_step_link()
async def async_step_zeroconf(
@@ -171,11 +177,16 @@ class NanoleafConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unknown error authorizing Nanoleaf")
return self.async_show_form(step_id="link", errors={"base": "unknown"})
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
- data_updates={CONF_TOKEN: self.nanoleaf.auth_token},
+ if self.reauth_entry is not None:
+ self.hass.config_entries.async_update_entry(
+ self.reauth_entry,
+ data={
+ **self.reauth_entry.data,
+ CONF_TOKEN: self.nanoleaf.auth_token,
+ },
)
+ await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return await self.async_setup_finish()
diff --git a/homeassistant/components/nasweb/__init__.py b/homeassistant/components/nasweb/__init__.py
deleted file mode 100644
index 1992cc41c75..00000000000
--- a/homeassistant/components/nasweb/__init__.py
+++ /dev/null
@@ -1,125 +0,0 @@
-"""The NASweb integration."""
-
-from __future__ import annotations
-
-import logging
-
-from webio_api import WebioAPI
-from webio_api.api_client import AuthError
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
-from homeassistant.helpers import device_registry as dr
-from homeassistant.helpers.network import NoURLAvailableError
-from homeassistant.util.hass_dict import HassKey
-
-from .const import DOMAIN, MANUFACTURER, SUPPORT_EMAIL
-from .coordinator import NASwebCoordinator
-from .nasweb_data import NASwebData
-
-PLATFORMS: list[Platform] = [Platform.SWITCH]
-
-NASWEB_CONFIG_URL = "https://{host}/page"
-
-_LOGGER = logging.getLogger(__name__)
-type NASwebConfigEntry = ConfigEntry[NASwebCoordinator]
-DATA_NASWEB: HassKey[NASwebData] = HassKey(DOMAIN)
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: NASwebConfigEntry) -> bool:
- """Set up NASweb from a config entry."""
-
- if DATA_NASWEB not in hass.data:
- data = NASwebData()
- data.initialize(hass)
- hass.data[DATA_NASWEB] = data
- nasweb_data = hass.data[DATA_NASWEB]
-
- webio_api = WebioAPI(
- entry.data[CONF_HOST], entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]
- )
- try:
- if not await webio_api.check_connection():
- raise ConfigEntryNotReady(
- f"[{entry.data[CONF_HOST]}] Check connection failed"
- )
- if not await webio_api.refresh_device_info():
- _LOGGER.error("[%s] Refresh device info failed", entry.data[CONF_HOST])
- raise ConfigEntryError(
- translation_key="config_entry_error_internal_error",
- translation_placeholders={"support_email": SUPPORT_EMAIL},
- )
- webio_serial = webio_api.get_serial_number()
- if webio_serial is None:
- _LOGGER.error("[%s] Serial number not available", entry.data[CONF_HOST])
- raise ConfigEntryError(
- translation_key="config_entry_error_internal_error",
- translation_placeholders={"support_email": SUPPORT_EMAIL},
- )
- if entry.unique_id != webio_serial:
- _LOGGER.error(
- "[%s] Serial number doesn't match config entry", entry.data[CONF_HOST]
- )
- raise ConfigEntryError(translation_key="config_entry_error_serial_mismatch")
-
- coordinator = NASwebCoordinator(
- hass, webio_api, name=f"NASweb[{webio_api.get_name()}]"
- )
- entry.runtime_data = coordinator
- nasweb_data.notify_coordinator.add_coordinator(webio_serial, entry.runtime_data)
-
- webhook_url = nasweb_data.get_webhook_url(hass)
- if not await webio_api.status_subscription(webhook_url, True):
- _LOGGER.error("Failed to subscribe for status updates from webio")
- raise ConfigEntryError(
- translation_key="config_entry_error_internal_error",
- translation_placeholders={"support_email": SUPPORT_EMAIL},
- )
- if not await nasweb_data.notify_coordinator.check_connection(webio_serial):
- _LOGGER.error("Did not receive status from device")
- raise ConfigEntryError(
- translation_key="config_entry_error_no_status_update",
- translation_placeholders={"support_email": SUPPORT_EMAIL},
- )
- except TimeoutError as error:
- raise ConfigEntryNotReady(
- f"[{entry.data[CONF_HOST]}] Check connection reached timeout"
- ) from error
- except AuthError as error:
- raise ConfigEntryError(
- translation_key="config_entry_error_invalid_authentication"
- ) from error
- except NoURLAvailableError as error:
- raise ConfigEntryError(
- translation_key="config_entry_error_missing_internal_url"
- ) from error
-
- device_registry = dr.async_get(hass)
- device_registry.async_get_or_create(
- config_entry_id=entry.entry_id,
- identifiers={(DOMAIN, webio_serial)},
- manufacturer=MANUFACTURER,
- name=webio_api.get_name(),
- configuration_url=NASWEB_CONFIG_URL.format(host=entry.data[CONF_HOST]),
- )
- await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
- return True
-
-
-async def async_unload_entry(hass: HomeAssistant, entry: NASwebConfigEntry) -> bool:
- """Unload a config entry."""
- if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
- nasweb_data = hass.data[DATA_NASWEB]
- coordinator = entry.runtime_data
- serial = entry.unique_id
- if serial is not None:
- nasweb_data.notify_coordinator.remove_coordinator(serial)
- if nasweb_data.can_be_deinitialized():
- nasweb_data.deinitialize(hass)
- hass.data.pop(DATA_NASWEB)
- webhook_url = nasweb_data.get_webhook_url(hass)
- await coordinator.webio_api.status_subscription(webhook_url, False)
-
- return unload_ok
diff --git a/homeassistant/components/nasweb/config_flow.py b/homeassistant/components/nasweb/config_flow.py
deleted file mode 100644
index 3a9ad3f7d49..00000000000
--- a/homeassistant/components/nasweb/config_flow.py
+++ /dev/null
@@ -1,137 +0,0 @@
-"""Config flow for NASweb integration."""
-
-from __future__ import annotations
-
-import logging
-from typing import Any
-
-import voluptuous as vol
-from webio_api import WebioAPI
-from webio_api.api_client import AuthError
-
-from homeassistant import config_entries
-from homeassistant.config_entries import ConfigFlowResult
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME
-from homeassistant.core import HomeAssistant
-from homeassistant.data_entry_flow import AbortFlow
-from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers.network import NoURLAvailableError
-
-from .const import DOMAIN
-from .coordinator import NASwebCoordinator
-from .nasweb_data import NASwebData
-
-NASWEB_SCHEMA_IMG_URL = (
- "https://home-assistant.io/images/integrations/nasweb/nasweb_scheme.png"
-)
-
-_LOGGER = logging.getLogger(__name__)
-
-STEP_USER_DATA_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_HOST): str,
- vol.Required(CONF_USERNAME): str,
- vol.Required(CONF_PASSWORD): str,
- }
-)
-
-
-async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
- """Validate user-provided data."""
- webio_api = WebioAPI(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD])
- if not await webio_api.check_connection():
- raise CannotConnect
- try:
- await webio_api.refresh_device_info()
- except AuthError as e:
- raise InvalidAuth from e
-
- nasweb_data = NASwebData()
- nasweb_data.initialize(hass)
- try:
- webio_serial = webio_api.get_serial_number()
- if webio_serial is None:
- raise MissingNASwebData("Device serial number is not available")
-
- coordinator = NASwebCoordinator(hass, webio_api)
- webhook_url = nasweb_data.get_webhook_url(hass)
- nasweb_data.notify_coordinator.add_coordinator(webio_serial, coordinator)
- subscription = await webio_api.status_subscription(webhook_url, True)
- if not subscription:
- nasweb_data.notify_coordinator.remove_coordinator(webio_serial)
- raise MissingNASwebData(
- "Failed to subscribe for status updates from device"
- )
-
- result = await nasweb_data.notify_coordinator.check_connection(webio_serial)
- nasweb_data.notify_coordinator.remove_coordinator(webio_serial)
- if not result:
- if subscription:
- await webio_api.status_subscription(webhook_url, False)
- raise MissingNASwebStatus("Did not receive status from device")
-
- name = webio_api.get_name()
- finally:
- nasweb_data.deinitialize(hass)
- return {"title": name, CONF_UNIQUE_ID: webio_serial}
-
-
-class NASwebConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
- """Handle a config flow for NASweb."""
-
- 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:
- try:
- info = await validate_input(self.hass, user_input)
- await self.async_set_unique_id(info[CONF_UNIQUE_ID])
- self._abort_if_unique_id_configured()
- except CannotConnect:
- errors["base"] = "cannot_connect"
- except InvalidAuth:
- errors["base"] = "invalid_auth"
- except NoURLAvailableError:
- errors["base"] = "missing_internal_url"
- except MissingNASwebData:
- errors["base"] = "missing_nasweb_data"
- except MissingNASwebStatus:
- errors["base"] = "missing_status"
- except AbortFlow:
- raise
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception("Unexpected exception")
- errors["base"] = "unknown"
- else:
- return self.async_create_entry(title=info["title"], data=user_input)
-
- return self.async_show_form(
- step_id="user",
- data_schema=self.add_suggested_values_to_schema(
- STEP_USER_DATA_SCHEMA, user_input
- ),
- errors=errors,
- description_placeholders={
- "nasweb_schema_img": '
',
- },
- )
-
-
-class CannotConnect(HomeAssistantError):
- """Error to indicate we cannot connect."""
-
-
-class InvalidAuth(HomeAssistantError):
- """Error to indicate there is invalid auth."""
-
-
-class MissingNASwebData(HomeAssistantError):
- """Error to indicate missing information from NASweb."""
-
-
-class MissingNASwebStatus(HomeAssistantError):
- """Error to indicate there was no status received from NASweb."""
diff --git a/homeassistant/components/nasweb/const.py b/homeassistant/components/nasweb/const.py
deleted file mode 100644
index ec750c90c8c..00000000000
--- a/homeassistant/components/nasweb/const.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""Constants for the NASweb integration."""
-
-DOMAIN = "nasweb"
-MANUFACTURER = "chomtech.pl"
-STATUS_UPDATE_MAX_TIME_INTERVAL = 60
-SUPPORT_EMAIL = "support@chomtech.eu"
-WEBHOOK_URL = "{internal_url}/api/webhook/{webhook_id}"
diff --git a/homeassistant/components/nasweb/coordinator.py b/homeassistant/components/nasweb/coordinator.py
deleted file mode 100644
index 90dca0f3022..00000000000
--- a/homeassistant/components/nasweb/coordinator.py
+++ /dev/null
@@ -1,191 +0,0 @@
-"""Message routing coordinators for handling NASweb push notifications."""
-
-from __future__ import annotations
-
-import asyncio
-from collections.abc import Callable
-from datetime import datetime, timedelta
-import logging
-import time
-from typing import Any
-
-from aiohttp.web import Request, Response
-from webio_api import WebioAPI
-from webio_api.const import KEY_DEVICE_SERIAL, KEY_OUTPUTS, KEY_TYPE, TYPE_STATUS_UPDATE
-
-from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
-from homeassistant.helpers import event
-from homeassistant.helpers.update_coordinator import BaseDataUpdateCoordinatorProtocol
-
-from .const import STATUS_UPDATE_MAX_TIME_INTERVAL
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class NotificationCoordinator:
- """Coordinator redirecting push notifications for this integration to appropriate NASwebCoordinator."""
-
- def __init__(self) -> None:
- """Initialize coordinator."""
- self._coordinators: dict[str, NASwebCoordinator] = {}
-
- def add_coordinator(self, serial: str, coordinator: NASwebCoordinator) -> None:
- """Add NASwebCoordinator to possible notification targets."""
- self._coordinators[serial] = coordinator
- _LOGGER.debug("Added NASwebCoordinator for NASweb[%s]", serial)
-
- def remove_coordinator(self, serial: str) -> None:
- """Remove NASwebCoordinator from possible notification targets."""
- self._coordinators.pop(serial)
- _LOGGER.debug("Removed NASwebCoordinator for NASweb[%s]", serial)
-
- def has_coordinators(self) -> bool:
- """Check if there is any registered coordinator for push notifications."""
- return len(self._coordinators) > 0
-
- async def check_connection(self, serial: str) -> bool:
- """Wait for first status update to confirm connection with NASweb."""
- nasweb_coordinator = self._coordinators.get(serial)
- if nasweb_coordinator is None:
- _LOGGER.error("Cannot check connection. No device match serial number")
- return False
- for counter in range(10):
- _LOGGER.debug("Checking connection with: %s (%s)", serial, counter)
- if nasweb_coordinator.is_connection_confirmed():
- return True
- await asyncio.sleep(1)
- return False
-
- async def handle_webhook_request(
- self, hass: HomeAssistant, webhook_id: str, request: Request
- ) -> Response | None:
- """Handle webhook request from Push API."""
- if not self.has_coordinators():
- return None
- notification = await request.json()
- serial = notification.get(KEY_DEVICE_SERIAL, None)
- _LOGGER.debug("Received push: %s", notification)
- if serial is None:
- _LOGGER.warning("Received notification without nasweb identifier")
- return None
- nasweb_coordinator = self._coordinators.get(serial)
- if nasweb_coordinator is None:
- _LOGGER.warning("Received notification for not registered nasweb")
- return None
- await nasweb_coordinator.handle_push_notification(notification)
- return Response(body='{"response": "ok"}', content_type="application/json")
-
-
-class NASwebCoordinator(BaseDataUpdateCoordinatorProtocol):
- """Coordinator managing status of single NASweb device.
-
- Since status updates are managed through push notifications, this class schedules
- periodic checks to ensure that devices are marked unavailable if updates
- haven't been received for a prolonged period.
- """
-
- def __init__(
- self, hass: HomeAssistant, webio_api: WebioAPI, name: str = "NASweb[default]"
- ) -> None:
- """Initialize NASweb coordinator."""
- self._hass = hass
- self.name = name
- self.webio_api = webio_api
- self._last_update: float | None = None
- job_name = f"NASwebCoordinator[{name}]"
- self._job = HassJob(self._handle_max_update_interval, job_name)
- self._unsub_last_update_check: CALLBACK_TYPE | None = None
- self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {}
- data: dict[str, Any] = {}
- data[KEY_OUTPUTS] = self.webio_api.outputs
- self.async_set_updated_data(data)
-
- def is_connection_confirmed(self) -> bool:
- """Check whether coordinator received status update from NASweb."""
- return self._last_update is not None
-
- @callback
- def async_add_listener(
- self, update_callback: CALLBACK_TYPE, context: Any = None
- ) -> Callable[[], None]:
- """Listen for data updates."""
- schedule_update_check = not self._listeners
-
- @callback
- def remove_listener() -> None:
- """Remove update listener."""
- self._listeners.pop(remove_listener)
- if not self._listeners:
- self._async_unsub_last_update_check()
-
- self._listeners[remove_listener] = (update_callback, context)
- # This is the first listener, set up interval.
- if schedule_update_check:
- self._schedule_last_update_check()
- return remove_listener
-
- @callback
- def async_set_updated_data(self, data: dict[str, Any]) -> None:
- """Update data and notify listeners."""
- self.data = data
- self.last_update = self._hass.loop.time()
- _LOGGER.debug("Updated %s data", self.name)
- if self._listeners:
- self._schedule_last_update_check()
- self.async_update_listeners()
-
- @callback
- def async_update_listeners(self) -> None:
- """Update all registered listeners."""
- for update_callback, _ in list(self._listeners.values()):
- update_callback()
-
- async def _handle_max_update_interval(self, now: datetime) -> None:
- """Handle max update interval occurrence.
-
- This method is called when `STATUS_UPDATE_MAX_TIME_INTERVAL` has passed without
- receiving a status update. It only needs to trigger state update of entities
- which then change their state accordingly.
- """
- self._unsub_last_update_check = None
- if self._listeners:
- self.async_update_listeners()
-
- def _schedule_last_update_check(self) -> None:
- """Schedule a task to trigger entities state update after `STATUS_UPDATE_MAX_TIME_INTERVAL`.
-
- This method schedules a task (`_handle_max_update_interval`) to be executed after
- `STATUS_UPDATE_MAX_TIME_INTERVAL` seconds without status update, which enables entities
- to change their state to unavailable. After each status update this task is rescheduled.
- """
- self._async_unsub_last_update_check()
- now = self._hass.loop.time()
- next_check = (
- now + timedelta(seconds=STATUS_UPDATE_MAX_TIME_INTERVAL).total_seconds()
- )
- self._unsub_last_update_check = event.async_call_at(
- self._hass,
- self._job,
- next_check,
- )
-
- def _async_unsub_last_update_check(self) -> None:
- """Cancel any scheduled update check call."""
- if self._unsub_last_update_check:
- self._unsub_last_update_check()
- self._unsub_last_update_check = None
-
- async def handle_push_notification(self, notification: dict) -> None:
- """Handle incoming push notification from NASweb."""
- msg_type = notification.get(KEY_TYPE)
- _LOGGER.debug("Received push notification: %s", msg_type)
-
- if msg_type == TYPE_STATUS_UPDATE:
- await self.process_status_update(notification)
- self._last_update = time.time()
-
- async def process_status_update(self, new_status: dict) -> None:
- """Process status update from NASweb."""
- self.webio_api.update_device_status(new_status)
- new_data = {KEY_OUTPUTS: self.webio_api.outputs}
- self.async_set_updated_data(new_data)
diff --git a/homeassistant/components/nasweb/manifest.json b/homeassistant/components/nasweb/manifest.json
deleted file mode 100644
index e7e06419dad..00000000000
--- a/homeassistant/components/nasweb/manifest.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "domain": "nasweb",
- "name": "NASweb",
- "codeowners": ["@nasWebio"],
- "config_flow": true,
- "dependencies": ["webhook"],
- "documentation": "https://www.home-assistant.io/integrations/nasweb",
- "homekit": {},
- "integration_type": "hub",
- "iot_class": "local_push",
- "requirements": ["webio-api==0.1.8"],
- "ssdp": [],
- "zeroconf": []
-}
diff --git a/homeassistant/components/nasweb/nasweb_data.py b/homeassistant/components/nasweb/nasweb_data.py
deleted file mode 100644
index 4f6a37e6cc7..00000000000
--- a/homeassistant/components/nasweb/nasweb_data.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""Dataclass storing integration data in hass.data[DOMAIN]."""
-
-from dataclasses import dataclass, field
-import logging
-
-from aiohttp.hdrs import METH_POST
-
-from homeassistant.components.webhook import (
- async_generate_id,
- async_register as webhook_register,
- async_unregister as webhook_unregister,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.network import get_url
-
-from .const import DOMAIN, WEBHOOK_URL
-from .coordinator import NotificationCoordinator
-
-_LOGGER = logging.getLogger(__name__)
-
-
-@dataclass
-class NASwebData:
- """Class storing integration data."""
-
- notify_coordinator: NotificationCoordinator = field(
- default_factory=NotificationCoordinator
- )
- webhook_id = ""
-
- def is_initialized(self) -> bool:
- """Return True if instance was initialized and is ready for use."""
- return bool(self.webhook_id)
-
- def can_be_deinitialized(self) -> bool:
- """Return whether this instance can be deinitialized."""
- return not self.notify_coordinator.has_coordinators()
-
- def initialize(self, hass: HomeAssistant) -> None:
- """Initialize NASwebData instance."""
- if self.is_initialized():
- return
- new_webhook_id = async_generate_id()
- webhook_register(
- hass,
- DOMAIN,
- "NASweb",
- new_webhook_id,
- self.notify_coordinator.handle_webhook_request,
- allowed_methods=[METH_POST],
- )
- self.webhook_id = new_webhook_id
- _LOGGER.debug("Registered webhook: %s", self.webhook_id)
-
- def deinitialize(self, hass: HomeAssistant) -> None:
- """Deinitialize NASwebData instance."""
- if not self.is_initialized():
- return
- webhook_unregister(hass, self.webhook_id)
-
- def get_webhook_url(self, hass: HomeAssistant) -> str:
- """Return webhook url for Push API."""
- hass_url = get_url(hass, allow_external=False)
- return WEBHOOK_URL.format(internal_url=hass_url, webhook_id=self.webhook_id)
diff --git a/homeassistant/components/nasweb/strings.json b/homeassistant/components/nasweb/strings.json
deleted file mode 100644
index b8af8cd54db..00000000000
--- a/homeassistant/components/nasweb/strings.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "config": {
- "step": {
- "user": {
- "title": "Add NASweb device",
- "description": "{nasweb_schema_img}NASweb combines the functions of a control panel and the ability to manage building automation. The device monitors the flow of information from sensors and programmable switches and stores settings, definitions and configured actions.",
- "data": {
- "host": "[%key:common::config_flow::data::host%]",
- "username": "[%key:common::config_flow::data::username%]",
- "password": "[%key:common::config_flow::data::password%]"
- }
- }
- },
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
- "missing_internal_url": "Make sure Home Assistant has valid internal url",
- "missing_nasweb_data": "Something isn't right with device internal configuration. Try restarting the device and HomeAssistant.",
- "missing_status": "Did not received any status updates within the expected time window. Make sure the Home Assistant Internal URL is reachable from the NASweb device.",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- },
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
- }
- },
- "exceptions": {
- "config_entry_error_invalid_authentication": {
- "message": "Invalid username/password. Most likely user changed password or was removed. Delete this entry and create new one with correct username/password."
- },
- "config_entry_error_internal_error": {
- "message": "Something isn't right with device internal configuration. Try restarting the device and HomeAssistant. If the issue persists contact support at {support_email}"
- },
- "config_entry_error_no_status_update": {
- "message": "Did not received any status updates within the expected time window. Make sure the Home Assistant Internal URL is reachable from the NASweb device. If the issue persists contact support at {support_email}"
- },
- "config_entry_error_missing_internal_url": {
- "message": "[%key:component::nasweb::config::error::missing_internal_url%]"
- },
- "serial_mismatch": {
- "message": "Connected to different NASweb device (serial number mismatch)."
- }
- },
- "entity": {
- "switch": {
- "switch_output": {
- "name": "Relay Switch {index}"
- }
- }
- }
-}
diff --git a/homeassistant/components/nasweb/switch.py b/homeassistant/components/nasweb/switch.py
deleted file mode 100644
index 00e5a21da18..00000000000
--- a/homeassistant/components/nasweb/switch.py
+++ /dev/null
@@ -1,133 +0,0 @@
-"""Platform for NASweb output."""
-
-from __future__ import annotations
-
-import logging
-import time
-from typing import Any
-
-from webio_api import Output as NASwebOutput
-
-from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH, SwitchEntity
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-import homeassistant.helpers.entity_registry as er
-from homeassistant.helpers.typing import DiscoveryInfoType
-from homeassistant.helpers.update_coordinator import (
- BaseCoordinatorEntity,
- BaseDataUpdateCoordinatorProtocol,
-)
-
-from . import NASwebConfigEntry
-from .const import DOMAIN, STATUS_UPDATE_MAX_TIME_INTERVAL
-from .coordinator import NASwebCoordinator
-
-OUTPUT_TRANSLATION_KEY = "switch_output"
-
-_LOGGER = logging.getLogger(__name__)
-
-
-def _get_output(coordinator: NASwebCoordinator, index: int) -> NASwebOutput | None:
- for out in coordinator.webio_api.outputs:
- if out.index == index:
- return out
- return None
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config: NASwebConfigEntry,
- async_add_entities: AddEntitiesCallback,
- discovery_info: DiscoveryInfoType | None = None,
-) -> None:
- """Set up switch platform."""
- coordinator = config.runtime_data
- current_outputs: set[int] = set()
-
- @callback
- def _check_entities() -> None:
- received_outputs = {out.index for out in coordinator.webio_api.outputs}
- added = {i for i in received_outputs if i not in current_outputs}
- removed = {i for i in current_outputs if i not in received_outputs}
- entities_to_add: list[RelaySwitch] = []
- for index in added:
- webio_output = _get_output(coordinator, index)
- if not isinstance(webio_output, NASwebOutput):
- _LOGGER.error("Cannot create RelaySwitch entity without NASwebOutput")
- continue
- new_output = RelaySwitch(coordinator, webio_output)
- entities_to_add.append(new_output)
- current_outputs.add(index)
- async_add_entities(entities_to_add)
- entity_registry = er.async_get(hass)
- for index in removed:
- unique_id = f"{DOMAIN}.{config.unique_id}.relay_switch.{index}"
- if entity_id := entity_registry.async_get_entity_id(
- DOMAIN_SWITCH, DOMAIN, unique_id
- ):
- entity_registry.async_remove(entity_id)
- current_outputs.remove(index)
- else:
- _LOGGER.warning("Failed to remove old output: no entity_id")
-
- coordinator.async_add_listener(_check_entities)
- _check_entities()
-
-
-class RelaySwitch(SwitchEntity, BaseCoordinatorEntity):
- """Entity representing NASweb Output."""
-
- def __init__(
- self,
- coordinator: BaseDataUpdateCoordinatorProtocol,
- nasweb_output: NASwebOutput,
- ) -> None:
- """Initialize RelaySwitch."""
- super().__init__(coordinator)
- self._output = nasweb_output
- self._attr_icon = "mdi:export"
- self._attr_has_entity_name = True
- self._attr_translation_key = OUTPUT_TRANSLATION_KEY
- self._attr_translation_placeholders = {"index": f"{nasweb_output.index:2d}"}
- self._attr_unique_id = (
- f"{DOMAIN}.{self._output.webio_serial}.relay_switch.{self._output.index}"
- )
- self._attr_device_info = DeviceInfo(
- identifiers={(DOMAIN, self._output.webio_serial)},
- )
-
- async def async_added_to_hass(self) -> None:
- """When entity is added to hass."""
- await super().async_added_to_hass()
- self._handle_coordinator_update()
-
- @callback
- def _handle_coordinator_update(self) -> None:
- """Handle updated data from the coordinator."""
- self._attr_is_on = self._output.state
- if (
- self.coordinator.last_update is None
- or time.time() - self._output.last_update >= STATUS_UPDATE_MAX_TIME_INTERVAL
- ):
- self._attr_available = False
- else:
- self._attr_available = (
- self._output.available if self._output.available is not None else False
- )
- self.async_write_ha_state()
-
- async def async_update(self) -> None:
- """Update the entity.
-
- Only used by the generic entity update service.
- Scheduling updates is not necessary, the coordinator takes care of updates via push notifications.
- """
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn On RelaySwitch."""
- await self._output.turn_on()
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn Off RelaySwitch."""
- await self._output.turn_off()
diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py
index 64b764c6872..e44c06ecc85 100644
--- a/homeassistant/components/ness_alarm/alarm_control_panel.py
+++ b/homeassistant/components/ness_alarm/alarm_control_panel.py
@@ -9,9 +9,18 @@ from nessclient import ArmingMode, ArmingState, Client
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -22,12 +31,12 @@ from . import DATA_NESS, SIGNAL_ARMING_STATE_CHANGED
_LOGGER = logging.getLogger(__name__)
ARMING_MODE_TO_STATE = {
- ArmingMode.ARMED_AWAY: AlarmControlPanelState.ARMED_AWAY,
- ArmingMode.ARMED_HOME: AlarmControlPanelState.ARMED_HOME,
- ArmingMode.ARMED_DAY: AlarmControlPanelState.ARMED_AWAY, # no applicable state, fallback to away
- ArmingMode.ARMED_NIGHT: AlarmControlPanelState.ARMED_NIGHT,
- ArmingMode.ARMED_VACATION: AlarmControlPanelState.ARMED_VACATION,
- ArmingMode.ARMED_HIGHEST: AlarmControlPanelState.ARMED_AWAY, # no applicable state, fallback to away
+ ArmingMode.ARMED_AWAY: STATE_ALARM_ARMED_AWAY,
+ ArmingMode.ARMED_HOME: STATE_ALARM_ARMED_HOME,
+ ArmingMode.ARMED_DAY: STATE_ALARM_ARMED_AWAY, # no applicable state, fallback to away
+ ArmingMode.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT,
+ ArmingMode.ARMED_VACATION: STATE_ALARM_ARMED_VACATION,
+ ArmingMode.ARMED_HIGHEST: STATE_ALARM_ARMED_AWAY, # no applicable state, fallback to away
}
@@ -92,19 +101,19 @@ class NessAlarmPanel(AlarmControlPanelEntity):
"""Handle arming state update."""
if arming_state == ArmingState.UNKNOWN:
- self._attr_alarm_state = None
+ self._attr_state = None
elif arming_state == ArmingState.DISARMED:
- self._attr_alarm_state = AlarmControlPanelState.DISARMED
+ self._attr_state = STATE_ALARM_DISARMED
elif arming_state in (ArmingState.ARMING, ArmingState.EXIT_DELAY):
- self._attr_alarm_state = AlarmControlPanelState.ARMING
+ self._attr_state = STATE_ALARM_ARMING
elif arming_state == ArmingState.ARMED:
- self._attr_alarm_state = ARMING_MODE_TO_STATE.get(
- arming_mode, AlarmControlPanelState.ARMED_AWAY
+ self._attr_state = ARMING_MODE_TO_STATE.get(
+ arming_mode, STATE_ALARM_ARMED_AWAY
)
elif arming_state == ArmingState.ENTRY_DELAY:
- self._attr_alarm_state = AlarmControlPanelState.PENDING
+ self._attr_state = STATE_ALARM_PENDING
elif arming_state == ArmingState.TRIGGERED:
- self._attr_alarm_state = AlarmControlPanelState.TRIGGERED
+ self._attr_state = STATE_ALARM_TRIGGERED
else:
_LOGGER.warning("Unhandled arming state: %s", arming_state)
diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py
index 6b094c68cb0..8a1719a9bd5 100644
--- a/homeassistant/components/nest/__init__.py
+++ b/homeassistant/components/nest/__init__.py
@@ -59,7 +59,6 @@ from .const import (
CONF_PROJECT_ID,
CONF_SUBSCRIBER_ID,
CONF_SUBSCRIBER_ID_IMPORTED,
- CONF_SUBSCRIPTION_NAME,
DATA_DEVICE_MANAGER,
DATA_SDM,
DATA_SUBSCRIBER,
@@ -104,10 +103,10 @@ CONFIG_SCHEMA = vol.Schema(
PLATFORMS = [Platform.CAMERA, Platform.CLIMATE, Platform.EVENT, Platform.SENSOR]
# Fetch media events with a disk backed cache, with a limit for each camera
-# device. The largest media items are mp4 clips at ~450kb each, and we target
+# device. The largest media items are mp4 clips at ~120kb each, and we target
# ~125MB of storage per camera to try to balance a reasonable user experience
# for event history not not filling the disk.
-EVENT_MEDIA_CACHE_SIZE = 256 # number of events
+EVENT_MEDIA_CACHE_SIZE = 1024 # number of events
THUMBNAIL_SIZE_PX = 175
@@ -290,9 +289,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle removal of pubsub subscriptions created during config flow."""
if (
DATA_SDM not in entry.data
- or not (
- CONF_SUBSCRIPTION_NAME in entry.data or CONF_SUBSCRIBER_ID in entry.data
- )
+ or CONF_SUBSCRIBER_ID not in entry.data
or CONF_SUBSCRIBER_ID_IMPORTED in entry.data
):
return
diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py
index 5c65a70c75d..3ef26747115 100644
--- a/homeassistant/components/nest/api.py
+++ b/homeassistant/components/nest/api.py
@@ -8,7 +8,6 @@ from typing import cast
from aiohttp import ClientSession
from google.oauth2.credentials import Credentials
-from google_nest_sdm.admin_client import PUBSUB_API_HOST, AdminClient
from google_nest_sdm.auth import AbstractAuth
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
@@ -20,7 +19,6 @@ from .const import (
API_URL,
CONF_PROJECT_ID,
CONF_SUBSCRIBER_ID,
- CONF_SUBSCRIPTION_NAME,
OAUTH2_TOKEN,
SDM_SCOPES,
)
@@ -46,7 +44,8 @@ class AsyncConfigEntryAuth(AbstractAuth):
async def async_get_access_token(self) -> str:
"""Return a valid access token for SDM API."""
- await self._oauth_session.async_ensure_token_valid()
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
return cast(str, self._oauth_session.token["access_token"])
async def async_get_creds(self) -> Credentials:
@@ -82,10 +81,9 @@ class AccessTokenAuthImpl(AbstractAuth):
self,
websession: ClientSession,
access_token: str,
- host: str,
) -> None:
"""Init the Nest client library auth implementation."""
- super().__init__(websession, host)
+ super().__init__(websession, API_URL)
self._access_token = access_token
async def async_get_access_token(self) -> str:
@@ -114,46 +112,29 @@ async def new_subscriber(
implementation, config_entry_oauth2_flow.LocalOAuth2Implementation
):
raise TypeError(f"Unexpected auth implementation {implementation}")
- if (subscription_name := entry.data.get(CONF_SUBSCRIPTION_NAME)) is None:
- subscription_name = entry.data[CONF_SUBSCRIBER_ID]
+ if not (subscriber_id := entry.data.get(CONF_SUBSCRIBER_ID)):
+ raise ValueError("Configuration option 'subscriber_id' missing")
auth = AsyncConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass),
config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation),
implementation.client_id,
implementation.client_secret,
)
- return GoogleNestSubscriber(auth, entry.data[CONF_PROJECT_ID], subscription_name)
+ return GoogleNestSubscriber(auth, entry.data[CONF_PROJECT_ID], subscriber_id)
def new_subscriber_with_token(
hass: HomeAssistant,
access_token: str,
project_id: str,
- subscription_name: str,
+ subscriber_id: str,
) -> GoogleNestSubscriber:
"""Create a GoogleNestSubscriber with an access token."""
return GoogleNestSubscriber(
AccessTokenAuthImpl(
aiohttp_client.async_get_clientsession(hass),
access_token,
- API_URL,
),
project_id,
- subscription_name,
- )
-
-
-def new_pubsub_admin_client(
- hass: HomeAssistant,
- access_token: str,
- cloud_project_id: str,
-) -> AdminClient:
- """Create a Nest AdminClient with an access token."""
- return AdminClient(
- auth=AccessTokenAuthImpl(
- aiohttp_client.async_get_clientsession(hass),
- access_token,
- PUBSUB_API_HOST,
- ),
- cloud_project_id=cloud_project_id,
+ subscriber_id,
)
diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py
index 0a46d67a3ad..e25ff82694f 100644
--- a/homeassistant/components/nest/camera.py
+++ b/homeassistant/components/nest/camera.py
@@ -2,36 +2,29 @@
from __future__ import annotations
-from abc import ABC
import asyncio
-from collections.abc import Awaitable, Callable
+from collections.abc import Callable
import datetime
import functools
import logging
from pathlib import Path
+from typing import cast
from google_nest_sdm.camera_traits import (
+ CameraImageTrait,
CameraLiveStreamTrait,
RtspStream,
StreamingProtocol,
- WebRtcStream,
)
from google_nest_sdm.device import Device
from google_nest_sdm.device_manager import DeviceManager
from google_nest_sdm.exceptions import ApiException
-from webrtc_models import RTCIceCandidate
-from homeassistant.components.camera import (
- Camera,
- CameraEntityFeature,
- StreamType,
- WebRTCAnswer,
- WebRTCClientConfiguration,
- WebRTCSendMessage,
-)
+from homeassistant.components.camera import Camera, CameraEntityFeature, StreamType
+from homeassistant.components.camera.webrtc import WebRTCClientConfiguration
from homeassistant.components.stream import CONF_EXTRA_PART_WAIT_TIME
from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_point_in_utc_time
@@ -47,11 +40,6 @@ PLACEHOLDER = Path(__file__).parent / "placeholder.png"
# Used to schedule an alarm to refresh the stream before expiration
STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30)
-# Refresh streams with a bounded interval and backoff on failure
-MIN_REFRESH_BACKOFF_INTERVAL = datetime.timedelta(minutes=1)
-MAX_REFRESH_BACKOFF_INTERVAL = datetime.timedelta(minutes=10)
-BACKOFF_MULTIPLIER = 1.5
-
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@@ -61,87 +49,19 @@ async def async_setup_entry(
device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][
DATA_DEVICE_MANAGER
]
- entities: list[NestCameraBaseEntity] = []
- for device in device_manager.devices.values():
- if (live_stream := device.traits.get(CameraLiveStreamTrait.NAME)) is None:
- continue
- if StreamingProtocol.WEB_RTC in live_stream.supported_protocols:
- entities.append(NestWebRTCEntity(device))
- elif StreamingProtocol.RTSP in live_stream.supported_protocols:
- entities.append(NestRTSPEntity(device))
-
- async_add_entities(entities)
+ async_add_entities(
+ NestCamera(device)
+ for device in device_manager.devices.values()
+ if CameraImageTrait.NAME in device.traits
+ or CameraLiveStreamTrait.NAME in device.traits
+ )
-class StreamRefresh:
- """Class that will refresh an expiring stream.
-
- This class will schedule an alarm for the next expiration time of a stream.
- When the alarm fires, it runs the provided `refresh_cb` to extend the
- lifetime of the stream and return a new expiration time.
-
- A simple backoff will be applied when the refresh callback fails.
- """
-
- def __init__(
- self,
- hass: HomeAssistant,
- expires_at: datetime.datetime,
- refresh_cb: Callable[[], Awaitable[datetime.datetime | None]],
- ) -> None:
- """Initialize StreamRefresh."""
- self._hass = hass
- self._unsub: Callable[[], None] | None = None
- self._min_refresh_interval = MIN_REFRESH_BACKOFF_INTERVAL
- self._refresh_cb = refresh_cb
- self._schedule_stream_refresh(expires_at - STREAM_EXPIRATION_BUFFER)
-
- def unsub(self) -> None:
- """Invalidates the stream."""
- if self._unsub:
- self._unsub()
-
- async def _handle_refresh(self, _: datetime.datetime) -> None:
- """Alarm that fires to check if the stream should be refreshed."""
- self._unsub = None
- try:
- expires_at = await self._refresh_cb()
- except ApiException as err:
- _LOGGER.debug("Failed to refresh stream: %s", err)
- # Increase backoff until the max backoff interval is reached
- self._min_refresh_interval = min(
- self._min_refresh_interval * BACKOFF_MULTIPLIER,
- MAX_REFRESH_BACKOFF_INTERVAL,
- )
- refresh_time = utcnow() + self._min_refresh_interval
- else:
- if expires_at is None:
- return
- self._min_refresh_interval = MIN_REFRESH_BACKOFF_INTERVAL # Reset backoff
- # Defend against invalid stream expiration time in the past
- refresh_time = max(
- expires_at - STREAM_EXPIRATION_BUFFER,
- utcnow() + self._min_refresh_interval,
- )
- self._schedule_stream_refresh(refresh_time)
-
- def _schedule_stream_refresh(self, refresh_time: datetime.datetime) -> None:
- """Schedules an alarm to refresh any streams before expiration."""
- _LOGGER.debug("Scheduling stream refresh for %s", refresh_time)
- self._unsub = async_track_point_in_utc_time(
- self._hass,
- self._handle_refresh,
- refresh_time,
- )
-
-
-class NestCameraBaseEntity(Camera, ABC):
+class NestCamera(Camera):
"""Devices that support cameras."""
_attr_has_entity_name = True
_attr_name = None
- _attr_is_streaming = True
- _attr_supported_features = CameraEntityFeature.STREAM
def __init__(self, device: Device) -> None:
"""Initialize the camera."""
@@ -151,34 +71,38 @@ class NestCameraBaseEntity(Camera, ABC):
self._attr_device_info = nest_device_info.device_info
self._attr_brand = nest_device_info.device_brand
self._attr_model = nest_device_info.device_model
+ self._stream: RtspStream | None = None
+ self._create_stream_url_lock = asyncio.Lock()
+ self._stream_refresh_unsub: Callable[[], None] | None = None
+ self._attr_is_streaming = False
+ self._attr_supported_features = CameraEntityFeature(0)
+ self._rtsp_live_stream_trait: CameraLiveStreamTrait | None = None
+ if CameraLiveStreamTrait.NAME in self._device.traits:
+ self._attr_is_streaming = True
+ self._attr_supported_features |= CameraEntityFeature.STREAM
+ trait = cast(
+ CameraLiveStreamTrait, self._device.traits[CameraLiveStreamTrait.NAME]
+ )
+ if StreamingProtocol.RTSP in trait.supported_protocols:
+ self._rtsp_live_stream_trait = trait
self.stream_options[CONF_EXTRA_PART_WAIT_TIME] = 3
# The API "name" field is a unique device identifier.
self._attr_unique_id = f"{self._device.name}-camera"
- async def async_added_to_hass(self) -> None:
- """Run when entity is added to register update signal handler."""
- self.async_on_remove(
- self._device.add_update_listener(self.async_write_ha_state)
- )
-
-
-class NestRTSPEntity(NestCameraBaseEntity):
- """Nest cameras that use RTSP."""
-
- _rtsp_stream: RtspStream | None = None
- _rtsp_live_stream_trait: CameraLiveStreamTrait
-
- def __init__(self, device: Device) -> None:
- """Initialize the camera."""
- super().__init__(device)
- self._create_stream_url_lock = asyncio.Lock()
- self._rtsp_live_stream_trait = device.traits[CameraLiveStreamTrait.NAME]
- self._refresh_unsub: Callable[[], None] | None = None
-
@property
def use_stream_for_stills(self) -> bool:
- """Always use the RTSP stream to generate snapshots."""
- return True
+ """Whether or not to use stream to generate stills."""
+ return self._rtsp_live_stream_trait is not None
+
+ @property
+ def frontend_stream_type(self) -> StreamType | None:
+ """Return the type of stream supported by this camera."""
+ if CameraLiveStreamTrait.NAME not in self._device.traits:
+ return None
+ trait = self._device.traits[CameraLiveStreamTrait.NAME]
+ if StreamingProtocol.WEB_RTC in trait.supported_protocols:
+ return StreamType.WEB_RTC
+ return super().frontend_stream_type
@property
def available(self) -> bool:
@@ -192,88 +116,83 @@ class NestRTSPEntity(NestCameraBaseEntity):
async def stream_source(self) -> str | None:
"""Return the source of the stream."""
+ if not self._rtsp_live_stream_trait:
+ return None
async with self._create_stream_url_lock:
- if not self._rtsp_stream:
+ if not self._stream:
_LOGGER.debug("Fetching stream url")
try:
- self._rtsp_stream = (
+ self._stream = (
await self._rtsp_live_stream_trait.generate_rtsp_stream()
)
except ApiException as err:
raise HomeAssistantError(f"Nest API error: {err}") from err
- refresh = StreamRefresh(
- self.hass,
- self._rtsp_stream.expires_at,
- self._async_refresh_stream,
- )
- self._refresh_unsub = refresh.unsub
- assert self._rtsp_stream
- if self._rtsp_stream.expires_at < utcnow():
+ self._schedule_stream_refresh()
+ assert self._stream
+ if self._stream.expires_at < utcnow():
_LOGGER.warning("Stream already expired")
- return self._rtsp_stream.rtsp_stream_url
+ return self._stream.rtsp_stream_url
- async def _async_refresh_stream(self) -> datetime.datetime | None:
- """Refresh stream to extend expiration time."""
- if not self._rtsp_stream:
- return None
- _LOGGER.debug("Extending RTSP stream")
+ def _schedule_stream_refresh(self) -> None:
+ """Schedules an alarm to refresh the stream url before expiration."""
+ assert self._stream
+ _LOGGER.debug("New stream url expires at %s", self._stream.expires_at)
+ refresh_time = self._stream.expires_at - STREAM_EXPIRATION_BUFFER
+ # Schedule an alarm to extend the stream
+ if self._stream_refresh_unsub is not None:
+ self._stream_refresh_unsub()
+
+ self._stream_refresh_unsub = async_track_point_in_utc_time(
+ self.hass,
+ self._handle_stream_refresh,
+ refresh_time,
+ )
+
+ async def _handle_stream_refresh(self, now: datetime.datetime) -> None:
+ """Alarm that fires to check if the stream should be refreshed."""
+ if not self._stream:
+ return
+ _LOGGER.debug("Extending stream url")
try:
- self._rtsp_stream = await self._rtsp_stream.extend_rtsp_stream()
+ self._stream = await self._stream.extend_rtsp_stream()
except ApiException as err:
_LOGGER.debug("Failed to extend stream: %s", err)
# Next attempt to catch a url will get a new one
- self._rtsp_stream = None
+ self._stream = None
if self.stream:
await self.stream.stop()
self.stream = None
- return None
+ return
# Update the stream worker with the latest valid url
if self.stream:
- self.stream.update_source(self._rtsp_stream.rtsp_stream_url)
- return self._rtsp_stream.expires_at
+ self.stream.update_source(self._stream.rtsp_stream_url)
+ self._schedule_stream_refresh()
async def async_will_remove_from_hass(self) -> None:
"""Invalidates the RTSP token when unloaded."""
- await super().async_will_remove_from_hass()
- if self._refresh_unsub is not None:
- self._refresh_unsub()
- if self._rtsp_stream:
+ if self._stream:
+ _LOGGER.debug("Invalidating stream")
try:
- await self._rtsp_stream.stop_stream()
+ await self._stream.stop_rtsp_stream()
except ApiException as err:
- _LOGGER.debug("Error stopping stream: %s", err)
- self._rtsp_stream = None
+ _LOGGER.debug(
+ "Failed to revoke stream token, will rely on ttl: %s", err
+ )
+ if self._stream_refresh_unsub:
+ self._stream_refresh_unsub()
-
-class NestWebRTCEntity(NestCameraBaseEntity):
- """Nest cameras that use WebRTC."""
-
- def __init__(self, device: Device) -> None:
- """Initialize the camera."""
- super().__init__(device)
- self._webrtc_sessions: dict[str, WebRtcStream] = {}
- self._refresh_unsub: dict[str, Callable[[], None]] = {}
-
- @property
- def frontend_stream_type(self) -> StreamType | None:
- """Return the type of stream supported by this camera."""
- return StreamType.WEB_RTC
-
- async def _async_refresh_stream(self, session_id: str) -> datetime.datetime | None:
- """Refresh stream to extend expiration time."""
- if not (webrtc_stream := self._webrtc_sessions.get(session_id)):
- return None
- _LOGGER.debug("Extending WebRTC stream %s", webrtc_stream.media_session_id)
- webrtc_stream = await webrtc_stream.extend_stream()
- if session_id in self._webrtc_sessions:
- self._webrtc_sessions[session_id] = webrtc_stream
- return webrtc_stream.expires_at
- return None
+ async def async_added_to_hass(self) -> None:
+ """Run when entity is added to register update signal handler."""
+ self.async_on_remove(
+ self._device.add_update_listener(self.async_write_ha_state)
+ )
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
- """Return a placeholder image for WebRTC cameras that don't support snapshots."""
+ """Return bytes of camera image."""
+ # Use the thumbnail from RTSP stream, or a placeholder if stream is
+ # not supported (e.g. WebRTC) as a fallback when 'use_stream_for_stills' if False
return await self.hass.async_add_executor_job(self.placeholder_image)
@classmethod
@@ -282,59 +201,17 @@ class NestWebRTCEntity(NestCameraBaseEntity):
"""Return placeholder image to use when no stream is available."""
return PLACEHOLDER.read_bytes()
- async def async_handle_async_webrtc_offer(
- self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
- ) -> None:
+ async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
"""Return the source of the stream."""
trait: CameraLiveStreamTrait = self._device.traits[CameraLiveStreamTrait.NAME]
+ if StreamingProtocol.WEB_RTC not in trait.supported_protocols:
+ return await super().async_handle_web_rtc_offer(offer_sdp)
try:
stream = await trait.generate_web_rtc_stream(offer_sdp)
except ApiException as err:
raise HomeAssistantError(f"Nest API error: {err}") from err
- _LOGGER.debug(
- "Started WebRTC session %s, %s", session_id, stream.media_session_id
- )
- self._webrtc_sessions[session_id] = stream
- send_message(WebRTCAnswer(stream.answer_sdp))
- refresh = StreamRefresh(
- self.hass,
- stream.expires_at,
- functools.partial(self._async_refresh_stream, session_id),
- )
- self._refresh_unsub[session_id] = refresh.unsub
+ return stream.answer_sdp
- async def async_on_webrtc_candidate(
- self, session_id: str, candidate: RTCIceCandidate
- ) -> None:
- """Ignore WebRTC candidates for Nest cloud based cameras."""
- return
-
- @callback
- def close_webrtc_session(self, session_id: str) -> None:
- """Close a WebRTC session."""
- if (stream := self._webrtc_sessions.pop(session_id, None)) is not None:
- _LOGGER.debug(
- "Closing WebRTC session %s, %s", session_id, stream.media_session_id
- )
- unsub = self._refresh_unsub.pop(session_id)
- unsub()
-
- async def stop_stream() -> None:
- try:
- await stream.stop_stream()
- except ApiException as err:
- _LOGGER.debug("Error stopping stream: %s", err)
-
- self.hass.async_create_task(stop_stream())
- super().close_webrtc_session(session_id)
-
- @callback
- def _async_get_webrtc_client_configuration(self) -> WebRTCClientConfiguration:
+ async def _async_get_webrtc_client_configuration(self) -> WebRTCClientConfiguration:
"""Return the WebRTC client configuration adjustable per integration."""
return WebRTCClientConfiguration(data_channel="dataSendChannel")
-
- async def async_will_remove_from_hass(self) -> None:
- """Invalidates the RTSP token when unloaded."""
- await super().async_will_remove_from_hass()
- for session_id in list(self._webrtc_sessions.keys()):
- self.close_webrtc_session(session_id)
diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py
index 274e4c288b4..29ae9f6a08e 100644
--- a/homeassistant/components/nest/config_flow.py
+++ b/homeassistant/components/nest/config_flow.py
@@ -12,18 +12,18 @@ from __future__ import annotations
from collections.abc import Iterable, Mapping
import logging
-from typing import TYPE_CHECKING, Any
+from typing import Any
-from google_nest_sdm.admin_client import (
- AdminClient,
- EligibleSubscriptions,
- EligibleTopics,
+from google_nest_sdm.exceptions import (
+ ApiException,
+ AuthException,
+ ConfigurationException,
+ SubscriberException,
)
-from google_nest_sdm.exceptions import ApiException
from google_nest_sdm.structure import Structure
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.util import get_random_string
@@ -31,9 +31,8 @@ from . import api
from .const import (
CONF_CLOUD_PROJECT_ID,
CONF_PROJECT_ID,
- CONF_SUBSCRIBER_ID_IMPORTED,
- CONF_SUBSCRIPTION_NAME,
- CONF_TOPIC_NAME,
+ CONF_SUBSCRIBER_ID,
+ DATA_NEST_CONFIG,
DATA_SDM,
DOMAIN,
OAUTH2_AUTHORIZE,
@@ -59,7 +58,7 @@ DEVICE_ACCESS_CONSOLE_URL = "https://console.nest.google.com/device-access/"
DEVICE_ACCESS_CONSOLE_EDIT_URL = (
"https://console.nest.google.com/device-access/project/{project_id}/information"
)
-CREATE_NEW_SUBSCRIPTION_KEY = "create_new_subscription"
+
_LOGGER = logging.getLogger(__name__)
@@ -96,9 +95,21 @@ class NestFlowHandler(
self._data: dict[str, Any] = {DATA_SDM: {}}
# Possible name to use for config entry based on the Google Home name
self._structure_config_title: str | None = None
- self._admin_client: AdminClient | None = None
- self._eligible_topics: EligibleTopics | None = None
- self._eligible_subscriptions: EligibleSubscriptions | None = None
+
+ def _async_reauth_entry(self) -> ConfigEntry | None:
+ """Return existing entry for reauth."""
+ if self.source != SOURCE_REAUTH or not (
+ entry_id := self.context.get("entry_id")
+ ):
+ return None
+ return next(
+ (
+ entry
+ for entry in self._async_current_entries()
+ if entry.entry_id == entry_id
+ ),
+ None,
+ )
@property
def logger(self) -> logging.Logger:
@@ -117,7 +128,8 @@ class NestFlowHandler(
async def async_generate_authorize_url(self) -> str:
"""Generate a url for the user to authorize based on user input."""
- project_id = self._data.get(CONF_PROJECT_ID)
+ config = self.hass.data.get(DOMAIN, {}).get(DATA_NEST_CONFIG, {})
+ project_id = self._data.get(CONF_PROJECT_ID, config.get(CONF_PROJECT_ID, ""))
query = await super().async_generate_authorize_url()
authorize_url = OAUTH2_AUTHORIZE.format(project_id=project_id)
return f"{authorize_url}{query}"
@@ -126,17 +138,15 @@ class NestFlowHandler(
"""Complete OAuth setup and finish pubsub or finish."""
_LOGGER.debug("Finishing post-oauth configuration")
self._data.update(data)
- _LOGGER.debug("self.source=%s", self.source)
if self.source == SOURCE_REAUTH:
_LOGGER.debug("Skipping Pub/Sub configuration")
- return await self._async_finish()
+ return await self.async_step_finish()
return await self.async_step_pubsub()
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
- _LOGGER.debug("async_step_reauth %s", self.source)
self._data.update(entry_data)
return await self.async_step_reauth_confirm()
@@ -243,114 +253,40 @@ class NestFlowHandler(
async def async_step_pubsub(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
- """Configure and the pre-requisites to configure Pub/Sub topics and subscriptions."""
+ """Configure and create Pub/Sub subscriber."""
data = {
**self._data,
**(user_input if user_input is not None else {}),
}
cloud_project_id = data.get(CONF_CLOUD_PROJECT_ID, "").strip()
- device_access_project_id = data[CONF_PROJECT_ID]
+ config = self.hass.data.get(DOMAIN, {}).get(DATA_NEST_CONFIG, {})
+ project_id = data.get(CONF_PROJECT_ID, config.get(CONF_PROJECT_ID))
errors: dict[str, str] = {}
if cloud_project_id:
- access_token = self._data["token"]["access_token"]
- self._admin_client = api.new_pubsub_admin_client(
- self.hass, access_token=access_token, cloud_project_id=cloud_project_id
+ # Create the subscriber id and/or verify it already exists. Note that
+ # the existing id is used, and create call below is idempotent
+ if not (subscriber_id := data.get(CONF_SUBSCRIBER_ID, "")):
+ subscriber_id = _generate_subscription_id(cloud_project_id)
+ _LOGGER.debug("Creating subscriber id '%s'", subscriber_id)
+ subscriber = api.new_subscriber_with_token(
+ self.hass,
+ self._data["token"]["access_token"],
+ project_id,
+ subscriber_id,
)
try:
- eligible_topics = await self._admin_client.list_eligible_topics(
- device_access_project_id=device_access_project_id
- )
- except ApiException as err:
- _LOGGER.error("Error listing eligible Pub/Sub topics: %s", err)
- errors["base"] = "pubsub_api_error"
- else:
- if not eligible_topics.topic_names:
- errors["base"] = "no_pubsub_topics"
+ await subscriber.create_subscription()
+ except AuthException as err:
+ _LOGGER.error("Subscriber authentication error: %s", err)
+ return self.async_abort(reason="invalid_access_token")
+ except ConfigurationException as err:
+ _LOGGER.error("Configuration error creating subscription: %s", err)
+ errors[CONF_CLOUD_PROJECT_ID] = "bad_project_id"
+ except SubscriberException as err:
+ _LOGGER.error("Error creating subscription: %s", err)
+ errors[CONF_CLOUD_PROJECT_ID] = "subscriber_error"
if not errors:
- self._data[CONF_CLOUD_PROJECT_ID] = cloud_project_id
- self._eligible_topics = eligible_topics
- return await self.async_step_pubsub_topic()
-
- return self.async_show_form(
- step_id="pubsub",
- data_schema=vol.Schema(
- {
- vol.Required(CONF_CLOUD_PROJECT_ID, default=cloud_project_id): str,
- }
- ),
- description_placeholders={
- "url": CLOUD_CONSOLE_URL,
- "device_access_console_url": DEVICE_ACCESS_CONSOLE_URL,
- "more_info_url": MORE_INFO_URL,
- },
- errors=errors,
- )
-
- async def async_step_pubsub_topic(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Configure and create Pub/Sub topic."""
- if TYPE_CHECKING:
- assert self._eligible_topics
- if user_input is not None:
- self._data.update(user_input)
- return await self.async_step_pubsub_subscription()
- topics = list(self._eligible_topics.topic_names)
- return self.async_show_form(
- step_id="pubsub_topic",
- data_schema=vol.Schema(
- {
- vol.Optional(CONF_TOPIC_NAME, default=topics[0]): vol.In(topics),
- }
- ),
- description_placeholders={
- "device_access_console_url": DEVICE_ACCESS_CONSOLE_URL,
- "more_info_url": MORE_INFO_URL,
- },
- )
-
- async def async_step_pubsub_subscription(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Configure and create Pub/Sub subscription."""
- if TYPE_CHECKING:
- assert self._admin_client
- errors = {}
- if user_input is not None:
- subscription_name = user_input[CONF_SUBSCRIPTION_NAME]
- if subscription_name == CREATE_NEW_SUBSCRIPTION_KEY:
- topic_name = self._data[CONF_TOPIC_NAME]
- subscription_name = _generate_subscription_id(
- self._data[CONF_CLOUD_PROJECT_ID]
- )
- _LOGGER.debug(
- "Creating subscription %s on topic %s",
- subscription_name,
- topic_name,
- )
- try:
- await self._admin_client.create_subscription(
- topic_name,
- subscription_name,
- )
- except ApiException as err:
- _LOGGER.error("Error creatingPub/Sub subscription: %s", err)
- errors["base"] = "pubsub_api_error"
- else:
- user_input[CONF_SUBSCRIPTION_NAME] = subscription_name
- else:
- # The user created this subscription themselves so do not delete when removing the integration.
- user_input[CONF_SUBSCRIBER_ID_IMPORTED] = True
-
- if not errors:
- self._data.update(user_input)
- subscriber = api.new_subscriber_with_token(
- self.hass,
- self._data["token"]["access_token"],
- self._data[CONF_PROJECT_ID],
- subscription_name,
- )
try:
device_manager = await subscriber.async_get_device_manager()
except ApiException as err:
@@ -360,51 +296,39 @@ class NestFlowHandler(
self._structure_config_title = generate_config_title(
device_manager.structures.values()
)
- return await self._async_finish()
- subscriptions = {}
- try:
- eligible_subscriptions = (
- await self._admin_client.list_eligible_subscriptions(
- expected_topic_name=self._data[CONF_TOPIC_NAME],
+ self._data.update(
+ {
+ CONF_SUBSCRIBER_ID: subscriber_id,
+ CONF_CLOUD_PROJECT_ID: cloud_project_id,
+ }
)
- )
- except ApiException as err:
- _LOGGER.error(
- "Error talking to API to list eligible Pub/Sub subscriptions: %s", err
- )
- errors["base"] = "pubsub_api_error"
- else:
- subscriptions.update(
- {name: name for name in eligible_subscriptions.subscription_names}
- )
- subscriptions[CREATE_NEW_SUBSCRIPTION_KEY] = "Create New"
+ return await self.async_step_finish()
+
return self.async_show_form(
- step_id="pubsub_subscription",
+ step_id="pubsub",
data_schema=vol.Schema(
{
- vol.Optional(
- CONF_SUBSCRIPTION_NAME,
- default=next(iter(subscriptions)),
- ): vol.In(subscriptions),
+ vol.Required(CONF_CLOUD_PROJECT_ID, default=cloud_project_id): str,
}
),
- description_placeholders={
- "topic": self._data[CONF_TOPIC_NAME],
- "more_info_url": MORE_INFO_URL,
- },
+ description_placeholders={"url": CLOUD_CONSOLE_URL},
errors=errors,
)
- async def _async_finish(self) -> ConfigFlowResult:
+ async def async_step_finish(
+ self, data: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
"""Create an entry for the SDM flow."""
_LOGGER.debug("Creating/updating configuration entry")
# Update existing config entry when in the reauth flow.
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
+ if entry := self._async_reauth_entry():
+ self.hass.config_entries.async_update_entry(
+ entry,
data=self._data,
)
+ await self.hass.config_entries.async_reload(entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
title = self.flow_impl.name
if self._structure_config_title:
title = self._structure_config_title
diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py
index 0a828dcbf78..853e778977d 100644
--- a/homeassistant/components/nest/const.py
+++ b/homeassistant/components/nest/const.py
@@ -4,14 +4,13 @@ DOMAIN = "nest"
DATA_SDM = "sdm"
DATA_SUBSCRIBER = "subscriber"
DATA_DEVICE_MANAGER = "device_manager"
+DATA_NEST_CONFIG = "nest_config"
WEB_AUTH_DOMAIN = DOMAIN
INSTALLED_AUTH_DOMAIN = f"{DOMAIN}.installed"
CONF_PROJECT_ID = "project_id"
-CONF_TOPIC_NAME = "topic_name"
-CONF_SUBSCRIPTION_NAME = "subscription_name"
-CONF_SUBSCRIBER_ID = "subscriber_id" # Old format
+CONF_SUBSCRIBER_ID = "subscriber_id"
CONF_SUBSCRIBER_ID_IMPORTED = "subscriber_id_imported"
CONF_CLOUD_PROJECT_ID = "cloud_project_id"
diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json
index 44eaeeaf62d..8453c51518d 100644
--- a/homeassistant/components/nest/manifest.json
+++ b/homeassistant/components/nest/manifest.json
@@ -20,5 +20,5 @@
"iot_class": "cloud_push",
"loggers": ["google_nest_sdm"],
"quality_scale": "platinum",
- "requirements": ["google-nest-sdm==6.1.5"]
+ "requirements": ["google-nest-sdm==5.0.1"]
}
diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json
index f6a64dd66e6..8e40bf27d1f 100644
--- a/homeassistant/components/nest/strings.json
+++ b/homeassistant/components/nest/strings.json
@@ -26,26 +26,12 @@
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"pubsub": {
- "title": "Configure Google Cloud Pub/Sub",
- "description": "Home Assistant uses Cloud Pub/Sub receive realtime Nest device updates. Nest servers publish updates to a Pub/Sub topic and Home Assistant receives the updates through a Pub/Sub subscription.\n\n1. Visit the [Device Access Console]({device_access_console_url}) and ensure a Pub/Sub topic is configured.\n2. Visit the [Cloud Console]({url}) to find your Google Cloud Project ID and confirm it is correct below.\n3. The next step will attempt to auto-discover Pub/Sub topics and subscriptions.\n\nSee the integration documentation for [more info]({more_info_url}).",
+ "title": "Configure Google Cloud",
+ "description": "Visit the [Cloud Console]({url}) to find your Google Cloud Project ID.",
"data": {
"cloud_project_id": "[%key:component::nest::config::step::cloud_project::data::cloud_project_id%]"
}
},
- "pubsub_topic": {
- "title": "Configure Cloud Pub/Sub topic",
- "description": "Nest devices publish updates on a Cloud Pub/Sub topic. Select the Pub/Sub topic below that is the same as the [Device Access Console]({device_access_console_url}). See the integration documentation for [more info]({more_info_url}).",
- "data": {
- "topic_name": "Pub/Sub topic Name"
- }
- },
- "pubsub_subscription": {
- "title": "Configure Cloud Pub/Sub subscription",
- "description": "Home Assistant receives realtime Nest device updates with a Cloud Pub/Sub subscription for topic `{topic}`.\n\nSelect an existing subscription below if one already exists, or the next step will create a new one for you. See the integration documentation for [more info]({more_info_url}).",
- "data": {
- "subscription_name": "Pub/Sub subscription Name"
- }
- },
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The Nest integration needs to re-authenticate your account"
@@ -54,14 +40,11 @@
"error": {
"bad_project_id": "Please enter a valid Cloud Project ID (check Cloud Console)",
"wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)",
- "subscriber_error": "Unknown subscriber error, see logs",
- "no_pubsub_topics": "No eligible Pub/Sub topics found, please ensure Device Access Console has a Pub/Sub topic.",
- "pubsub_api_error": "Unknown error talking to Cloud Pub/Sub, see logs"
+ "subscriber_error": "Unknown subscriber error, see logs"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
- "missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py
index f01436a45d5..f5fe591bfbf 100644
--- a/homeassistant/components/netatmo/api.py
+++ b/homeassistant/components/netatmo/api.py
@@ -40,5 +40,6 @@ class AsyncConfigEntryNetatmoAuth(pyatmo.AbstractAsyncAuth):
async def async_get_access_token(self) -> str:
"""Return a valid access token for Netatmo API."""
- await self._oauth_session.async_ensure_token_valid()
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
return cast(str, self._oauth_session.token["access_token"])
diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py
index 752dee5a952..c2953b9d49d 100644
--- a/homeassistant/components/netatmo/climate.py
+++ b/homeassistant/components/netatmo/climate.py
@@ -58,9 +58,9 @@ from .entity import NetatmoRoomEntity
_LOGGER = logging.getLogger(__name__)
-PRESET_FROST_GUARD = "frost_guard"
-PRESET_SCHEDULE = "schedule"
-PRESET_MANUAL = "manual"
+PRESET_FROST_GUARD = "Frost Guard"
+PRESET_SCHEDULE = "Schedule"
+PRESET_MANUAL = "Manual"
SUPPORT_FLAGS = (
ClimateEntityFeature.TARGET_TEMPERATURE
@@ -188,7 +188,6 @@ class NetatmoThermostat(NetatmoRoomEntity, ClimateEntity):
_attr_supported_features = SUPPORT_FLAGS
_attr_target_temperature_step = PRECISION_HALVES
_attr_temperature_unit = UnitOfTemperature.CELSIUS
- _attr_translation_key = "thermostat"
_attr_name = None
_away: bool | None = None
_connected: bool | None = None
diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py
index d853694ffea..0da4d6f16b7 100644
--- a/homeassistant/components/netatmo/config_flow.py
+++ b/homeassistant/components/netatmo/config_flow.py
@@ -101,6 +101,7 @@ class NetatmoOptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize Netatmo options flow."""
+ self.config_entry = config_entry
self.options = dict(config_entry.options)
self.options.setdefault(CONF_WEATHER_AREAS, {})
diff --git a/homeassistant/components/netatmo/icons.json b/homeassistant/components/netatmo/icons.json
index 9f712e08f33..70a51542126 100644
--- a/homeassistant/components/netatmo/icons.json
+++ b/homeassistant/components/netatmo/icons.json
@@ -1,18 +1,5 @@
{
"entity": {
- "climate": {
- "thermostat": {
- "state_attributes": {
- "preset_mode": {
- "state": {
- "frost_guard": "mdi:snowflake-thermometer",
- "schedule": "mdi:clock-outline",
- "manual": "mdi:gesture-tap"
- }
- }
- }
- }
- },
"sensor": {
"temp_trend": {
"default": "mdi:trending-up"
diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json
index 6b91aa204b2..3c360634147 100644
--- a/homeassistant/components/netatmo/strings.json
+++ b/homeassistant/components/netatmo/strings.json
@@ -168,19 +168,6 @@
}
},
"entity": {
- "climate": {
- "thermostat": {
- "state_attributes": {
- "preset_mode": {
- "state": {
- "frost_guard": "Frost guard",
- "schedule": "Schedule",
- "manual": "Manual"
- }
- }
- }
- }
- },
"sensor": {
"temp_trend": {
"name": "Temperature trend"
diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py
index fa18c3510ba..58f63e5212a 100644
--- a/homeassistant/components/netgear/__init__.py
+++ b/homeassistant/components/netgear/__init__.py
@@ -93,7 +93,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"{router.device_name} Devices",
update_method=async_update_devices,
update_interval=SCAN_INTERVAL,
@@ -101,7 +100,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator_traffic_meter = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"{router.device_name} Traffic meter",
update_method=async_update_traffic_meter,
update_interval=SCAN_INTERVAL,
@@ -109,7 +107,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator_speed_test = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"{router.device_name} Speed test",
update_method=async_update_speed_test,
update_interval=SPEED_TEST_INTERVAL,
@@ -117,7 +114,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator_firmware = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"{router.device_name} Firmware",
update_method=async_check_firmware,
update_interval=SCAN_INTERVAL_FIRMWARE,
@@ -125,7 +121,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator_utilization = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"{router.device_name} Utilization",
update_method=async_update_utilization,
update_interval=SCAN_INTERVAL,
@@ -133,7 +128,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator_link = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"{router.device_name} Ethernet Link Status",
update_method=async_check_link_status,
update_interval=SCAN_INTERVAL,
diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py
index 965e3618645..fba934af38d 100644
--- a/homeassistant/components/netgear/config_flow.py
+++ b/homeassistant/components/netgear/config_flow.py
@@ -63,6 +63,10 @@ def _ordered_shared_schema(schema_input):
class OptionsFlowHandler(OptionsFlow):
"""Options for the component."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Init object."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, int] | None = None
) -> ConfigFlowResult:
@@ -105,7 +109,7 @@ class NetgearFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def _show_setup_form(
self,
diff --git a/homeassistant/components/network/websocket.py b/homeassistant/components/network/websocket.py
index 22f7dc23f1e..b97bd2d58d1 100644
--- a/homeassistant/components/network/websocket.py
+++ b/homeassistant/components/network/websocket.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-from contextlib import suppress
from typing import Any
import voluptuous as vol
@@ -10,7 +9,6 @@ import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.websocket_api import ActiveConnection
from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.network import NoURLAvailableError, get_url
from .const import ATTR_ADAPTERS, ATTR_CONFIGURED_ADAPTERS, NETWORK_CONFIG_SCHEMA
from .network import async_get_network
@@ -21,7 +19,6 @@ def async_register_websocket_commands(hass: HomeAssistant) -> None:
"""Register network websocket commands."""
websocket_api.async_register_command(hass, websocket_network_adapters)
websocket_api.async_register_command(hass, websocket_network_adapters_configure)
- websocket_api.async_register_command(hass, websocket_network_url)
@websocket_api.require_admin
@@ -65,40 +62,3 @@ async def websocket_network_adapters_configure(
msg["id"],
{ATTR_CONFIGURED_ADAPTERS: network.configured_adapters},
)
-
-
-@callback
-@websocket_api.require_admin
-@websocket_api.websocket_command(
- {
- vol.Required("type"): "network/url",
- }
-)
-def websocket_network_url(
- hass: HomeAssistant,
- connection: ActiveConnection,
- msg: dict[str, Any],
-) -> None:
- """Get the internal, external, and cloud URLs."""
- internal_url = None
- external_url = None
- cloud_url = None
- with suppress(NoURLAvailableError):
- internal_url = get_url(
- hass, allow_internal=True, allow_external=False, allow_cloud=False
- )
- with suppress(NoURLAvailableError):
- external_url = get_url(
- hass, allow_internal=False, allow_external=True, prefer_external=True
- )
- with suppress(NoURLAvailableError):
- cloud_url = get_url(hass, allow_internal=False, require_cloud=True)
-
- connection.send_result(
- msg["id"],
- {
- "internal": internal_url,
- "external": external_url,
- "cloud": cloud_url,
- },
- )
diff --git a/homeassistant/components/nexia/switch.py b/homeassistant/components/nexia/switch.py
index 9505538e86a..f92443517c8 100644
--- a/homeassistant/components/nexia/switch.py
+++ b/homeassistant/components/nexia/switch.py
@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .coordinator import NexiaDataUpdateCoordinator
-from .entity import NexiaThermostatEntity, NexiaThermostatZoneEntity
+from .entity import NexiaThermostatZoneEntity
from .types import NexiaConfigEntry
@@ -28,11 +28,11 @@ async def async_setup_entry(
entities: list[NexiaHoldSwitch | NexiaEmergencyHeatSwitch] = []
for thermostat_id in nexia_home.get_thermostat_ids():
thermostat: NexiaThermostat = nexia_home.get_thermostat_by_id(thermostat_id)
- if thermostat.has_emergency_heat():
- entities.append(NexiaEmergencyHeatSwitch(coordinator, thermostat))
for zone_id in thermostat.get_zone_ids():
zone: NexiaThermostatZone = thermostat.get_zone_by_id(zone_id)
entities.append(NexiaHoldSwitch(coordinator, zone))
+ if thermostat.has_emergency_heat():
+ entities.append(NexiaEmergencyHeatSwitch(coordinator, zone))
async_add_entities(entities)
@@ -68,20 +68,17 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity):
self._signal_zone_update()
-class NexiaEmergencyHeatSwitch(NexiaThermostatEntity, SwitchEntity):
+class NexiaEmergencyHeatSwitch(NexiaThermostatZoneEntity, SwitchEntity):
"""Provides Nexia emergency heat switch support."""
_attr_translation_key = "emergency_heat"
def __init__(
- self, coordinator: NexiaDataUpdateCoordinator, thermostat: NexiaThermostat
+ self, coordinator: NexiaDataUpdateCoordinator, zone: NexiaThermostatZone
) -> None:
"""Initialize the emergency heat mode switch."""
- super().__init__(
- coordinator,
- thermostat,
- unique_id=f"{thermostat.thermostat_id}_emergency_heat",
- )
+ zone_id = zone.zone_id
+ super().__init__(coordinator, zone, zone_id)
@property
def is_on(self) -> bool:
diff --git a/homeassistant/components/nextbus/__init__.py b/homeassistant/components/nextbus/__init__.py
index 168488e1940..817990620fe 100644
--- a/homeassistant/components/nextbus/__init__.py
+++ b/homeassistant/components/nextbus/__init__.py
@@ -3,7 +3,6 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_STOP, Platform
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from .const import CONF_AGENCY, CONF_ROUTE, DOMAIN
from .coordinator import NextBusDataUpdateCoordinator
@@ -28,9 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator.add_stop_route(entry_stop, entry.data[CONF_ROUTE])
- await coordinator.async_refresh()
- if not coordinator.last_update_success:
- raise ConfigEntryNotReady from coordinator.last_exception
+ await coordinator.async_config_entry_first_refresh()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
diff --git a/homeassistant/components/nextbus/coordinator.py b/homeassistant/components/nextbus/coordinator.py
index 617669adf2f..dcaafa9573b 100644
--- a/homeassistant/components/nextbus/coordinator.py
+++ b/homeassistant/components/nextbus/coordinator.py
@@ -24,7 +24,6 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
super().__init__(
hass,
_LOGGER,
- config_entry=None, # It is shared between multiple entries
name=DOMAIN,
update_interval=timedelta(seconds=30),
)
@@ -49,6 +48,13 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
"""Check if this coordinator is tracking any routes."""
return len(self._route_stops) > 0
+ async def async_shutdown(self) -> None:
+ """If there are no more routes, cancel any scheduled call, and ignore new runs."""
+ if self.has_routes():
+ return
+
+ await super().async_shutdown()
+
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from NextBus."""
diff --git a/homeassistant/components/nextcloud/config_flow.py b/homeassistant/components/nextcloud/config_flow.py
index 6c59dd271d5..c469936ac48 100644
--- a/homeassistant/components/nextcloud/config_flow.py
+++ b/homeassistant/components/nextcloud/config_flow.py
@@ -13,7 +13,7 @@ from nextcloudmonitor import (
)
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL
from .const import DEFAULT_VERIFY_SSL, DOMAIN
@@ -39,6 +39,8 @@ class NextcloudConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ _entry: ConfigEntry | None = None
+
def _try_connect_nc(self, user_input: dict) -> NextcloudMonitor:
"""Try to connect to nextcloud server."""
return NextcloudMonitor(
@@ -77,6 +79,7 @@ class NextcloudConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle flow upon an API authentication error."""
+ self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -84,29 +87,32 @@ class NextcloudConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle reauthorization flow."""
errors = {}
+ assert self._entry is not None
- reauth_entry = self._get_reauth_entry()
if user_input is not None:
try:
await self.hass.async_add_executor_job(
- self._try_connect_nc, {**reauth_entry.data, **user_input}
+ self._try_connect_nc, {**self._entry.data, **user_input}
)
except NextcloudMonitorAuthorizationError:
errors["base"] = "invalid_auth"
except (NextcloudMonitorConnectionError, NextcloudMonitorRequestError):
errors["base"] = "connection_error"
else:
- return self.async_update_reload_and_abort(
- reauth_entry, data_updates=user_input
+ self.hass.config_entries.async_update_entry(
+ self._entry,
+ data={**self._entry.data, **user_input},
)
+ await self.hass.config_entries.async_reload(self._entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
data_schema = self.add_suggested_values_to_schema(
DATA_SCHEMA_REAUTH,
- {CONF_USERNAME: reauth_entry.data[CONF_USERNAME], **(user_input or {})},
+ {CONF_USERNAME: self._entry.data[CONF_USERNAME], **(user_input or {})},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=data_schema,
- description_placeholders={"url": reauth_entry.data[CONF_URL]},
+ description_placeholders={"url": self._entry.data[CONF_URL]},
errors=errors,
)
diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py
index d3327c4c08b..80caba6ec7e 100644
--- a/homeassistant/components/nextdns/config_flow.py
+++ b/homeassistant/components/nextdns/config_flow.py
@@ -3,14 +3,14 @@
from __future__ import annotations
from collections.abc import Mapping
-from typing import Any
+from typing import TYPE_CHECKING, Any
from aiohttp.client_exceptions import ClientConnectorError
from nextdns import ApiError, InvalidApiKeyError, NextDns
from tenacity import RetryError
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_PROFILE_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -36,6 +36,7 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
"""Initialize the config flow."""
self.nextdns: NextDns
self.api_key: str
+ self.entry: ConfigEntry | None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -96,6 +97,7 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -114,8 +116,11 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
except Exception: # noqa: BLE001
errors["base"] = "unknown"
else:
+ if TYPE_CHECKING:
+ assert self.entry is not None
+
return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=user_input
+ self.entry, data={**self.entry.data, **user_input}
)
return self.async_show_form(
diff --git a/homeassistant/components/nice_go/config_flow.py b/homeassistant/components/nice_go/config_flow.py
index da3940117e9..94594bbd11f 100644
--- a/homeassistant/components/nice_go/config_flow.py
+++ b/homeassistant/components/nice_go/config_flow.py
@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Mapping
from datetime import datetime
import logging
-from typing import Any
+from typing import TYPE_CHECKING, Any
from nice_go import AuthFailedError, NiceGOApi
import voluptuous as vol
@@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_NAME, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from . import NiceGOConfigEntry
from .const import CONF_REFRESH_TOKEN, CONF_REFRESH_TOKEN_CREATION_TIME, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -30,6 +31,7 @@ class NiceGOConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Nice G.O."""
VERSION = 1
+ reauth_entry: NiceGOConfigEntry | None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -72,6 +74,10 @@ class NiceGOConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
+
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -80,7 +86,9 @@ class NiceGOConfigFlow(ConfigFlow, domain=DOMAIN):
"""Confirm re-authentication."""
errors = {}
- reauth_entry = self._get_reauth_entry()
+ if TYPE_CHECKING:
+ assert self.reauth_entry is not None
+
if user_input is not None:
hub = NiceGOApi()
@@ -97,7 +105,7 @@ class NiceGOConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
- reauth_entry,
+ self.reauth_entry,
data={
**user_input,
CONF_REFRESH_TOKEN: refresh_token,
@@ -110,8 +118,8 @@ class NiceGOConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA,
- user_input or {CONF_EMAIL: reauth_entry.data[CONF_EMAIL]},
+ user_input or {CONF_EMAIL: self.reauth_entry.data[CONF_EMAIL]},
),
- description_placeholders={CONF_NAME: reauth_entry.title},
+ description_placeholders={CONF_NAME: self.reauth_entry.title},
errors=errors,
)
diff --git a/homeassistant/components/nice_go/const.py b/homeassistant/components/nice_go/const.py
index a6635368f7b..c3caa92c8be 100644
--- a/homeassistant/components/nice_go/const.py
+++ b/homeassistant/components/nice_go/const.py
@@ -2,8 +2,6 @@
from datetime import timedelta
-from homeassistant.const import Platform
-
DOMAIN = "nice_go"
# Configuration
@@ -13,22 +11,3 @@ CONF_REFRESH_TOKEN = "refresh_token"
CONF_REFRESH_TOKEN_CREATION_TIME = "refresh_token_creation_time"
REFRESH_TOKEN_EXPIRY_TIME = timedelta(days=30)
-
-SUPPORTED_DEVICE_TYPES = {
- Platform.LIGHT: ["WallStation"],
- Platform.SWITCH: ["WallStation"],
-}
-KNOWN_UNSUPPORTED_DEVICE_TYPES = {
- Platform.LIGHT: ["Mms100"],
- Platform.SWITCH: ["Mms100"],
-}
-
-UNSUPPORTED_DEVICE_WARNING = (
- "Device '%s' has unknown device type '%s', "
- "which is not supported by this integration. "
- "We try to support it with a cover and event entity, but nothing else. "
- "Please create an issue with your device model in additional info"
- " at https://github.com/home-assistant/core/issues/new"
- "?assignees=&labels=&projects=&template=bug_report.yml"
- "&title=New%%20Nice%%20G.O.%%20device%%20type%%20'%s'%%20found"
-)
diff --git a/homeassistant/components/nice_go/coordinator.py b/homeassistant/components/nice_go/coordinator.py
index 29c0d8233fe..dd2d7ccb45e 100644
--- a/homeassistant/components/nice_go/coordinator.py
+++ b/homeassistant/components/nice_go/coordinator.py
@@ -44,14 +44,13 @@ RECONNECT_DELAY = 5
class NiceGODevice:
"""Nice G.O. device dataclass."""
- type: str
id: str
name: str
barrier_status: str
light_status: bool | None
fw_version: str
connected: bool
- vacation_mode: bool | None
+ vacation_mode: bool
class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
@@ -86,9 +85,7 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
"""Stop reconnecting if hass is stopping."""
self._hass_stopping = True
- async def _parse_barrier(
- self, device_type: str, barrier_state: BarrierState
- ) -> NiceGODevice | None:
+ async def _parse_barrier(self, barrier_state: BarrierState) -> NiceGODevice | None:
"""Parse barrier data."""
device_id = barrier_state.deviceId
@@ -124,15 +121,11 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
fw_version = barrier_state.reported["deviceFwVersion"]
if barrier_state.connectionState:
connected = barrier_state.connectionState.connected
- elif device_type == "Mms100":
- connected = barrier_state.reported.get("radioConnected", 0) == 1
else:
- # Assume connected
- connected = True
- vacation_mode = barrier_state.reported.get("vcnMode", None)
+ connected = False
+ vacation_mode = barrier_state.reported["vcnMode"]
return NiceGODevice(
- type=device_type,
id=device_id,
name=name,
barrier_status=barrier_status,
@@ -163,8 +156,7 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
barriers = await self.api.get_all_barriers()
parsed_barriers = [
- await self._parse_barrier(barrier.type, barrier.state)
- for barrier in barriers
+ await self._parse_barrier(barrier.state) for barrier in barriers
]
# Parse the barriers and save them in a dictionary
@@ -234,9 +226,6 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
_LOGGER.debug(data)
raw_data = data["data"]["devicesStatesUpdateFeed"]["item"]
parsed_data = await self._parse_barrier(
- self.data[
- raw_data["deviceId"]
- ].type, # Device type is not sent in device state update, and it can't change, so we just reuse the existing one
BarrierState(
deviceId=raw_data["deviceId"],
desired=json.loads(raw_data["desired"]),
@@ -249,7 +238,7 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
else None,
version=raw_data["version"],
timestamp=raw_data["timestamp"],
- ),
+ )
)
if parsed_data is None:
return
diff --git a/homeassistant/components/nice_go/cover.py b/homeassistant/components/nice_go/cover.py
index a823e931804..7ded43de165 100644
--- a/homeassistant/components/nice_go/cover.py
+++ b/homeassistant/components/nice_go/cover.py
@@ -18,10 +18,6 @@ from . import NiceGOConfigEntry
from .const import DOMAIN
from .entity import NiceGOEntity
-DEVICE_CLASSES = {
- "WallStation": CoverDeviceClass.GARAGE,
- "Mms100": CoverDeviceClass.GATE,
-}
PARALLEL_UPDATES = 1
@@ -44,11 +40,7 @@ class NiceGOCoverEntity(NiceGOEntity, CoverEntity):
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
_attr_name = None
-
- @property
- def device_class(self) -> CoverDeviceClass:
- """Return the class of this device, from component DEVICE_CLASSES."""
- return DEVICE_CLASSES.get(self.data.type, CoverDeviceClass.GARAGE)
+ _attr_device_class = CoverDeviceClass.GARAGE
@property
def is_closed(self) -> bool:
diff --git a/homeassistant/components/nice_go/light.py b/homeassistant/components/nice_go/light.py
index abb192adde1..6b5f5cd39ee 100644
--- a/homeassistant/components/nice_go/light.py
+++ b/homeassistant/components/nice_go/light.py
@@ -1,28 +1,19 @@
"""Nice G.O. light."""
-import logging
from typing import TYPE_CHECKING, Any
from aiohttp import ClientError
from nice_go import ApiError
from homeassistant.components.light import ColorMode, LightEntity
-from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import NiceGOConfigEntry
-from .const import (
- DOMAIN,
- KNOWN_UNSUPPORTED_DEVICE_TYPES,
- SUPPORTED_DEVICE_TYPES,
- UNSUPPORTED_DEVICE_WARNING,
-)
+from .const import DOMAIN
from .entity import NiceGOEntity
-_LOGGER = logging.getLogger(__name__)
-
async def async_setup_entry(
hass: HomeAssistant,
@@ -33,20 +24,11 @@ async def async_setup_entry(
coordinator = config_entry.runtime_data
- entities = []
-
- for device_id, device_data in coordinator.data.items():
- if device_data.type in SUPPORTED_DEVICE_TYPES[Platform.LIGHT]:
- entities.append(NiceGOLightEntity(coordinator, device_id, device_data.name))
- elif device_data.type not in KNOWN_UNSUPPORTED_DEVICE_TYPES[Platform.LIGHT]:
- _LOGGER.warning(
- UNSUPPORTED_DEVICE_WARNING,
- device_data.name,
- device_data.type,
- device_data.type,
- )
-
- async_add_entities(entities)
+ async_add_entities(
+ NiceGOLightEntity(coordinator, device_id, device_data.name)
+ for device_id, device_data in coordinator.data.items()
+ if device_data.light_status is not None
+ )
class NiceGOLightEntity(NiceGOEntity, LightEntity):
diff --git a/homeassistant/components/nice_go/manifest.json b/homeassistant/components/nice_go/manifest.json
index 817d7ef9bc9..d3f54e5e668 100644
--- a/homeassistant/components/nice_go/manifest.json
+++ b/homeassistant/components/nice_go/manifest.json
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["nice_go"],
- "requirements": ["nice-go==0.3.10"]
+ "requirements": ["nice-go==0.3.9"]
}
diff --git a/homeassistant/components/nice_go/switch.py b/homeassistant/components/nice_go/switch.py
index e3b85528f3b..a74a18328c9 100644
--- a/homeassistant/components/nice_go/switch.py
+++ b/homeassistant/components/nice_go/switch.py
@@ -3,24 +3,18 @@
from __future__ import annotations
import logging
-from typing import TYPE_CHECKING, Any
+from typing import Any
from aiohttp import ClientError
from nice_go import ApiError
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
-from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import NiceGOConfigEntry
-from .const import (
- DOMAIN,
- KNOWN_UNSUPPORTED_DEVICE_TYPES,
- SUPPORTED_DEVICE_TYPES,
- UNSUPPORTED_DEVICE_WARNING,
-)
+from .const import DOMAIN
from .entity import NiceGOEntity
_LOGGER = logging.getLogger(__name__)
@@ -34,22 +28,10 @@ async def async_setup_entry(
"""Set up Nice G.O. switch."""
coordinator = config_entry.runtime_data
- entities = []
-
- for device_id, device_data in coordinator.data.items():
- if device_data.type in SUPPORTED_DEVICE_TYPES[Platform.SWITCH]:
- entities.append(
- NiceGOSwitchEntity(coordinator, device_id, device_data.name)
- )
- elif device_data.type not in KNOWN_UNSUPPORTED_DEVICE_TYPES[Platform.SWITCH]:
- _LOGGER.warning(
- UNSUPPORTED_DEVICE_WARNING,
- device_data.name,
- device_data.type,
- device_data.type,
- )
-
- async_add_entities(entities)
+ async_add_entities(
+ NiceGOSwitchEntity(coordinator, device_id, device_data.name)
+ for device_id, device_data in coordinator.data.items()
+ )
class NiceGOSwitchEntity(NiceGOEntity, SwitchEntity):
@@ -61,8 +43,6 @@ class NiceGOSwitchEntity(NiceGOEntity, SwitchEntity):
@property
def is_on(self) -> bool:
"""Return if switch is on."""
- if TYPE_CHECKING:
- assert self.data.vacation_mode is not None
return self.data.vacation_mode
async def async_turn_on(self, **kwargs: Any) -> None:
diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py
index 620349ec3c3..92291bdc4f9 100644
--- a/homeassistant/components/nightscout/sensor.py
+++ b/homeassistant/components/nightscout/sensor.py
@@ -9,9 +9,9 @@ from typing import Any
from aiohttp import ClientError
from py_nightscout import Api as NightscoutAPI
-from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
+from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_DATE, UnitOfBloodGlucoseConcentration
+from homeassistant.const import ATTR_DATE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -37,10 +37,7 @@ async def async_setup_entry(
class NightscoutSensor(SensorEntity):
"""Implementation of a Nightscout sensor."""
- _attr_device_class = SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION
- _attr_native_unit_of_measurement = (
- UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER
- )
+ _attr_native_unit_of_measurement = "mg/dL"
_attr_icon = "mdi:cloud-question"
def __init__(self, api: NightscoutAPI, name: str, unique_id: str | None) -> None:
diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py
index a1ba9ae0c61..dd4319d566b 100644
--- a/homeassistant/components/nina/config_flow.py
+++ b/homeassistant/components/nina/config_flow.py
@@ -171,7 +171,8 @@ class OptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
- self.data = dict(config_entry.data)
+ self.config_entry = config_entry
+ self.data = dict(self.config_entry.data)
self._all_region_codes_sorted: dict[str, str] = {}
self.regions: dict[str, dict[str, Any]] = {}
diff --git a/homeassistant/components/nina/strings.json b/homeassistant/components/nina/strings.json
index 98ea88d8798..9747feaddb7 100644
--- a/homeassistant/components/nina/strings.json
+++ b/homeassistant/components/nina/strings.json
@@ -38,10 +38,12 @@
}
}
},
+ "abort": {
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
"error": {
"no_selection": "[%key:component::nina::config::error::no_selection%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "unknown": "[%key:common::config_flow::error::unknown%]"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
}
}
diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py
index e05150995aa..b724dca1a81 100644
--- a/homeassistant/components/nmap_tracker/config_flow.py
+++ b/homeassistant/components/nmap_tracker/config_flow.py
@@ -213,6 +213,6 @@ class NmapTrackerConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json
index 5b2dab50812..08d9b94cf2d 100644
--- a/homeassistant/components/nmap_tracker/manifest.json
+++ b/homeassistant/components/nmap_tracker/manifest.json
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/nmap_tracker",
"iot_class": "local_polling",
"loggers": ["nmap"],
- "requirements": ["netmap==0.7.0.2", "getmac==0.9.4", "aiooui==0.1.7"]
+ "requirements": ["netmap==0.7.0.2", "getmac==0.9.4", "aiooui==0.1.6"]
}
diff --git a/homeassistant/components/nobo_hub/config_flow.py b/homeassistant/components/nobo_hub/config_flow.py
index 7e1ae4c1d9b..8aed520f21e 100644
--- a/homeassistant/components/nobo_hub/config_flow.py
+++ b/homeassistant/components/nobo_hub/config_flow.py
@@ -175,7 +175,7 @@ class NoboHubConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class NoboHubConnectError(HomeAssistantError):
@@ -190,6 +190,10 @@ class NoboHubConnectError(HomeAssistantError):
class OptionsFlowHandler(OptionsFlow):
"""Handles options flow for the component."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize the options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(self, user_input=None) -> ConfigFlowResult:
"""Manage the options."""
diff --git a/homeassistant/components/nordpool/__init__.py b/homeassistant/components/nordpool/__init__.py
deleted file mode 100644
index b688bf74a37..00000000000
--- a/homeassistant/components/nordpool/__init__.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""The Nord Pool component."""
-
-from __future__ import annotations
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
-from homeassistant.util import dt as dt_util
-
-from .const import PLATFORMS
-from .coordinator import NordPoolDataUpdateCoordinator
-
-type NordPoolConfigEntry = ConfigEntry[NordPoolDataUpdateCoordinator]
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: NordPoolConfigEntry) -> bool:
- """Set up Nord Pool from a config entry."""
-
- coordinator = NordPoolDataUpdateCoordinator(hass, entry)
- await coordinator.fetch_data(dt_util.utcnow())
- entry.runtime_data = coordinator
-
- await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
-
- return True
-
-
-async def async_unload_entry(hass: HomeAssistant, entry: NordPoolConfigEntry) -> bool:
- """Unload Nord Pool config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/nordpool/config_flow.py b/homeassistant/components/nordpool/config_flow.py
deleted file mode 100644
index 1d75d825e47..00000000000
--- a/homeassistant/components/nordpool/config_flow.py
+++ /dev/null
@@ -1,115 +0,0 @@
-"""Adds config flow for Nord Pool integration."""
-
-from __future__ import annotations
-
-from typing import Any
-
-from pynordpool import (
- Currency,
- NordPoolClient,
- NordPoolEmptyResponseError,
- NordPoolError,
-)
-from pynordpool.const import AREAS
-import voluptuous as vol
-
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_CURRENCY
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.selector import (
- SelectOptionDict,
- SelectSelector,
- SelectSelectorConfig,
- SelectSelectorMode,
-)
-from homeassistant.util import dt as dt_util
-
-from .const import CONF_AREAS, DEFAULT_NAME, DOMAIN
-
-SELECT_AREAS = [
- SelectOptionDict(value=area, label=name) for area, name in AREAS.items()
-]
-SELECT_CURRENCY = [currency.value for currency in Currency]
-
-DATA_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_AREAS, default=[]): SelectSelector(
- SelectSelectorConfig(
- options=SELECT_AREAS,
- multiple=True,
- mode=SelectSelectorMode.DROPDOWN,
- sort=True,
- )
- ),
- vol.Required(CONF_CURRENCY, default="SEK"): SelectSelector(
- SelectSelectorConfig(
- options=SELECT_CURRENCY,
- multiple=False,
- mode=SelectSelectorMode.DROPDOWN,
- sort=True,
- )
- ),
- }
-)
-
-
-async def test_api(hass: HomeAssistant, user_input: dict[str, Any]) -> dict[str, str]:
- """Test fetch data from Nord Pool."""
- client = NordPoolClient(async_get_clientsession(hass))
- try:
- await client.async_get_delivery_period(
- dt_util.now(),
- Currency(user_input[CONF_CURRENCY]),
- user_input[CONF_AREAS],
- )
- except NordPoolEmptyResponseError:
- return {"base": "no_data"}
- except NordPoolError:
- return {"base": "cannot_connect"}
-
- return {}
-
-
-class NordpoolConfigFlow(ConfigFlow, domain=DOMAIN):
- """Handle a config flow for Nord Pool integration."""
-
- 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:
- errors = await test_api(self.hass, user_input)
- if not errors:
- return self.async_create_entry(
- title=DEFAULT_NAME,
- data=user_input,
- )
-
- return self.async_show_form(
- step_id="user",
- data_schema=DATA_SCHEMA,
- errors=errors,
- )
-
- async def async_step_reconfigure(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle the reconfiguration step."""
- errors: dict[str, str] = {}
- if user_input:
- errors = await test_api(self.hass, user_input)
- reconfigure_entry = self._get_reconfigure_entry()
- if not errors:
- return self.async_update_reload_and_abort(
- reconfigure_entry, data_updates=user_input
- )
-
- return self.async_show_form(
- step_id="reconfigure",
- data_schema=DATA_SCHEMA,
- errors=errors,
- )
diff --git a/homeassistant/components/nordpool/const.py b/homeassistant/components/nordpool/const.py
deleted file mode 100644
index 19a978d946c..00000000000
--- a/homeassistant/components/nordpool/const.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""Constants for Nord Pool."""
-
-import logging
-
-from homeassistant.const import Platform
-
-LOGGER = logging.getLogger(__package__)
-
-DEFAULT_SCAN_INTERVAL = 60
-DOMAIN = "nordpool"
-PLATFORMS = [Platform.SENSOR]
-DEFAULT_NAME = "Nord Pool"
-
-CONF_AREAS = "areas"
diff --git a/homeassistant/components/nordpool/coordinator.py b/homeassistant/components/nordpool/coordinator.py
deleted file mode 100644
index fa4e9ca2548..00000000000
--- a/homeassistant/components/nordpool/coordinator.py
+++ /dev/null
@@ -1,91 +0,0 @@
-"""DataUpdateCoordinator for the Nord Pool integration."""
-
-from __future__ import annotations
-
-from collections.abc import Callable
-from datetime import datetime, timedelta
-from typing import TYPE_CHECKING
-
-from pynordpool import (
- Currency,
- DeliveryPeriodData,
- NordPoolClient,
- NordPoolEmptyResponseError,
- NordPoolError,
- NordPoolResponseError,
-)
-
-from homeassistant.const import CONF_CURRENCY
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.event import async_track_point_in_utc_time
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
-from homeassistant.util import dt as dt_util
-
-from .const import CONF_AREAS, DOMAIN, LOGGER
-
-if TYPE_CHECKING:
- from . import NordPoolConfigEntry
-
-
-class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodData]):
- """A Nord Pool Data Update Coordinator."""
-
- config_entry: NordPoolConfigEntry
-
- def __init__(self, hass: HomeAssistant, config_entry: NordPoolConfigEntry) -> None:
- """Initialize the Nord Pool coordinator."""
- super().__init__(
- hass,
- LOGGER,
- config_entry=config_entry,
- name=DOMAIN,
- )
- self.client = NordPoolClient(session=async_get_clientsession(hass))
- self.unsub: Callable[[], None] | None = None
-
- def get_next_interval(self, now: datetime) -> datetime:
- """Compute next time an update should occur."""
- next_hour = dt_util.utcnow() + timedelta(hours=1)
- next_run = datetime(
- next_hour.year,
- next_hour.month,
- next_hour.day,
- next_hour.hour,
- tzinfo=dt_util.UTC,
- )
- LOGGER.debug("Next update at %s", next_run)
- return next_run
-
- async def async_shutdown(self) -> None:
- """Cancel any scheduled call, and ignore new runs."""
- await super().async_shutdown()
- if self.unsub:
- self.unsub()
- self.unsub = None
-
- async def fetch_data(self, now: datetime) -> None:
- """Fetch data from Nord Pool."""
- self.unsub = async_track_point_in_utc_time(
- self.hass, self.fetch_data, self.get_next_interval(dt_util.utcnow())
- )
- try:
- data = await self.client.async_get_delivery_period(
- dt_util.now(),
- Currency(self.config_entry.data[CONF_CURRENCY]),
- self.config_entry.data[CONF_AREAS],
- )
- except NordPoolEmptyResponseError as error:
- LOGGER.debug("Empty response error: %s", error)
- self.async_set_update_error(error)
- return
- except NordPoolResponseError as error:
- LOGGER.debug("Response error: %s", error)
- self.async_set_update_error(error)
- return
- except NordPoolError as error:
- LOGGER.debug("Connection error: %s", error)
- self.async_set_update_error(error)
- return
-
- self.async_set_updated_data(data)
diff --git a/homeassistant/components/nordpool/diagnostics.py b/homeassistant/components/nordpool/diagnostics.py
deleted file mode 100644
index 3160c2bfa6d..00000000000
--- a/homeassistant/components/nordpool/diagnostics.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""Diagnostics support for Nord Pool."""
-
-from __future__ import annotations
-
-from typing import Any
-
-from homeassistant.core import HomeAssistant
-
-from . import NordPoolConfigEntry
-
-
-async def async_get_config_entry_diagnostics(
- hass: HomeAssistant, entry: NordPoolConfigEntry
-) -> dict[str, Any]:
- """Return diagnostics for Nord Pool config entry."""
- return {"raw": entry.runtime_data.data.raw}
diff --git a/homeassistant/components/nordpool/entity.py b/homeassistant/components/nordpool/entity.py
deleted file mode 100644
index 32240aad12c..00000000000
--- a/homeassistant/components/nordpool/entity.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""Base entity for Nord Pool."""
-
-from __future__ import annotations
-
-from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.entity import EntityDescription
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from .const import DOMAIN
-from .coordinator import NordPoolDataUpdateCoordinator
-
-
-class NordpoolBaseEntity(CoordinatorEntity[NordPoolDataUpdateCoordinator]):
- """Representation of a Nord Pool base entity."""
-
- _attr_has_entity_name = True
-
- def __init__(
- self,
- coordinator: NordPoolDataUpdateCoordinator,
- entity_description: EntityDescription,
- area: str,
- ) -> None:
- """Initiate Nord Pool base entity."""
- super().__init__(coordinator)
- self.entity_description = entity_description
- self._attr_unique_id = f"{area}-{entity_description.key}"
- self.area = area
- self._attr_device_info = DeviceInfo(
- identifiers={(DOMAIN, area)},
- name=f"Nord Pool {area}",
- )
diff --git a/homeassistant/components/nordpool/icons.json b/homeassistant/components/nordpool/icons.json
deleted file mode 100644
index 85434a2d09b..00000000000
--- a/homeassistant/components/nordpool/icons.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "entity": {
- "sensor": {
- "updated_at": {
- "default": "mdi:clock-outline"
- },
- "currency": {
- "default": "mdi:currency-usd"
- },
- "exchange_rate": {
- "default": "mdi:currency-usd"
- },
- "current_price": {
- "default": "mdi:cash"
- },
- "last_price": {
- "default": "mdi:cash"
- },
- "next_price": {
- "default": "mdi:cash"
- },
- "block_average": {
- "default": "mdi:cash-multiple"
- },
- "block_min": {
- "default": "mdi:cash-multiple"
- },
- "block_max": {
- "default": "mdi:cash-multiple"
- },
- "block_start_time": {
- "default": "mdi:clock-time-twelve-outline"
- },
- "block_end_time": {
- "default": "mdi:clock-time-two-outline"
- },
- "daily_average": {
- "default": "mdi:cash-multiple"
- }
- }
- }
-}
diff --git a/homeassistant/components/nordpool/manifest.json b/homeassistant/components/nordpool/manifest.json
deleted file mode 100644
index bf093eb3ee9..00000000000
--- a/homeassistant/components/nordpool/manifest.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "domain": "nordpool",
- "name": "Nord Pool",
- "codeowners": ["@gjohansson-ST"],
- "config_flow": true,
- "documentation": "https://www.home-assistant.io/integrations/nordpool",
- "integration_type": "hub",
- "iot_class": "cloud_polling",
- "loggers": ["pynordpool"],
- "requirements": ["pynordpool==0.2.2"],
- "single_config_entry": true
-}
diff --git a/homeassistant/components/nordpool/sensor.py b/homeassistant/components/nordpool/sensor.py
deleted file mode 100644
index e7e655a6657..00000000000
--- a/homeassistant/components/nordpool/sensor.py
+++ /dev/null
@@ -1,328 +0,0 @@
-"""Sensor platform for Nord Pool integration."""
-
-from __future__ import annotations
-
-from collections.abc import Callable
-from dataclasses import dataclass
-from datetime import datetime, timedelta
-
-from pynordpool import DeliveryPeriodData
-
-from homeassistant.components.sensor import (
- EntityCategory,
- SensorDeviceClass,
- SensorEntity,
- SensorEntityDescription,
- SensorStateClass,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.util import dt as dt_util, slugify
-
-from . import NordPoolConfigEntry
-from .const import LOGGER
-from .coordinator import NordPoolDataUpdateCoordinator
-from .entity import NordpoolBaseEntity
-
-PARALLEL_UPDATES = 0
-
-
-def get_prices(data: DeliveryPeriodData) -> dict[str, tuple[float, float, float]]:
- """Return previous, current and next prices.
-
- Output: {"SE3": (10.0, 10.5, 12.1)}
- """
- last_price_entries: dict[str, float] = {}
- current_price_entries: dict[str, float] = {}
- next_price_entries: dict[str, float] = {}
- current_time = dt_util.utcnow()
- previous_time = current_time - timedelta(hours=1)
- next_time = current_time + timedelta(hours=1)
- price_data = data.entries
- for entry in price_data:
- if entry.start <= current_time <= entry.end:
- current_price_entries = entry.entry
- if entry.start <= previous_time <= entry.end:
- last_price_entries = entry.entry
- if entry.start <= next_time <= entry.end:
- next_price_entries = entry.entry
-
- result = {}
- for area, price in current_price_entries.items():
- result[area] = (last_price_entries[area], price, next_price_entries[area])
- LOGGER.debug("Prices: %s", result)
- return result
-
-
-def get_blockprices(
- data: DeliveryPeriodData,
-) -> dict[str, dict[str, tuple[datetime, datetime, float, float, float]]]:
- """Return average, min and max for block prices.
-
- Output: {"SE3": {"Off-peak 1": (_datetime_, _datetime_, 9.3, 10.5, 12.1)}}
- """
- result: dict[str, dict[str, tuple[datetime, datetime, float, float, float]]] = {}
- block_prices = data.block_prices
- for entry in block_prices:
- for _area in entry.average:
- if _area not in result:
- result[_area] = {}
- result[_area][entry.name] = (
- entry.start,
- entry.end,
- entry.average[_area]["average"],
- entry.average[_area]["min"],
- entry.average[_area]["max"],
- )
-
- LOGGER.debug("Block prices: %s", result)
- return result
-
-
-@dataclass(frozen=True, kw_only=True)
-class NordpoolDefaultSensorEntityDescription(SensorEntityDescription):
- """Describes Nord Pool default sensor entity."""
-
- value_fn: Callable[[DeliveryPeriodData], str | float | datetime | None]
-
-
-@dataclass(frozen=True, kw_only=True)
-class NordpoolPricesSensorEntityDescription(SensorEntityDescription):
- """Describes Nord Pool prices sensor entity."""
-
- value_fn: Callable[[tuple[float, float, float]], float | None]
-
-
-@dataclass(frozen=True, kw_only=True)
-class NordpoolBlockPricesSensorEntityDescription(SensorEntityDescription):
- """Describes Nord Pool block prices sensor entity."""
-
- value_fn: Callable[
- [tuple[datetime, datetime, float, float, float]], float | datetime | None
- ]
-
-
-DEFAULT_SENSOR_TYPES: tuple[NordpoolDefaultSensorEntityDescription, ...] = (
- NordpoolDefaultSensorEntityDescription(
- key="updated_at",
- translation_key="updated_at",
- device_class=SensorDeviceClass.TIMESTAMP,
- value_fn=lambda data: data.updated_at,
- entity_category=EntityCategory.DIAGNOSTIC,
- ),
- NordpoolDefaultSensorEntityDescription(
- key="currency",
- translation_key="currency",
- value_fn=lambda data: data.currency,
- entity_category=EntityCategory.DIAGNOSTIC,
- ),
- NordpoolDefaultSensorEntityDescription(
- key="exchange_rate",
- translation_key="exchange_rate",
- value_fn=lambda data: data.exchange_rate,
- state_class=SensorStateClass.MEASUREMENT,
- entity_registry_enabled_default=False,
- entity_category=EntityCategory.DIAGNOSTIC,
- ),
-)
-PRICES_SENSOR_TYPES: tuple[NordpoolPricesSensorEntityDescription, ...] = (
- NordpoolPricesSensorEntityDescription(
- key="current_price",
- translation_key="current_price",
- value_fn=lambda data: data[1] / 1000,
- state_class=SensorStateClass.MEASUREMENT,
- suggested_display_precision=2,
- ),
- NordpoolPricesSensorEntityDescription(
- key="last_price",
- translation_key="last_price",
- value_fn=lambda data: data[0] / 1000,
- suggested_display_precision=2,
- ),
- NordpoolPricesSensorEntityDescription(
- key="next_price",
- translation_key="next_price",
- value_fn=lambda data: data[2] / 1000,
- suggested_display_precision=2,
- ),
-)
-BLOCK_PRICES_SENSOR_TYPES: tuple[NordpoolBlockPricesSensorEntityDescription, ...] = (
- NordpoolBlockPricesSensorEntityDescription(
- key="block_average",
- translation_key="block_average",
- value_fn=lambda data: data[2] / 1000,
- state_class=SensorStateClass.MEASUREMENT,
- suggested_display_precision=2,
- entity_registry_enabled_default=False,
- ),
- NordpoolBlockPricesSensorEntityDescription(
- key="block_min",
- translation_key="block_min",
- value_fn=lambda data: data[3] / 1000,
- state_class=SensorStateClass.MEASUREMENT,
- suggested_display_precision=2,
- entity_registry_enabled_default=False,
- ),
- NordpoolBlockPricesSensorEntityDescription(
- key="block_max",
- translation_key="block_max",
- value_fn=lambda data: data[4] / 1000,
- state_class=SensorStateClass.MEASUREMENT,
- suggested_display_precision=2,
- entity_registry_enabled_default=False,
- ),
- NordpoolBlockPricesSensorEntityDescription(
- key="block_start_time",
- translation_key="block_start_time",
- value_fn=lambda data: data[0],
- device_class=SensorDeviceClass.TIMESTAMP,
- entity_registry_enabled_default=False,
- ),
- NordpoolBlockPricesSensorEntityDescription(
- key="block_end_time",
- translation_key="block_end_time",
- value_fn=lambda data: data[1],
- device_class=SensorDeviceClass.TIMESTAMP,
- entity_registry_enabled_default=False,
- ),
-)
-DAILY_AVERAGE_PRICES_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
- SensorEntityDescription(
- key="daily_average",
- translation_key="daily_average",
- state_class=SensorStateClass.MEASUREMENT,
- suggested_display_precision=2,
- entity_registry_enabled_default=False,
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: NordPoolConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up Nord Pool sensor platform."""
-
- coordinator = entry.runtime_data
-
- entities: list[NordpoolBaseEntity] = []
- currency = entry.runtime_data.data.currency
-
- for area in get_prices(entry.runtime_data.data):
- LOGGER.debug("Setting up base sensors for area %s", area)
- entities.extend(
- NordpoolSensor(coordinator, description, area)
- for description in DEFAULT_SENSOR_TYPES
- )
- LOGGER.debug(
- "Setting up price sensors for area %s with currency %s", area, currency
- )
- entities.extend(
- NordpoolPriceSensor(coordinator, description, area, currency)
- for description in PRICES_SENSOR_TYPES
- )
- entities.extend(
- NordpoolDailyAveragePriceSensor(coordinator, description, area, currency)
- for description in DAILY_AVERAGE_PRICES_SENSOR_TYPES
- )
- for block_name in get_blockprices(coordinator.data)[area]:
- LOGGER.debug(
- "Setting up block price sensors for area %s with currency %s in block %s",
- area,
- currency,
- block_name,
- )
- entities.extend(
- NordpoolBlockPriceSensor(
- coordinator, description, area, currency, block_name
- )
- for description in BLOCK_PRICES_SENSOR_TYPES
- )
- async_add_entities(entities)
-
-
-class NordpoolSensor(NordpoolBaseEntity, SensorEntity):
- """Representation of a Nord Pool sensor."""
-
- entity_description: NordpoolDefaultSensorEntityDescription
-
- @property
- def native_value(self) -> str | float | datetime | None:
- """Return value of sensor."""
- return self.entity_description.value_fn(self.coordinator.data)
-
-
-class NordpoolPriceSensor(NordpoolBaseEntity, SensorEntity):
- """Representation of a Nord Pool price sensor."""
-
- entity_description: NordpoolPricesSensorEntityDescription
-
- def __init__(
- self,
- coordinator: NordPoolDataUpdateCoordinator,
- entity_description: NordpoolPricesSensorEntityDescription,
- area: str,
- currency: str,
- ) -> None:
- """Initiate Nord Pool sensor."""
- super().__init__(coordinator, entity_description, area)
- self._attr_native_unit_of_measurement = f"{currency}/kWh"
-
- @property
- def native_value(self) -> float | None:
- """Return value of sensor."""
- return self.entity_description.value_fn(
- get_prices(self.coordinator.data)[self.area]
- )
-
-
-class NordpoolBlockPriceSensor(NordpoolBaseEntity, SensorEntity):
- """Representation of a Nord Pool block price sensor."""
-
- entity_description: NordpoolBlockPricesSensorEntityDescription
-
- def __init__(
- self,
- coordinator: NordPoolDataUpdateCoordinator,
- entity_description: NordpoolBlockPricesSensorEntityDescription,
- area: str,
- currency: str,
- block_name: str,
- ) -> None:
- """Initiate Nord Pool sensor."""
- super().__init__(coordinator, entity_description, area)
- if entity_description.device_class is not SensorDeviceClass.TIMESTAMP:
- self._attr_native_unit_of_measurement = f"{currency}/kWh"
- self._attr_unique_id = f"{slugify(block_name)}-{area}-{entity_description.key}"
- self.block_name = block_name
- self._attr_translation_placeholders = {"block": block_name}
-
- @property
- def native_value(self) -> float | datetime | None:
- """Return value of sensor."""
- return self.entity_description.value_fn(
- get_blockprices(self.coordinator.data)[self.area][self.block_name]
- )
-
-
-class NordpoolDailyAveragePriceSensor(NordpoolBaseEntity, SensorEntity):
- """Representation of a Nord Pool daily average price sensor."""
-
- entity_description: SensorEntityDescription
-
- def __init__(
- self,
- coordinator: NordPoolDataUpdateCoordinator,
- entity_description: SensorEntityDescription,
- area: str,
- currency: str,
- ) -> None:
- """Initiate Nord Pool sensor."""
- super().__init__(coordinator, entity_description, area)
- self._attr_native_unit_of_measurement = f"{currency}/kWh"
-
- @property
- def native_value(self) -> float | None:
- """Return value of sensor."""
- return self.coordinator.data.area_average[self.area] / 1000
diff --git a/homeassistant/components/nordpool/strings.json b/homeassistant/components/nordpool/strings.json
deleted file mode 100644
index 59ba009eb90..00000000000
--- a/homeassistant/components/nordpool/strings.json
+++ /dev/null
@@ -1,65 +0,0 @@
-{
- "config": {
- "abort": {
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
- },
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "no_data": "API connected but the response was empty"
- },
- "step": {
- "user": {
- "data": {
- "currency": "Currency",
- "areas": "Areas"
- }
- },
- "reconfigure": {
- "data": {
- "currency": "[%key:component::nordpool::config::step::user::data::currency%]",
- "areas": "[%key:component::nordpool::config::step::user::data::areas%]"
- }
- }
- }
- },
- "entity": {
- "sensor": {
- "updated_at": {
- "name": "Last updated"
- },
- "currency": {
- "name": "Currency"
- },
- "exchange_rate": {
- "name": "Exchange rate"
- },
- "current_price": {
- "name": "Current price"
- },
- "last_price": {
- "name": "Previous price"
- },
- "next_price": {
- "name": "Next price"
- },
- "block_average": {
- "name": "{block} average"
- },
- "block_min": {
- "name": "{block} lowest price"
- },
- "block_max": {
- "name": "{block} highest price"
- },
- "block_start_time": {
- "name": "{block} time from"
- },
- "block_end_time": {
- "name": "{block} time until"
- },
- "daily_average": {
- "name": "Daily average"
- }
- }
- }
-}
diff --git a/homeassistant/components/notify/strings.json b/homeassistant/components/notify/strings.json
index b7d4ec1ad25..d1deca0a6c4 100644
--- a/homeassistant/components/notify/strings.json
+++ b/homeassistant/components/notify/strings.json
@@ -74,7 +74,7 @@
}
},
"migrate_notify_service": {
- "title": "Legacy action notify.{service_name} still being used",
+ "title": "Legacy action notify.{service_name} stll being used",
"fix_flow": {
"step": {
"confirm": {
diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py
index f7347a8f595..c803992c2e2 100644
--- a/homeassistant/components/notion/config_flow.py
+++ b/homeassistant/components/notion/config_flow.py
@@ -9,7 +9,7 @@ from typing import Any
from aionotion.errors import InvalidCredentialsError, NotionError
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
@@ -68,29 +68,36 @@ class NotionFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ def __init__(self) -> None:
+ """Initialize."""
+ self._reauth_entry: ConfigEntry | None = None
+
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle re-auth completion."""
+ assert self._reauth_entry
- reauth_entry = self._get_reauth_entry()
if not user_input:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=REAUTH_SCHEMA,
description_placeholders={
- CONF_USERNAME: reauth_entry.data[CONF_USERNAME]
+ CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME]
},
)
credentials_validation_result = await async_validate_credentials(
- self.hass, reauth_entry.data[CONF_USERNAME], user_input[CONF_PASSWORD]
+ self.hass, self._reauth_entry.data[CONF_USERNAME], user_input[CONF_PASSWORD]
)
if credentials_validation_result.errors:
@@ -99,16 +106,19 @@ class NotionFlowHandler(ConfigFlow, domain=DOMAIN):
data_schema=REAUTH_SCHEMA,
errors=credentials_validation_result.errors,
description_placeholders={
- CONF_USERNAME: reauth_entry.data[CONF_USERNAME]
+ CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME]
},
)
- return self.async_update_reload_and_abort(
- reauth_entry,
- data_updates={
- CONF_REFRESH_TOKEN: credentials_validation_result.refresh_token
- },
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry,
+ data=self._reauth_entry.data
+ | {CONF_REFRESH_TOKEN: credentials_validation_result.refresh_token},
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
async def async_step_user(
self, user_input: dict[str, str] | None = None
diff --git a/homeassistant/components/nsw_fuel_station/__init__.py b/homeassistant/components/nsw_fuel_station/__init__.py
index 85e204b6f51..76dc9d4c6ff 100644
--- a/homeassistant/components/nsw_fuel_station/__init__.py
+++ b/homeassistant/components/nsw_fuel_station/__init__.py
@@ -33,7 +33,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=None,
name="sensor",
update_interval=SCAN_INTERVAL,
update_method=async_update_data,
diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py
index fb17e6b45bf..fdb49688eba 100644
--- a/homeassistant/components/nuheat/__init__.py
+++ b/homeassistant/components/nuheat/__init__.py
@@ -60,7 +60,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"nuheat {serial_number}",
update_method=_async_update_data,
update_interval=timedelta(minutes=5),
diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py
index 23e3ce0910b..ad95c9b5358 100644
--- a/homeassistant/components/number/const.py
+++ b/homeassistant/components/number/const.py
@@ -17,7 +17,6 @@ from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfApparentPower,
- UnitOfBloodGlucoseConcentration,
UnitOfConductivity,
UnitOfDataRate,
UnitOfElectricCurrent,
@@ -110,12 +109,6 @@ class NumberDeviceClass(StrEnum):
Unit of measurement: `%`
"""
- BLOOD_GLUCOSE_CONCENTRATION = "blood_glucose_concentration"
- """Blood glucose concentration.
-
- Unit of measurement: `mg/dL`, `mmol/L`
- """
-
CO = "carbon_monoxide"
"""Carbon Monoxide gas concentration.
@@ -169,7 +162,7 @@ class NumberDeviceClass(StrEnum):
ENERGY = "energy"
"""Energy.
- Unit of measurement: `Wh`, `kWh`, `MWh`, `GWh`, `TWh`, `MJ`, `GJ`
+ Unit of measurement: `Wh`, `kWh`, `MWh`, `MJ`, `GJ`
"""
ENERGY_STORAGE = "energy_storage"
@@ -178,7 +171,7 @@ class NumberDeviceClass(StrEnum):
Use this device class for sensors measuring stored energy, for example the amount
of electric energy currently stored in a battery or the capacity of a battery.
- Unit of measurement: `Wh`, `kWh`, `MWh`, `GWh`, `TWh`, `MJ`, `GJ`
+ Unit of measurement: `Wh`, `kWh`, `MWh`, `MJ`, `GJ`
"""
FREQUENCY = "frequency"
@@ -286,7 +279,7 @@ class NumberDeviceClass(StrEnum):
POWER = "power"
"""Power.
- Unit of measurement: `W`, `kW`, `MW`, `GW`, `TW`
+ Unit of measurement: `W`, `kW`
"""
PRECIPITATION = "precipitation"
@@ -436,7 +429,6 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.AQI: {None},
NumberDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure),
NumberDeviceClass.BATTERY: {PERCENTAGE},
- NumberDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),
NumberDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION},
NumberDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION},
NumberDeviceClass.CONDUCTIVITY: set(UnitOfConductivity),
diff --git a/homeassistant/components/number/icons.json b/homeassistant/components/number/icons.json
index 5e0fc6e44d2..a122aaecb09 100644
--- a/homeassistant/components/number/icons.json
+++ b/homeassistant/components/number/icons.json
@@ -15,9 +15,6 @@
"battery": {
"default": "mdi:battery"
},
- "blood_glucose_concentration": {
- "default": "mdi:spoon-sugar"
- },
"carbon_dioxide": {
"default": "mdi:molecule-co2"
},
diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json
index b9aec880ecc..580385172e3 100644
--- a/homeassistant/components/number/strings.json
+++ b/homeassistant/components/number/strings.json
@@ -43,9 +43,6 @@
"battery": {
"name": "[%key:component::sensor::entity_component::battery::name%]"
},
- "blood_glucose_concentration": {
- "name": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]"
- },
"carbon_dioxide": {
"name": "[%key:component::sensor::entity_component::carbon_dioxide::name%]"
},
diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py
index 169dbbbff5d..2ce67c76649 100644
--- a/homeassistant/components/nut/__init__.py
+++ b/homeassistant/components/nut/__init__.py
@@ -86,7 +86,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: NutConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name="NUT resource status",
update_method=async_update_data,
update_interval=timedelta(seconds=scan_interval),
@@ -130,10 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: NutConfigEntry) -> bool:
name=data.name.title(),
manufacturer=data.device_info.manufacturer,
model=data.device_info.model,
- model_id=data.device_info.model_id,
sw_version=data.device_info.firmware,
- serial_number=data.device_info.serial,
- suggested_area=data.device_info.device_location,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -211,10 +207,7 @@ class NUTDeviceInfo:
manufacturer: str | None = None
model: str | None = None
- model_id: str | None = None
firmware: str | None = None
- serial: str | None = None
- device_location: str | None = None
class PyNUTData:
@@ -273,13 +266,8 @@ class PyNUTData:
manufacturer = _manufacturer_from_status(self._status)
model = _model_from_status(self._status)
- model_id: str | None = self._status.get("device.part")
firmware = _firmware_from_status(self._status)
- serial = _serial_from_status(self._status)
- device_location: str | None = self._status.get("device.location")
- return NUTDeviceInfo(
- manufacturer, model, model_id, firmware, serial, device_location
- )
+ return NUTDeviceInfo(manufacturer, model, firmware)
async def _async_get_status(self) -> dict[str, str]:
"""Get the ups status from NUT."""
diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py
index 966c51e98e9..d0a2da124a6 100644
--- a/homeassistant/components/nut/config_flow.py
+++ b/homeassistant/components/nut/config_flow.py
@@ -235,12 +235,16 @@ class NutConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Handle a option flow for nut."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py
index bb702873052..7f211d5452b 100644
--- a/homeassistant/components/nut/sensor.py
+++ b/homeassistant/components/nut/sensor.py
@@ -15,7 +15,6 @@ from homeassistant.components.sensor import (
from homeassistant.const import (
ATTR_MANUFACTURER,
ATTR_MODEL,
- ATTR_SERIAL_NUMBER,
ATTR_SW_VERSION,
PERCENTAGE,
STATE_UNKNOWN,
@@ -43,7 +42,6 @@ NUT_DEV_INFO_TO_DEV_INFO: dict[str, str] = {
"manufacturer": ATTR_MANUFACTURER,
"model": ATTR_MODEL,
"firmware": ATTR_SW_VERSION,
- "serial": ATTR_SERIAL_NUMBER,
}
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py
index c700476ed3d..2e643d7dbc6 100644
--- a/homeassistant/components/nws/__init__.py
+++ b/homeassistant/components/nws/__init__.py
@@ -110,7 +110,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: NWSConfigEntry) -> bool:
coordinator_forecast = TimestampDataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"NWS forecast station {station}",
update_method=async_setup_update_forecast(0, 0),
update_interval=DEFAULT_SCAN_INTERVAL,
@@ -122,7 +121,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: NWSConfigEntry) -> bool:
coordinator_forecast_hourly = TimestampDataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=f"NWS forecast hourly station {station}",
update_method=async_setup_update_forecast_hourly(0, 0),
update_interval=DEFAULT_SCAN_INTERVAL,
diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py
index 6622eec530f..61de4f611b8 100644
--- a/homeassistant/components/nx584/alarm_control_panel.py
+++ b/homeassistant/components/nx584/alarm_control_panel.py
@@ -13,10 +13,17 @@ from homeassistant.components.alarm_control_panel import (
PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA,
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
-from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_NAME,
+ CONF_PORT,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv, entity_platform
@@ -88,6 +95,7 @@ class NX584Alarm(AlarmControlPanelEntity):
"""Representation of a NX584-based alarm panel."""
_attr_code_format = CodeFormat.NUMBER
+ _attr_state: str | None
_attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
@@ -110,11 +118,11 @@ class NX584Alarm(AlarmControlPanelEntity):
"Unable to connect to %(host)s: %(reason)s",
{"host": self._url, "reason": ex},
)
- self._attr_alarm_state = None
+ self._attr_state = None
zones = []
except IndexError:
_LOGGER.error("NX584 reports no partitions")
- self._attr_alarm_state = None
+ self._attr_state = None
zones = []
bypassed = False
@@ -128,15 +136,15 @@ class NX584Alarm(AlarmControlPanelEntity):
break
if not part["armed"]:
- self._attr_alarm_state = AlarmControlPanelState.DISARMED
+ self._attr_state = STATE_ALARM_DISARMED
elif bypassed:
- self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
+ self._attr_state = STATE_ALARM_ARMED_HOME
else:
- self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
+ self._attr_state = STATE_ALARM_ARMED_AWAY
for flag in part["condition_flags"]:
if flag == "Siren on":
- self._attr_alarm_state = AlarmControlPanelState.TRIGGERED
+ self._attr_state = STATE_ALARM_TRIGGERED
def alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
diff --git a/homeassistant/components/nyt_games/manifest.json b/homeassistant/components/nyt_games/manifest.json
index c32de754782..a2cd5629ed1 100644
--- a/homeassistant/components/nyt_games/manifest.json
+++ b/homeassistant/components/nyt_games/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nyt_games",
"integration_type": "service",
"iot_class": "cloud_polling",
- "requirements": ["nyt_games==0.4.4"]
+ "requirements": ["nyt_games==0.4.3"]
}
diff --git a/homeassistant/components/nyt_games/sensor.py b/homeassistant/components/nyt_games/sensor.py
index 01b2db4620b..57759fb354d 100644
--- a/homeassistant/components/nyt_games/sensor.py
+++ b/homeassistant/components/nyt_games/sensor.py
@@ -139,7 +139,7 @@ CONNECTIONS_SENSORS: tuple[NYTGamesConnectionsSensorEntityDescription, ...] = (
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfTime.DAYS,
device_class=SensorDeviceClass.DURATION,
- value_fn=lambda connections: connections.max_streak,
+ value_fn=lambda connections: connections.current_streak,
),
)
diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py
index a99d3d3f328..47d35f32f9f 100644
--- a/homeassistant/components/nzbget/config_flow.py
+++ b/homeassistant/components/nzbget/config_flow.py
@@ -50,6 +50,9 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initiated by the user."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
errors = {}
if user_input is not None:
diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json
index 60e90e372ff..34f6f37873b 100644
--- a/homeassistant/components/nzbget/manifest.json
+++ b/homeassistant/components/nzbget/manifest.json
@@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nzbget",
"iot_class": "local_polling",
"loggers": ["pynzbgetapi"],
- "requirements": ["pynzbgetapi==0.2.0"],
- "single_config_entry": true
+ "requirements": ["pynzbgetapi==0.2.0"]
}
diff --git a/homeassistant/components/nzbget/strings.json b/homeassistant/components/nzbget/strings.json
index 84a2ed0b821..4da9a0b505e 100644
--- a/homeassistant/components/nzbget/strings.json
+++ b/homeassistant/components/nzbget/strings.json
@@ -19,6 +19,7 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
diff --git a/homeassistant/components/ollama/config_flow.py b/homeassistant/components/ollama/config_flow.py
index 1024a824c25..65b8efaf525 100644
--- a/homeassistant/components/ollama/config_flow.py
+++ b/homeassistant/components/ollama/config_flow.py
@@ -207,8 +207,9 @@ class OllamaOptionsFlow(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
- self.url: str = config_entry.data[CONF_URL]
- self.model: str = config_entry.data[CONF_MODEL]
+ self.config_entry = config_entry
+ self.url: str = self.config_entry.data[CONF_URL]
+ self.model: str = self.config_entry.data[CONF_MODEL]
async def async_step_init(
self, user_input: dict[str, Any] | None = None
diff --git a/homeassistant/components/ollama/const.py b/homeassistant/components/ollama/const.py
index 69c0a3d6296..6152b223d6d 100644
--- a/homeassistant/components/ollama/const.py
+++ b/homeassistant/components/ollama/const.py
@@ -24,12 +24,8 @@ MAX_HISTORY_SECONDS = 60 * 60 # 1 hour
MODEL_NAMES = [ # https://ollama.com/library
"alfred",
"all-minilm",
- "aya-expanse",
"aya",
"bakllava",
- "bespoke-minicheck",
- "bge-large",
- "bge-m3",
"codebooga",
"codegeex4",
"codegemma",
@@ -37,19 +33,18 @@ MODEL_NAMES = [ # https://ollama.com/library
"codeqwen",
"codestral",
"codeup",
- "command-r-plus",
"command-r",
+ "command-r-plus",
"dbrx",
- "deepseek-coder-v2",
"deepseek-coder",
+ "deepseek-coder-v2",
"deepseek-llm",
- "deepseek-v2.5",
"deepseek-v2",
+ "dolphincoder",
"dolphin-llama3",
"dolphin-mistral",
"dolphin-mixtral",
"dolphin-phi",
- "dolphincoder",
"duckdb-nsql",
"everythinglm",
"falcon",
@@ -60,97 +55,74 @@ MODEL_NAMES = [ # https://ollama.com/library
"glm4",
"goliath",
"granite-code",
- "granite3-dense",
- "granite3-guardian" "granite3-moe",
- "hermes3",
"internlm2",
- "llama-guard3",
- "llama-pro",
+ "llama2",
"llama2-chinese",
"llama2-uncensored",
- "llama2",
+ "llama3",
"llama3-chatqa",
"llama3-gradient",
"llama3-groq-tool-use",
- "llama3.1",
- "llama3.2",
- "llama3",
+ "llama-pro",
+ "llava",
"llava-llama3",
"llava-phi3",
- "llava",
"magicoder",
"mathstral",
"meditron",
"medllama2",
"megadolphin",
- "minicpm-v",
- "mistral-large",
- "mistral-nemo",
- "mistral-openorca",
- "mistral-small",
"mistral",
"mistrallite",
+ "mistral-nemo",
+ "mistral-openorca",
"mixtral",
"moondream",
"mxbai-embed-large",
- "nemotron-mini",
- "nemotron",
"neural-chat",
"nexusraven",
"nomic-embed-text",
"notus",
"notux",
"nous-hermes",
- "nous-hermes2-mixtral",
"nous-hermes2",
+ "nous-hermes2-mixtral",
"nuextract",
- "open-orca-platypus2",
"openchat",
"openhermes",
- "orca-mini",
+ "open-orca-platypus2",
"orca2",
- "paraphrase-multilingual",
+ "orca-mini",
"phi",
- "phi3.5",
"phi3",
"phind-codellama",
"qwen",
- "qwen2-math",
- "qwen2.5-coder",
- "qwen2.5",
"qwen2",
- "reader-lm",
- "reflection",
"samantha-mistral",
- "shieldgemma",
- "smollm",
- "smollm2",
"snowflake-arctic-embed",
- "solar-pro",
"solar",
"sqlcoder",
"stable-beluga",
"stable-code",
- "stablelm-zephyr",
"stablelm2",
+ "stablelm-zephyr",
"starcoder",
"starcoder2",
"starling-lm",
"tinydolphin",
"tinyllama",
"vicuna",
- "wizard-math",
- "wizard-vicuna-uncensored",
- "wizard-vicuna",
"wizardcoder",
- "wizardlm-uncensored",
"wizardlm",
"wizardlm2",
+ "wizardlm-uncensored",
+ "wizard-math",
+ "wizard-vicuna",
+ "wizard-vicuna-uncensored",
"xwinlm",
"yarn-llama2",
"yarn-mistral",
- "yi-coder",
"yi",
"zephyr",
]
-DEFAULT_MODEL = "llama3.2:latest"
+DEFAULT_MODEL = "llama3.1:latest"
diff --git a/homeassistant/components/ollama/strings.json b/homeassistant/components/ollama/strings.json
index 248cac34f11..c307f160228 100644
--- a/homeassistant/components/ollama/strings.json
+++ b/homeassistant/components/ollama/strings.json
@@ -11,11 +11,9 @@
"title": "Downloading model"
}
},
- "abort": {
- "download_failed": "Model downloading failed"
- },
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "download_failed": "Model downloading failed",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"progress": {
diff --git a/homeassistant/components/omnilogic/config_flow.py b/homeassistant/components/omnilogic/config_flow.py
index dfbd010ea98..77bca0039a9 100644
--- a/homeassistant/components/omnilogic/config_flow.py
+++ b/homeassistant/components/omnilogic/config_flow.py
@@ -34,7 +34,7 @@ class OmniLogicConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -42,6 +42,12 @@ class OmniLogicConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors: dict[str, str] = {}
+ config_entry = self._async_current_entries()
+ if config_entry:
+ return self.async_abort(reason="single_instance_allowed")
+
+ errors = {}
+
if user_input is not None:
username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
@@ -78,6 +84,10 @@ class OmniLogicConfigFlow(ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(OptionsFlow):
"""Handle Omnilogic client options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/omnilogic/manifest.json b/homeassistant/components/omnilogic/manifest.json
index 361a15e2d9c..252718d2c21 100644
--- a/homeassistant/components/omnilogic/manifest.json
+++ b/homeassistant/components/omnilogic/manifest.json
@@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/omnilogic",
"iot_class": "cloud_polling",
"loggers": ["config", "omnilogic"],
- "requirements": ["omnilogic==0.4.5"],
- "single_config_entry": true
+ "requirements": ["omnilogic==0.4.5"]
}
diff --git a/homeassistant/components/omnilogic/strings.json b/homeassistant/components/omnilogic/strings.json
index 5b193b7f5ba..454644be244 100644
--- a/homeassistant/components/omnilogic/strings.json
+++ b/homeassistant/components/omnilogic/strings.json
@@ -14,7 +14,8 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"options": {
diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py
index b33440a9eb7..1ecfc10d974 100644
--- a/homeassistant/components/onboarding/views.py
+++ b/homeassistant/components/onboarding/views.py
@@ -20,7 +20,6 @@ from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import area_registry as ar
-from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.helpers.translation import async_get_translations
from homeassistant.setup import async_setup_component
@@ -217,7 +216,7 @@ class CoreConfigOnboardingView(_BaseOnboardingView):
from homeassistant.components import hassio
if (
- is_hassio(hass)
+ hassio.is_hassio(hass)
and (core_info := hassio.get_core_info(hass))
and "raspberrypi" in core_info["machine"]
):
diff --git a/homeassistant/components/oncue/__init__.py b/homeassistant/components/oncue/__init__.py
index 19d134a398f..53443b9ed81 100644
--- a/homeassistant/components/oncue/__init__.py
+++ b/homeassistant/components/oncue/__init__.py
@@ -43,7 +43,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: OncueConfigEntry) -> boo
coordinator = DataUpdateCoordinator[dict[str, OncueDevice]](
hass,
_LOGGER,
- config_entry=entry,
name=f"Oncue {entry.data[CONF_USERNAME]}",
update_interval=timedelta(minutes=10),
update_method=_async_update,
diff --git a/homeassistant/components/oncue/config_flow.py b/homeassistant/components/oncue/config_flow.py
index 872fe84350b..92cd037734e 100644
--- a/homeassistant/components/oncue/config_flow.py
+++ b/homeassistant/components/oncue/config_flow.py
@@ -9,7 +9,7 @@ from typing import Any
from aiooncue import LoginFailedException, Oncue
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -23,6 +23,10 @@ class OncueConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ def __init__(self) -> None:
+ """Initialize the oncue config flow."""
+ self.reauth_entry: ConfigEntry | None = None
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -76,6 +80,8 @@ class OncueConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth."""
+ entry_id = self.context["entry_id"]
+ self.reauth_entry = self.hass.config_entries.async_get_entry(entry_id)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -83,15 +89,18 @@ class OncueConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle reauth input."""
errors: dict[str, str] = {}
- reauth_entry = self._get_reauth_entry()
- existing_data = reauth_entry.data
+ existing_entry = self.reauth_entry
+ assert existing_entry
+ existing_data = existing_entry.data
description_placeholders: dict[str, str] = {
CONF_USERNAME: existing_data[CONF_USERNAME]
}
if user_input is not None:
new_config = {**existing_data, CONF_PASSWORD: user_input[CONF_PASSWORD]}
if not (errors := await self._async_validate_or_error(new_config)):
- return self.async_update_reload_and_abort(reauth_entry, data=new_config)
+ return self.async_update_reload_and_abort(
+ existing_entry, data=new_config
+ )
return self.async_show_form(
description_placeholders=description_placeholders,
diff --git a/homeassistant/components/ondilo_ico/config_flow.py b/homeassistant/components/ondilo_ico/config_flow.py
index fe0b89e7258..d65c1b15e2a 100644
--- a/homeassistant/components/ondilo_ico/config_flow.py
+++ b/homeassistant/components/ondilo_ico/config_flow.py
@@ -21,6 +21,9 @@ class OndiloIcoOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Handle a flow initialized by the user."""
await self.async_set_unique_id(DOMAIN)
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
self.async_register_implementation(
self.hass,
OndiloOauth2Implementation(self.hass),
diff --git a/homeassistant/components/ondilo_ico/manifest.json b/homeassistant/components/ondilo_ico/manifest.json
index 84862a89fbb..2f522f1b77c 100644
--- a/homeassistant/components/ondilo_ico/manifest.json
+++ b/homeassistant/components/ondilo_ico/manifest.json
@@ -8,6 +8,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["ondilo"],
- "requirements": ["ondilo==0.5.0"],
- "single_config_entry": true
+ "requirements": ["ondilo==0.5.0"]
}
diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py
index abb4c884974..a217674e3b4 100644
--- a/homeassistant/components/onewire/config_flow.py
+++ b/homeassistant/components/onewire/config_flow.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-from copy import deepcopy
from typing import Any
import voluptuous as vol
@@ -11,7 +10,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
@@ -101,14 +100,12 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> OnewireOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OnewireOptionsFlowHandler:
"""Get the options flow for this handler."""
return OnewireOptionsFlowHandler(config_entry)
-class OnewireOptionsFlowHandler(OptionsFlow):
+class OnewireOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle OneWire Config options."""
configurable_devices: dict[str, str]
@@ -126,10 +123,6 @@ class OnewireOptionsFlowHandler(OptionsFlow):
current_device: str
"""Friendly name of the currently selected device."""
- def __init__(self, config_entry: ConfigEntry) -> None:
- """Initialize options flow."""
- self.options = deepcopy(dict(config_entry.options))
-
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/onkyo/__init__.py b/homeassistant/components/onkyo/__init__.py
index fd5c0ba634a..02c026d1973 100644
--- a/homeassistant/components/onkyo/__init__.py
+++ b/homeassistant/components/onkyo/__init__.py
@@ -1,76 +1 @@
"""The onkyo component."""
-
-from dataclasses import dataclass
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST, Platform
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.typing import ConfigType
-
-from .const import DOMAIN, OPTION_INPUT_SOURCES, InputSource
-from .receiver import Receiver, async_interview
-from .services import DATA_MP_ENTITIES, async_register_services
-
-PLATFORMS = [Platform.MEDIA_PLAYER]
-
-CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
-
-
-@dataclass
-class OnkyoData:
- """Config Entry data."""
-
- receiver: Receiver
- sources: dict[InputSource, str]
-
-
-type OnkyoConfigEntry = ConfigEntry[OnkyoData]
-
-
-async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
- """Set up Onkyo component."""
- await async_register_services(hass)
- return True
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: OnkyoConfigEntry) -> bool:
- """Set up the Onkyo config entry."""
- entry.async_on_unload(entry.add_update_listener(update_listener))
-
- host = entry.data[CONF_HOST]
-
- info = await async_interview(host)
- if info is None:
- raise ConfigEntryNotReady(f"Unable to connect to: {host}")
-
- receiver = await Receiver.async_create(info)
-
- sources_store: dict[str, str] = entry.options[OPTION_INPUT_SOURCES]
- sources = {InputSource(k): v for k, v in sources_store.items()}
-
- entry.runtime_data = OnkyoData(receiver, sources)
-
- await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
-
- await receiver.conn.connect()
-
- return True
-
-
-async def async_unload_entry(hass: HomeAssistant, entry: OnkyoConfigEntry) -> bool:
- """Unload Onkyo config entry."""
- del hass.data[DATA_MP_ENTITIES][entry.entry_id]
-
- unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
-
- receiver = entry.runtime_data.receiver
- receiver.conn.close()
-
- return unload_ok
-
-
-async def update_listener(hass: HomeAssistant, entry: OnkyoConfigEntry) -> None:
- """Handle options update."""
- await hass.config_entries.async_reload(entry.entry_id)
diff --git a/homeassistant/components/onkyo/config_flow.py b/homeassistant/components/onkyo/config_flow.py
deleted file mode 100644
index a8ced6fae64..00000000000
--- a/homeassistant/components/onkyo/config_flow.py
+++ /dev/null
@@ -1,371 +0,0 @@
-"""Config flow for Onkyo."""
-
-import logging
-from typing import Any
-
-import voluptuous as vol
-
-from homeassistant.config_entries import (
- SOURCE_RECONFIGURE,
- ConfigEntry,
- ConfigFlow,
- ConfigFlowResult,
- OptionsFlow,
-)
-from homeassistant.const import CONF_HOST, CONF_NAME
-from homeassistant.core import callback
-from homeassistant.helpers.selector import (
- NumberSelector,
- NumberSelectorConfig,
- NumberSelectorMode,
- Selector,
- SelectSelector,
- SelectSelectorConfig,
- SelectSelectorMode,
- TextSelector,
-)
-
-from .const import (
- CONF_RECEIVER_MAX_VOLUME,
- CONF_SOURCES,
- DOMAIN,
- OPTION_INPUT_SOURCES,
- OPTION_MAX_VOLUME,
- OPTION_MAX_VOLUME_DEFAULT,
- OPTION_VOLUME_RESOLUTION,
- OPTION_VOLUME_RESOLUTION_DEFAULT,
- VOLUME_RESOLUTION_ALLOWED,
- InputSource,
-)
-from .receiver import ReceiverInfo, async_discover, async_interview
-
-_LOGGER = logging.getLogger(__name__)
-
-CONF_DEVICE = "device"
-
-INPUT_SOURCES_ALL_MEANINGS = [
- input_source.value_meaning for input_source in InputSource
-]
-STEP_MANUAL_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
-STEP_CONFIGURE_SCHEMA = vol.Schema(
- {
- vol.Required(OPTION_VOLUME_RESOLUTION): vol.In(VOLUME_RESOLUTION_ALLOWED),
- vol.Required(OPTION_INPUT_SOURCES): SelectSelector(
- SelectSelectorConfig(
- options=INPUT_SOURCES_ALL_MEANINGS,
- multiple=True,
- mode=SelectSelectorMode.DROPDOWN,
- )
- ),
- }
-)
-
-
-class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
- """Onkyo config flow."""
-
- _receiver_info: ReceiverInfo
- _discovered_infos: dict[str, ReceiverInfo]
-
- async def async_step_user(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle a flow initialized by the user."""
- return self.async_show_menu(
- step_id="user", menu_options=["manual", "eiscp_discovery"]
- )
-
- async def async_step_manual(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle manual device entry."""
- errors = {}
-
- if user_input is not None:
- host = user_input[CONF_HOST]
- _LOGGER.debug("Config flow start manual: %s", host)
- try:
- info = await async_interview(host)
- except Exception:
- _LOGGER.exception("Unexpected exception")
- errors["base"] = "unknown"
- else:
- if info is None:
- errors["base"] = "cannot_connect"
- else:
- self._receiver_info = info
-
- await self.async_set_unique_id(
- info.identifier, raise_on_progress=False
- )
- if self.source == SOURCE_RECONFIGURE:
- self._abort_if_unique_id_mismatch()
- else:
- self._abort_if_unique_id_configured()
-
- return await self.async_step_configure_receiver()
-
- suggested_values = user_input
- if suggested_values is None and self.source == SOURCE_RECONFIGURE:
- suggested_values = {
- CONF_HOST: self._get_reconfigure_entry().data[CONF_HOST]
- }
-
- return self.async_show_form(
- step_id="manual",
- data_schema=self.add_suggested_values_to_schema(
- STEP_MANUAL_SCHEMA, suggested_values
- ),
- errors=errors,
- )
-
- async def async_step_eiscp_discovery(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Start eiscp discovery and handle user device selection."""
- if user_input is not None:
- self._receiver_info = self._discovered_infos[user_input[CONF_DEVICE]]
- await self.async_set_unique_id(
- self._receiver_info.identifier, raise_on_progress=False
- )
- self._abort_if_unique_id_configured(
- updates={CONF_HOST: self._receiver_info.host}
- )
- return await self.async_step_configure_receiver()
-
- _LOGGER.debug("Config flow start eiscp discovery")
-
- try:
- infos = await async_discover()
- except Exception:
- _LOGGER.exception("Unexpected exception")
- return self.async_abort(reason="unknown")
-
- _LOGGER.debug("Discovered devices: %s", infos)
-
- self._discovered_infos = {}
- discovered_names = {}
- current_unique_ids = self._async_current_ids()
- for info in infos:
- if info.identifier in current_unique_ids:
- continue
- self._discovered_infos[info.identifier] = info
- device_name = f"{info.model_name} ({info.host})"
- discovered_names[info.identifier] = device_name
-
- _LOGGER.debug("Discovered new devices: %s", self._discovered_infos)
-
- if not discovered_names:
- return self.async_abort(reason="no_devices_found")
-
- return self.async_show_form(
- step_id="eiscp_discovery",
- data_schema=vol.Schema(
- {vol.Required(CONF_DEVICE): vol.In(discovered_names)}
- ),
- )
-
- async def async_step_configure_receiver(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle the configuration of a single receiver."""
- errors = {}
-
- entry = None
- entry_options = None
- if self.source == SOURCE_RECONFIGURE:
- entry = self._get_reconfigure_entry()
- entry_options = entry.options
-
- if user_input is not None:
- source_meanings: list[str] = user_input[OPTION_INPUT_SOURCES]
- if not source_meanings:
- errors[OPTION_INPUT_SOURCES] = "empty_input_source_list"
- else:
- sources_store: dict[str, str] = {}
- for source_meaning in source_meanings:
- source = InputSource.from_meaning(source_meaning)
-
- source_name = source_meaning
- if entry_options is not None:
- source_name = entry_options[OPTION_INPUT_SOURCES].get(
- source.value, source_name
- )
- sources_store[source.value] = source_name
-
- volume_resolution = user_input[OPTION_VOLUME_RESOLUTION]
-
- if entry_options is None:
- result = self.async_create_entry(
- title=self._receiver_info.model_name,
- data={
- CONF_HOST: self._receiver_info.host,
- },
- options={
- OPTION_VOLUME_RESOLUTION: volume_resolution,
- OPTION_MAX_VOLUME: OPTION_MAX_VOLUME_DEFAULT,
- OPTION_INPUT_SOURCES: sources_store,
- },
- )
- else:
- assert entry is not None
- result = self.async_update_reload_and_abort(
- entry,
- data={
- CONF_HOST: self._receiver_info.host,
- },
- options={
- OPTION_VOLUME_RESOLUTION: volume_resolution,
- OPTION_MAX_VOLUME: entry_options[OPTION_MAX_VOLUME],
- OPTION_INPUT_SOURCES: sources_store,
- },
- )
-
- _LOGGER.debug("Configured receiver, result: %s", result)
- return result
-
- _LOGGER.debug("Configuring receiver, info: %s", self._receiver_info)
-
- suggested_values = user_input
- if suggested_values is None:
- if entry_options is None:
- suggested_values = {
- OPTION_VOLUME_RESOLUTION: OPTION_VOLUME_RESOLUTION_DEFAULT,
- OPTION_INPUT_SOURCES: [],
- }
- else:
- suggested_values = {
- OPTION_VOLUME_RESOLUTION: entry_options[OPTION_VOLUME_RESOLUTION],
- OPTION_INPUT_SOURCES: [
- InputSource(input_source).value_meaning
- for input_source in entry_options[OPTION_INPUT_SOURCES]
- ],
- }
-
- return self.async_show_form(
- step_id="configure_receiver",
- data_schema=self.add_suggested_values_to_schema(
- STEP_CONFIGURE_SCHEMA, suggested_values
- ),
- errors=errors,
- description_placeholders={
- "name": f"{self._receiver_info.model_name} ({self._receiver_info.host})"
- },
- )
-
- async def async_step_reconfigure(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle reconfiguration of the receiver."""
- return await self.async_step_manual()
-
- async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
- """Import the yaml config."""
- _LOGGER.debug("Import flow user input: %s", user_input)
-
- host: str = user_input[CONF_HOST]
- name: str | None = user_input.get(CONF_NAME)
- user_max_volume: int = user_input[OPTION_MAX_VOLUME]
- user_volume_resolution: int = user_input[CONF_RECEIVER_MAX_VOLUME]
- user_sources: dict[InputSource, str] = user_input[CONF_SOURCES]
-
- info: ReceiverInfo | None = user_input.get("info")
- if info is None:
- try:
- info = await async_interview(host)
- except Exception:
- _LOGGER.exception("Import flow interview error for host %s", host)
- return self.async_abort(reason="cannot_connect")
-
- if info is None:
- _LOGGER.error("Import flow interview error for host %s", host)
- return self.async_abort(reason="cannot_connect")
-
- unique_id = info.identifier
- await self.async_set_unique_id(unique_id)
- self._abort_if_unique_id_configured()
-
- name = name or info.model_name
-
- volume_resolution = VOLUME_RESOLUTION_ALLOWED[-1]
- for volume_resolution_allowed in VOLUME_RESOLUTION_ALLOWED:
- if user_volume_resolution <= volume_resolution_allowed:
- volume_resolution = volume_resolution_allowed
- break
-
- max_volume = min(
- 100, user_max_volume * user_volume_resolution / volume_resolution
- )
-
- sources_store: dict[str, str] = {}
- for source, source_name in user_sources.items():
- sources_store[source.value] = source_name
-
- return self.async_create_entry(
- title=name,
- data={
- CONF_HOST: host,
- },
- options={
- OPTION_VOLUME_RESOLUTION: volume_resolution,
- OPTION_MAX_VOLUME: max_volume,
- OPTION_INPUT_SOURCES: sources_store,
- },
- )
-
- @staticmethod
- @callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> OptionsFlow:
- """Return the options flow."""
- return OnkyoOptionsFlowHandler(config_entry)
-
-
-class OnkyoOptionsFlowHandler(OptionsFlow):
- """Handle an options flow for Onkyo."""
-
- def __init__(self, config_entry: ConfigEntry) -> None:
- """Initialize options flow."""
- sources_store: dict[str, str] = config_entry.options[OPTION_INPUT_SOURCES]
- self._input_sources = {InputSource(k): v for k, v in sources_store.items()}
-
- async def async_step_init(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Manage the options."""
- if user_input is not None:
- sources_store: dict[str, str] = {}
- for source_meaning, source_name in user_input.items():
- if source_meaning in INPUT_SOURCES_ALL_MEANINGS:
- source = InputSource.from_meaning(source_meaning)
- sources_store[source.value] = source_name
-
- return self.async_create_entry(
- data={
- OPTION_VOLUME_RESOLUTION: self.config_entry.options[
- OPTION_VOLUME_RESOLUTION
- ],
- OPTION_MAX_VOLUME: user_input[OPTION_MAX_VOLUME],
- OPTION_INPUT_SOURCES: sources_store,
- }
- )
-
- schema_dict: dict[Any, Selector] = {}
-
- max_volume: float = self.config_entry.options[OPTION_MAX_VOLUME]
- schema_dict[vol.Required(OPTION_MAX_VOLUME, default=max_volume)] = (
- NumberSelector(
- NumberSelectorConfig(min=1, max=100, mode=NumberSelectorMode.BOX)
- )
- )
-
- for source, source_name in self._input_sources.items():
- schema_dict[vol.Required(source.value_meaning, default=source_name)] = (
- TextSelector()
- )
-
- return self.async_show_form(
- step_id="init",
- data_schema=vol.Schema(schema_dict),
- )
diff --git a/homeassistant/components/onkyo/const.py b/homeassistant/components/onkyo/const.py
deleted file mode 100644
index bd4fe98ae7d..00000000000
--- a/homeassistant/components/onkyo/const.py
+++ /dev/null
@@ -1,141 +0,0 @@
-"""Constants for the Onkyo integration."""
-
-from enum import Enum
-import typing
-from typing import ClassVar, Literal, Self
-
-import pyeiscp
-
-DOMAIN = "onkyo"
-
-DEVICE_INTERVIEW_TIMEOUT = 5
-DEVICE_DISCOVERY_TIMEOUT = 5
-
-CONF_SOURCES = "sources"
-CONF_RECEIVER_MAX_VOLUME = "receiver_max_volume"
-
-type VolumeResolution = Literal[50, 80, 100, 200]
-OPTION_VOLUME_RESOLUTION = "volume_resolution"
-OPTION_VOLUME_RESOLUTION_DEFAULT: VolumeResolution = 50
-VOLUME_RESOLUTION_ALLOWED: tuple[VolumeResolution, ...] = typing.get_args(
- VolumeResolution.__value__
-)
-
-OPTION_MAX_VOLUME = "max_volume"
-OPTION_MAX_VOLUME_DEFAULT = 100.0
-
-OPTION_INPUT_SOURCES = "input_sources"
-
-_INPUT_SOURCE_MEANINGS = {
- "00": "VIDEO1 ··· VCR/DVR ··· STB/DVR",
- "01": "VIDEO2 ··· CBL/SAT",
- "02": "VIDEO3 ··· GAME/TV ··· GAME",
- "03": "VIDEO4 ··· AUX",
- "04": "VIDEO5 ··· AUX2 ··· GAME2",
- "05": "VIDEO6 ··· PC",
- "06": "VIDEO7",
- "07": "HIDDEN1 ··· EXTRA1",
- "08": "HIDDEN2 ··· EXTRA2",
- "09": "HIDDEN3 ··· EXTRA3",
- "10": "DVD ··· BD/DVD",
- "11": "STRM BOX",
- "12": "TV",
- "20": "TAPE ··· TV/TAPE",
- "21": "TAPE2",
- "22": "PHONO",
- "23": "CD ··· TV/CD",
- "24": "FM",
- "25": "AM",
- "26": "TUNER",
- "27": "MUSIC SERVER ··· P4S ··· DLNA",
- "28": "INTERNET RADIO ··· IRADIO FAVORITE",
- "29": "USB ··· USB(FRONT)",
- "2A": "USB(REAR)",
- "2B": "NETWORK ··· NET",
- "2D": "AIRPLAY",
- "2E": "BLUETOOTH",
- "2F": "USB DAC IN",
- "30": "MULTI CH",
- "31": "XM",
- "32": "SIRIUS",
- "33": "DAB",
- "40": "UNIVERSAL PORT",
- "41": "LINE",
- "42": "LINE2",
- "44": "OPTICAL",
- "45": "COAXIAL",
- "55": "HDMI 5",
- "56": "HDMI 6",
- "57": "HDMI 7",
- "80": "MAIN SOURCE",
-}
-
-
-class InputSource(Enum):
- """Receiver input source."""
-
- DVR = "00"
- CBL = "01"
- GAME = "02"
- AUX = "03"
- GAME2 = "04"
- PC = "05"
- VIDEO7 = "06"
- EXTRA1 = "07"
- EXTRA2 = "08"
- EXTRA3 = "09"
- DVD = "10"
- STRM_BOX = "11"
- TV = "12"
- TAPE = "20"
- TAPE2 = "21"
- PHONO = "22"
- CD = "23"
- FM = "24"
- AM = "25"
- TUNER = "26"
- MUSIC_SERVER = "27"
- INTERNET_RADIO = "28"
- USB = "29"
- USB_REAR = "2A"
- NETWORK = "2B"
- AIRPLAY = "2D"
- BLUETOOTH = "2E"
- USB_DAC_IN = "2F"
- MULTI_CH = "30"
- XM = "31"
- SIRIUS = "32"
- DAB = "33"
- UNIVERSAL_PORT = "40"
- LINE = "41"
- LINE2 = "42"
- OPTICAL = "44"
- COAXIAL = "45"
- HDMI_5 = "55"
- HDMI_6 = "56"
- HDMI_7 = "57"
- MAIN_SOURCE = "80"
-
- __meaning_mapping: ClassVar[dict[str, Self]] = {} # type: ignore[misc]
-
- value_meaning: str
-
- def __new__(cls, value: str) -> Self:
- """Create InputSource enum."""
- obj = object.__new__(cls)
- obj._value_ = value
- obj.value_meaning = _INPUT_SOURCE_MEANINGS[value]
-
- cls.__meaning_mapping[obj.value_meaning] = obj
-
- return obj
-
- @classmethod
- def from_meaning(cls, meaning: str) -> Self:
- """Get InputSource enum from its meaning."""
- return cls.__meaning_mapping[meaning]
-
-
-ZONES = {"main": "Main", "zone2": "Zone 2", "zone3": "Zone 3", "zone4": "Zone 4"}
-
-PYEISCP_COMMANDS = pyeiscp.commands.COMMANDS
diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json
index 0e75404b3eb..072dc9f9e3b 100644
--- a/homeassistant/components/onkyo/manifest.json
+++ b/homeassistant/components/onkyo/manifest.json
@@ -2,9 +2,7 @@
"domain": "onkyo",
"name": "Onkyo",
"codeowners": ["@arturpragacz"],
- "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/onkyo",
- "integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyeiscp"],
"requirements": ["pyeiscp==0.0.7"]
diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py
index 41e36a7f237..af4285e2abd 100644
--- a/homeassistant/components/onkyo/media_player.py
+++ b/homeassistant/components/onkyo/media_player.py
@@ -6,73 +6,45 @@ import asyncio
import logging
from typing import Any, Literal
+import pyeiscp
import voluptuous as vol
from homeassistant.components.media_player import (
+ DOMAIN as MEDIA_PLAYER_DOMAIN,
PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
)
-from homeassistant.config_entries import SOURCE_IMPORT
-from homeassistant.const import CONF_HOST, CONF_NAME
-from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
-from homeassistant.data_entry_flow import FlowResultType
-from homeassistant.helpers import config_validation as cv, entity_registry as er
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
-from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
-
-from . import OnkyoConfigEntry
-from .const import (
- CONF_RECEIVER_MAX_VOLUME,
- CONF_SOURCES,
- DOMAIN,
- OPTION_MAX_VOLUME,
- OPTION_VOLUME_RESOLUTION,
- PYEISCP_COMMANDS,
- ZONES,
- InputSource,
- VolumeResolution,
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_HOST,
+ CONF_NAME,
+ EVENT_HOMEASSISTANT_STOP,
)
-from .receiver import Receiver, async_discover
-from .services import DATA_MP_ENTITIES
+from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+from homeassistant.util.hass_dict import HassKey
+
+from .receiver import Receiver, ReceiverInfo
_LOGGER = logging.getLogger(__name__)
-CONF_MAX_VOLUME_DEFAULT = 100
-CONF_RECEIVER_MAX_VOLUME_DEFAULT = 80
-CONF_SOURCES_DEFAULT = {
- "tv": "TV",
- "bd": "Bluray",
- "game": "Game",
- "aux1": "Aux1",
- "video1": "Video 1",
- "video2": "Video 2",
- "video3": "Video 3",
- "video4": "Video 4",
- "video5": "Video 5",
- "video6": "Video 6",
- "video7": "Video 7",
- "fm": "Radio",
-}
+DOMAIN = "onkyo"
-PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_HOST): cv.string,
- vol.Optional(CONF_NAME): cv.string,
- vol.Optional(OPTION_MAX_VOLUME, default=CONF_MAX_VOLUME_DEFAULT): vol.All(
- vol.Coerce(int), vol.Range(min=1, max=100)
- ),
- vol.Optional(
- CONF_RECEIVER_MAX_VOLUME, default=CONF_RECEIVER_MAX_VOLUME_DEFAULT
- ): cv.positive_int,
- vol.Optional(CONF_SOURCES, default=CONF_SOURCES_DEFAULT): {
- cv.string: cv.string
- },
- }
-)
+DATA_MP_ENTITIES: HassKey[list[dict[str, OnkyoMediaPlayer]]] = HassKey(DOMAIN)
+
+CONF_SOURCES = "sources"
+CONF_MAX_VOLUME = "max_volume"
+CONF_RECEIVER_MAX_VOLUME = "receiver_max_volume"
+
+DEFAULT_NAME = "Onkyo Receiver"
+SUPPORTED_MAX_VOLUME = 100
+DEFAULT_RECEIVER_MAX_VOLUME = 80
+ZONES = {"zone2": "Zone 2", "zone3": "Zone 3", "zone4": "Zone 4"}
SUPPORT_ONKYO_WO_VOLUME = (
MediaPlayerEntityFeature.TURN_ON
@@ -87,12 +59,39 @@ SUPPORT_ONKYO = (
| MediaPlayerEntityFeature.VOLUME_STEP
)
-DEFAULT_PLAYABLE_SOURCES = (
- InputSource.from_meaning("FM"),
- InputSource.from_meaning("AM"),
- InputSource.from_meaning("TUNER"),
+KNOWN_HOSTS: list[str] = []
+
+DEFAULT_SOURCES = {
+ "tv": "TV",
+ "bd": "Bluray",
+ "game": "Game",
+ "aux1": "Aux1",
+ "video1": "Video 1",
+ "video2": "Video 2",
+ "video3": "Video 3",
+ "video4": "Video 4",
+ "video5": "Video 5",
+ "video6": "Video 6",
+ "video7": "Video 7",
+ "fm": "Radio",
+}
+DEFAULT_PLAYABLE_SOURCES = ("fm", "am", "tuner")
+
+PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_HOST): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_MAX_VOLUME, default=SUPPORTED_MAX_VOLUME): vol.All(
+ vol.Coerce(int), vol.Range(min=1, max=100)
+ ),
+ vol.Optional(
+ CONF_RECEIVER_MAX_VOLUME, default=DEFAULT_RECEIVER_MAX_VOLUME
+ ): cv.positive_int,
+ vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string},
+ }
)
+ATTR_HDMI_OUTPUT = "hdmi_output"
ATTR_PRESET = "preset"
ATTR_AUDIO_INFORMATION = "audio_information"
ATTR_VIDEO_INFORMATION = "video_information"
@@ -124,31 +123,52 @@ VIDEO_INFORMATION_MAPPING = [
"output_color_depth",
"picture_mode",
]
-ISSUE_URL_PLACEHOLDER = "/config/integrations/dashboard/add?domain=onkyo"
-type InputLibValue = str | tuple[str, ...]
+ACCEPTED_VALUES = [
+ "no",
+ "analog",
+ "yes",
+ "out",
+ "out-sub",
+ "sub",
+ "hdbaset",
+ "both",
+ "up",
+]
+ONKYO_SELECT_OUTPUT_SCHEMA = vol.Schema(
+ {
+ vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Required(ATTR_HDMI_OUTPUT): vol.In(ACCEPTED_VALUES),
+ }
+)
+SERVICE_SELECT_HDMI_OUTPUT = "onkyo_select_hdmi_output"
-def _input_lib_cmds(zone: str) -> dict[InputSource, InputLibValue]:
- match zone:
- case "main":
- cmds = PYEISCP_COMMANDS["main"]["SLI"]
- case "zone2":
- cmds = PYEISCP_COMMANDS["zone2"]["SLZ"]
- case "zone3":
- cmds = PYEISCP_COMMANDS["zone3"]["SL3"]
- case "zone4":
- cmds = PYEISCP_COMMANDS["zone4"]["SL4"]
+async def async_register_services(hass: HomeAssistant) -> None:
+ """Register Onkyo services."""
- result: dict[InputSource, InputLibValue] = {}
- for k, v in cmds["values"].items():
- try:
- source = InputSource(k)
- except ValueError:
- continue
- result[source] = v["name"]
+ async def async_service_handle(service: ServiceCall) -> None:
+ """Handle for services."""
+ entity_ids = service.data[ATTR_ENTITY_ID]
- return result
+ targets: list[OnkyoMediaPlayer] = []
+ for receiver_entities in hass.data[DATA_MP_ENTITIES]:
+ targets.extend(
+ entity
+ for entity in receiver_entities.values()
+ if entity.entity_id in entity_ids
+ )
+
+ for target in targets:
+ if service.service == SERVICE_SELECT_HDMI_OUTPUT:
+ await target.async_select_output(service.data[ATTR_HDMI_OUTPUT])
+
+ hass.services.async_register(
+ MEDIA_PLAYER_DOMAIN,
+ SERVICE_SELECT_HDMI_OUTPUT,
+ async_service_handle,
+ schema=ONKYO_SELECT_OUTPUT_SCHEMA,
+ )
async def async_setup_platform(
@@ -157,167 +177,130 @@ async def async_setup_platform(
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
- """Import config from yaml."""
+ """Set up the Onkyo platform."""
+ await async_register_services(hass)
+
+ receivers: dict[str, Receiver] = {} # indexed by host
+ all_entities = hass.data.setdefault(DATA_MP_ENTITIES, [])
+
host = config.get(CONF_HOST)
+ name = config.get(CONF_NAME)
+ max_volume = config[CONF_MAX_VOLUME]
+ receiver_max_volume = config[CONF_RECEIVER_MAX_VOLUME]
+ sources = config[CONF_SOURCES]
- source_mapping: dict[str, InputSource] = {}
- for zone in ZONES:
- for source, source_lib in _input_lib_cmds(zone).items():
- if isinstance(source_lib, str):
- source_mapping.setdefault(source_lib, source)
- else:
- for source_lib_single in source_lib:
- source_mapping.setdefault(source_lib_single, source)
+ async def async_setup_receiver(
+ info: ReceiverInfo, discovered: bool, name: str | None
+ ) -> None:
+ entities: dict[str, OnkyoMediaPlayer] = {}
+ all_entities.append(entities)
- sources: dict[InputSource, str] = {}
- for source_lib_single, source_name in config[CONF_SOURCES].items():
- user_source = source_mapping.get(source_lib_single.lower())
- if user_source is not None:
- sources[user_source] = source_name
-
- config[CONF_SOURCES] = sources
-
- results = []
- if host is not None:
- _LOGGER.debug("Importing yaml single: %s", host)
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_IMPORT}, data=config
- )
- results.append((host, result))
- else:
- for info in await async_discover():
- host = info.host
-
- # Migrate legacy entities.
- registry = er.async_get(hass)
- old_unique_id = f"{info.model_name}_{info.identifier}"
- new_unique_id = f"{info.identifier}_main"
- entity_id = registry.async_get_entity_id(
- "media_player", DOMAIN, old_unique_id
- )
- if entity_id is not None:
- _LOGGER.debug(
- "Migrating unique_id from [%s] to [%s] for entity %s",
- old_unique_id,
- new_unique_id,
- entity_id,
- )
- registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
-
- _LOGGER.debug("Importing yaml discover: %s", info.host)
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_IMPORT},
- data=config | {CONF_HOST: info.host} | {"info": info},
- )
- results.append((host, result))
-
- _LOGGER.debug("Importing yaml results: %s", results)
- if not results:
- async_create_issue(
- hass,
- DOMAIN,
- "deprecated_yaml_import_issue_no_discover",
- breaks_in_ha_version="2025.5.0",
- is_fixable=False,
- issue_domain=DOMAIN,
- severity=IssueSeverity.WARNING,
- translation_key="deprecated_yaml_import_issue_no_discover",
- translation_placeholders={"url": ISSUE_URL_PLACEHOLDER},
- )
-
- all_successful = True
- for host, result in results:
- if (
- result.get("type") == FlowResultType.CREATE_ENTRY
- or result.get("reason") == "already_configured"
- ):
- continue
- if error := result.get("reason"):
- all_successful = False
- async_create_issue(
- hass,
- DOMAIN,
- f"deprecated_yaml_import_issue_{host}_{error}",
- breaks_in_ha_version="2025.5.0",
- is_fixable=False,
- issue_domain=DOMAIN,
- severity=IssueSeverity.WARNING,
- translation_key=f"deprecated_yaml_import_issue_{error}",
- translation_placeholders={
- "host": host,
- "url": ISSUE_URL_PLACEHOLDER,
- },
- )
-
- if all_successful:
- async_create_issue(
- hass,
- HOMEASSISTANT_DOMAIN,
- f"deprecated_yaml_{DOMAIN}",
- is_fixable=False,
- issue_domain=DOMAIN,
- breaks_in_ha_version="2025.5.0",
- severity=IssueSeverity.WARNING,
- translation_key="deprecated_yaml",
- translation_placeholders={
- "domain": DOMAIN,
- "integration_title": "onkyo",
- },
- )
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: OnkyoConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up MediaPlayer for config entry."""
- data = entry.runtime_data
-
- receiver = data.receiver
- all_entities = hass.data[DATA_MP_ENTITIES]
-
- entities: dict[str, OnkyoMediaPlayer] = {}
- all_entities[entry.entry_id] = entities
-
- volume_resolution: VolumeResolution = entry.options[OPTION_VOLUME_RESOLUTION]
- max_volume: float = entry.options[OPTION_MAX_VOLUME]
- sources = data.sources
-
- def connect_callback(receiver: Receiver) -> None:
- if not receiver.first_connect:
- for entity in entities.values():
- if entity.enabled:
- entity.backfill_state()
-
- def update_callback(receiver: Receiver, message: tuple[str, str, Any]) -> None:
- zone, _, value = message
- entity = entities.get(zone)
- if entity is not None:
- if entity.enabled:
- entity.process_update(message)
- elif zone in ZONES and value != "N/A":
- # When we receive the status for a zone, and the value is not "N/A",
- # then zone is available on the receiver, so we create the entity for it.
+ @callback
+ def async_onkyo_update_callback(
+ message: tuple[str, str, Any], origin: str
+ ) -> None:
+ """Process new message from receiver."""
+ receiver = receivers[origin]
_LOGGER.debug(
- "Discovered %s on %s (%s)",
- ZONES[zone],
- receiver.model_name,
- receiver.host,
+ "Received update callback from %s: %s", receiver.name, message
)
- zone_entity = OnkyoMediaPlayer(
- receiver,
- zone,
- volume_resolution=volume_resolution,
- max_volume=max_volume,
- sources=sources,
- )
- entities[zone] = zone_entity
- async_add_entities([zone_entity])
- receiver.callbacks.connect.append(connect_callback)
- receiver.callbacks.update.append(update_callback)
+ zone, _, value = message
+ entity = entities.get(zone)
+ if entity is not None:
+ if entity.enabled:
+ entity.process_update(message)
+ elif zone in ZONES and value != "N/A":
+ # When we receive the status for a zone, and the value is not "N/A",
+ # then zone is available on the receiver, so we create the entity for it.
+ _LOGGER.debug("Discovered %s on %s", ZONES[zone], receiver.name)
+ zone_entity = OnkyoMediaPlayer(
+ receiver, sources, zone, max_volume, receiver_max_volume
+ )
+ entities[zone] = zone_entity
+ async_add_entities([zone_entity])
+
+ @callback
+ def async_onkyo_connect_callback(origin: str) -> None:
+ """Receiver (re)connected."""
+ receiver = receivers[origin]
+ _LOGGER.debug(
+ "Receiver (re)connected: %s (%s)", receiver.name, receiver.conn.host
+ )
+
+ for entity in entities.values():
+ entity.backfill_state()
+
+ _LOGGER.debug("Creating receiver: %s (%s)", info.model_name, info.host)
+ connection = await pyeiscp.Connection.create(
+ host=info.host,
+ port=info.port,
+ update_callback=async_onkyo_update_callback,
+ connect_callback=async_onkyo_connect_callback,
+ )
+
+ receiver = Receiver(
+ conn=connection,
+ model_name=info.model_name,
+ identifier=info.identifier,
+ name=name or info.model_name,
+ discovered=discovered,
+ )
+
+ receivers[connection.host] = receiver
+
+ # Discover what zones are available for the receiver by querying the power.
+ # If we get a response for the specific zone, it means it is available.
+ for zone in ZONES:
+ receiver.conn.query_property(zone, "power")
+
+ # Add the main zone to entities, since it is always active.
+ _LOGGER.debug("Adding Main Zone on %s", receiver.name)
+ main_entity = OnkyoMediaPlayer(
+ receiver, sources, "main", max_volume, receiver_max_volume
+ )
+ entities["main"] = main_entity
+ async_add_entities([main_entity])
+
+ if host is not None:
+ if host in KNOWN_HOSTS:
+ return
+
+ _LOGGER.debug("Manually creating receiver: %s (%s)", name, host)
+
+ async def async_onkyo_interview_callback(conn: pyeiscp.Connection) -> None:
+ """Receiver interviewed, connection not yet active."""
+ info = ReceiverInfo(conn.host, conn.port, conn.name, conn.identifier)
+ _LOGGER.debug("Receiver interviewed: %s (%s)", info.model_name, info.host)
+ if info.host not in KNOWN_HOSTS:
+ KNOWN_HOSTS.append(info.host)
+ await async_setup_receiver(info, False, name)
+
+ await pyeiscp.Connection.discover(
+ host=host,
+ discovery_callback=async_onkyo_interview_callback,
+ )
+ else:
+ _LOGGER.debug("Discovering receivers")
+
+ async def async_onkyo_discovery_callback(conn: pyeiscp.Connection) -> None:
+ """Receiver discovered, connection not yet active."""
+ info = ReceiverInfo(conn.host, conn.port, conn.name, conn.identifier)
+ _LOGGER.debug("Receiver discovered: %s (%s)", info.model_name, info.host)
+ if info.host not in KNOWN_HOSTS:
+ KNOWN_HOSTS.append(info.host)
+ await async_setup_receiver(info, True, None)
+
+ await pyeiscp.Connection.discover(
+ discovery_callback=async_onkyo_discovery_callback,
+ )
+
+ @callback
+ def close_receiver(_event: Event) -> None:
+ for receiver in receivers.values():
+ receiver.conn.close()
+
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_receiver)
class OnkyoMediaPlayer(MediaPlayerEntity):
@@ -333,30 +316,27 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
def __init__(
self,
receiver: Receiver,
+ sources: dict[str, str],
zone: str,
- *,
- volume_resolution: VolumeResolution,
- max_volume: float,
- sources: dict[InputSource, str],
+ max_volume: int,
+ volume_resolution: int,
) -> None:
"""Initialize the Onkyo Receiver."""
self._receiver = receiver
- name = receiver.model_name
+ name = receiver.name
identifier = receiver.identifier
self._attr_name = f"{name}{' ' + ZONES[zone] if zone != 'main' else ''}"
- self._attr_unique_id = f"{identifier}_{zone}"
+ if receiver.discovered and zone == "main":
+ # keep legacy unique_id
+ self._attr_unique_id = f"{name}_{identifier}"
+ else:
+ self._attr_unique_id = f"{identifier}_{zone}"
self._zone = zone
-
- self._volume_resolution = volume_resolution
+ self._source_mapping = sources
+ self._reverse_mapping = {value: key for key, value in sources.items()}
self._max_volume = max_volume
-
- self._name_mapping = sources
- self._reverse_name_mapping = {value: key for key, value in sources.items()}
- self._lib_mapping = _input_lib_cmds(zone)
- self._reverse_lib_mapping = {
- value: key for key, value in self._lib_mapping.items()
- }
+ self._volume_resolution = volume_resolution
self._attr_source_list = list(sources.values())
self._attr_extra_state_attributes = {}
@@ -428,13 +408,9 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
async def async_select_source(self, source: str) -> None:
"""Select input source."""
if self.source_list and source in self.source_list:
- source_lib = self._lib_mapping[self._reverse_name_mapping[source]]
- if isinstance(source_lib, str):
- source_lib_single = source_lib
- else:
- source_lib_single = source_lib[0]
+ source = self._reverse_mapping[source]
self._update_receiver(
- "input-selector" if self._zone == "main" else "selector", source_lib_single
+ "input-selector" if self._zone == "main" else "selector", source
)
async def async_select_output(self, hdmi_output: str) -> None:
@@ -446,7 +422,7 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
) -> None:
"""Play radio station by preset number."""
if self.source is not None:
- source = self._reverse_name_mapping[self.source]
+ source = self._reverse_mapping[self.source]
if media_type.lower() == "radio" and source in DEFAULT_PLAYABLE_SOURCES:
self._update_receiver("preset", media_id)
@@ -490,10 +466,9 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
elif command in ["volume", "master-volume"] and value != "N/A":
self._supports_volume = True
# AMP_VOL / (VOL_RESOLUTION * (MAX_VOL / 100))
- volume_level: float = value / (
+ self._attr_volume_level = value / (
self._volume_resolution * self._max_volume / 100
)
- self._attr_volume_level = min(1, volume_level)
elif command in ["muting", "audio-muting"]:
self._attr_is_volume_muted = bool(value == "on")
elif command in ["selector", "input-selector"]:
@@ -518,17 +493,18 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
self.async_write_ha_state()
@callback
- def _parse_source(self, source_lib: InputLibValue) -> None:
- source = self._reverse_lib_mapping[source_lib]
- if source in self._name_mapping:
- self._attr_source = self._name_mapping[source]
- return
-
- source_meaning = source.value_meaning
- _LOGGER.error(
- 'Input source "%s" not in source list: %s', source_meaning, self.entity_id
- )
- self._attr_source = source_meaning
+ def _parse_source(self, source_raw: str | int | tuple[str]) -> None:
+ # source is either a tuple of values or a single value,
+ # so we convert to a tuple, when it is a single value.
+ if isinstance(source_raw, str | int):
+ source = (str(source_raw),)
+ else:
+ source = source_raw
+ for value in source:
+ if value in self._source_mapping:
+ self._attr_source = self._source_mapping[value]
+ return
+ self._attr_source = "_".join(source)
@callback
def _parse_audio_information(
diff --git a/homeassistant/components/onkyo/receiver.py b/homeassistant/components/onkyo/receiver.py
index cc6cbbc95fb..eb20f327b69 100644
--- a/homeassistant/components/onkyo/receiver.py
+++ b/homeassistant/components/onkyo/receiver.py
@@ -2,29 +2,10 @@
from __future__ import annotations
-import asyncio
-from collections.abc import Callable, Iterable
-import contextlib
-from dataclasses import dataclass, field
-import logging
-from typing import Any
+from dataclasses import dataclass
import pyeiscp
-from .const import DEVICE_DISCOVERY_TIMEOUT, DEVICE_INTERVIEW_TIMEOUT, ZONES
-
-_LOGGER = logging.getLogger(__name__)
-
-
-@dataclass
-class Callbacks:
- """Onkyo Receiver Callbacks."""
-
- connect: list[Callable[[Receiver], None]] = field(default_factory=list)
- update: list[Callable[[Receiver, tuple[str, str, Any]], None]] = field(
- default_factory=list
- )
-
@dataclass
class Receiver:
@@ -33,62 +14,8 @@ class Receiver:
conn: pyeiscp.Connection
model_name: str
identifier: str
- host: str
- first_connect: bool = True
- callbacks: Callbacks = field(default_factory=Callbacks)
-
- @classmethod
- async def async_create(cls, info: ReceiverInfo) -> Receiver:
- """Set up Onkyo Receiver."""
-
- receiver: Receiver | None = None
-
- def on_connect(_origin: str) -> None:
- assert receiver is not None
- receiver.on_connect()
-
- def on_update(message: tuple[str, str, Any], _origin: str) -> None:
- assert receiver is not None
- receiver.on_update(message)
-
- _LOGGER.debug("Creating receiver: %s (%s)", info.model_name, info.host)
-
- connection = await pyeiscp.Connection.create(
- host=info.host,
- port=info.port,
- connect_callback=on_connect,
- update_callback=on_update,
- auto_connect=False,
- )
-
- return (
- receiver := cls(
- conn=connection,
- model_name=info.model_name,
- identifier=info.identifier,
- host=info.host,
- )
- )
-
- def on_connect(self) -> None:
- """Receiver (re)connected."""
- _LOGGER.debug("Receiver (re)connected: %s (%s)", self.model_name, self.host)
-
- # Discover what zones are available for the receiver by querying the power.
- # If we get a response for the specific zone, it means it is available.
- for zone in ZONES:
- self.conn.query_property(zone, "power")
-
- for callback in self.callbacks.connect:
- callback(self)
-
- self.first_connect = False
-
- def on_update(self, message: tuple[str, str, Any]) -> None:
- """Process new message from the receiver."""
- _LOGGER.debug("Received update callback from %s: %s", self.model_name, message)
- for callback in self.callbacks.update:
- callback(self, message)
+ name: str
+ discovered: bool
@dataclass
@@ -99,53 +26,3 @@ class ReceiverInfo:
port: int
model_name: str
identifier: str
-
-
-async def async_interview(host: str) -> ReceiverInfo | None:
- """Interview Onkyo Receiver."""
- _LOGGER.debug("Interviewing receiver: %s", host)
-
- receiver_info: ReceiverInfo | None = None
-
- event = asyncio.Event()
-
- async def _callback(conn: pyeiscp.Connection) -> None:
- """Receiver interviewed, connection not yet active."""
- nonlocal receiver_info
- if receiver_info is None:
- info = ReceiverInfo(host, conn.port, conn.name, conn.identifier)
- _LOGGER.debug("Receiver interviewed: %s (%s)", info.model_name, info.host)
- receiver_info = info
- event.set()
-
- timeout = DEVICE_INTERVIEW_TIMEOUT
-
- await pyeiscp.Connection.discover(
- host=host, discovery_callback=_callback, timeout=timeout
- )
-
- with contextlib.suppress(asyncio.TimeoutError):
- await asyncio.wait_for(event.wait(), timeout)
-
- return receiver_info
-
-
-async def async_discover() -> Iterable[ReceiverInfo]:
- """Discover Onkyo Receivers."""
- _LOGGER.debug("Discovering receivers")
-
- receiver_infos: list[ReceiverInfo] = []
-
- async def _callback(conn: pyeiscp.Connection) -> None:
- """Receiver discovered, connection not yet active."""
- info = ReceiverInfo(conn.host, conn.port, conn.name, conn.identifier)
- _LOGGER.debug("Receiver discovered: %s (%s)", info.model_name, info.host)
- receiver_infos.append(info)
-
- timeout = DEVICE_DISCOVERY_TIMEOUT
-
- await pyeiscp.Connection.discover(discovery_callback=_callback, timeout=timeout)
-
- await asyncio.sleep(timeout)
-
- return receiver_infos
diff --git a/homeassistant/components/onkyo/services.py b/homeassistant/components/onkyo/services.py
deleted file mode 100644
index d875d8287fe..00000000000
--- a/homeassistant/components/onkyo/services.py
+++ /dev/null
@@ -1,69 +0,0 @@
-"""Onkyo services."""
-
-from __future__ import annotations
-
-from typing import TYPE_CHECKING
-
-import voluptuous as vol
-
-from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
-from homeassistant.const import ATTR_ENTITY_ID
-from homeassistant.core import HomeAssistant, ServiceCall
-from homeassistant.helpers import config_validation as cv
-from homeassistant.util.hass_dict import HassKey
-
-from .const import DOMAIN
-
-if TYPE_CHECKING:
- from .media_player import OnkyoMediaPlayer
-
-DATA_MP_ENTITIES: HassKey[dict[str, dict[str, OnkyoMediaPlayer]]] = HassKey(DOMAIN)
-
-ATTR_HDMI_OUTPUT = "hdmi_output"
-ACCEPTED_VALUES = [
- "no",
- "analog",
- "yes",
- "out",
- "out-sub",
- "sub",
- "hdbaset",
- "both",
- "up",
-]
-ONKYO_SELECT_OUTPUT_SCHEMA = vol.Schema(
- {
- vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
- vol.Required(ATTR_HDMI_OUTPUT): vol.In(ACCEPTED_VALUES),
- }
-)
-SERVICE_SELECT_HDMI_OUTPUT = "onkyo_select_hdmi_output"
-
-
-async def async_register_services(hass: HomeAssistant) -> None:
- """Register Onkyo services."""
-
- hass.data.setdefault(DATA_MP_ENTITIES, {})
-
- async def async_service_handle(service: ServiceCall) -> None:
- """Handle for services."""
- entity_ids = service.data[ATTR_ENTITY_ID]
-
- targets: list[OnkyoMediaPlayer] = []
- for receiver_entities in hass.data[DATA_MP_ENTITIES].values():
- targets.extend(
- entity
- for entity in receiver_entities.values()
- if entity.entity_id in entity_ids
- )
-
- for target in targets:
- if service.service == SERVICE_SELECT_HDMI_OUTPUT:
- await target.async_select_output(service.data[ATTR_HDMI_OUTPUT])
-
- hass.services.async_register(
- MEDIA_PLAYER_DOMAIN,
- SERVICE_SELECT_HDMI_OUTPUT,
- async_service_handle,
- schema=ONKYO_SELECT_OUTPUT_SCHEMA,
- )
diff --git a/homeassistant/components/onkyo/strings.json b/homeassistant/components/onkyo/strings.json
deleted file mode 100644
index 1b0eadcc45e..00000000000
--- a/homeassistant/components/onkyo/strings.json
+++ /dev/null
@@ -1,60 +0,0 @@
-{
- "config": {
- "step": {
- "user": {
- "menu_options": {
- "manual": "Manual entry",
- "eiscp_discovery": "Onkyo discovery"
- }
- },
- "manual": {
- "data": {
- "host": "[%key:common::config_flow::data::host%]"
- }
- },
- "eiscp_discovery": {
- "data": {
- "device": "[%key:common::config_flow::data::device%]"
- }
- },
- "configure_receiver": {
- "description": "Configure {name}",
- "data": {
- "volume_resolution": "Number of steps it takes for the receiver to go from the lowest to the highest possible volume",
- "input_sources": "List of input sources supported by the receiver"
- }
- }
- },
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "empty_input_source_list": "Input source list cannot be empty",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- },
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
- "unique_id_mismatch": "The serial number of the device does not match the previous serial number",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- }
- },
- "options": {
- "step": {
- "init": {
- "data": {
- "max_volume": "Maximum volume limit (%)"
- }
- }
- }
- },
- "issues": {
- "deprecated_yaml_import_issue_no_discover": {
- "title": "The Onkyo YAML configuration import failed",
- "description": "Configuring Onkyo using YAML is being removed but no receivers were discovered when importing your YAML configuration.\n\nEnsure the connection to the receiver works and restart Home Assistant to try again or remove the Onkyo YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
- },
- "deprecated_yaml_import_issue_cannot_connect": {
- "title": "The Onkyo YAML configuration import failed",
- "description": "Configuring Onkyo using YAML is being removed but there was a connection error when importing your YAML configuration for host {host}.\n\nEnsure the connection to the receiver works and restart Home Assistant to try again or remove the Onkyo YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
- }
- }
-}
diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py
index 66e566af0bf..f4e3f11d0b7 100644
--- a/homeassistant/components/onvif/config_flow.py
+++ b/homeassistant/components/onvif/config_flow.py
@@ -102,6 +102,7 @@ class OnvifFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a ONVIF config flow."""
VERSION = 1
+ _reauth_entry: ConfigEntry
@staticmethod
@callback
@@ -135,28 +136,30 @@ class OnvifFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication of an existing config entry."""
+ reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
+ assert reauth_entry is not None
+ self._reauth_entry = reauth_entry
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauth."""
+ entry = self._reauth_entry
errors: dict[str, str] | None = {}
- reauth_entry = self._get_reauth_entry()
description_placeholders: dict[str, str] | None = None
if user_input is not None:
- self.onvif_config = reauth_entry.data | user_input
+ entry_data = entry.data
+ self.onvif_config = entry_data | user_input
errors, description_placeholders = await self.async_setup_profiles(
configure_unique_id=False
)
if not errors:
- return self.async_update_reload_and_abort(
- reauth_entry, data=self.onvif_config
- )
+ return self.async_update_reload_and_abort(entry, data=self.onvif_config)
- username = (user_input or {}).get(CONF_USERNAME) or reauth_entry.data[
- CONF_USERNAME
- ]
+ username = (user_input or {}).get(CONF_USERNAME) or entry.data[CONF_USERNAME]
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
@@ -391,6 +394,7 @@ class OnvifOptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize ONVIF options flow."""
+ self.config_entry = config_entry
self.options = dict(config_entry.options)
async def async_step_init(self, user_input: None = None) -> ConfigFlowResult:
diff --git a/homeassistant/components/open_meteo/__init__.py b/homeassistant/components/open_meteo/__init__.py
index 6deb63904ff..e3bf763f429 100644
--- a/homeassistant/components/open_meteo/__init__.py
+++ b/homeassistant/components/open_meteo/__init__.py
@@ -62,7 +62,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator: DataUpdateCoordinator[Forecast] = DataUpdateCoordinator(
hass,
LOGGER,
- config_entry=entry,
name=f"{DOMAIN}_{entry.data[CONF_ZONE]}",
update_interval=SCAN_INTERVAL,
update_method=async_update_forecast,
diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py
index 2a1764e6b5e..c6b8487ad0d 100644
--- a/homeassistant/components/openai_conversation/config_flow.py
+++ b/homeassistant/components/openai_conversation/config_flow.py
@@ -115,6 +115,7 @@ class OpenAIOptionsFlow(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
+ self.config_entry = config_entry
self.last_rendered_recommended = config_entry.options.get(
CONF_RECOMMENDED, False
)
diff --git a/homeassistant/components/openexchangerates/config_flow.py b/homeassistant/components/openexchangerates/config_flow.py
index ffcc60bfa26..df83690d2e3 100644
--- a/homeassistant/components/openexchangerates/config_flow.py
+++ b/homeassistant/components/openexchangerates/config_flow.py
@@ -13,7 +13,7 @@ from aioopenexchangerates import (
)
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_BASE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import AbortFlow
@@ -54,6 +54,7 @@ class OpenExchangeRatesConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
self.currencies: dict[str, str] = {}
+ self._reauth_entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -62,9 +63,9 @@ class OpenExchangeRatesConfigFlow(ConfigFlow, domain=DOMAIN):
currencies = await self.async_get_currencies()
if user_input is None:
- existing_data: Mapping[str, Any] = {}
- if self.source == SOURCE_REAUTH:
- existing_data = self._get_reauth_entry().data
+ existing_data: Mapping[str, str] | dict[str, str] = (
+ self._reauth_entry.data if self._reauth_entry else {}
+ )
return self.async_show_form(
step_id="user",
data_schema=get_data_schema(currencies, existing_data),
@@ -94,10 +95,12 @@ class OpenExchangeRatesConfigFlow(ConfigFlow, domain=DOMAIN):
}
)
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=user_input
+ if self._reauth_entry is not None:
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry, data=self._reauth_entry.data | user_input
)
+ await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=info["title"], data=user_input)
@@ -112,6 +115,9 @@ class OpenExchangeRatesConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_user()
async def async_get_currencies(self) -> dict[str, str]:
diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json
index 9e5cd95a93d..cce90d0fb12 100644
--- a/homeassistant/components/openexchangerates/manifest.json
+++ b/homeassistant/components/openexchangerates/manifest.json
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/openexchangerates",
"iot_class": "cloud_polling",
- "requirements": ["aioopenexchangerates==0.6.8"]
+ "requirements": ["aioopenexchangerates==0.6.2"]
}
diff --git a/homeassistant/components/opensky/config_flow.py b/homeassistant/components/opensky/config_flow.py
index 867a4781265..3cfd1ad30a0 100644
--- a/homeassistant/components/opensky/config_flow.py
+++ b/homeassistant/components/opensky/config_flow.py
@@ -13,11 +13,12 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
+ CONF_NAME,
CONF_PASSWORD,
CONF_RADIUS,
CONF_USERNAME,
@@ -44,7 +45,7 @@ class OpenSkyConfigFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OpenSkyOptionsFlowHandler:
"""Get the options flow for this handler."""
- return OpenSkyOptionsFlowHandler()
+ return OpenSkyOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -82,7 +83,7 @@ class OpenSkyConfigFlowHandler(ConfigFlow, domain=DOMAIN):
)
-class OpenSkyOptionsFlowHandler(OptionsFlow):
+class OpenSkyOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""OpenSky Options flow handler."""
async def async_step_init(
@@ -111,7 +112,10 @@ class OpenSkyOptionsFlowHandler(OptionsFlow):
except OpenSkyUnauthenticatedError:
errors["base"] = "invalid_auth"
if not errors:
- return self.async_create_entry(data=user_input)
+ return self.async_create_entry(
+ title=self.options.get(CONF_NAME, "OpenSky"),
+ data=user_input,
+ )
return self.async_show_form(
step_id="init",
@@ -126,6 +130,6 @@ class OpenSkyOptionsFlowHandler(OptionsFlow):
vol.Optional(CONF_CONTRIBUTING_USER, default=False): bool,
}
),
- user_input or self.config_entry.options,
+ user_input or self.options,
),
)
diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py
index 80c16ee88e1..1f52b47cbad 100644
--- a/homeassistant/components/opentherm_gw/config_flow.py
+++ b/homeassistant/components/opentherm_gw/config_flow.py
@@ -49,7 +49,7 @@ class OpenThermGwConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OpenThermGwOptionsFlow:
"""Get the options flow for this handler."""
- return OpenThermGwOptionsFlow()
+ return OpenThermGwOptionsFlow(config_entry)
async def async_step_init(
self, info: dict[str, Any] | None = None
@@ -132,6 +132,10 @@ class OpenThermGwConfigFlow(ConfigFlow, domain=DOMAIN):
class OpenThermGwOptionsFlow(OptionsFlow):
"""Handle opentherm_gw options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize the options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json
index ecd0a6b99d5..927f9c9ca3e 100644
--- a/homeassistant/components/opentherm_gw/manifest.json
+++ b/homeassistant/components/opentherm_gw/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
"iot_class": "local_push",
"loggers": ["pyotgw"],
- "requirements": ["pyotgw==2.2.2"]
+ "requirements": ["pyotgw==2.2.1"]
}
diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py
index 8d33e117287..5fe06ea2dcd 100644
--- a/homeassistant/components/openweathermap/config_flow.py
+++ b/homeassistant/components/openweathermap/config_flow.py
@@ -44,7 +44,7 @@ class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OpenWeatherMapOptionsFlow:
"""Get the options flow for this handler."""
- return OpenWeatherMapOptionsFlow()
+ return OpenWeatherMapOptionsFlow(config_entry)
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
@@ -97,6 +97,10 @@ class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN):
class OpenWeatherMapOptionsFlow(OptionsFlow):
"""Handle options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(self, user_input: dict | None = None) -> ConfigFlowResult:
"""Manage the options."""
if user_input is not None:
diff --git a/homeassistant/components/openweathermap/coordinator.py b/homeassistant/components/openweathermap/coordinator.py
index 3ef0eda0c8f..f7672a1290b 100644
--- a/homeassistant/components/openweathermap/coordinator.py
+++ b/homeassistant/components/openweathermap/coordinator.py
@@ -192,13 +192,12 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
@staticmethod
def _get_precipitation_value(precipitation):
"""Get precipitation value from weather data."""
- if precipitation is not None:
- if "all" in precipitation:
- return round(precipitation["all"], 2)
- if "3h" in precipitation:
- return round(precipitation["3h"], 2)
- if "1h" in precipitation:
- return round(precipitation["1h"], 2)
+ if "all" in precipitation:
+ return round(precipitation["all"], 2)
+ if "3h" in precipitation:
+ return round(precipitation["3h"], 2)
+ if "1h" in precipitation:
+ return round(precipitation["1h"], 2)
return 0
def _get_condition(self, weather_code, timestamp=None):
diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json
index 14313a5a77e..199e750ad4f 100644
--- a/homeassistant/components/openweathermap/manifest.json
+++ b/homeassistant/components/openweathermap/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/openweathermap",
"iot_class": "cloud_polling",
"loggers": ["pyopenweathermap"],
- "requirements": ["pyopenweathermap==0.2.1"]
+ "requirements": ["pyopenweathermap==0.1.1"]
}
diff --git a/homeassistant/components/opower/config_flow.py b/homeassistant/components/opower/config_flow.py
index 6396ba24a15..a9162b060a2 100644
--- a/homeassistant/components/opower/config_flow.py
+++ b/homeassistant/components/opower/config_flow.py
@@ -15,7 +15,7 @@ from opower import (
)
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_create_clientsession
@@ -49,12 +49,8 @@ async def _validate_login(
try:
await api.async_login()
except InvalidAuth:
- _LOGGER.exception(
- "Invalid auth when connecting to %s", login_data[CONF_UTILITY]
- )
errors["base"] = "invalid_auth"
except CannotConnect:
- _LOGGER.exception("Could not connect to %s", login_data[CONF_UTILITY])
errors["base"] = "cannot_connect"
return errors
@@ -66,6 +62,7 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize a new OpowerConfigFlow."""
+ self.reauth_entry: ConfigEntry | None = None
self.utility_info: dict[str, Any] | None = None
async def async_step_user(
@@ -134,29 +131,35 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
+ assert self.reauth_entry
errors: dict[str, str] = {}
- reauth_entry = self._get_reauth_entry()
if user_input is not None:
- data = {**reauth_entry.data, **user_input}
+ data = {**self.reauth_entry.data, **user_input}
errors = await _validate_login(self.hass, data)
if not errors:
- return self.async_update_reload_and_abort(reauth_entry, data=data)
-
+ self.hass.config_entries.async_update_entry(
+ self.reauth_entry, data=data
+ )
+ await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
schema: VolDictType = {
- vol.Required(CONF_USERNAME): reauth_entry.data[CONF_USERNAME],
+ vol.Required(CONF_USERNAME): self.reauth_entry.data[CONF_USERNAME],
vol.Required(CONF_PASSWORD): str,
}
- if select_utility(reauth_entry.data[CONF_UTILITY]).accepts_mfa():
+ if select_utility(self.reauth_entry.data[CONF_UTILITY]).accepts_mfa():
schema[vol.Optional(CONF_TOTP_SECRET)] = str
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(schema),
errors=errors,
- description_placeholders={CONF_NAME: reauth_entry.title},
+ description_placeholders={CONF_NAME: self.reauth_entry.title},
)
diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py
index 629dce0823c..cd2e28ed638 100644
--- a/homeassistant/components/opower/coordinator.py
+++ b/homeassistant/components/opower/coordinator.py
@@ -130,32 +130,20 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
continue
start = cost_reads[0].start_time
_LOGGER.debug("Getting statistics at: %s", start)
- # In the common case there should be a previous statistic at start time
- # so we only need to fetch one statistic. If there isn't any, fetch all.
- for end in (start + timedelta(seconds=1), None):
- stats = await get_instance(self.hass).async_add_executor_job(
- statistics_during_period,
- self.hass,
- start,
- end,
- {cost_statistic_id, consumption_statistic_id},
- "hour",
- None,
- {"sum"},
- )
- if stats:
- break
- if end:
- _LOGGER.debug(
- "Not found. Trying to find the oldest statistic after %s",
- start,
- )
- # We are in this code path only if get_last_statistics found a stat
- # so statistics_during_period should also have found at least one.
- assert stats
+ stats = await get_instance(self.hass).async_add_executor_job(
+ statistics_during_period,
+ self.hass,
+ start,
+ start + timedelta(seconds=1),
+ {cost_statistic_id, consumption_statistic_id},
+ "hour",
+ None,
+ {"sum"},
+ )
cost_sum = cast(float, stats[cost_statistic_id][0]["sum"])
consumption_sum = cast(float, stats[consumption_statistic_id][0]["sum"])
last_stats_time = stats[consumption_statistic_id][0]["start"]
+ assert last_stats_time == start.timestamp()
cost_statistics = []
consumption_statistics = []
diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json
index 593e4cf34b8..23386a777d2 100644
--- a/homeassistant/components/opower/manifest.json
+++ b/homeassistant/components/opower/manifest.json
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
- "requirements": ["opower==0.8.6"]
+ "requirements": ["opower==0.8.2"]
}
diff --git a/homeassistant/components/osoenergy/config_flow.py b/homeassistant/components/osoenergy/config_flow.py
index a47f90e3c04..0642250e9ed 100644
--- a/homeassistant/components/osoenergy/config_flow.py
+++ b/homeassistant/components/osoenergy/config_flow.py
@@ -7,7 +7,12 @@ from typing import Any
from apyosoenergyapi import OSOEnergy
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import (
+ SOURCE_REAUTH,
+ ConfigEntry,
+ ConfigFlow,
+ ConfigFlowResult,
+)
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import aiohttp_client
@@ -22,6 +27,10 @@ class OSOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ def __init__(self) -> None:
+ """Initialize."""
+ self.entry: ConfigEntry | None = None
+
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors = {}
@@ -31,10 +40,12 @@ class OSOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
if user_email := await self.get_user_email(user_input[CONF_API_KEY]):
await self.async_set_unique_id(user_email)
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), title=user_email, data=user_input
+ if self.context["source"] == SOURCE_REAUTH and self.entry:
+ self.hass.config_entries.async_update_entry(
+ self.entry, title=user_email, data=user_input
)
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
self._abort_if_unique_id_configured()
return self.async_create_entry(title=user_email, data=user_input)
@@ -61,9 +72,6 @@ class OSOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Re Authenticate a user."""
- return self.async_show_form(
- step_id="user",
- data_schema=self.add_suggested_values_to_schema(
- _SCHEMA_STEP_USER, self._get_reauth_entry().data
- ),
- )
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+ data = {CONF_API_KEY: entry_data[CONF_API_KEY]}
+ return await self.async_step_user(data)
diff --git a/homeassistant/components/osoenergy/icons.json b/homeassistant/components/osoenergy/icons.json
index 42d1f2cc480..60b2d257b8a 100644
--- a/homeassistant/components/osoenergy/icons.json
+++ b/homeassistant/components/osoenergy/icons.json
@@ -11,22 +11,5 @@
"default": "mdi:water-boiler"
}
}
- },
- "services": {
- "get_profile": {
- "service": "mdi:thermometer-lines"
- },
- "set_profile": {
- "service": "mdi:thermometer-lines"
- },
- "set_v40_min": {
- "service": "mdi:car-coolant-level"
- },
- "turn_off": {
- "service": "mdi:water-boiler-off"
- },
- "turn_on": {
- "service": "mdi:water-boiler"
- }
}
}
diff --git a/homeassistant/components/osoenergy/services.yaml b/homeassistant/components/osoenergy/services.yaml
deleted file mode 100644
index 6c8f5512215..00000000000
--- a/homeassistant/components/osoenergy/services.yaml
+++ /dev/null
@@ -1,261 +0,0 @@
-get_profile:
- target:
- entity:
- domain: water_heater
-set_profile:
- target:
- entity:
- domain: water_heater
- fields:
- hour_00:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_01:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_02:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_03:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_04:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_05:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_06:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_07:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_08:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_09:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_10:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_11:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_12:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_13:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_14:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_15:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_16:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_17:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_18:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_19:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_20:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_21:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_22:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
- hour_23:
- required: false
- example: 75
- selector:
- number:
- min: 10
- max: 75
- step: 1
- unit_of_measurement: °C
-set_v40_min:
- target:
- entity:
- domain: water_heater
- fields:
- v40_min:
- required: true
- example: 240
- selector:
- number:
- min: 200
- max: 550
- step: 1
- unit_of_measurement: L
-turn_off:
- target:
- entity:
- domain: water_heater
- fields:
- until_temp_limit:
- required: true
- default: false
- example: false
- selector:
- boolean:
-turn_on:
- target:
- entity:
- domain: water_heater
- fields:
- until_temp_limit:
- required: true
- default: false
- example: false
- selector:
- boolean:
diff --git a/homeassistant/components/osoenergy/strings.json b/homeassistant/components/osoenergy/strings.json
index b8f95c021fa..a7963bfa436 100644
--- a/homeassistant/components/osoenergy/strings.json
+++ b/homeassistant/components/osoenergy/strings.json
@@ -91,143 +91,5 @@
"name": "Temperature one"
}
}
- },
- "services": {
- "get_profile": {
- "name": "Get heater profile",
- "description": "Get the temperature profile of water heater"
- },
- "set_profile": {
- "name": "Set heater profile",
- "description": "Set the temperature profile of water heater",
- "fields": {
- "hour_00": {
- "name": "00:00",
- "description": "00:00 hour"
- },
- "hour_01": {
- "name": "01:00",
- "description": "01:00 hour"
- },
- "hour_02": {
- "name": "02:00",
- "description": "02:00 hour"
- },
- "hour_03": {
- "name": "03:00",
- "description": "03:00 hour"
- },
- "hour_04": {
- "name": "04:00",
- "description": "04:00 hour"
- },
- "hour_05": {
- "name": "05:00",
- "description": "05:00 hour"
- },
- "hour_06": {
- "name": "06:00",
- "description": "06:00 hour"
- },
- "hour_07": {
- "name": "07:00",
- "description": "07:00 hour"
- },
- "hour_08": {
- "name": "08:00",
- "description": "08:00 hour"
- },
- "hour_09": {
- "name": "09:00",
- "description": "09:00 hour"
- },
- "hour_10": {
- "name": "10:00",
- "description": "10:00 hour"
- },
- "hour_11": {
- "name": "11:00",
- "description": "11:00 hour"
- },
- "hour_12": {
- "name": "12:00",
- "description": "12:00 hour"
- },
- "hour_13": {
- "name": "13:00",
- "description": "13:00 hour"
- },
- "hour_14": {
- "name": "14:00",
- "description": "14:00 hour"
- },
- "hour_15": {
- "name": "15:00",
- "description": "15:00 hour"
- },
- "hour_16": {
- "name": "16:00",
- "description": "16:00 hour"
- },
- "hour_17": {
- "name": "17:00",
- "description": "17:00 hour"
- },
- "hour_18": {
- "name": "18:00",
- "description": "18:00 hour"
- },
- "hour_19": {
- "name": "19:00",
- "description": "19:00 hour"
- },
- "hour_20": {
- "name": "20:00",
- "description": "20:00 hour"
- },
- "hour_21": {
- "name": "21:00",
- "description": "21:00 hour"
- },
- "hour_22": {
- "name": "22:00",
- "description": "22:00 hour"
- },
- "hour_23": {
- "name": "23:00",
- "description": "23:00 hour"
- }
- }
- },
- "set_v40_min": {
- "name": "Set v40 min",
- "description": "Set the minimum quantity of water at 40°C for a heater",
- "fields": {
- "v40_min": {
- "name": "V40 Min",
- "description": "Minimum quantity of water at 40°C (200-350 for SAGA S200, 300-550 for SAGA S300)"
- }
- }
- },
- "turn_off": {
- "name": "Turn off heating",
- "description": "Turn off heating for one hour or until min temperature is reached",
- "fields": {
- "until_temp_limit": {
- "name": "Until temperature limit",
- "description": "Choose if heating should be off until min temperature (True) is reached or for one hour (False)"
- }
- }
- },
- "turn_on": {
- "name": "Turn on heating",
- "description": "Turn on heating for one hour or until max temperature is reached",
- "fields": {
- "until_temp_limit": {
- "name": "Until temperature limit",
- "description": "Choose if heating should be on until max temperature (True) is reached or for one hour (False)"
- }
- }
- }
}
}
diff --git a/homeassistant/components/osoenergy/water_heater.py b/homeassistant/components/osoenergy/water_heater.py
index ff117d6577d..55229e42c2f 100644
--- a/homeassistant/components/osoenergy/water_heater.py
+++ b/homeassistant/components/osoenergy/water_heater.py
@@ -1,11 +1,9 @@
"""Support for OSO Energy water heaters."""
-import datetime as dt
from typing import Any
from apyosoenergyapi import OSOEnergy
from apyosoenergyapi.helper.const import OSOEnergyWaterHeaterData
-import voluptuous as vol
from homeassistant.components.water_heater import (
STATE_ECO,
@@ -17,17 +15,12 @@ from homeassistant.components.water_heater import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTemperature
-from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
-from homeassistant.helpers import config_validation as cv, entity_platform
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-import homeassistant.util.dt as dt_util
-from homeassistant.util.json import JsonValueType
from .const import DOMAIN
from .entity import OSOEnergyEntity
-ATTR_UNTIL_TEMP_LIMIT = "until_temp_limit"
-ATTR_V40MIN = "v40_min"
CURRENT_OPERATION_MAP: dict[str, Any] = {
"default": {
"off": STATE_OFF,
@@ -41,11 +34,6 @@ CURRENT_OPERATION_MAP: dict[str, Any] = {
"extraenergy": STATE_HIGH_DEMAND,
},
}
-SERVICE_GET_PROFILE = "get_profile"
-SERVICE_SET_PROFILE = "set_profile"
-SERVICE_SET_V40MIN = "set_v40_min"
-SERVICE_TURN_OFF = "turn_off"
-SERVICE_TURN_ON = "turn_on"
async def async_setup_entry(
@@ -58,102 +46,6 @@ async def async_setup_entry(
return
async_add_entities((OSOEnergyWaterHeater(osoenergy, dev) for dev in devices), True)
- platform = entity_platform.async_get_current_platform()
-
- platform.async_register_entity_service(
- SERVICE_GET_PROFILE,
- {},
- OSOEnergyWaterHeater.async_get_profile.__name__,
- supports_response=SupportsResponse.ONLY,
- )
-
- service_set_profile_schema = cv.make_entity_service_schema(
- {
- vol.Optional(f"hour_{hour:02d}"): vol.All(
- vol.Coerce(int), vol.Range(min=10, max=75)
- )
- for hour in range(24)
- }
- )
-
- platform.async_register_entity_service(
- SERVICE_SET_PROFILE,
- service_set_profile_schema,
- OSOEnergyWaterHeater.async_set_profile.__name__,
- )
-
- platform.async_register_entity_service(
- SERVICE_SET_V40MIN,
- {
- vol.Required(ATTR_V40MIN): vol.All(
- vol.Coerce(float), vol.Range(min=200, max=550)
- ),
- },
- OSOEnergyWaterHeater.async_set_v40_min.__name__,
- )
-
- platform.async_register_entity_service(
- SERVICE_TURN_OFF,
- {vol.Required(ATTR_UNTIL_TEMP_LIMIT): vol.All(cv.boolean)},
- OSOEnergyWaterHeater.async_oso_turn_off.__name__,
- )
-
- platform.async_register_entity_service(
- SERVICE_TURN_ON,
- {vol.Required(ATTR_UNTIL_TEMP_LIMIT): vol.All(cv.boolean)},
- OSOEnergyWaterHeater.async_oso_turn_on.__name__,
- )
-
-
-def _get_utc_hour(local_hour: int) -> dt.datetime:
- """Convert the requested local hour to a utc hour for the day.
-
- Args:
- local_hour: the local hour (0-23) for the current day to be converted.
-
- Returns:
- Datetime representation for the requested hour in utc time for the day.
-
- """
- now = dt_util.now()
- local_time = now.replace(hour=local_hour, minute=0, second=0, microsecond=0)
- return dt_util.as_utc(local_time)
-
-
-def _get_local_hour(utc_hour: int) -> dt.datetime:
- """Convert the requested utc hour to a local hour for the day.
-
- Args:
- utc_hour: the utc hour (0-23) for the current day to be converted.
-
- Returns:
- Datetime representation for the requested hour in local time for the day.
-
- """
- utc_now = dt_util.utcnow()
- utc_time = utc_now.replace(hour=utc_hour, minute=0, second=0, microsecond=0)
- return dt_util.as_local(utc_time)
-
-
-def _convert_profile_to_local(values: list[float]) -> list[JsonValueType]:
- """Convert UTC profile to local.
-
- Receives a device temperature schedule - 24 values for the day where the index represents the hour of the day in UTC.
- Converts the schedule to local time.
-
- Args:
- values: list of floats representing the 24 hour temperature schedule for the device
- Returns:
- The device temperature schedule in local time.
-
- """
- profile: list[JsonValueType] = [0.0] * 24
- for hour in range(24):
- local_hour = _get_local_hour(hour)
- profile[local_hour.hour] = float(values[hour])
-
- return profile
-
class OSOEnergyWaterHeater(
OSOEnergyEntity[OSOEnergyWaterHeaterData], WaterHeaterEntity
@@ -161,9 +53,7 @@ class OSOEnergyWaterHeater(
"""OSO Energy Water Heater Device."""
_attr_name = None
- _attr_supported_features = (
- WaterHeaterEntityFeature.TARGET_TEMPERATURE | WaterHeaterEntityFeature.ON_OFF
- )
+ _attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
def __init__(
@@ -241,36 +131,6 @@ class OSOEnergyWaterHeater(
await self.osoenergy.hotwater.set_profile(self.entity_data, profile)
- async def async_get_profile(self) -> ServiceResponse:
- """Return the current temperature profile of the device."""
-
- profile = self.entity_data.profile
- return {"profile": _convert_profile_to_local(profile)}
-
- async def async_set_profile(self, **kwargs: Any) -> None:
- """Handle the service call."""
- profile = self.entity_data.profile
-
- for hour in range(24):
- hour_key = f"hour_{hour:02d}"
-
- if hour_key in kwargs:
- profile[_get_utc_hour(hour).hour] = kwargs[hour_key]
-
- await self.osoenergy.hotwater.set_profile(self.entity_data, profile)
-
- async def async_set_v40_min(self, v40_min) -> None:
- """Handle the service call."""
- await self.osoenergy.hotwater.set_v40_min(self.entity_data, v40_min)
-
- async def async_oso_turn_off(self, until_temp_limit) -> None:
- """Handle the service call."""
- await self.osoenergy.hotwater.turn_off(self.entity_data, until_temp_limit)
-
- async def async_oso_turn_on(self, until_temp_limit) -> None:
- """Handle the service call."""
- await self.osoenergy.hotwater.turn_on(self.entity_data, until_temp_limit)
-
async def async_update(self) -> None:
"""Update all Node data from Hive."""
await self.osoenergy.session.update_data()
diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py
index aff79ca4651..f24d141247d 100644
--- a/homeassistant/components/otbr/config_flow.py
+++ b/homeassistant/components/otbr/config_flow.py
@@ -13,7 +13,7 @@ from python_otbr_api.tlv_parser import MeshcopTLVType
import voluptuous as vol
import yarl
-from homeassistant.components.hassio import AddonError, AddonManager
+from homeassistant.components.hassio import AddonError, AddonManager, HassioServiceInfo
from homeassistant.components.homeassistant_yellow import hardware as yellow_hardware
from homeassistant.components.thread import async_get_preferred_dataset
from homeassistant.config_entries import SOURCE_HASSIO, ConfigFlow, ConfigFlowResult
@@ -21,7 +21,6 @@ from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from .const import DEFAULT_CHANNEL, DOMAIN
from .util import (
diff --git a/homeassistant/components/overkiz/alarm_control_panel.py b/homeassistant/components/overkiz/alarm_control_panel.py
index bdbf4d0cc8d..151f91790cf 100644
--- a/homeassistant/components/overkiz/alarm_control_panel.py
+++ b/homeassistant/components/overkiz/alarm_control_panel.py
@@ -14,10 +14,18 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityDescription,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import Platform
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
+ Platform,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -33,7 +41,7 @@ class OverkizAlarmDescription(AlarmControlPanelEntityDescription):
"""Class to describe an Overkiz alarm control panel."""
supported_features: AlarmControlPanelEntityFeature
- fn_state: Callable[[Callable[[str], OverkizStateType]], AlarmControlPanelState]
+ fn_state: Callable[[Callable[[str], OverkizStateType]], str]
alarm_disarm: str | None = None
alarm_disarm_args: OverkizStateType | list[OverkizStateType] = None
@@ -47,44 +55,42 @@ class OverkizAlarmDescription(AlarmControlPanelEntityDescription):
alarm_trigger_args: OverkizStateType | list[OverkizStateType] = None
-MAP_INTERNAL_STATUS_STATE: dict[str, AlarmControlPanelState] = {
- OverkizCommandParam.OFF: AlarmControlPanelState.DISARMED,
- OverkizCommandParam.ZONE_1: AlarmControlPanelState.ARMED_HOME,
- OverkizCommandParam.ZONE_2: AlarmControlPanelState.ARMED_NIGHT,
- OverkizCommandParam.TOTAL: AlarmControlPanelState.ARMED_AWAY,
+MAP_INTERNAL_STATUS_STATE: dict[str, str] = {
+ OverkizCommandParam.OFF: STATE_ALARM_DISARMED,
+ OverkizCommandParam.ZONE_1: STATE_ALARM_ARMED_HOME,
+ OverkizCommandParam.ZONE_2: STATE_ALARM_ARMED_NIGHT,
+ OverkizCommandParam.TOTAL: STATE_ALARM_ARMED_AWAY,
}
-def _state_tsk_alarm_controller(
- select_state: Callable[[str], OverkizStateType],
-) -> AlarmControlPanelState:
+def _state_tsk_alarm_controller(select_state: Callable[[str], OverkizStateType]) -> str:
"""Return the state of the device."""
if (
cast(str, select_state(OverkizState.INTERNAL_INTRUSION_DETECTED))
== OverkizCommandParam.DETECTED
):
- return AlarmControlPanelState.TRIGGERED
+ return STATE_ALARM_TRIGGERED
if cast(str, select_state(OverkizState.INTERNAL_CURRENT_ALARM_MODE)) != cast(
str, select_state(OverkizState.INTERNAL_TARGET_ALARM_MODE)
):
- return AlarmControlPanelState.PENDING
+ return STATE_ALARM_PENDING
return MAP_INTERNAL_STATUS_STATE[
cast(str, select_state(OverkizState.INTERNAL_TARGET_ALARM_MODE))
]
-MAP_CORE_ACTIVE_ZONES: dict[str, AlarmControlPanelState] = {
- OverkizCommandParam.A: AlarmControlPanelState.ARMED_HOME,
- f"{OverkizCommandParam.A},{OverkizCommandParam.B}": AlarmControlPanelState.ARMED_NIGHT,
- f"{OverkizCommandParam.A},{OverkizCommandParam.B},{OverkizCommandParam.C}": AlarmControlPanelState.ARMED_AWAY,
+MAP_CORE_ACTIVE_ZONES: dict[str, str] = {
+ OverkizCommandParam.A: STATE_ALARM_ARMED_HOME,
+ f"{OverkizCommandParam.A},{OverkizCommandParam.B}": STATE_ALARM_ARMED_NIGHT,
+ f"{OverkizCommandParam.A},{OverkizCommandParam.B},{OverkizCommandParam.C}": STATE_ALARM_ARMED_AWAY,
}
def _state_stateful_alarm_controller(
select_state: Callable[[str], OverkizStateType],
-) -> AlarmControlPanelState:
+) -> str:
"""Return the state of the device."""
if state := cast(str, select_state(OverkizState.CORE_ACTIVE_ZONES)):
# The Stateful Alarm Controller has 3 zones with the following options:
@@ -93,44 +99,44 @@ def _state_stateful_alarm_controller(
if state in MAP_CORE_ACTIVE_ZONES:
return MAP_CORE_ACTIVE_ZONES[state]
- return AlarmControlPanelState.ARMED_CUSTOM_BYPASS
+ return STATE_ALARM_ARMED_CUSTOM_BYPASS
- return AlarmControlPanelState.DISARMED
+ return STATE_ALARM_DISARMED
-MAP_MYFOX_STATUS_STATE: dict[str, AlarmControlPanelState] = {
- OverkizCommandParam.ARMED: AlarmControlPanelState.ARMED_AWAY,
- OverkizCommandParam.DISARMED: AlarmControlPanelState.DISARMED,
- OverkizCommandParam.PARTIAL: AlarmControlPanelState.ARMED_NIGHT,
+MAP_MYFOX_STATUS_STATE: dict[str, str] = {
+ OverkizCommandParam.ARMED: STATE_ALARM_ARMED_AWAY,
+ OverkizCommandParam.DISARMED: STATE_ALARM_DISARMED,
+ OverkizCommandParam.PARTIAL: STATE_ALARM_ARMED_NIGHT,
}
def _state_myfox_alarm_controller(
select_state: Callable[[str], OverkizStateType],
-) -> AlarmControlPanelState:
+) -> str:
"""Return the state of the device."""
if (
cast(str, select_state(OverkizState.CORE_INTRUSION))
== OverkizCommandParam.DETECTED
):
- return AlarmControlPanelState.TRIGGERED
+ return STATE_ALARM_TRIGGERED
return MAP_MYFOX_STATUS_STATE[
cast(str, select_state(OverkizState.MYFOX_ALARM_STATUS))
]
-MAP_ARM_TYPE: dict[str, AlarmControlPanelState] = {
- OverkizCommandParam.DISARMED: AlarmControlPanelState.DISARMED,
- OverkizCommandParam.ARMED_DAY: AlarmControlPanelState.ARMED_HOME,
- OverkizCommandParam.ARMED_NIGHT: AlarmControlPanelState.ARMED_NIGHT,
- OverkizCommandParam.ARMED: AlarmControlPanelState.ARMED_AWAY,
+MAP_ARM_TYPE: dict[str, str] = {
+ OverkizCommandParam.DISARMED: STATE_ALARM_DISARMED,
+ OverkizCommandParam.ARMED_DAY: STATE_ALARM_ARMED_HOME,
+ OverkizCommandParam.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT,
+ OverkizCommandParam.ARMED: STATE_ALARM_ARMED_AWAY,
}
def _state_alarm_panel_controller(
select_state: Callable[[str], OverkizStateType],
-) -> AlarmControlPanelState:
+) -> str:
"""Return the state of the device."""
return MAP_ARM_TYPE[
cast(str, select_state(OverkizState.VERISURE_ALARM_PANEL_MAIN_ARM_TYPE))
@@ -248,7 +254,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
self._attr_supported_features = self.entity_description.supported_features
@property
- def alarm_state(self) -> AlarmControlPanelState:
+ def state(self) -> str:
"""Return the state of the device."""
return self.entity_description.fn_state(self.executor.select_state)
diff --git a/homeassistant/components/overkiz/climate/__init__.py b/homeassistant/components/overkiz/climate/__init__.py
index 97840df7a41..f05a716031e 100644
--- a/homeassistant/components/overkiz/climate/__init__.py
+++ b/homeassistant/components/overkiz/climate/__init__.py
@@ -96,7 +96,7 @@ async def async_setup_entry(
# ie Atlantic APC
entities_based_on_widget_and_controllable: list[Entity] = [
WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget][
- device.controllable_name # type: ignore[index]
+ device.controllable_name
](device.device_url, data.coordinator)
for device in data.platforms[Platform.CLIMATE]
if device.widget in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY
diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py
index 471a13d0de2..4b88cd4a3e8 100644
--- a/homeassistant/components/overkiz/config_flow.py
+++ b/homeassistant/components/overkiz/config_flow.py
@@ -24,7 +24,7 @@ from pyoverkiz.utils import generate_local_server, is_overkiz_gateway
import voluptuous as vol
from homeassistant.components import dhcp, zeroconf
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -47,6 +47,7 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ _reauth_entry: ConfigEntry | None = None
_api_type: APIType = APIType.CLOUD
_user: str | None = None
_server: str = DEFAULT_SERVER
@@ -173,13 +174,27 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
LOGGER.exception("Unknown error")
else:
- if self.source == SOURCE_REAUTH:
- self._abort_if_unique_id_mismatch(reason="reauth_wrong_account")
+ if self._reauth_entry:
+ if self._reauth_entry.unique_id != self.unique_id:
+ return self.async_abort(reason="reauth_wrong_account")
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=user_input
+ # Update existing entry during reauth
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry,
+ data={
+ **self._reauth_entry.data,
+ **user_input,
+ },
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(
+ self._reauth_entry.entry_id
+ )
+ )
+
+ return self.async_abort(reason="reauth_successful")
+
# Create new entry
self._abort_if_unique_id_configured()
@@ -242,13 +257,27 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
LOGGER.exception("Unknown error")
else:
- if self.source == SOURCE_REAUTH:
- self._abort_if_unique_id_mismatch(reason="reauth_wrong_account")
+ if self._reauth_entry:
+ if self._reauth_entry.unique_id != self.unique_id:
+ return self.async_abort(reason="reauth_wrong_account")
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=user_input
+ # Update existing entry during reauth
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry,
+ data={
+ **self._reauth_entry.data,
+ **user_input,
+ },
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(
+ self._reauth_entry.entry_id
+ )
+ )
+
+ return self.async_abort(reason="reauth_successful")
+
# Create new entry
self._abort_if_unique_id_configured()
@@ -317,15 +346,22 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth."""
- # overkiz entries always have unique IDs
- self.context["title_placeholders"] = {"gateway_id": cast(str, self.unique_id)}
+ self._reauth_entry = cast(
+ ConfigEntry,
+ self.hass.config_entries.async_get_entry(self.context["entry_id"]),
+ )
- self._user = entry_data[CONF_USERNAME]
- self._server = entry_data[CONF_HUB]
- self._api_type = entry_data.get(CONF_API_TYPE, APIType.CLOUD)
+ # overkiz entries always have unique IDs
+ self.context["title_placeholders"] = {
+ "gateway_id": cast(str, self._reauth_entry.unique_id)
+ }
+
+ self._user = self._reauth_entry.data[CONF_USERNAME]
+ self._server = self._reauth_entry.data[CONF_HUB]
+ self._api_type = self._reauth_entry.data.get(CONF_API_TYPE, APIType.CLOUD)
if self._api_type == APIType.LOCAL:
- self._host = entry_data[CONF_HOST]
+ self._host = self._reauth_entry.data[CONF_HOST]
return await self.async_step_user(dict(entry_data))
diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py
index 436180407f4..0576421fa71 100644
--- a/homeassistant/components/ovo_energy/__init__.py
+++ b/homeassistant/components/ovo_energy/__init__.py
@@ -67,7 +67,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator[OVODailyUsage](
hass,
_LOGGER,
- config_entry=entry,
# Name of the data. For logging purposes.
name="sensor",
update_method=async_update_data,
diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py
index 53fc4f8eff6..60a2870ef59 100644
--- a/homeassistant/components/ovo_energy/config_flow.py
+++ b/homeassistant/components/ovo_energy/config_flow.py
@@ -79,26 +79,22 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_reauth(
self,
- entry_data: Mapping[str, Any],
+ user_input: Mapping[str, Any],
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
- self.username = entry_data.get(CONF_USERNAME)
- self.account = entry_data.get(CONF_ACCOUNT)
+ errors = {}
+
+ if user_input and user_input.get(CONF_USERNAME):
+ self.username = user_input[CONF_USERNAME]
+
+ if user_input and user_input.get(CONF_ACCOUNT):
+ self.account = user_input[CONF_ACCOUNT]
if self.username:
# If we have a username, use it as flow title
self.context["title_placeholders"] = {CONF_USERNAME: self.username}
- return await self.async_step_reauth_confirm()
-
- async def async_step_reauth_confirm(
- self,
- user_input: Mapping[str, Any] | None = None,
- ) -> ConfigFlowResult:
- """Handle configuration by re-auth."""
- errors = {}
-
- if user_input is not None:
+ if user_input is not None and user_input.get(CONF_PASSWORD) is not None:
client = OVOEnergy(
client_session=async_get_clientsession(self.hass),
)
@@ -115,13 +111,19 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
errors["base"] = "connection_error"
else:
if authenticated:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
- data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]},
- )
+ entry = await self.async_set_unique_id(self.username)
+ if entry:
+ self.hass.config_entries.async_update_entry(
+ entry,
+ data={
+ CONF_USERNAME: self.username,
+ CONF_PASSWORD: user_input[CONF_PASSWORD],
+ },
+ )
+ return self.async_abort(reason="reauth_successful")
errors["base"] = "authorization_error"
return self.async_show_form(
- step_id="reauth_confirm", data_schema=REAUTH_SCHEMA, errors=errors
+ step_id="reauth", data_schema=REAUTH_SCHEMA, errors=errors
)
diff --git a/homeassistant/components/ovo_energy/strings.json b/homeassistant/components/ovo_energy/strings.json
index 3dc11e3a601..fda0c2996dc 100644
--- a/homeassistant/components/ovo_energy/strings.json
+++ b/homeassistant/components/ovo_energy/strings.json
@@ -1,15 +1,10 @@
{
"config": {
"flow_title": "{username}",
- "abort": {
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
- },
"error": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "connection_error": "[%key:common::config_flow::error::cannot_connect%]",
- "authorization_error": "[%key:common::config_flow::error::invalid_auth%]"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"user": {
@@ -21,7 +16,7 @@
"description": "Set up an OVO Energy instance to access your energy usage.",
"title": "Add OVO Energy Account"
},
- "reauth_confirm": {
+ "reauth": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py
index b92f5d7ce06..390cc880c1e 100644
--- a/homeassistant/components/owntracks/config_flow.py
+++ b/homeassistant/components/owntracks/config_flow.py
@@ -23,6 +23,9 @@ class OwnTracksFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a user initiated set up flow to create OwnTracks webhook."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
if user_input is None:
return self.async_show_form(step_id="user")
diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json
index 7ff5a143451..79af00627a4 100644
--- a/homeassistant/components/owntracks/manifest.json
+++ b/homeassistant/components/owntracks/manifest.json
@@ -8,6 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/owntracks",
"iot_class": "local_push",
"loggers": ["nacl"],
- "requirements": ["PyNaCl==1.5.0"],
- "single_config_entry": true
+ "requirements": ["PyNaCl==1.5.0"]
}
diff --git a/homeassistant/components/owntracks/strings.json b/homeassistant/components/owntracks/strings.json
index 3c08550dab7..8fdd771b95e 100644
--- a/homeassistant/components/owntracks/strings.json
+++ b/homeassistant/components/owntracks/strings.json
@@ -7,7 +7,8 @@
}
},
"abort": {
- "cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]"
+ "cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]",
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
},
"create_entry": {
"default": "On Android, open [the OwnTracks app]({android_url}), go to Preferences > Connection. Change the following settings:\n - Mode: HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `'(Your name)'`\n - Device ID: `'(Your device name)'`\n\nOn iOS, open [the OwnTracks app]({ios_url}), tap (i) icon in top left > Settings. Change the following settings:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `'(Your name)'`\n\n{secret}\n\nSee [the documentation]({docs_url}) for more information."
diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py
index 3361506dafb..8125e9f7a55 100644
--- a/homeassistant/components/p1_monitor/__init__.py
+++ b/homeassistant/components/p1_monitor/__init__.py
@@ -3,11 +3,11 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST, CONF_PORT, Platform
+from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
-from .const import DOMAIN, LOGGER
+from .const import DOMAIN
from .coordinator import P1MonitorDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
@@ -30,29 +30,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
-async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
- """Migrate old entry."""
- LOGGER.debug("Migrating from version %s", config_entry.version)
-
- if config_entry.version == 1:
- # Migrate to split host and port
- host = config_entry.data[CONF_HOST]
- if ":" in host:
- host, port = host.split(":")
- else:
- port = 80
-
- new_data = {
- **config_entry.data,
- CONF_HOST: host,
- CONF_PORT: int(port),
- }
-
- hass.config_entries.async_update_entry(config_entry, data=new_data, version=2)
- LOGGER.debug("Migration to version %s successful", config_entry.version)
- return True
-
-
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload P1 Monitor config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/p1_monitor/config_flow.py b/homeassistant/components/p1_monitor/config_flow.py
index a7ede186d72..9c039d06b94 100644
--- a/homeassistant/components/p1_monitor/config_flow.py
+++ b/homeassistant/components/p1_monitor/config_flow.py
@@ -8,14 +8,9 @@ from p1monitor import P1Monitor, P1MonitorError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.selector import (
- NumberSelector,
- NumberSelectorConfig,
- NumberSelectorMode,
- TextSelector,
-)
+from homeassistant.helpers.selector import TextSelector
from .const import DOMAIN
@@ -23,7 +18,7 @@ from .const import DOMAIN
class P1MonitorFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for P1 Monitor."""
- VERSION = 2
+ VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -36,9 +31,7 @@ class P1MonitorFlowHandler(ConfigFlow, domain=DOMAIN):
session = async_get_clientsession(self.hass)
try:
async with P1Monitor(
- host=user_input[CONF_HOST],
- port=user_input[CONF_PORT],
- session=session,
+ host=user_input[CONF_HOST], session=session
) as client:
await client.smartmeter()
except P1MonitorError:
@@ -48,7 +41,6 @@ class P1MonitorFlowHandler(ConfigFlow, domain=DOMAIN):
title="P1 Monitor",
data={
CONF_HOST: user_input[CONF_HOST],
- CONF_PORT: user_input[CONF_PORT],
},
)
@@ -57,14 +49,6 @@ class P1MonitorFlowHandler(ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema(
{
vol.Required(CONF_HOST): TextSelector(),
- vol.Required(CONF_PORT, default=80): vol.All(
- NumberSelector(
- NumberSelectorConfig(
- min=1, max=65535, mode=NumberSelectorMode.BOX
- ),
- ),
- vol.Coerce(int),
- ),
}
),
errors=errors,
diff --git a/homeassistant/components/p1_monitor/coordinator.py b/homeassistant/components/p1_monitor/coordinator.py
index 5459f88c388..49844adf39b 100644
--- a/homeassistant/components/p1_monitor/coordinator.py
+++ b/homeassistant/components/p1_monitor/coordinator.py
@@ -15,7 +15,7 @@ from p1monitor import (
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@@ -59,9 +59,7 @@ class P1MonitorDataUpdateCoordinator(DataUpdateCoordinator[P1MonitorData]):
)
self.p1monitor = P1Monitor(
- host=self.config_entry.data[CONF_HOST],
- port=self.config_entry.data[CONF_PORT],
- session=async_get_clientsession(hass),
+ self.config_entry.data[CONF_HOST], session=async_get_clientsession(hass)
)
async def _async_update_data(self) -> P1MonitorData:
diff --git a/homeassistant/components/p1_monitor/diagnostics.py b/homeassistant/components/p1_monitor/diagnostics.py
index c8b4e99099e..5fb8cb472e8 100644
--- a/homeassistant/components/p1_monitor/diagnostics.py
+++ b/homeassistant/components/p1_monitor/diagnostics.py
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any, cast
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from .const import (
@@ -22,7 +22,9 @@ from .coordinator import P1MonitorDataUpdateCoordinator
if TYPE_CHECKING:
from _typeshed import DataclassInstance
-TO_REDACT = {CONF_HOST, CONF_PORT}
+TO_REDACT = {
+ CONF_HOST,
+}
async def async_get_config_entry_diagnostics(
diff --git a/homeassistant/components/p1_monitor/strings.json b/homeassistant/components/p1_monitor/strings.json
index b64f1dcc291..781ca109235 100644
--- a/homeassistant/components/p1_monitor/strings.json
+++ b/homeassistant/components/p1_monitor/strings.json
@@ -4,12 +4,10 @@
"user": {
"description": "Set up P1 Monitor to integrate with Home Assistant.",
"data": {
- "host": "[%key:common::config_flow::data::host%]",
- "port": "[%key:common::config_flow::data::port%]"
+ "host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
- "host": "The IP address or hostname of your P1 Monitor installation.",
- "port": "The port of your P1 Monitor installation."
+ "host": "The IP address or hostname of your P1 Monitor installation."
}
}
},
diff --git a/homeassistant/components/palazzetti/__init__.py b/homeassistant/components/palazzetti/__init__.py
deleted file mode 100644
index ecaa8089097..00000000000
--- a/homeassistant/components/palazzetti/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""The Palazzetti integration."""
-
-from __future__ import annotations
-
-from homeassistant.const import Platform
-from homeassistant.core import HomeAssistant
-
-from .coordinator import PalazzettiConfigEntry, PalazzettiDataUpdateCoordinator
-
-PLATFORMS: list[Platform] = [Platform.CLIMATE]
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: PalazzettiConfigEntry) -> bool:
- """Set up Palazzetti from a config entry."""
-
- coordinator = PalazzettiDataUpdateCoordinator(hass)
-
- 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: PalazzettiConfigEntry) -> bool:
- """Unload a config entry."""
-
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/palazzetti/climate.py b/homeassistant/components/palazzetti/climate.py
deleted file mode 100644
index aff988051f3..00000000000
--- a/homeassistant/components/palazzetti/climate.py
+++ /dev/null
@@ -1,160 +0,0 @@
-"""Support for Palazzetti climates."""
-
-from typing import Any
-
-from pypalazzetti.exceptions import CommunicationError, ValidationError
-
-from homeassistant.components.climate import (
- ClimateEntity,
- ClimateEntityFeature,
- HVACMode,
-)
-from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
-from homeassistant.helpers import device_registry as dr
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from . import PalazzettiConfigEntry
-from .const import DOMAIN, FAN_AUTO, FAN_HIGH, FAN_MODES, FAN_SILENT, PALAZZETTI
-from .coordinator import PalazzettiDataUpdateCoordinator
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: PalazzettiConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up Palazzetti climates based on a config entry."""
- async_add_entities([PalazzettiClimateEntity(entry.runtime_data)])
-
-
-class PalazzettiClimateEntity(
- CoordinatorEntity[PalazzettiDataUpdateCoordinator], ClimateEntity
-):
- """Defines a Palazzetti climate."""
-
- _attr_has_entity_name = True
- _attr_name = None
- _attr_translation_key = DOMAIN
- _attr_target_temperature_step = 1.0
- _attr_temperature_unit = UnitOfTemperature.CELSIUS
- _attr_supported_features = (
- ClimateEntityFeature.TARGET_TEMPERATURE
- | ClimateEntityFeature.FAN_MODE
- | ClimateEntityFeature.TURN_ON
- | ClimateEntityFeature.TURN_OFF
- )
-
- def __init__(self, coordinator: PalazzettiDataUpdateCoordinator) -> None:
- """Initialize Palazzetti climate."""
- super().__init__(coordinator)
- client = coordinator.client
- mac = coordinator.config_entry.unique_id
- assert mac is not None
- self._attr_unique_id = mac
- self._attr_device_info = dr.DeviceInfo(
- connections={(dr.CONNECTION_NETWORK_MAC, mac)},
- name=client.name,
- manufacturer=PALAZZETTI,
- sw_version=client.sw_version,
- hw_version=client.hw_version,
- )
- self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
- self._attr_min_temp = client.target_temperature_min
- self._attr_max_temp = client.target_temperature_max
- self._attr_fan_modes = list(
- map(str, range(client.fan_speed_min, client.fan_speed_max + 1))
- )
- if client.has_fan_silent:
- self._attr_fan_modes.insert(0, FAN_SILENT)
- if client.has_fan_high:
- self._attr_fan_modes.append(FAN_HIGH)
- if client.has_fan_auto:
- self._attr_fan_modes.append(FAN_AUTO)
-
- @property
- def available(self) -> bool:
- """Is the entity available."""
- return super().available and self.coordinator.client.connected
-
- @property
- def hvac_mode(self) -> HVACMode:
- """Return hvac operation ie. heat or off mode."""
- is_heating = bool(self.coordinator.client.is_heating)
- return HVACMode.HEAT if is_heating else HVACMode.OFF
-
- async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
- """Set new target hvac mode."""
- try:
- await self.coordinator.client.set_on(hvac_mode != HVACMode.OFF)
- except CommunicationError as err:
- raise HomeAssistantError(
- translation_domain=DOMAIN, translation_key="cannot_connect"
- ) from err
- except ValidationError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN, translation_key="on_off_not_available"
- ) from err
- await self.coordinator.async_refresh()
-
- @property
- def current_temperature(self) -> float | None:
- """Return current temperature."""
- return self.coordinator.client.room_temperature
-
- @property
- def target_temperature(self) -> int | None:
- """Return the temperature."""
- return self.coordinator.client.target_temperature
-
- async def async_set_temperature(self, **kwargs: Any) -> None:
- """Set new temperature."""
- temperature = int(kwargs[ATTR_TEMPERATURE])
- try:
- await self.coordinator.client.set_target_temperature(temperature)
- except CommunicationError as err:
- raise HomeAssistantError(
- translation_domain=DOMAIN, translation_key="cannot_connect"
- ) from err
- except ValidationError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="invalid_target_temperature",
- translation_placeholders={
- "value": str(temperature),
- },
- ) from err
- await self.coordinator.async_refresh()
-
- @property
- def fan_mode(self) -> str | None:
- """Return the fan mode."""
- api_state = self.coordinator.client.fan_speed
- return FAN_MODES[api_state]
-
- async def async_set_fan_mode(self, fan_mode: str) -> None:
- """Set new fan mode."""
- try:
- if fan_mode == FAN_SILENT:
- await self.coordinator.client.set_fan_silent()
- elif fan_mode == FAN_HIGH:
- await self.coordinator.client.set_fan_high()
- elif fan_mode == FAN_AUTO:
- await self.coordinator.client.set_fan_auto()
- else:
- await self.coordinator.client.set_fan_speed(FAN_MODES.index(fan_mode))
- except CommunicationError as err:
- raise HomeAssistantError(
- translation_domain=DOMAIN, translation_key="cannot_connect"
- ) from err
- except ValidationError as err:
- raise ServiceValidationError(
- translation_domain=DOMAIN,
- translation_key="invalid_fan_mode",
- translation_placeholders={
- "value": fan_mode,
- },
- ) from err
- await self.coordinator.async_refresh()
diff --git a/homeassistant/components/palazzetti/config_flow.py b/homeassistant/components/palazzetti/config_flow.py
deleted file mode 100644
index fe892b6624d..00000000000
--- a/homeassistant/components/palazzetti/config_flow.py
+++ /dev/null
@@ -1,91 +0,0 @@
-"""Config flow for Palazzetti."""
-
-from typing import Any
-
-from pypalazzetti.client import PalazzettiClient
-from pypalazzetti.exceptions import CommunicationError
-import voluptuous as vol
-
-from homeassistant.components import dhcp
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_HOST
-from homeassistant.helpers import device_registry as dr
-
-from .const import DOMAIN, LOGGER
-
-
-class PalazzettiConfigFlow(ConfigFlow, domain=DOMAIN):
- """Palazzetti config flow."""
-
- _discovered_device: PalazzettiClient
-
- async def async_step_user(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """User configuration step."""
- errors: dict[str, str] = {}
- if user_input is not None:
- host = user_input[CONF_HOST]
- client = PalazzettiClient(hostname=host)
- try:
- await client.connect()
- except CommunicationError:
- LOGGER.exception("Communication error")
- errors["base"] = "cannot_connect"
- else:
- formatted_mac = dr.format_mac(client.mac)
-
- # Assign a unique ID to the flow
- await self.async_set_unique_id(formatted_mac)
-
- # Abort the flow if a config entry with the same unique ID exists
- self._abort_if_unique_id_configured()
-
- return self.async_create_entry(
- title=client.name,
- data=user_input,
- )
-
- return self.async_show_form(
- step_id="user",
- data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
- errors=errors,
- )
-
- async def async_step_dhcp(
- self, discovery_info: dhcp.DhcpServiceInfo
- ) -> ConfigFlowResult:
- """Handle DHCP discovery."""
-
- LOGGER.debug(
- "DHCP discovery detected Palazzetti: %s", discovery_info.macaddress
- )
-
- await self.async_set_unique_id(dr.format_mac(discovery_info.macaddress))
- self._abort_if_unique_id_configured()
- self._discovered_device = PalazzettiClient(hostname=discovery_info.ip)
- try:
- await self._discovered_device.connect()
- except CommunicationError:
- return self.async_abort(reason="cannot_connect")
-
- return await self.async_step_discovery_confirm()
-
- async def async_step_discovery_confirm(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Confirm discovery."""
- if user_input is not None:
- return self.async_create_entry(
- title=self._discovered_device.name,
- data={CONF_HOST: self._discovered_device.host},
- )
-
- self._set_confirm_only()
- return self.async_show_form(
- step_id="discovery_confirm",
- description_placeholders={
- "name": self._discovered_device.name,
- "host": self._discovered_device.host,
- },
- )
diff --git a/homeassistant/components/palazzetti/const.py b/homeassistant/components/palazzetti/const.py
deleted file mode 100644
index 4cb8b1f14a6..00000000000
--- a/homeassistant/components/palazzetti/const.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Constants for the Palazzetti integration."""
-
-from datetime import timedelta
-import logging
-from typing import Final
-
-DOMAIN: Final = "palazzetti"
-PALAZZETTI: Final = "Palazzetti"
-LOGGER = logging.getLogger(__package__)
-SCAN_INTERVAL = timedelta(seconds=30)
-ON_OFF_NOT_AVAILABLE = "on_off_not_available"
-ERROR_INVALID_FAN_MODE = "invalid_fan_mode"
-ERROR_INVALID_TARGET_TEMPERATURE = "invalid_target_temperature"
-ERROR_CANNOT_CONNECT = "cannot_connect"
-
-FAN_SILENT: Final = "silent"
-FAN_HIGH: Final = "high"
-FAN_AUTO: Final = "auto"
-FAN_MODES: Final = [FAN_SILENT, "1", "2", "3", "4", "5", FAN_HIGH, FAN_AUTO]
diff --git a/homeassistant/components/palazzetti/coordinator.py b/homeassistant/components/palazzetti/coordinator.py
deleted file mode 100644
index d992bd3fb62..00000000000
--- a/homeassistant/components/palazzetti/coordinator.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""Helpers to help coordinate updates."""
-
-from pypalazzetti.client import PalazzettiClient
-from pypalazzetti.exceptions import CommunicationError, ValidationError
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-
-from .const import DOMAIN, LOGGER, SCAN_INTERVAL
-
-type PalazzettiConfigEntry = ConfigEntry[PalazzettiDataUpdateCoordinator]
-
-
-class PalazzettiDataUpdateCoordinator(DataUpdateCoordinator[None]):
- """Class to manage fetching Palazzetti data from a Palazzetti hub."""
-
- config_entry: PalazzettiConfigEntry
- client: PalazzettiClient
-
- def __init__(
- self,
- hass: HomeAssistant,
- ) -> None:
- """Initialize global Palazzetti data updater."""
- super().__init__(
- hass,
- LOGGER,
- name=DOMAIN,
- update_interval=SCAN_INTERVAL,
- )
- self.client = PalazzettiClient(self.config_entry.data[CONF_HOST])
-
- async def _async_setup(self) -> None:
- try:
- await self.client.connect()
- await self.client.update_state()
- except (CommunicationError, ValidationError) as err:
- raise UpdateFailed(f"Error communicating with the API: {err}") from err
-
- async def _async_update_data(self) -> None:
- """Fetch data from Palazzetti."""
- try:
- await self.client.update_state()
- except (CommunicationError, ValidationError) as err:
- raise UpdateFailed(f"Error communicating with the API: {err}") from err
diff --git a/homeassistant/components/palazzetti/manifest.json b/homeassistant/components/palazzetti/manifest.json
deleted file mode 100644
index aff82275e2e..00000000000
--- a/homeassistant/components/palazzetti/manifest.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "domain": "palazzetti",
- "name": "Palazzetti",
- "codeowners": ["@dotvav"],
- "config_flow": true,
- "dhcp": [
- {
- "hostname": "connbox*",
- "macaddress": "40F3857*"
- },
- {
- "registered_devices": true
- }
- ],
- "documentation": "https://www.home-assistant.io/integrations/palazzetti",
- "integration_type": "device",
- "iot_class": "local_polling",
- "requirements": ["pypalazzetti==0.1.11"]
-}
diff --git a/homeassistant/components/palazzetti/strings.json b/homeassistant/components/palazzetti/strings.json
deleted file mode 100644
index cc10c8ed5c6..00000000000
--- a/homeassistant/components/palazzetti/strings.json
+++ /dev/null
@@ -1,52 +0,0 @@
-{
- "config": {
- "step": {
- "user": {
- "data": {
- "host": "[%key:common::config_flow::data::host%]"
- },
- "data_description": {
- "host": "The host name or the IP address of the Palazzetti CBox"
- }
- },
- "discovery_confirm": {
- "description": "Do you want to add {name} ({host}) to Home Assistant?"
- }
- },
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
- },
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
- }
- },
- "exceptions": {
- "on_off_not_available": {
- "message": "The appliance cannot be turned on or off."
- },
- "invalid_fan_mode": {
- "message": "Fan mode {value} is invalid."
- },
- "invalid_target_temperatures": {
- "message": "Target temperature {value} is invalid."
- },
- "cannot_connect": {
- "message": "Could not connect to the device."
- }
- },
- "entity": {
- "climate": {
- "palazzetti": {
- "state_attributes": {
- "fan_mode": {
- "state": {
- "silent": "Silent",
- "auto": "Auto",
- "high": "High"
- }
- }
- }
- }
- }
- }
-}
diff --git a/homeassistant/components/panel_iframe/__init__.py b/homeassistant/components/panel_iframe/__init__.py
new file mode 100644
index 00000000000..1b6dfebd6b0
--- /dev/null
+++ b/homeassistant/components/panel_iframe/__init__.py
@@ -0,0 +1,98 @@
+"""Register an iFrame front end panel."""
+
+import voluptuous as vol
+
+from homeassistant.components import lovelace
+from homeassistant.components.lovelace import dashboard
+from homeassistant.const import CONF_ICON, CONF_URL
+from homeassistant.core import HomeAssistant
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
+from homeassistant.helpers.storage import Store
+from homeassistant.helpers.typing import ConfigType
+
+DOMAIN = "panel_iframe"
+
+CONF_TITLE = "title"
+
+CONF_RELATIVE_URL_ERROR_MSG = "Invalid relative URL. Absolute path required."
+CONF_RELATIVE_URL_REGEX = r"\A/"
+CONF_REQUIRE_ADMIN = "require_admin"
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: cv.schema_with_slug_keys(
+ vol.Schema(
+ {
+ vol.Optional(CONF_TITLE): cv.string,
+ vol.Optional(CONF_ICON): cv.icon,
+ vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean,
+ vol.Required(CONF_URL): vol.Any(
+ vol.Match(
+ CONF_RELATIVE_URL_REGEX, msg=CONF_RELATIVE_URL_ERROR_MSG
+ ),
+ vol.Url(),
+ ),
+ }
+ )
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+STORAGE_KEY = DOMAIN
+STORAGE_VERSION_MAJOR = 1
+
+
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+ """Set up the iFrame frontend panels."""
+ async_create_issue(
+ hass,
+ DOMAIN,
+ "deprecated_yaml",
+ breaks_in_ha_version="2024.10.0",
+ is_fixable=False,
+ is_persistent=False,
+ issue_domain=DOMAIN,
+ severity=IssueSeverity.WARNING,
+ translation_key="deprecated_yaml",
+ translation_placeholders={
+ "domain": DOMAIN,
+ "integration_title": "iframe Panel",
+ },
+ )
+
+ store: Store[dict[str, bool]] = Store(
+ hass,
+ STORAGE_VERSION_MAJOR,
+ STORAGE_KEY,
+ )
+ data = await store.async_load()
+ if data:
+ return True
+
+ dashboards_collection: dashboard.DashboardsCollection = hass.data[lovelace.DOMAIN][
+ "dashboards_collection"
+ ]
+
+ for url_path, info in config[DOMAIN].items():
+ dashboard_create_data = {
+ lovelace.CONF_ALLOW_SINGLE_WORD: True,
+ lovelace.CONF_URL_PATH: url_path,
+ }
+ for key in (CONF_ICON, CONF_REQUIRE_ADMIN, CONF_TITLE):
+ if key in info:
+ dashboard_create_data[key] = info[key]
+
+ await dashboards_collection.async_create_item(dashboard_create_data)
+
+ dashboard_store: dashboard.LovelaceStorage = hass.data[lovelace.DOMAIN][
+ "dashboards"
+ ][url_path]
+ await dashboard_store.async_save(
+ {"strategy": {"type": "iframe", "url": info[CONF_URL]}}
+ )
+
+ await store.async_save({"migrated": True})
+
+ return True
diff --git a/homeassistant/components/panel_iframe/manifest.json b/homeassistant/components/panel_iframe/manifest.json
new file mode 100644
index 00000000000..7a39e0ba17d
--- /dev/null
+++ b/homeassistant/components/panel_iframe/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "panel_iframe",
+ "name": "iframe Panel",
+ "codeowners": ["@home-assistant/frontend"],
+ "dependencies": ["frontend", "lovelace"],
+ "documentation": "https://www.home-assistant.io/integrations/panel_iframe",
+ "quality_scale": "internal"
+}
diff --git a/homeassistant/components/panel_iframe/strings.json b/homeassistant/components/panel_iframe/strings.json
new file mode 100644
index 00000000000..595b1f04818
--- /dev/null
+++ b/homeassistant/components/panel_iframe/strings.json
@@ -0,0 +1,8 @@
+{
+ "issues": {
+ "deprecated_yaml": {
+ "title": "The {integration_title} YAML configuration is being removed",
+ "description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically as a regular dashboard.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
+ }
+ }
+}
diff --git a/homeassistant/components/peco/__init__.py b/homeassistant/components/peco/__init__.py
index 1de5d4bb6a2..12979f27793 100644
--- a/homeassistant/components/peco/__init__.py
+++ b/homeassistant/components/peco/__init__.py
@@ -68,7 +68,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
outage_coordinator = DataUpdateCoordinator(
hass,
LOGGER,
- config_entry=entry,
name="PECO Outage Count",
update_method=async_update_outage_data,
update_interval=timedelta(minutes=OUTAGE_SCAN_INTERVAL),
@@ -98,7 +97,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
meter_coordinator = DataUpdateCoordinator(
hass,
LOGGER,
- config_entry=entry,
name="PECO Smart Meter",
update_method=async_update_meter_data,
update_interval=timedelta(minutes=SMART_METER_SCAN_INTERVAL),
diff --git a/homeassistant/components/pegel_online/diagnostics.py b/homeassistant/components/pegel_online/diagnostics.py
deleted file mode 100644
index b68437c5ee7..00000000000
--- a/homeassistant/components/pegel_online/diagnostics.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""Diagnostics support for pegel_online."""
-
-from __future__ import annotations
-
-from typing import Any
-
-from homeassistant.core import HomeAssistant
-
-from . import PegelOnlineConfigEntry
-
-
-async def async_get_config_entry_diagnostics(
- hass: HomeAssistant, entry: PegelOnlineConfigEntry
-) -> dict[str, Any]:
- """Return diagnostics for a config entry."""
- coordinator = entry.runtime_data
-
- return {
- "entry": entry.as_dict(),
- "data": coordinator.data,
- }
diff --git a/homeassistant/components/pegel_online/entity.py b/homeassistant/components/pegel_online/entity.py
index 4e157a5f63b..4ad12f12913 100644
--- a/homeassistant/components/pegel_online/entity.py
+++ b/homeassistant/components/pegel_online/entity.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
+from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
@@ -29,5 +29,4 @@ class PegelOnlineEntity(CoordinatorEntity[PegelOnlineDataUpdateCoordinator]):
name=f"{self.station.name} {self.station.water_name}",
manufacturer=self.station.agency,
configuration_url=self.station.base_data_url,
- entry_type=DeviceEntryType.SERVICE,
)
diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py
index a5eb8bb4f4d..a785d015ffb 100644
--- a/homeassistant/components/persistent_notification/__init__.py
+++ b/homeassistant/components/persistent_notification/__init__.py
@@ -184,8 +184,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
create_service,
vol.Schema(
{
- vol.Required(ATTR_MESSAGE): cv.string,
- vol.Optional(ATTR_TITLE): cv.string,
+ vol.Required(ATTR_MESSAGE): vol.Any(cv.dynamic_template, cv.string),
+ vol.Optional(ATTR_TITLE): vol.Any(cv.dynamic_template, cv.string),
vol.Optional(ATTR_NOTIFICATION_ID): cv.string,
}
),
diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py
index 66b4439acd8..a73145f7c1c 100644
--- a/homeassistant/components/philips_js/config_flow.py
+++ b/homeassistant/components/philips_js/config_flow.py
@@ -9,12 +9,7 @@ from typing import Any
from haphilipsjs import ConnectionFailure, PairingFailure, PhilipsTV
import voluptuous as vol
-from homeassistant.config_entries import (
- SOURCE_REAUTH,
- ConfigEntry,
- ConfigFlow,
- ConfigFlowResult,
-)
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_API_VERSION,
CONF_HOST,
@@ -80,13 +75,18 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
self._current: dict[str, Any] = {}
self._hub: PhilipsTV | None = None
self._pair_state: Any = None
+ self._entry: ConfigEntry | None = None
async def _async_create_current(self) -> ConfigFlowResult:
system = self._current[CONF_SYSTEM]
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=self._current
+ if self._entry:
+ self.hass.config_entries.async_update_entry(
+ self._entry, data=self._entry.data | self._current
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self._entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
return self.async_create_entry(
title=f"{system['name']} ({system['serialnumber']})",
@@ -150,6 +150,7 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
self._current[CONF_HOST] = entry_data[CONF_HOST]
self._current[CONF_API_VERSION] = entry_data[CONF_API_VERSION]
return await self.async_step_user()
@@ -174,7 +175,7 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
else:
if serialnumber := hub.system.get("serialnumber"):
await self.async_set_unique_id(serialnumber)
- if self.source != SOURCE_REAUTH:
+ if self._entry is None:
self._abort_if_unique_id_configured()
self._current[CONF_SYSTEM] = hub.system
diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py
index 5cc21cef3a9..64e73a20c59 100644
--- a/homeassistant/components/pi_hole/__init__.py
+++ b/homeassistant/components/pi_hole/__init__.py
@@ -118,7 +118,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: PiHoleConfigEntry) -> bo
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=name,
update_method=async_update_data,
update_interval=MIN_TIME_BETWEEN_UPDATES,
diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py
index e50b018caa4..d6f42d57deb 100644
--- a/homeassistant/components/pi_hole/config_flow.py
+++ b/homeassistant/components/pi_hole/config_flow.py
@@ -136,9 +136,15 @@ class PiHoleFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is not None:
self._config = {**self._config, CONF_API_KEY: user_input[CONF_API_KEY]}
if not (errors := await self._async_try_connect()):
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=self._config
+ entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
)
+ assert entry
+ self.hass.config_entries.async_update_entry(entry, data=self._config)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.context["entry_id"])
+ )
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
diff --git a/homeassistant/components/ping/config_flow.py b/homeassistant/components/ping/config_flow.py
index 4f2adb0d2c0..9470b2134d4 100644
--- a/homeassistant/components/ping/config_flow.py
+++ b/homeassistant/components/ping/config_flow.py
@@ -66,12 +66,16 @@ class PingConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Create the options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Handle an options flow for Ping."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py
index f398a733cd6..74967c417a4 100644
--- a/homeassistant/components/plaato/config_flow.py
+++ b/homeassistant/components/plaato/config_flow.py
@@ -176,19 +176,23 @@ class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> PlaatoOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> PlaatoOptionsFlowHandler:
"""Get the options flow for this handler."""
- return PlaatoOptionsFlowHandler()
+ return PlaatoOptionsFlowHandler(config_entry)
class PlaatoOptionsFlowHandler(OptionsFlow):
"""Handle Plaato options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize domain options flow."""
+ super().__init__()
+
+ self._config_entry = config_entry
+
async def async_step_init(self, user_input: None = None) -> ConfigFlowResult:
"""Manage the options."""
- use_webhook = self.config_entry.data.get(CONF_USE_WEBHOOK, False)
+ use_webhook = self._config_entry.data.get(CONF_USE_WEBHOOK, False)
if use_webhook:
return await self.async_step_webhook()
@@ -207,7 +211,7 @@ class PlaatoOptionsFlowHandler(OptionsFlow):
{
vol.Optional(
CONF_SCAN_INTERVAL,
- default=self.config_entry.options.get(
+ default=self._config_entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
): cv.positive_int
@@ -222,7 +226,7 @@ class PlaatoOptionsFlowHandler(OptionsFlow):
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
- webhook_id = self.config_entry.data.get(CONF_WEBHOOK_ID, None)
+ webhook_id = self._config_entry.data.get(CONF_WEBHOOK_ID, None)
webhook_url = (
""
if webhook_id is None
diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py
index ae7cbb12574..fcd5751effb 100644
--- a/homeassistant/components/plex/config_flow.py
+++ b/homeassistant/components/plex/config_flow.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from collections.abc import Mapping
-from copy import deepcopy
+import copy
import logging
from typing import TYPE_CHECKING, Any
@@ -385,7 +385,7 @@ class PlexOptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize Plex options flow."""
- self.options = deepcopy(dict(config_entry.options))
+ self.options = copy.deepcopy(dict(config_entry.options))
self.server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
async def async_step_init(self, user_input: None = None) -> ConfigFlowResult:
diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py
index 7d1b9ceac8a..f7677e39f7a 100644
--- a/homeassistant/components/plugwise/__init__.py
+++ b/homeassistant/components/plugwise/__init__.py
@@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) ->
model=coordinator.api.smile_model,
model_id=coordinator.api.smile_model_id,
name=coordinator.api.smile_name,
- sw_version=str(coordinator.api.smile_version),
+ sw_version=coordinator.api.smile_version[0],
) # required for adding the entity-less P1 Gateway
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py
index 57abb1ccb86..b0d68aaa33b 100644
--- a/homeassistant/components/plugwise/config_flow.py
+++ b/homeassistant/components/plugwise/config_flow.py
@@ -71,6 +71,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
password=data[CONF_PASSWORD],
port=data[CONF_PORT],
username=data[CONF_USERNAME],
+ timeout=30,
websession=websession,
)
await api.connect()
diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py
index b897a8bf833..c3fe33c64d2 100644
--- a/homeassistant/components/plugwise/coordinator.py
+++ b/homeassistant/components/plugwise/coordinator.py
@@ -2,7 +2,6 @@
from datetime import timedelta
-from packaging.version import Version
from plugwise import PlugwiseData, Smile
from plugwise.exceptions import (
ConnectionFailedError,
@@ -54,6 +53,7 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
username=self.config_entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
password=self.config_entry.data[CONF_PASSWORD],
port=self.config_entry.data.get(CONF_PORT, DEFAULT_PORT),
+ timeout=30,
websession=async_get_clientsession(hass, verify_ssl=False),
)
self._current_devices: set[str] = set()
@@ -61,10 +61,8 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
async def _connect(self) -> None:
"""Connect to the Plugwise Smile."""
- version = await self.api.connect()
- self._connected = isinstance(version, Version)
- if self._connected:
- self.api.get_all_devices()
+ self._connected = await self.api.connect()
+ self.api.get_all_devices()
async def _async_update_data(self) -> PlugwiseData:
"""Fetch data from Plugwise."""
diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json
index dbbad15c0dc..b1ce8961110 100644
--- a/homeassistant/components/plugwise/manifest.json
+++ b/homeassistant/components/plugwise/manifest.json
@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["plugwise"],
- "requirements": ["plugwise==1.5.0"],
+ "requirements": ["plugwise==1.4.0"],
"zeroconf": ["_plugwise._tcp.local."]
}
diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py
index 4e4e4238176..3657bad28ae 100644
--- a/homeassistant/components/point/alarm_control_panel.py
+++ b/homeassistant/components/point/alarm_control_panel.py
@@ -9,9 +9,13 @@ from homeassistant.components.alarm_control_panel import (
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -24,9 +28,9 @@ _LOGGER = logging.getLogger(__name__)
EVENT_MAP = {
- "off": AlarmControlPanelState.DISARMED,
- "alarm_silenced": AlarmControlPanelState.DISARMED,
- "alarm_grace_period_expired": AlarmControlPanelState.TRIGGERED,
+ "off": STATE_ALARM_DISARMED,
+ "alarm_silenced": STATE_ALARM_DISARMED,
+ "alarm_grace_period_expired": STATE_ALARM_TRIGGERED,
}
@@ -99,11 +103,9 @@ class MinutPointAlarmControl(AlarmControlPanelEntity):
self.async_write_ha_state()
@property
- def alarm_state(self) -> AlarmControlPanelState:
+ def state(self) -> str:
"""Return state of the device."""
- return EVENT_MAP.get(
- self._home["alarm_status"], AlarmControlPanelState.ARMED_AWAY
- )
+ return EVENT_MAP.get(self._home["alarm_status"], STATE_ALARM_ARMED_AWAY)
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
diff --git a/homeassistant/components/point/api.py b/homeassistant/components/point/api.py
index cd854c2b7ec..b55a7704cbf 100644
--- a/homeassistant/components/point/api.py
+++ b/homeassistant/components/point/api.py
@@ -20,6 +20,7 @@ class AsyncConfigEntryAuth(pypoint.AbstractAuth):
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
- await self._oauth_session.async_ensure_token_valid()
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token["access_token"]
diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py
index a0a51c7b9e6..0e4f88ab578 100644
--- a/homeassistant/components/point/config_flow.py
+++ b/homeassistant/components/point/config_flow.py
@@ -5,7 +5,7 @@ import logging
from typing import Any
from homeassistant.components.webhook import async_generate_id
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.const import CONF_TOKEN, CONF_WEBHOOK_ID
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
@@ -17,6 +17,8 @@ class OAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
DOMAIN = DOMAIN
+ reauth_entry: ConfigEntry | None = None
+
@property
def logger(self) -> logging.Logger:
"""Return logger."""
@@ -30,6 +32,9 @@ class OAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -43,8 +48,8 @@ class OAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create an oauth config entry or update existing entry for reauth."""
user_id = str(data[CONF_TOKEN]["user_id"])
- await self.async_set_unique_id(user_id)
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
+ await self.async_set_unique_id(user_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
@@ -52,11 +57,15 @@ class OAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
data={**data, CONF_WEBHOOK_ID: async_generate_id()},
)
- reauth_entry = self._get_reauth_entry()
- if reauth_entry.unique_id is not None:
- self._abort_if_unique_id_mismatch(reason="wrong_account")
+ if (
+ self.reauth_entry.unique_id is None
+ or self.reauth_entry.unique_id == user_id
+ ):
+ logging.debug("user_id: %s", user_id)
+ return self.async_update_reload_and_abort(
+ self.reauth_entry,
+ data={**self.reauth_entry.data, **data},
+ unique_id=user_id,
+ )
- logging.debug("user_id: %s", user_id)
- return self.async_update_reload_and_abort(
- reauth_entry, data_updates=data, unique_id=user_id
- )
+ return self.async_abort(reason="wrong_account")
diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py
index 6a2522ac43b..0b6f889b90a 100644
--- a/homeassistant/components/powerwall/__init__.py
+++ b/homeassistant/components/powerwall/__init__.py
@@ -168,7 +168,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: PowerwallConfigEntry) ->
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name="Powerwall site",
update_method=manager.async_update_data,
update_interval=timedelta(seconds=UPDATE_INTERVAL),
diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py
index 0c39392ca19..5d832cb6ae4 100644
--- a/homeassistant/components/powerwall/config_flow.py
+++ b/homeassistant/components/powerwall/config_flow.py
@@ -99,6 +99,7 @@ class PowerwallConfigFlow(ConfigFlow, domain=DOMAIN):
"""Initialize the powerwall flow."""
self.ip_address: str | None = None
self.title: str | None = None
+ self.reauth_entry: ConfigEntry | None = None
async def _async_powerwall_is_offline(self, entry: ConfigEntry) -> bool:
"""Check if the power wall is offline.
@@ -249,22 +250,19 @@ class PowerwallConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauth confirmation."""
+ assert self.reauth_entry is not None
errors: dict[str, str] | None = {}
description_placeholders: dict[str, str] = {}
- reauth_entry = self._get_reauth_entry()
if user_input is not None:
+ entry_data = self.reauth_entry.data
errors, _, description_placeholders = await self._async_try_connect(
- {CONF_IP_ADDRESS: reauth_entry.data[CONF_IP_ADDRESS], **user_input}
+ {CONF_IP_ADDRESS: entry_data[CONF_IP_ADDRESS], **user_input}
)
if not errors:
return self.async_update_reload_and_abort(
- reauth_entry, data_updates=user_input
+ self.reauth_entry, data={**entry_data, **user_input}
)
- self.context["title_placeholders"] = {
- "name": reauth_entry.title,
- "ip_address": reauth_entry.data[CONF_IP_ADDRESS],
- }
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({vol.Optional(CONF_PASSWORD): str}),
@@ -276,6 +274,9 @@ class PowerwallConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py
index 389e3384ad9..9b2b9736574 100644
--- a/homeassistant/components/profiler/__init__.py
+++ b/homeassistant/components/profiler/__init__.py
@@ -436,10 +436,6 @@ async def _async_generate_memory_profile(hass: HomeAssistant, call: ServiceCall)
# Imports deferred to avoid loading modules
# in memory since usually only one part of this
# integration is used at a time
- if sys.version_info >= (3, 13):
- raise HomeAssistantError(
- "Memory profiling is not supported on Python 3.13. Please use Python 3.12."
- )
from guppy import hpy # pylint: disable=import-outside-toplevel
start_time = int(time.time() * 1000000)
diff --git a/homeassistant/components/profiler/config_flow.py b/homeassistant/components/profiler/config_flow.py
index 766d847e4a4..19995cf79aa 100644
--- a/homeassistant/components/profiler/config_flow.py
+++ b/homeassistant/components/profiler/config_flow.py
@@ -16,6 +16,9 @@ class ProfilerConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
if user_input is not None:
return self.async_create_entry(title=DEFAULT_NAME, data={})
diff --git a/homeassistant/components/profiler/manifest.json b/homeassistant/components/profiler/manifest.json
index 8d2814c8c7f..ceaab458e69 100644
--- a/homeassistant/components/profiler/manifest.json
+++ b/homeassistant/components/profiler/manifest.json
@@ -7,8 +7,7 @@
"quality_scale": "internal",
"requirements": [
"pyprof2calltree==1.4.5",
- "guppy3==3.1.4.post1;python_version<'3.13'",
+ "guppy3==3.1.4.post1",
"objgraph==3.5.0"
- ],
- "single_config_entry": true
+ ]
}
diff --git a/homeassistant/components/profiler/strings.json b/homeassistant/components/profiler/strings.json
index f363b5a22cb..7a31c567040 100644
--- a/homeassistant/components/profiler/strings.json
+++ b/homeassistant/components/profiler/strings.json
@@ -4,6 +4,9 @@
"user": {
"description": "[%key:common::config_flow::description::confirm_setup%]"
}
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"services": {
diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py
index c243bf90dc0..8cc0a8f4b6a 100644
--- a/homeassistant/components/prometheus/__init__.py
+++ b/homeassistant/components/prometheus/__init__.py
@@ -14,7 +14,6 @@ from prometheus_client.metrics import MetricWrapperBase
import voluptuous as vol
from homeassistant import core as hacore
-from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
@@ -52,6 +51,16 @@ from homeassistant.const import (
CONTENT_TYPE_TEXT_PLAIN,
EVENT_STATE_CHANGED,
PERCENTAGE,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_DISARMING,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
STATE_CLOSED,
STATE_CLOSING,
STATE_ON,
@@ -76,8 +85,6 @@ from homeassistant.util.unit_conversion import TemperatureConverter
_LOGGER = logging.getLogger(__name__)
API_ENDPOINT = "/api/prometheus"
-IGNORED_STATES = frozenset({STATE_UNAVAILABLE, STATE_UNKNOWN})
-
DOMAIN = "prometheus"
CONF_FILTER = "filter"
@@ -91,7 +98,6 @@ CONF_OVERRIDE_METRIC = "override_metric"
COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema(
{vol.Optional(CONF_OVERRIDE_METRIC): cv.string}
)
-ALLOWED_METRIC_CHARS = set(string.ascii_letters + string.digits + "_:")
DEFAULT_NAMESPACE = "homeassistant"
@@ -213,6 +219,14 @@ class PrometheusMetrics:
"""Add/update a state in Prometheus."""
entity_id = state.entity_id
_LOGGER.debug("Handling state update for %s", entity_id)
+ domain, _ = hacore.split_entity_id(entity_id)
+
+ ignored_states = (STATE_UNAVAILABLE, STATE_UNKNOWN)
+
+ handler = f"_handle_{domain}"
+
+ if hasattr(self, handler) and state.state not in ignored_states:
+ getattr(self, handler)(state)
labels = self._labels(state)
state_change = self._metric(
@@ -225,7 +239,7 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"Entity is available (not in the unavailable or unknown state)",
)
- entity_available.labels(**labels).set(float(state.state not in IGNORED_STATES))
+ entity_available.labels(**labels).set(float(state.state not in ignored_states))
last_updated_time_seconds = self._metric(
"last_updated_time_seconds",
@@ -234,18 +248,6 @@ class PrometheusMetrics:
)
last_updated_time_seconds.labels(**labels).set(state.last_updated.timestamp())
- if state.state in IGNORED_STATES:
- self._remove_labelsets(
- entity_id,
- None,
- {state_change, entity_available, last_updated_time_seconds},
- )
- else:
- domain, _ = hacore.split_entity_id(entity_id)
- handler = f"_handle_{domain}"
- if hasattr(self, handler) and state.state:
- getattr(self, handler)(state)
-
def handle_entity_registry_updated(
self, event: Event[EventEntityRegistryUpdatedData]
) -> None:
@@ -272,17 +274,10 @@ class PrometheusMetrics:
self._remove_labelsets(metrics_entity_id)
def _remove_labelsets(
- self,
- entity_id: str,
- friendly_name: str | None = None,
- ignored_metrics: set[MetricWrapperBase] | None = None,
+ self, entity_id: str, friendly_name: str | None = None
) -> None:
- """Remove labelsets matching the given entity id from all non-ignored metrics."""
- if ignored_metrics is None:
- ignored_metrics = set()
+ """Remove labelsets matching the given entity id from all metrics."""
for metric in list(self._metrics.values()):
- if metric in ignored_metrics:
- continue
for sample in cast(list[prometheus_client.Metric], metric.collect())[
0
].samples:
@@ -339,12 +334,17 @@ class PrometheusMetrics:
@staticmethod
def _sanitize_metric_name(metric: str) -> str:
return "".join(
- [c if c in ALLOWED_METRIC_CHARS else f"u{hex(ord(c))}" for c in metric]
+ [
+ c
+ if c in string.ascii_letters + string.digits + "_:"
+ else f"u{hex(ord(c))}"
+ for c in metric
+ ]
)
@staticmethod
- def state_as_number(state: State) -> float | None:
- """Return state as a float, or None if state cannot be converted."""
+ def state_as_number(state: State) -> float:
+ """Return a state casted to a float."""
try:
if state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP:
value = as_timestamp(state.state)
@@ -352,7 +352,7 @@ class PrometheusMetrics:
value = state_helper.state_as_number(state)
except ValueError:
_LOGGER.debug("Could not convert %s to float", state)
- value = None
+ value = 0
return value
@staticmethod
@@ -382,8 +382,8 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"State of the binary sensor (0/1)",
)
- if (value := self.state_as_number(state)) is not None:
- metric.labels(**self._labels(state)).set(value)
+ value = self.state_as_number(state)
+ metric.labels(**self._labels(state)).set(value)
def _handle_input_boolean(self, state: State) -> None:
metric = self._metric(
@@ -391,8 +391,8 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"State of the input boolean (0/1)",
)
- if (value := self.state_as_number(state)) is not None:
- metric.labels(**self._labels(state)).set(value)
+ value = self.state_as_number(state)
+ metric.labels(**self._labels(state)).set(value)
def _numeric_handler(self, state: State, domain: str, title: str) -> None:
if unit := self._unit_string(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)):
@@ -408,7 +408,8 @@ class PrometheusMetrics:
f"State of the {title}",
)
- if (value := self.state_as_number(state)) is not None:
+ with suppress(ValueError):
+ value = self.state_as_number(state)
if (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfTemperature.FAHRENHEIT
@@ -430,15 +431,15 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"State of the device tracker (0/1)",
)
- if (value := self.state_as_number(state)) is not None:
- metric.labels(**self._labels(state)).set(value)
+ value = self.state_as_number(state)
+ metric.labels(**self._labels(state)).set(value)
def _handle_person(self, state: State) -> None:
metric = self._metric(
"person_state", prometheus_client.Gauge, "State of the person (0/1)"
)
- if (value := self.state_as_number(state)) is not None:
- metric.labels(**self._labels(state)).set(value)
+ value = self.state_as_number(state)
+ metric.labels(**self._labels(state)).set(value)
def _handle_cover(self, state: State) -> None:
metric = self._metric(
@@ -479,19 +480,23 @@ class PrometheusMetrics:
"Light brightness percentage (0..100)",
)
- if (value := self.state_as_number(state)) is not None:
+ try:
brightness = state.attributes.get(ATTR_BRIGHTNESS)
if state.state == STATE_ON and brightness is not None:
- value = float(brightness) / 255.0
+ value = brightness / 255.0
+ else:
+ value = self.state_as_number(state)
value = value * 100
metric.labels(**self._labels(state)).set(value)
+ except ValueError:
+ pass
def _handle_lock(self, state: State) -> None:
metric = self._metric(
"lock_state", prometheus_client.Gauge, "State of the lock (0/1)"
)
- if (value := self.state_as_number(state)) is not None:
- metric.labels(**self._labels(state)).set(value)
+ value = self.state_as_number(state)
+ metric.labels(**self._labels(state)).set(value)
def _handle_climate_temp(
self, state: State, attr: str, metric_name: str, metric_description: str
@@ -603,8 +608,11 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"State of the humidifier (0/1)",
)
- if (value := self.state_as_number(state)) is not None:
+ try:
+ value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
+ except ValueError:
+ pass
current_mode = state.attributes.get(ATTR_MODE)
available_modes = state.attributes.get(ATTR_AVAILABLE_MODES)
@@ -635,7 +643,8 @@ class PrometheusMetrics:
_metric = self._metric(metric, prometheus_client.Gauge, documentation)
- if (value := self.state_as_number(state)) is not None:
+ try:
+ value = self.state_as_number(state)
if (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfTemperature.FAHRENHEIT
@@ -644,6 +653,8 @@ class PrometheusMetrics:
value, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS
)
_metric.labels(**self._labels(state)).set(value)
+ except ValueError:
+ pass
self._battery(state)
@@ -676,15 +687,20 @@ class PrometheusMetrics:
def _sensor_override_component_metric(
self, state: State, unit: str | None
) -> str | None:
- """Get metric from override in component configuration."""
+ """Get metric from override in component confioguration."""
return self._component_config.get(state.entity_id).get(CONF_OVERRIDE_METRIC)
@staticmethod
def _sensor_fallback_metric(state: State, unit: str | None) -> str | None:
"""Get metric from fallback logic for compatibility."""
- if unit not in (None, ""):
- return f"sensor_unit_{unit}"
- return "sensor_state"
+ if unit in (None, ""):
+ try:
+ state_helper.state_as_number(state)
+ except ValueError:
+ _LOGGER.debug("Unsupported sensor: %s", state.entity_id)
+ return None
+ return "sensor_state"
+ return f"sensor_unit_{unit}"
@staticmethod
def _unit_string(unit: str | None) -> str | None:
@@ -706,8 +722,11 @@ class PrometheusMetrics:
"switch_state", prometheus_client.Gauge, "State of the switch (0/1)"
)
- if (value := self.state_as_number(state)) is not None:
+ try:
+ value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
+ except ValueError:
+ pass
self._handle_attributes(state)
@@ -716,8 +735,11 @@ class PrometheusMetrics:
"fan_state", prometheus_client.Gauge, "State of the fan (0/1)"
)
- if (value := self.state_as_number(state)) is not None:
+ try:
+ value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
+ except ValueError:
+ pass
fan_speed_percent = state.attributes.get(ATTR_PERCENTAGE)
if fan_speed_percent is not None:
@@ -783,8 +805,8 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"Value of counter entities",
)
- if (value := self.state_as_number(state)) is not None:
- metric.labels(**self._labels(state)).set(value)
+
+ metric.labels(**self._labels(state)).set(self.state_as_number(state))
def _handle_update(self, state: State) -> None:
metric = self._metric(
@@ -792,8 +814,8 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"Update state, indicating if an update is available (0/1)",
)
- if (value := self.state_as_number(state)) is not None:
- metric.labels(**self._labels(state)).set(value)
+ value = self.state_as_number(state)
+ metric.labels(**self._labels(state)).set(value)
def _handle_alarm_control_panel(self, state: State) -> None:
current_state = state.state
@@ -806,9 +828,22 @@ class PrometheusMetrics:
["state"],
)
- for alarm_state in AlarmControlPanelState:
- metric.labels(**dict(self._labels(state), state=alarm_state.value)).set(
- float(alarm_state.value == current_state)
+ alarm_states = [
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMING,
+ ]
+
+ for alarm_state in alarm_states:
+ metric.labels(**dict(self._labels(state), state=alarm_state)).set(
+ float(alarm_state == current_state)
)
diff --git a/homeassistant/components/prosegur/alarm_control_panel.py b/homeassistant/components/prosegur/alarm_control_panel.py
index 1c58b64cf55..ffedcf30770 100644
--- a/homeassistant/components/prosegur/alarm_control_panel.py
+++ b/homeassistant/components/prosegur/alarm_control_panel.py
@@ -10,9 +10,13 @@ from pyprosegur.installation import Installation, Status
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -22,10 +26,10 @@ from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
STATE_MAPPING = {
- Status.DISARMED: AlarmControlPanelState.DISARMED,
- Status.ARMED: AlarmControlPanelState.ARMED_AWAY,
- Status.PARTIALLY: AlarmControlPanelState.ARMED_HOME,
- Status.ERROR_PARTIALLY: AlarmControlPanelState.ARMED_HOME,
+ Status.DISARMED: STATE_ALARM_DISARMED,
+ Status.ARMED: STATE_ALARM_ARMED_AWAY,
+ Status.PARTIALLY: STATE_ALARM_ARMED_HOME,
+ Status.ERROR_PARTIALLY: STATE_ALARM_ARMED_HOME,
}
@@ -78,7 +82,7 @@ class ProsegurAlarm(AlarmControlPanelEntity):
self._attr_available = False
return
- self._attr_alarm_state = STATE_MAPPING.get(self._installation.status)
+ self._attr_state = STATE_MAPPING.get(self._installation.status)
self._attr_available = True
async def async_alarm_disarm(self, code: str | None = None) -> None:
diff --git a/homeassistant/components/prosegur/config_flow.py b/homeassistant/components/prosegur/config_flow.py
index 74e4d268144..7bd87e405ef 100644
--- a/homeassistant/components/prosegur/config_flow.py
+++ b/homeassistant/components/prosegur/config_flow.py
@@ -2,13 +2,13 @@
from collections.abc import Mapping
import logging
-from typing import Any
+from typing import Any, cast
from pyprosegur.auth import COUNTRY, Auth
from pyprosegur.installation import Installation
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -46,6 +46,7 @@ class ProsegurConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Prosegur Alarm."""
VERSION = 1
+ entry: ConfigEntry
auth: Auth
user_input: dict
contracts: list[dict[str, str]]
@@ -109,6 +110,10 @@ class ProsegurConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle initiation of re-authentication with Prosegur."""
+ self.entry = cast(
+ ConfigEntry,
+ self.hass.config_entries.async_get_entry(self.context["entry_id"]),
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -117,10 +122,9 @@ class ProsegurConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle re-authentication with Prosegur."""
errors: dict[str, str] = {}
- reauth_entry = self._get_reauth_entry()
if user_input:
try:
- user_input[CONF_COUNTRY] = reauth_entry.data[CONF_COUNTRY]
+ user_input[CONF_COUNTRY] = self.entry.data[CONF_COUNTRY]
self.auth, self.contracts = await validate_input(self.hass, user_input)
except CannotConnect:
@@ -131,20 +135,25 @@ class ProsegurConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
- return self.async_update_reload_and_abort(
- reauth_entry,
- data_updates={
+ self.hass.config_entries.async_update_entry(
+ self.entry,
+ data={
+ **self.entry.data,
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(
- CONF_USERNAME, default=reauth_entry.data[CONF_USERNAME]
+ CONF_USERNAME, default=self.entry.data[CONF_USERNAME]
): str,
vol.Required(CONF_PASSWORD): str,
}
diff --git a/homeassistant/components/proximity/config_flow.py b/homeassistant/components/proximity/config_flow.py
index 5818ec2979b..1758b182ad7 100644
--- a/homeassistant/components/proximity/config_flow.py
+++ b/homeassistant/components/proximity/config_flow.py
@@ -89,7 +89,7 @@ class ProximityConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
- return ProximityOptionsFlow()
+ return ProximityOptionsFlow(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -121,6 +121,10 @@ class ProximityConfigFlow(ConfigFlow, domain=DOMAIN):
class ProximityOptionsFlow(OptionsFlow):
"""Handle a option flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
def _user_form_schema(self, user_input: dict[str, Any]) -> vol.Schema:
return vol.Schema(_base_schema(user_input))
diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json
index f13799422df..1e70c4d3e10 100644
--- a/homeassistant/components/proxy/manifest.json
+++ b/homeassistant/components/proxy/manifest.json
@@ -3,5 +3,5 @@
"name": "Camera Proxy",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/proxy",
- "requirements": ["Pillow==11.0.0"]
+ "requirements": ["Pillow==10.4.0"]
}
diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py
index 3ca7870b3cb..050200f50d4 100644
--- a/homeassistant/components/purpleair/config_flow.py
+++ b/homeassistant/components/purpleair/config_flow.py
@@ -202,6 +202,7 @@ class PurpleAirConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize."""
self._flow_data: dict[str, Any] = {}
+ self._reauth_entry: ConfigEntry | None = None
@staticmethod
@callback
@@ -209,7 +210,7 @@ class PurpleAirConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> PurpleAirOptionsFlowHandler:
"""Define the config flow to handle options."""
- return PurpleAirOptionsFlowHandler()
+ return PurpleAirOptionsFlowHandler(config_entry)
async def async_step_by_coordinates(
self, user_input: dict[str, Any] | None = None
@@ -264,6 +265,9 @@ class PurpleAirConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -285,9 +289,15 @@ class PurpleAirConfigFlow(ConfigFlow, domain=DOMAIN):
errors=validation.errors,
)
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data={CONF_API_KEY: api_key}
+ assert self._reauth_entry
+
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry, data={CONF_API_KEY: api_key}
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -315,9 +325,10 @@ class PurpleAirConfigFlow(ConfigFlow, domain=DOMAIN):
class PurpleAirOptionsFlowHandler(OptionsFlow):
"""Handle a PurpleAir options flow."""
- def __init__(self) -> None:
+ def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize."""
self._flow_data: dict[str, Any] = {}
+ self.config_entry = config_entry
@property
def settings_schema(self) -> vol.Schema:
diff --git a/homeassistant/components/pvoutput/config_flow.py b/homeassistant/components/pvoutput/config_flow.py
index ad2d759056f..9d18952e7b4 100644
--- a/homeassistant/components/pvoutput/config_flow.py
+++ b/homeassistant/components/pvoutput/config_flow.py
@@ -8,7 +8,7 @@ from typing import Any
from pvo import PVOutput, PVOutputAuthenticationError, PVOutputError
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -33,6 +33,7 @@ class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
imported_name: str | None = None
+ reauth_entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -87,6 +88,9 @@ class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle initiation of re-authentication with PVOutput."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -95,22 +99,29 @@ class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle re-authentication with PVOutput."""
errors = {}
- if user_input is not None:
- reauth_entry = self._get_reauth_entry()
+ if user_input is not None and self.reauth_entry:
try:
await validate_input(
self.hass,
api_key=user_input[CONF_API_KEY],
- system_id=reauth_entry.data[CONF_SYSTEM_ID],
+ system_id=self.reauth_entry.data[CONF_SYSTEM_ID],
)
except PVOutputAuthenticationError:
errors["base"] = "invalid_auth"
except PVOutputError:
errors["base"] = "cannot_connect"
else:
- return self.async_update_reload_and_abort(
- reauth_entry, data_updates=user_input
+ self.hass.config_entries.async_update_entry(
+ self.reauth_entry,
+ data={
+ **self.reauth_entry.data,
+ CONF_API_KEY: user_input[CONF_API_KEY],
+ },
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
diff --git a/homeassistant/components/pvpc_hourly_pricing/config_flow.py b/homeassistant/components/pvpc_hourly_pricing/config_flow.py
index 3c6b510004a..239e1bcb0e9 100644
--- a/homeassistant/components/pvpc_hourly_pricing/config_flow.py
+++ b/homeassistant/components/pvpc_hourly_pricing/config_flow.py
@@ -9,11 +9,10 @@ from aiopvpc import DEFAULT_POWER_KW, PVPCData
import voluptuous as vol
from homeassistant.config_entries import (
- SOURCE_REAUTH,
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_API_TOKEN, CONF_NAME
from homeassistant.core import callback
@@ -49,6 +48,7 @@ class TariffSelectorConfigFlow(ConfigFlow, domain=DOMAIN):
_use_api_token: bool = False
_api_token: str | None = None
_api: PVPCData | None = None
+ _reauth_entry: ConfigEntry | None = None
@staticmethod
@callback
@@ -56,7 +56,7 @@ class TariffSelectorConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> PVPCOptionsFlowHandler:
"""Get the options flow for this handler."""
- return PVPCOptionsFlowHandler()
+ return PVPCOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -141,10 +141,12 @@ class TariffSelectorConfigFlow(ConfigFlow, domain=DOMAIN):
ATTR_POWER_P3: self._power_p3,
CONF_API_TOKEN: self._api_token if self._use_api_token else None,
}
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=data
+ if self._reauth_entry:
+ self.hass.config_entries.async_update_entry(self._reauth_entry, data=data)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
)
+ return self.async_abort(reason="reauth_successful")
assert self._name is not None
return self.async_create_entry(title=self._name, data=data)
@@ -153,6 +155,9 @@ class TariffSelectorConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication with ESIOS Token."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
self._api_token = entry_data.get(CONF_API_TOKEN)
self._use_api_token = self._api_token is not None
self._name = entry_data[CONF_NAME]
@@ -178,7 +183,7 @@ class TariffSelectorConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="reauth_confirm", data_schema=data_schema)
-class PVPCOptionsFlowHandler(OptionsFlow):
+class PVPCOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle PVPC options."""
_power: float | None = None
@@ -199,7 +204,7 @@ class PVPCOptionsFlowHandler(OptionsFlow):
)
# Fill options with entry data
- api_token = self.config_entry.options.get(
+ api_token = self.options.get(
CONF_API_TOKEN, self.config_entry.data.get(CONF_API_TOKEN)
)
return self.async_show_form(
@@ -229,11 +234,13 @@ class PVPCOptionsFlowHandler(OptionsFlow):
)
# Fill options with entry data
- options = self.config_entry.options
- data = self.config_entry.data
- power = options.get(ATTR_POWER, data[ATTR_POWER])
- power_valley = options.get(ATTR_POWER_P3, data[ATTR_POWER_P3])
- api_token = options.get(CONF_API_TOKEN, data.get(CONF_API_TOKEN))
+ power = self.options.get(ATTR_POWER, self.config_entry.data[ATTR_POWER])
+ power_valley = self.options.get(
+ ATTR_POWER_P3, self.config_entry.data[ATTR_POWER_P3]
+ )
+ api_token = self.options.get(
+ CONF_API_TOKEN, self.config_entry.data.get(CONF_API_TOKEN)
+ )
use_api_token = api_token is not None
schema = vol.Schema(
{
diff --git a/homeassistant/components/pyload/config_flow.py b/homeassistant/components/pyload/config_flow.py
index 3e6cbd33bb3..bac0f795343 100644
--- a/homeassistant/components/pyload/config_flow.py
+++ b/homeassistant/components/pyload/config_flow.py
@@ -193,6 +193,12 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Perform a reconfiguration."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the reconfiguration flow."""
errors = {}
@@ -216,7 +222,7 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA,
user_input or reconfig_entry.data,
diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json
index 4ae4c4fee67..bbe6989f5e7 100644
--- a/homeassistant/components/pyload/strings.json
+++ b/homeassistant/components/pyload/strings.json
@@ -15,7 +15,7 @@
"port": "pyLoad uses port 8000 by default."
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json
index 4348fdd9911..594012dabb1 100644
--- a/homeassistant/components/python_script/manifest.json
+++ b/homeassistant/components/python_script/manifest.json
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/python_script",
"loggers": ["RestrictedPython"],
"quality_scale": "internal",
- "requirements": ["RestrictedPython==7.4"]
+ "requirements": ["RestrictedPython==7.3"]
}
diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py
index abc23f39975..68de7e1d5e5 100644
--- a/homeassistant/components/qbittorrent/sensor.py
+++ b/homeassistant/components/qbittorrent/sensor.py
@@ -11,7 +11,6 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
- SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_IDLE, UnitOfDataRate
@@ -80,7 +79,6 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_DOWNLOAD_SPEED,
translation_key="download_speed",
- state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DATA_RATE,
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
suggested_display_precision=2,
@@ -90,7 +88,6 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
QBittorrentSensorEntityDescription(
key=SENSOR_TYPE_UPLOAD_SPEED,
translation_key="upload_speed",
- state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DATA_RATE,
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
suggested_display_precision=2,
diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py
index 383a4e5f572..526516bfcdd 100644
--- a/homeassistant/components/qnap/sensor.py
+++ b/homeassistant/components/qnap/sensor.py
@@ -13,6 +13,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
+ ATTR_NAME,
PERCENTAGE,
EntityCategory,
UnitOfDataRate,
@@ -374,6 +375,17 @@ class QNAPMemorySensor(QNAPSensor):
return None
+ # Deprecated since Home Assistant 2024.6.0
+ # Can be removed completely in 2024.12.0
+ @property
+ def extra_state_attributes(self) -> dict[str, Any] | None:
+ """Return the state attributes."""
+ if self.coordinator.data:
+ data = self.coordinator.data["system_stats"]["memory"]
+ size = round(float(data["total"]) / 1024, 2)
+ return {ATTR_MEMORY_SIZE: f"{size} {UnitOfInformation.GIBIBYTES}"}
+ return None
+
class QNAPNetworkSensor(QNAPSensor):
"""A QNAP sensor that monitors network stats."""
@@ -402,6 +414,22 @@ class QNAPNetworkSensor(QNAPSensor):
return None
+ # Deprecated since Home Assistant 2024.6.0
+ # Can be removed completely in 2024.12.0
+ @property
+ def extra_state_attributes(self) -> dict[str, Any] | None:
+ """Return the state attributes."""
+ if self.coordinator.data:
+ data = self.coordinator.data["system_stats"]["nics"][self.monitor_device]
+ return {
+ ATTR_IP: data["ip"],
+ ATTR_MASK: data["mask"],
+ ATTR_MAC: data["mac"],
+ ATTR_MAX_SPEED: data["max_speed"],
+ ATTR_PACKETS_ERR: data["err_packets"],
+ }
+ return None
+
class QNAPSystemSensor(QNAPSensor):
"""A QNAP sensor that monitors overall system health."""
@@ -427,6 +455,25 @@ class QNAPSystemSensor(QNAPSensor):
return None
+ # Deprecated since Home Assistant 2024.6.0
+ # Can be removed completely in 2024.12.0
+ @property
+ def extra_state_attributes(self) -> dict[str, Any] | None:
+ """Return the state attributes."""
+ if self.coordinator.data:
+ data = self.coordinator.data["system_stats"]
+ days = int(data["uptime"]["days"])
+ hours = int(data["uptime"]["hours"])
+ minutes = int(data["uptime"]["minutes"])
+
+ return {
+ ATTR_NAME: data["system"]["name"],
+ ATTR_MODEL: data["system"]["model"],
+ ATTR_SERIAL: data["system"]["serial_number"],
+ ATTR_UPTIME: f"{days:0>2d}d {hours:0>2d}h {minutes:0>2d}m",
+ }
+ return None
+
class QNAPDriveSensor(QNAPSensor):
"""A QNAP sensor that monitors HDD/SSD drive stats."""
@@ -486,3 +533,17 @@ class QNAPVolumeSensor(QNAPSensor):
return used_gb / total_gb * 100
return None
+
+ # Deprecated since Home Assistant 2024.6.0
+ # Can be removed completely in 2024.12.0
+ @property
+ def extra_state_attributes(self) -> dict[str, Any] | None:
+ """Return the state attributes."""
+ if self.coordinator.data:
+ data = self.coordinator.data["volumes"][self.monitor_device]
+ total_gb = int(data["total_size"]) / 1024 / 1024 / 1024
+
+ return {
+ ATTR_VOLUME_SIZE: f"{round(total_gb, 1)} {UnitOfInformation.GIBIBYTES}"
+ }
+ return None
diff --git a/homeassistant/components/qnap_qsw/sensor.py b/homeassistant/components/qnap_qsw/sensor.py
index 45ec1828b9d..009bc63b2c6 100644
--- a/homeassistant/components/qnap_qsw/sensor.py
+++ b/homeassistant/components/qnap_qsw/sensor.py
@@ -2,9 +2,7 @@
from __future__ import annotations
-from collections.abc import Callable
from dataclasses import dataclass, replace
-from datetime import datetime
from typing import Final
from aioqsw.const import (
@@ -28,11 +26,8 @@ from aioqsw.const import (
QSD_TX_OCTETS,
QSD_TX_SPEED,
QSD_UPTIME_SECONDS,
- QSD_UPTIME_TIMESTAMP,
)
-from homeassistant.components.automation import automations_with_entity
-from homeassistant.components.script import scripts_with_entity
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
@@ -48,10 +43,8 @@ from homeassistant.const import (
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import UNDEFINED, StateType
-from homeassistant.util import dt as dt_util
+from homeassistant.helpers.typing import UNDEFINED
from .const import ATTR_MAX, DOMAIN, QSW_COORD_DATA, RPM
from .coordinator import QswDataCoordinator
@@ -65,17 +58,6 @@ class QswSensorEntityDescription(SensorEntityDescription, QswEntityDescription):
attributes: dict[str, list[str]] | None = None
qsw_type: QswEntityType | None = None
sep_key: str = "_"
- value_fn: Callable[[str], datetime | StateType] = lambda value: value
-
-
-DEPRECATED_UPTIME_SECONDS = QswSensorEntityDescription(
- translation_key="uptime",
- key=QSD_SYSTEM_TIME,
- entity_category=EntityCategory.DIAGNOSTIC,
- native_unit_of_measurement=UnitOfTime.SECONDS,
- state_class=SensorStateClass.TOTAL_INCREASING,
- subkey=QSD_UPTIME_SECONDS,
-)
SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = (
@@ -158,12 +140,12 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = (
subkey=QSD_TX_SPEED,
),
QswSensorEntityDescription(
- translation_key="uptime_timestamp",
+ translation_key="uptime",
key=QSD_SYSTEM_TIME,
- device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
- subkey=QSD_UPTIME_TIMESTAMP,
- value_fn=dt_util.parse_datetime,
+ native_unit_of_measurement=UnitOfTime.SECONDS,
+ state_class=SensorStateClass.TOTAL_INCREASING,
+ subkey=QSD_UPTIME_SECONDS,
),
)
@@ -355,46 +337,6 @@ async def async_setup_entry(
)
entities.append(QswSensor(coordinator, _desc, entry, port_id))
- # Can be removed in HA 2025.5.0
- entity_reg = er.async_get(hass)
- reg_entities = er.async_entries_for_config_entry(entity_reg, entry.entry_id)
- for entity in reg_entities:
- if entity.domain == "sensor" and entity.unique_id.endswith(
- ("_uptime", "_uptime_seconds")
- ):
- entity_id = entity.entity_id
-
- if entity.disabled:
- entity_reg.async_remove(entity_id)
- continue
-
- if (
- DEPRECATED_UPTIME_SECONDS.key in coordinator.data
- and DEPRECATED_UPTIME_SECONDS.subkey
- in coordinator.data[DEPRECATED_UPTIME_SECONDS.key]
- ):
- entities.append(
- QswSensor(coordinator, DEPRECATED_UPTIME_SECONDS, entry)
- )
-
- entity_automations = automations_with_entity(hass, entity_id)
- entity_scripts = scripts_with_entity(hass, entity_id)
-
- for item in entity_automations + entity_scripts:
- ir.async_create_issue(
- hass,
- DOMAIN,
- f"uptime_seconds_deprecated_{entity_id}_{item}",
- breaks_in_ha_version="2025.5.0",
- is_fixable=False,
- severity=ir.IssueSeverity.WARNING,
- translation_key="uptime_seconds_deprecated",
- translation_placeholders={
- "entity": entity_id,
- "info": item,
- },
- )
-
async_add_entities(entities)
@@ -432,5 +374,5 @@ class QswSensor(QswSensorEntity, SensorEntity):
self.entity_description.subkey,
self.entity_description.qsw_type,
)
- self._attr_native_value = self.entity_description.value_fn(value)
+ self._attr_native_value = value
super()._async_update_attrs()
diff --git a/homeassistant/components/qnap_qsw/strings.json b/homeassistant/components/qnap_qsw/strings.json
index 462e66a25c3..c8cd5ffb861 100644
--- a/homeassistant/components/qnap_qsw/strings.json
+++ b/homeassistant/components/qnap_qsw/strings.json
@@ -52,16 +52,7 @@
},
"uptime": {
"name": "Uptime"
- },
- "uptime_timestamp": {
- "name": "Uptime timestamp"
}
}
- },
- "issues": {
- "uptime_seconds_deprecated": {
- "title": "QNAP QSW uptime seconds sensor deprecated",
- "description": "The QNAP QSW uptime seconds sensor entity is deprecated and will be removed in HA 2025.2.0.\nHome Assistant detected that entity `{entity}` is being used in `{info}`\n\nYou should remove the uptime seconds entity from `{info}` then click submit to fix this issue."
- }
}
}
diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json
index 3fcc895c2b9..14f2d093f37 100644
--- a/homeassistant/components/qrcode/manifest.json
+++ b/homeassistant/components/qrcode/manifest.json
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/qrcode",
"iot_class": "calculated",
"loggers": ["pyzbar"],
- "requirements": ["Pillow==11.0.0", "pyzbar==0.1.7"]
+ "requirements": ["Pillow==10.4.0", "pyzbar==0.1.7"]
}
diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py
index fac93952b35..66811091820 100644
--- a/homeassistant/components/rachio/config_flow.py
+++ b/homeassistant/components/rachio/config_flow.py
@@ -108,12 +108,16 @@ class RachioConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Handle a option flow for Rachio."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, int] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/radarr/config_flow.py b/homeassistant/components/radarr/config_flow.py
index d02038d7131..ab32a5d7352 100644
--- a/homeassistant/components/radarr/config_flow.py
+++ b/homeassistant/components/radarr/config_flow.py
@@ -12,11 +12,12 @@ from aiopyarr.radarr_client import RadarrClient
import voluptuous as vol
from yarl import URL
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from . import RadarrConfigEntry
from .const import DEFAULT_NAME, DEFAULT_URL, DOMAIN
@@ -24,11 +25,14 @@ class RadarrConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Radarr."""
VERSION = 1
+ entry: RadarrConfigEntry | None = None
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -47,7 +51,10 @@ class RadarrConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a flow initiated by the user."""
errors = {}
- if user_input is not None:
+ if user_input is None:
+ user_input = dict(self.entry.data) if self.entry else None
+
+ else:
# aiopyarr defaults to the service port if one isn't given
# this is counter to standard practice where http = 80
# and https = 443.
@@ -68,21 +75,20 @@ class RadarrConfigFlow(ConfigFlow, domain=DOMAIN):
except exceptions.ArrException:
errors = {"base": "unknown"}
if not errors:
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=user_input
+ if self.entry:
+ self.hass.config_entries.async_update_entry(
+ self.entry, data=user_input
)
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
+
+ return self.async_abort(reason="reauth_successful")
return self.async_create_entry(
title=DEFAULT_NAME,
data=user_input,
)
- if user_input is None:
- user_input = {}
- if self.source == SOURCE_REAUTH:
- user_input = dict(self._get_reauth_entry().data)
-
+ user_input = user_input or {}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
diff --git a/homeassistant/components/radio_browser/config_flow.py b/homeassistant/components/radio_browser/config_flow.py
index 411259f31d3..137ee7c8e87 100644
--- a/homeassistant/components/radio_browser/config_flow.py
+++ b/homeassistant/components/radio_browser/config_flow.py
@@ -18,6 +18,9 @@ class RadioBrowserConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
if user_input is not None:
return self.async_create_entry(title="Radio Browser", data={})
diff --git a/homeassistant/components/radio_browser/manifest.json b/homeassistant/components/radio_browser/manifest.json
index 943187596d7..5a52d29d27a 100644
--- a/homeassistant/components/radio_browser/manifest.json
+++ b/homeassistant/components/radio_browser/manifest.json
@@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/radio_browser",
"integration_type": "service",
"iot_class": "cloud_polling",
- "requirements": ["radios==0.3.2", "pycountry==24.6.1"],
- "single_config_entry": true
+ "requirements": ["radios==0.3.1", "pycountry==23.12.11"]
}
diff --git a/homeassistant/components/radio_browser/strings.json b/homeassistant/components/radio_browser/strings.json
index 5dd0ad3dcf7..fd0470d26dc 100644
--- a/homeassistant/components/radio_browser/strings.json
+++ b/homeassistant/components/radio_browser/strings.json
@@ -4,6 +4,9 @@
"user": {
"description": "Do you want to add Radio Browser to Home Assistant?"
}
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}
diff --git a/homeassistant/components/rainbird/config_flow.py b/homeassistant/components/rainbird/config_flow.py
index abeb1b5da15..c1c814b05c4 100644
--- a/homeassistant/components/rainbird/config_flow.py
+++ b/homeassistant/components/rainbird/config_flow.py
@@ -65,7 +65,7 @@ class RainbirdConfigFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> RainBirdOptionsFlowHandler:
"""Define the config flow to handle options."""
- return RainBirdOptionsFlowHandler()
+ return RainBirdOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -165,6 +165,10 @@ class RainbirdConfigFlowHandler(ConfigFlow, domain=DOMAIN):
class RainBirdOptionsFlowHandler(OptionsFlow):
"""Handle a RainBird options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize RainBirdOptionsFlowHandler."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/rainforest_raven/__init__.py b/homeassistant/components/rainforest_raven/__init__.py
index b68d995262a..76f82624160 100644
--- a/homeassistant/components/rainforest_raven/__init__.py
+++ b/homeassistant/components/rainforest_raven/__init__.py
@@ -2,23 +2,29 @@
from __future__ import annotations
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
-from .coordinator import RAVEnConfigEntry, RAVEnDataCoordinator
+from .const import DOMAIN
+from .coordinator import RAVEnDataCoordinator
PLATFORMS = (Platform.SENSOR,)
-async def async_setup_entry(hass: HomeAssistant, entry: RAVEnConfigEntry) -> bool:
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Rainforest RAVEn device from a config entry."""
coordinator = RAVEnDataCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
- entry.runtime_data = coordinator
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
-async def async_unload_entry(hass: HomeAssistant, entry: RAVEnConfigEntry) -> bool:
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/rainforest_raven/coordinator.py b/homeassistant/components/rainforest_raven/coordinator.py
index 31df922a168..d08a10c2670 100644
--- a/homeassistant/components/rainforest_raven/coordinator.py
+++ b/homeassistant/components/rainforest_raven/coordinator.py
@@ -20,8 +20,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DOMAIN
-type RAVEnConfigEntry = ConfigEntry[RAVEnDataCoordinator]
-
_LOGGER = logging.getLogger(__name__)
@@ -69,18 +67,32 @@ class RAVEnDataCoordinator(DataUpdateCoordinator):
_raven_device: RAVEnSerialDevice | None = None
_device_info: RAVEnDeviceInfo | None = None
- config_entry: RAVEnConfigEntry
- def __init__(self, hass: HomeAssistant, entry: RAVEnConfigEntry) -> None:
+ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the data object."""
+ self.entry = entry
+
super().__init__(
hass,
_LOGGER,
- config_entry=entry,
name=DOMAIN,
update_interval=timedelta(seconds=30),
)
+ @property
+ def device_fw_version(self) -> str | None:
+ """Return the firmware version of the device."""
+ if self._device_info:
+ return self._device_info.fw_version
+ return None
+
+ @property
+ def device_hw_version(self) -> str | None:
+ """Return the hardware version of the device."""
+ if self._device_info:
+ return self._device_info.hw_version
+ return None
+
@property
def device_mac_address(self) -> str | None:
"""Return the MAC address of the device."""
@@ -88,20 +100,36 @@ class RAVEnDataCoordinator(DataUpdateCoordinator):
return self._device_info.device_mac_id.hex()
return None
+ @property
+ def device_manufacturer(self) -> str | None:
+ """Return the manufacturer of the device."""
+ if self._device_info:
+ return self._device_info.manufacturer
+ return None
+
+ @property
+ def device_model(self) -> str | None:
+ """Return the model of the device."""
+ if self._device_info:
+ return self._device_info.model_id
+ return None
+
+ @property
+ def device_name(self) -> str:
+ """Return the product name of the device."""
+ return "RAVEn Device"
+
@property
def device_info(self) -> DeviceInfo | None:
"""Return device info."""
- if (device_info := self._device_info) and (
- mac_address := self.device_mac_address
- ):
+ if self._device_info and self.device_mac_address:
return DeviceInfo(
- identifiers={(DOMAIN, mac_address)},
- manufacturer=device_info.manufacturer,
- model=device_info.model_id,
- model_id=device_info.model_id,
- name="RAVEn Device",
- sw_version=device_info.fw_version,
- hw_version=device_info.hw_version,
+ identifiers={(DOMAIN, self.device_mac_address)},
+ manufacturer=self.device_manufacturer,
+ model=self.device_model,
+ name=self.device_name,
+ sw_version=self.device_fw_version,
+ hw_version=self.device_hw_version,
)
return None
@@ -114,7 +142,7 @@ class RAVEnDataCoordinator(DataUpdateCoordinator):
try:
device = await self._get_device()
async with asyncio.timeout(5):
- return await _get_all_data(device, self.config_entry.data[CONF_MAC])
+ return await _get_all_data(device, self.entry.data[CONF_MAC])
except RAVEnConnectionError as err:
await self._cleanup_device()
raise UpdateFailed(f"RAVEnConnectionError: {err}") from err
@@ -131,7 +159,7 @@ class RAVEnDataCoordinator(DataUpdateCoordinator):
if self._raven_device is not None:
return self._raven_device
- device = RAVEnSerialDevice(self.config_entry.data[CONF_DEVICE])
+ device = RAVEnSerialDevice(self.entry.data[CONF_DEVICE])
try:
async with asyncio.timeout(5):
diff --git a/homeassistant/components/rainforest_raven/diagnostics.py b/homeassistant/components/rainforest_raven/diagnostics.py
index 6c06b0d65cc..820c4826f00 100644
--- a/homeassistant/components/rainforest_raven/diagnostics.py
+++ b/homeassistant/components/rainforest_raven/diagnostics.py
@@ -6,10 +6,12 @@ from collections.abc import Mapping
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC
from homeassistant.core import HomeAssistant, callback
-from .coordinator import RAVEnConfigEntry
+from .const import DOMAIN
+from .coordinator import RAVEnDataCoordinator
TO_REDACT_CONFIG = {CONF_MAC}
TO_REDACT_DATA = {"device_mac_id", "meter_mac_id"}
@@ -29,13 +31,14 @@ def async_redact_meter_macs(data: dict) -> dict:
async def async_get_config_entry_diagnostics(
- hass: HomeAssistant, config_entry: RAVEnConfigEntry
+ hass: HomeAssistant, config_entry: ConfigEntry
) -> Mapping[str, Any]:
"""Return diagnostics for a config entry."""
+ coordinator: RAVEnDataCoordinator = hass.data[DOMAIN][config_entry.entry_id]
return {
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT_CONFIG),
"data": async_redact_meter_macs(
- async_redact_data(config_entry.runtime_data.data, TO_REDACT_DATA)
+ async_redact_data(coordinator.data, TO_REDACT_DATA)
),
}
diff --git a/homeassistant/components/rainforest_raven/sensor.py b/homeassistant/components/rainforest_raven/sensor.py
index 1025e92ef86..23ca3220694 100644
--- a/homeassistant/components/rainforest_raven/sensor.py
+++ b/homeassistant/components/rainforest_raven/sensor.py
@@ -10,7 +10,9 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
+ StateType,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_MAC,
PERCENTAGE,
@@ -20,10 +22,10 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from .coordinator import RAVEnConfigEntry, RAVEnDataCoordinator
+from .const import DOMAIN
+from .coordinator import RAVEnDataCoordinator
@dataclass(frozen=True, kw_only=True)
@@ -78,12 +80,10 @@ DIAGNOSTICS = (
async def async_setup_entry(
- hass: HomeAssistant,
- entry: RAVEnConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up a config entry."""
- coordinator = entry.runtime_data
+ coordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[RAVEnSensor] = [
RAVEnSensor(coordinator, description) for description in DIAGNOSTICS
]
diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py
index 0b40d506566..5c07f04c163 100644
--- a/homeassistant/components/rainmachine/config_flow.py
+++ b/homeassistant/components/rainmachine/config_flow.py
@@ -63,7 +63,7 @@ class RainMachineFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> RainMachineOptionsFlowHandler:
"""Define the config flow to handle options."""
- return RainMachineOptionsFlowHandler()
+ return RainMachineOptionsFlowHandler(config_entry)
async def async_step_homekit(
self, discovery_info: zeroconf.ZeroconfServiceInfo
@@ -168,6 +168,10 @@ class RainMachineFlowHandler(ConfigFlow, domain=DOMAIN):
class RainMachineOptionsFlowHandler(OptionsFlow):
"""Handle a RainMachine options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/rainmachine/update.py b/homeassistant/components/rainmachine/update.py
index 39156b05cd4..dbb91b70c85 100644
--- a/homeassistant/components/rainmachine/update.py
+++ b/homeassistant/components/rainmachine/update.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-from dataclasses import dataclass
from enum import Enum
from typing import Any
@@ -11,7 +10,6 @@ from regenmaschine.errors import RequestError
from homeassistant.components.update import (
UpdateDeviceClass,
UpdateEntity,
- UpdateEntityDescription,
UpdateEntityFeature,
)
from homeassistant.core import HomeAssistant, callback
@@ -44,14 +42,7 @@ UPDATE_STATE_MAP = {
}
-@dataclass(frozen=True, kw_only=True)
-class RainMachineUpdateEntityDescription(
- UpdateEntityDescription, RainMachineEntityDescription
-):
- """Describe a RainMachine update."""
-
-
-UPDATE_DESCRIPTION = RainMachineUpdateEntityDescription(
+UPDATE_DESCRIPTION = RainMachineEntityDescription(
key="update",
api_category=DATA_MACHINE_FIRMWARE_UPDATE_STATUS,
)
diff --git a/homeassistant/components/random/binary_sensor.py b/homeassistant/components/random/binary_sensor.py
index ae9a5886d59..9d33ad52692 100644
--- a/homeassistant/components/random/binary_sensor.py
+++ b/homeassistant/components/random/binary_sensor.py
@@ -59,9 +59,10 @@ class RandomBinarySensor(BinarySensorEntity):
def __init__(self, config: Mapping[str, Any], entry_id: str | None = None) -> None:
"""Initialize the Random binary sensor."""
- self._attr_name = config[CONF_NAME]
+ self._attr_name = config.get(CONF_NAME)
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
- self._attr_unique_id = entry_id
+ if entry_id:
+ self._attr_unique_id = entry_id
async def async_update(self) -> None:
"""Get new state and update the sensor's state."""
diff --git a/homeassistant/components/random/config_flow.py b/homeassistant/components/random/config_flow.py
index 00314169260..fcbd77916a9 100644
--- a/homeassistant/components/random/config_flow.py
+++ b/homeassistant/components/random/config_flow.py
@@ -95,7 +95,7 @@ def _generate_schema(domain: str, flow_type: _FlowType) -> vol.Schema:
async def choose_options_step(options: dict[str, Any]) -> str:
- """Return next step_id for options flow according to entity_type."""
+ """Return next step_id for options flow according to template_type."""
return cast(str, options["entity_type"])
@@ -122,7 +122,7 @@ def _validate_unit(options: dict[str, Any]) -> None:
def validate_user_input(
- entity_type: str,
+ template_type: str,
) -> Callable[
[SchemaCommonFlowHandler, dict[str, Any]],
Coroutine[Any, Any, dict[str, Any]],
@@ -136,10 +136,10 @@ def validate_user_input(
_: SchemaCommonFlowHandler,
user_input: dict[str, Any],
) -> dict[str, Any]:
- """Add entity type to user input."""
- if entity_type == Platform.SENSOR:
+ """Add template type to user input."""
+ if template_type == Platform.SENSOR:
_validate_unit(user_input)
- return {"entity_type": entity_type} | user_input
+ return {"entity_type": template_type} | user_input
return _validate_user_input
diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py
index aad4fcb851c..3c6e67c9918 100644
--- a/homeassistant/components/random/sensor.py
+++ b/homeassistant/components/random/sensor.py
@@ -70,22 +70,22 @@ class RandomSensor(SensorEntity):
"""Representation of a Random number sensor."""
_attr_translation_key = "random"
- _unrecorded_attributes = frozenset({ATTR_MAXIMUM, ATTR_MINIMUM})
def __init__(self, config: Mapping[str, Any], entry_id: str | None = None) -> None:
"""Initialize the Random sensor."""
- self._attr_name = config[CONF_NAME]
- self._minimum = config[CONF_MINIMUM]
- self._maximum = config[CONF_MAXIMUM]
+ self._attr_name = config.get(CONF_NAME)
+ self._minimum = config.get(CONF_MINIMUM, DEFAULT_MIN)
+ self._maximum = config.get(CONF_MAXIMUM, DEFAULT_MAX)
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
self._attr_extra_state_attributes = {
ATTR_MAXIMUM: self._maximum,
ATTR_MINIMUM: self._minimum,
}
- self._attr_unique_id = entry_id
+ if entry_id:
+ self._attr_unique_id = entry_id
async def async_update(self) -> None:
- """Get a new number and update the state."""
+ """Get a new number and updates the states."""
self._attr_native_value = randrange(self._minimum, self._maximum + 1)
diff --git a/homeassistant/components/random/strings.json b/homeassistant/components/random/strings.json
index ef19dd6dd67..98072a21fe1 100644
--- a/homeassistant/components/random/strings.json
+++ b/homeassistant/components/random/strings.json
@@ -1,5 +1,4 @@
{
- "title": "Random",
"config": {
"step": {
"binary_sensor": {
diff --git a/homeassistant/components/raspberry_pi/__init__.py b/homeassistant/components/raspberry_pi/__init__.py
index 8095eb9dfe0..d1dcd04922f 100644
--- a/homeassistant/components/raspberry_pi/__init__.py
+++ b/homeassistant/components/raspberry_pi/__init__.py
@@ -2,11 +2,10 @@
from __future__ import annotations
-from homeassistant.components.hassio import get_os_info
+from homeassistant.components.hassio import get_os_info, is_hassio
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers.hassio import is_hassio
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
diff --git a/homeassistant/components/rdw/__init__.py b/homeassistant/components/rdw/__init__.py
index 6051576026b..f123db7c697 100644
--- a/homeassistant/components/rdw/__init__.py
+++ b/homeassistant/components/rdw/__init__.py
@@ -23,7 +23,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator: DataUpdateCoordinator[Vehicle] = DataUpdateCoordinator(
hass,
LOGGER,
- config_entry=entry,
name=f"{DOMAIN}_APK",
update_interval=SCAN_INTERVAL,
update_method=rdw.vehicle,
diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py
index 1710fb8c816..6606f31a42d 100644
--- a/homeassistant/components/recollect_waste/__init__.py
+++ b/homeassistant/components/recollect_waste/__init__.py
@@ -52,7 +52,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
LOGGER,
- config_entry=entry,
name=(
f"Place {entry.data[CONF_PLACE_ID]}, Service {entry.data[CONF_SERVICE_ID]}"
),
diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py
index 299af2609e3..882eb6a00d2 100644
--- a/homeassistant/components/recollect_waste/config_flow.py
+++ b/homeassistant/components/recollect_waste/config_flow.py
@@ -34,9 +34,9 @@ class RecollectWasteConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
- ) -> RecollectWasteOptionsFlowHandler:
+ ) -> OptionsFlow:
"""Define the config flow to handle options."""
- return RecollectWasteOptionsFlowHandler()
+ return RecollectWasteOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -79,6 +79,10 @@ class RecollectWasteConfigFlow(ConfigFlow, domain=DOMAIN):
class RecollectWasteOptionsFlowHandler(OptionsFlow):
"""Handle a Recollect Waste options flow."""
+ def __init__(self, entry: ConfigEntry) -> None:
+ """Initialize."""
+ self._entry = entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -92,7 +96,7 @@ class RecollectWasteOptionsFlowHandler(OptionsFlow):
{
vol.Optional(
CONF_FRIENDLY_NAME,
- default=self.config_entry.options.get(CONF_FRIENDLY_NAME),
+ default=self._entry.options.get(CONF_FRIENDLY_NAME),
): bool
}
),
diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py
index 6ba64d4a571..77d01088d67 100644
--- a/homeassistant/components/recorder/core.py
+++ b/homeassistant/components/recorder/core.py
@@ -78,8 +78,16 @@ from .db_schema import (
StatisticsShortTerm,
)
from .executor import DBInterruptibleThreadPoolExecutor
+from .migration import (
+ EntityIDMigration,
+ EventIDPostMigration,
+ EventsContextIDMigration,
+ EventTypeIDMigration,
+ StatesContextIDMigration,
+)
from .models import DatabaseEngine, StatisticData, StatisticMetaData, UnsupportedDialect
from .pool import POOL_SIZE, MutexPool, RecorderPool
+from .queries import get_migration_changes
from .table_managers.event_data import EventDataManager
from .table_managers.event_types import EventTypeManager
from .table_managers.recorder_runs import RecorderRunsManager
@@ -112,6 +120,7 @@ from .util import (
build_mysqldb_conv,
dburl_to_path,
end_incomplete_runs,
+ execute_stmt_lambda_element,
is_second_sunday,
move_away_broken_database,
session_scope,
@@ -731,17 +740,12 @@ class Recorder(threading.Thread):
# First do non-live migration steps, if needed
if schema_status.migration_needed:
- # Do non-live schema migration
result, schema_status = self._migrate_schema_offline(schema_status)
if not result:
self._notify_migration_failed()
self.migration_in_progress = False
return
self.schema_version = schema_status.current_version
-
- # Do non-live data migration
- migration.migrate_data_non_live(self, self.get_session, schema_status)
-
# Non-live migration is now completed, remaining steps are live
self.migration_is_live = True
@@ -797,7 +801,20 @@ class Recorder(threading.Thread):
# there are a lot of statistics graphs on the frontend.
self.statistics_meta_manager.load(session)
- migration.migrate_data_live(self, self.get_session, schema_status)
+ migration_changes: dict[str, int] = {
+ row[0]: row[1]
+ for row in execute_stmt_lambda_element(session, get_migration_changes())
+ }
+
+ for migrator_cls in (
+ StatesContextIDMigration,
+ EventsContextIDMigration,
+ EventTypeIDMigration,
+ EntityIDMigration,
+ EventIDPostMigration,
+ ):
+ migrator = migrator_cls(schema_status.start_version, migration_changes)
+ migrator.do_migrate(self, session)
# We must only set the db ready after we have set the table managers
# to active if there is no data to migrate.
@@ -964,7 +981,6 @@ class Recorder(threading.Thread):
new_schema_status = migration.SchemaValidationStatus(
current_version=SCHEMA_VERSION,
migration_needed=False,
- non_live_data_migration_needed=False,
schema_errors=set(),
start_version=SCHEMA_VERSION,
)
diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py
index 02ab05288c5..5180a0c440c 100644
--- a/homeassistant/components/recorder/migration.py
+++ b/homeassistant/components/recorder/migration.py
@@ -91,7 +91,6 @@ from .queries import (
find_states_context_ids_to_migrate,
find_unmigrated_short_term_statistics_rows,
find_unmigrated_statistics_rows,
- get_migration_changes,
has_entity_ids_to_migrate,
has_event_type_to_migrate,
has_events_context_ids_to_migrate,
@@ -105,7 +104,6 @@ from .statistics import cleanup_statistics_timestamp_migration, get_start_time
from .tasks import RecorderTask
from .util import (
database_job_retry_wrapper,
- database_job_retry_wrapper_method,
execute_stmt_lambda_element,
get_index_by_name,
retryable_database_job_method,
@@ -200,13 +198,12 @@ def get_schema_version(session_maker: Callable[[], Session]) -> int | None:
return None
-@dataclass(frozen=True, kw_only=True)
+@dataclass(frozen=True)
class SchemaValidationStatus:
"""Store schema validation status."""
current_version: int
migration_needed: bool
- non_live_data_migration_needed: bool
schema_errors: set[str]
start_version: int
@@ -236,17 +233,8 @@ def validate_db_schema(
# columns may otherwise not exist etc.
schema_errors = _find_schema_errors(hass, instance, session_maker)
- schema_migration_needed = not is_current
- _non_live_data_migration_needed = non_live_data_migration_needed(
- instance, session_maker, current_version
- )
-
return SchemaValidationStatus(
- current_version=current_version,
- non_live_data_migration_needed=_non_live_data_migration_needed,
- migration_needed=schema_migration_needed or _non_live_data_migration_needed,
- schema_errors=schema_errors,
- start_version=current_version,
+ current_version, not is_current, schema_errors, current_version
)
@@ -263,10 +251,7 @@ def _find_schema_errors(
def live_migration(schema_status: SchemaValidationStatus) -> bool:
"""Check if live migration is possible."""
- return (
- schema_status.current_version >= LIVE_MIGRATION_MIN_SCHEMA_VERSION
- and not schema_status.non_live_data_migration_needed
- )
+ return schema_status.current_version >= LIVE_MIGRATION_MIN_SCHEMA_VERSION
def pre_migrate_schema(engine: Engine) -> None:
@@ -365,68 +350,6 @@ def migrate_schema_live(
return schema_status
-def _get_migration_changes(session: Session) -> dict[str, int]:
- """Return migration changes as a dict."""
- migration_changes: dict[str, int] = {
- row[0]: row[1]
- for row in execute_stmt_lambda_element(session, get_migration_changes())
- }
- return migration_changes
-
-
-def non_live_data_migration_needed(
- instance: Recorder,
- session_maker: Callable[[], Session],
- schema_version: int,
-) -> bool:
- """Return True if non-live data migration is needed.
-
- This must only be called if database schema is current.
- """
- migration_needed = False
- with session_scope(session=session_maker()) as session:
- migration_changes = _get_migration_changes(session)
- for migrator_cls in NON_LIVE_DATA_MIGRATORS:
- migrator = migrator_cls(schema_version, migration_changes)
- migration_needed |= migrator.needs_migrate(instance, session)
-
- return migration_needed
-
-
-def migrate_data_non_live(
- instance: Recorder,
- session_maker: Callable[[], Session],
- schema_status: SchemaValidationStatus,
-) -> None:
- """Do non-live data migration.
-
- This must be called after non-live schema migration is completed.
- """
- with session_scope(session=session_maker()) as session:
- migration_changes = _get_migration_changes(session)
-
- for migrator_cls in NON_LIVE_DATA_MIGRATORS:
- migrator = migrator_cls(schema_status.start_version, migration_changes)
- migrator.migrate_all(instance, session_maker)
-
-
-def migrate_data_live(
- instance: Recorder,
- session_maker: Callable[[], Session],
- schema_status: SchemaValidationStatus,
-) -> None:
- """Queue live schema migration tasks.
-
- This must be called after live schema migration is completed.
- """
- with session_scope(session=session_maker()) as session:
- migration_changes = _get_migration_changes(session)
-
- for migrator_cls in LIVE_DATA_MIGRATORS:
- migrator = migrator_cls(schema_status.start_version, migration_changes)
- migrator.queue_migration(instance, session)
-
-
def _create_index(
session_maker: Callable[[], Session], table_name: str, index_name: str
) -> None:
@@ -2273,24 +2196,29 @@ class DataMigrationStatus:
migration_done: bool
-class BaseMigration(ABC):
- """Base class for migrations."""
+class BaseRunTimeMigration(ABC):
+ """Base class for run time migrations."""
index_to_drop: tuple[str, str] | None = None
required_schema_version = 0
migration_version = 1
migration_id: str
+ task = MigrationTask
def __init__(self, schema_version: int, migration_changes: dict[str, int]) -> None:
"""Initialize a new BaseRunTimeMigration."""
self.schema_version = schema_version
self.migration_changes = migration_changes
- @abstractmethod
- def migrate_data(self, instance: Recorder) -> bool:
- """Migrate some data, return True if migration is completed."""
+ def do_migrate(self, instance: Recorder, session: Session) -> None:
+ """Start migration if needed."""
+ if self.needs_migrate(instance, session):
+ instance.queue_task(self.task(self))
+ else:
+ self.migration_done(instance, session)
- def _migrate_data(self, instance: Recorder) -> bool:
+ @retryable_database_job_method("migrate data")
+ def migrate_data(self, instance: Recorder) -> bool:
"""Migrate some data, returns True if migration is completed."""
status = self.migrate_data_impl(instance)
if status.migration_done:
@@ -2345,45 +2273,7 @@ class BaseMigration(ABC):
return needs_migrate.needs_migrate
-class BaseOffLineMigration(BaseMigration):
- """Base class for off line migrations."""
-
- def migrate_all(
- self, instance: Recorder, session_maker: Callable[[], Session]
- ) -> None:
- """Migrate all data."""
- with session_scope(session=session_maker()) as session:
- if not self.needs_migrate(instance, session):
- self.migration_done(instance, session)
- return
- while not self.migrate_data(instance):
- pass
-
- @database_job_retry_wrapper_method("migrate data", 10)
- def migrate_data(self, instance: Recorder) -> bool:
- """Migrate some data, returns True if migration is completed."""
- return self._migrate_data(instance)
-
-
-class BaseRunTimeMigration(BaseMigration):
- """Base class for run time migrations."""
-
- task = MigrationTask
-
- def queue_migration(self, instance: Recorder, session: Session) -> None:
- """Start migration if needed."""
- if self.needs_migrate(instance, session):
- instance.queue_task(self.task(self))
- else:
- self.migration_done(instance, session)
-
- @retryable_database_job_method("migrate data")
- def migrate_data(self, instance: Recorder) -> bool:
- """Migrate some data, returns True if migration is completed."""
- return self._migrate_data(instance)
-
-
-class BaseMigrationWithQuery(BaseMigration):
+class BaseRunTimeMigrationWithQuery(BaseRunTimeMigration):
"""Base class for run time migrations."""
@abstractmethod
@@ -2400,7 +2290,7 @@ class BaseMigrationWithQuery(BaseMigration):
)
-class StatesContextIDMigration(BaseMigrationWithQuery, BaseOffLineMigration):
+class StatesContextIDMigration(BaseRunTimeMigrationWithQuery):
"""Migration to migrate states context_ids to binary format."""
required_schema_version = CONTEXT_ID_AS_BINARY_SCHEMA_VERSION
@@ -2443,7 +2333,7 @@ class StatesContextIDMigration(BaseMigrationWithQuery, BaseOffLineMigration):
return has_states_context_ids_to_migrate()
-class EventsContextIDMigration(BaseMigrationWithQuery, BaseOffLineMigration):
+class EventsContextIDMigration(BaseRunTimeMigrationWithQuery):
"""Migration to migrate events context_ids to binary format."""
required_schema_version = CONTEXT_ID_AS_BINARY_SCHEMA_VERSION
@@ -2486,7 +2376,7 @@ class EventsContextIDMigration(BaseMigrationWithQuery, BaseOffLineMigration):
return has_events_context_ids_to_migrate()
-class EventTypeIDMigration(BaseMigrationWithQuery, BaseRunTimeMigration):
+class EventTypeIDMigration(BaseRunTimeMigrationWithQuery):
"""Migration to migrate event_type to event_type_ids."""
required_schema_version = EVENT_TYPE_IDS_SCHEMA_VERSION
@@ -2564,7 +2454,7 @@ class EventTypeIDMigration(BaseMigrationWithQuery, BaseRunTimeMigration):
return has_event_type_to_migrate()
-class EntityIDMigration(BaseMigrationWithQuery, BaseRunTimeMigration):
+class EntityIDMigration(BaseRunTimeMigrationWithQuery):
"""Migration to migrate entity_ids to states_meta."""
required_schema_version = STATES_META_SCHEMA_VERSION
@@ -2652,7 +2542,7 @@ class EntityIDMigration(BaseMigrationWithQuery, BaseRunTimeMigration):
instance.states_meta_manager.active = True
with contextlib.suppress(SQLAlchemyError):
migrate = EntityIDPostMigration(self.schema_version, self.migration_changes)
- migrate.queue_migration(instance, session)
+ migrate.do_migrate(instance, session)
def needs_migrate_query(self) -> StatementLambdaElement:
"""Check if the data is migrated."""
@@ -2741,7 +2631,7 @@ class EventIDPostMigration(BaseRunTimeMigration):
return DataMigrationStatus(needs_migrate=False, migration_done=True)
-class EntityIDPostMigration(BaseMigrationWithQuery, BaseRunTimeMigration):
+class EntityIDPostMigration(BaseRunTimeMigrationWithQuery):
"""Migration to remove old entity_id strings from states."""
migration_id = "entity_id_post_migration"
@@ -2758,19 +2648,9 @@ class EntityIDPostMigration(BaseMigrationWithQuery, BaseRunTimeMigration):
return has_used_states_entity_ids()
-NON_LIVE_DATA_MIGRATORS = (
- StatesContextIDMigration, # Introduced in HA Core 2023.4
- EventsContextIDMigration, # Introduced in HA Core 2023.4
-)
-
-LIVE_DATA_MIGRATORS = (
- EventTypeIDMigration,
- EntityIDMigration,
- EventIDPostMigration,
-)
-
-
-def _mark_migration_done(session: Session, migration: type[BaseMigration]) -> None:
+def _mark_migration_done(
+ session: Session, migration: type[BaseRunTimeMigration]
+) -> None:
"""Mark a migration as done in the database."""
session.merge(
MigrationChanges(
diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py
index fc2a8ccb1cc..30f8fa8d07a 100644
--- a/homeassistant/components/recorder/pool.py
+++ b/homeassistant/components/recorder/pool.py
@@ -16,7 +16,7 @@ from sqlalchemy.pool import (
StaticPool,
)
-from homeassistant.helpers.frame import ReportBehavior, report_usage
+from homeassistant.helpers.frame import report
from homeassistant.util.loop import raise_for_blocking_call
_LOGGER = logging.getLogger(__name__)
@@ -108,14 +108,14 @@ class RecorderPool(SingletonThreadPool, NullPool):
# raise_for_blocking_call will raise an exception
def _do_get_db_connection_protected(self) -> ConnectionPoolEntry:
- report_usage(
+ report(
(
"accesses the database without the database executor; "
f"{ADVISE_MSG} "
"for faster database operations"
),
exclude_integrations={"recorder"},
- core_behavior=ReportBehavior.LOG,
+ error_if_core=False,
)
return NullPool._create_connection(self) # noqa: SLF001
diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py
index 7243af9d4d5..4ffe7c72971 100644
--- a/homeassistant/components/recorder/statistics.py
+++ b/homeassistant/components/recorder/statistics.py
@@ -28,7 +28,6 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import (
BaseUnitConverter,
- BloodGlucoseConcentrationConverter,
ConductivityConverter,
DataRateConverter,
DistanceConverter,
@@ -129,10 +128,6 @@ QUERY_STATISTICS_SUMMARY_SUM = (
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
- **{
- unit: BloodGlucoseConcentrationConverter
- for unit in BloodGlucoseConcentrationConverter.VALID_UNITS
- },
**{unit: ConductivityConverter for unit in ConductivityConverter.VALID_UNITS},
**{unit: DataRateConverter for unit in DataRateConverter.VALID_UNITS},
**{unit: DistanceConverter for unit in DistanceConverter.VALID_UNITS},
diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py
index a59519ef38d..d078c32cb88 100644
--- a/homeassistant/components/recorder/util.py
+++ b/homeassistant/components/recorder/util.py
@@ -652,13 +652,13 @@ type _FuncOrMethType[**_P, _R] = Callable[_P, _R]
def retryable_database_job[**_P](
description: str,
) -> Callable[[_FuncType[_P, bool]], _FuncType[_P, bool]]:
- """Execute a database job repeatedly until it succeeds.
+ """Try to execute a database job.
The job should return True if it finished, and False if it needs to be rescheduled.
"""
def decorator(job: _FuncType[_P, bool]) -> _FuncType[_P, bool]:
- return _wrap_retryable_database_job_func_or_meth(job, description, False)
+ return _wrap_func_or_meth(job, description, False)
return decorator
@@ -666,18 +666,18 @@ def retryable_database_job[**_P](
def retryable_database_job_method[_Self, **_P](
description: str,
) -> Callable[[_MethType[_Self, _P, bool]], _MethType[_Self, _P, bool]]:
- """Execute a database job repeatedly until it succeeds.
+ """Try to execute a database job.
The job should return True if it finished, and False if it needs to be rescheduled.
"""
def decorator(job: _MethType[_Self, _P, bool]) -> _MethType[_Self, _P, bool]:
- return _wrap_retryable_database_job_func_or_meth(job, description, True)
+ return _wrap_func_or_meth(job, description, True)
return decorator
-def _wrap_retryable_database_job_func_or_meth[**_P](
+def _wrap_func_or_meth[**_P](
job: _FuncOrMethType[_P, bool], description: str, method: bool
) -> _FuncOrMethType[_P, bool]:
recorder_pos = 1 if method else 0
@@ -705,10 +705,10 @@ def _wrap_retryable_database_job_func_or_meth[**_P](
return wrapper
-def database_job_retry_wrapper[**_P, _R](
- description: str, attempts: int
-) -> Callable[[_FuncType[_P, _R]], _FuncType[_P, _R]]:
- """Execute a database job repeatedly until it succeeds, at most attempts times.
+def database_job_retry_wrapper[**_P](
+ description: str, attempts: int = 5
+) -> Callable[[_FuncType[_P, None]], _FuncType[_P, None]]:
+ """Try to execute a database job multiple times.
This wrapper handles InnoDB deadlocks and lock timeouts.
@@ -717,63 +717,32 @@ def database_job_retry_wrapper[**_P, _R](
"""
def decorator(
- job: _FuncType[_P, _R],
- ) -> _FuncType[_P, _R]:
- return _database_job_retry_wrapper_func_or_meth(
- job, description, attempts, False
- )
+ job: _FuncType[_P, None],
+ ) -> _FuncType[_P, None]:
+ @functools.wraps(job)
+ def wrapper(instance: Recorder, *args: _P.args, **kwargs: _P.kwargs) -> None:
+ for attempt in range(attempts):
+ try:
+ job(instance, *args, **kwargs)
+ except OperationalError as err:
+ if attempt == attempts - 1 or not _is_retryable_error(
+ instance, err
+ ):
+ raise
+ assert isinstance(err.orig, BaseException) # noqa: PT017
+ _LOGGER.info(
+ "%s; %s failed, retrying", err.orig.args[1], description
+ )
+ time.sleep(instance.db_retry_wait)
+ # Failed with retryable error
+ else:
+ return
+
+ return wrapper
return decorator
-def database_job_retry_wrapper_method[_Self, **_P, _R](
- description: str, attempts: int
-) -> Callable[[_MethType[_Self, _P, _R]], _MethType[_Self, _P, _R]]:
- """Execute a database job repeatedly until it succeeds, at most attempts times.
-
- This wrapper handles InnoDB deadlocks and lock timeouts.
-
- This is different from retryable_database_job in that it will retry the job
- attempts number of times instead of returning False if the job fails.
- """
-
- def decorator(
- job: _MethType[_Self, _P, _R],
- ) -> _MethType[_Self, _P, _R]:
- return _database_job_retry_wrapper_func_or_meth(
- job, description, attempts, True
- )
-
- return decorator
-
-
-def _database_job_retry_wrapper_func_or_meth[**_P, _R](
- job: _FuncOrMethType[_P, _R],
- description: str,
- attempts: int,
- method: bool,
-) -> _FuncOrMethType[_P, _R]:
- recorder_pos = 1 if method else 0
-
- @functools.wraps(job)
- def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
- instance: Recorder = args[recorder_pos] # type: ignore[assignment]
- for attempt in range(attempts):
- try:
- return job(*args, **kwargs)
- except OperationalError as err:
- # Failed with retryable error
- if attempt == attempts - 1 or not _is_retryable_error(instance, err):
- raise
- assert isinstance(err.orig, BaseException) # noqa: PT017
- _LOGGER.info("%s; %s failed, retrying", err.orig.args[1], description)
- time.sleep(instance.db_retry_wait)
-
- raise ValueError("attempts must be a positive integer")
-
- return wrapper
-
-
def periodic_db_cleanups(instance: Recorder) -> None:
"""Run any database cleanups that need to happen periodically.
diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py
index f4dce73fa47..ac917e903df 100644
--- a/homeassistant/components/recorder/websocket_api.py
+++ b/homeassistant/components/recorder/websocket_api.py
@@ -16,7 +16,6 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.json import json_bytes
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import (
- BloodGlucoseConcentrationConverter,
ConductivityConverter,
DataRateConverter,
DistanceConverter,
@@ -55,9 +54,6 @@ UPDATE_STATISTICS_METADATA_TIME_OUT = 10
UNIT_SCHEMA = vol.Schema(
{
- vol.Optional("blood_glucose_concentration"): vol.In(
- BloodGlucoseConcentrationConverter.VALID_UNITS
- ),
vol.Optional("conductivity"): vol.In(ConductivityConverter.VALID_UNITS),
vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS),
vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS),
diff --git a/homeassistant/components/refoss/const.py b/homeassistant/components/refoss/const.py
index 851f8ba8f77..0542afe8afb 100644
--- a/homeassistant/components/refoss/const.py
+++ b/homeassistant/components/refoss/const.py
@@ -20,9 +20,6 @@ COORDINATOR = "coordinator"
MAX_ERRORS = 2
-# Energy monitoring
-SENSOR_EM = "em"
-
CHANNEL_DISPLAY_NAME: dict[str, dict[int, str]] = {
"em06": {
1: "A1",
@@ -31,25 +28,5 @@ CHANNEL_DISPLAY_NAME: dict[str, dict[int, str]] = {
4: "A2",
5: "B2",
6: "C2",
- },
- "em16": {
- 1: "A1",
- 2: "A2",
- 3: "A3",
- 4: "A4",
- 5: "A5",
- 6: "A6",
- 7: "B1",
- 8: "B2",
- 9: "B3",
- 10: "B4",
- 11: "B5",
- 12: "B6",
- 13: "C1",
- 14: "C2",
- 15: "C3",
- 16: "C4",
- 17: "C5",
- 18: "C6",
- },
+ }
}
diff --git a/homeassistant/components/refoss/sensor.py b/homeassistant/components/refoss/sensor.py
index 26454cae48d..f65724ddd77 100644
--- a/homeassistant/components/refoss/sensor.py
+++ b/homeassistant/components/refoss/sensor.py
@@ -31,7 +31,6 @@ from .const import (
COORDINATORS,
DISPATCH_DEVICE_DISCOVERED,
DOMAIN,
- SENSOR_EM,
)
from .entity import RefossEntity
@@ -44,13 +43,8 @@ class RefossSensorEntityDescription(SensorEntityDescription):
fn: Callable[[float], float] = lambda x: x
-DEVICETYPE_SENSOR: dict[str, str] = {
- "em06": SENSOR_EM,
- "em16": SENSOR_EM,
-}
-
SENSORS: dict[str, tuple[RefossSensorEntityDescription, ...]] = {
- SENSOR_EM: (
+ "em06": (
RefossSensorEntityDescription(
key="power",
translation_key="power",
@@ -127,11 +121,8 @@ async def async_setup_entry(
if not isinstance(device, ElectricityXMix):
return
-
- sensor_type = DEVICETYPE_SENSOR.get(device.device_type, "")
-
descriptions: tuple[RefossSensorEntityDescription, ...] = SENSORS.get(
- sensor_type, ()
+ device.device_type, ()
)
async_add_entities(
diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py
index 7a36991201a..4f0b8ae2664 100644
--- a/homeassistant/components/reolink/__init__.py
+++ b/homeassistant/components/reolink/__init__.py
@@ -10,7 +10,7 @@ from reolink_aio.api import RETRY_ATTEMPTS
from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError
from homeassistant.config_entries import ConfigEntryState
-from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import (
@@ -22,7 +22,7 @@ from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-from .const import CONF_USE_HTTPS, DOMAIN
+from .const import DOMAIN
from .exceptions import PasswordIncompatible, ReolinkException, UserNotAdmin
from .host import ReolinkHost
from .services import async_setup_services
@@ -83,24 +83,6 @@ async def async_setup_entry(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop)
)
- # update the port info if needed for the next time
- if (
- host.api.port != config_entry.data[CONF_PORT]
- or host.api.use_https != config_entry.data[CONF_USE_HTTPS]
- ):
- _LOGGER.warning(
- "HTTP(s) port of Reolink %s, changed from %s to %s",
- host.api.nvr_name,
- config_entry.data[CONF_PORT],
- host.api.port,
- )
- data = {
- **config_entry.data,
- CONF_PORT: host.api.port,
- CONF_USE_HTTPS: host.api.use_https,
- }
- hass.config_entries.async_update_entry(config_entry, data=data)
-
async def async_device_config_update() -> None:
"""Update the host state cache and renew the ONVIF-subscription."""
async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
@@ -152,7 +134,6 @@ async def async_setup_entry(
device_coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=config_entry,
name=f"reolink.{host.api.nvr_name}",
update_method=async_device_config_update,
update_interval=DEVICE_UPDATE_INTERVAL,
@@ -160,7 +141,6 @@ async def async_setup_entry(
firmware_coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=config_entry,
name=f"reolink.{host.api.nvr_name}.firmware",
update_method=async_check_firmware_update,
update_interval=FIRMWARE_UPDATE_INTERVAL,
diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py
index f6c64d0b060..c11161b11c7 100644
--- a/homeassistant/components/reolink/binary_sensor.py
+++ b/homeassistant/components/reolink/binary_sensor.py
@@ -42,34 +42,29 @@ class ReolinkBinarySensorEntityDescription(
BINARY_PUSH_SENSORS = (
ReolinkBinarySensorEntityDescription(
key="motion",
- cmd_id=33,
device_class=BinarySensorDeviceClass.MOTION,
value=lambda api, ch: api.motion_detected(ch),
),
ReolinkBinarySensorEntityDescription(
key=FACE_DETECTION_TYPE,
- cmd_id=33,
translation_key="face",
value=lambda api, ch: api.ai_detected(ch, FACE_DETECTION_TYPE),
supported=lambda api, ch: api.ai_supported(ch, FACE_DETECTION_TYPE),
),
ReolinkBinarySensorEntityDescription(
key=PERSON_DETECTION_TYPE,
- cmd_id=33,
translation_key="person",
value=lambda api, ch: api.ai_detected(ch, PERSON_DETECTION_TYPE),
supported=lambda api, ch: api.ai_supported(ch, PERSON_DETECTION_TYPE),
),
ReolinkBinarySensorEntityDescription(
key=VEHICLE_DETECTION_TYPE,
- cmd_id=33,
translation_key="vehicle",
value=lambda api, ch: api.ai_detected(ch, VEHICLE_DETECTION_TYPE),
supported=lambda api, ch: api.ai_supported(ch, VEHICLE_DETECTION_TYPE),
),
ReolinkBinarySensorEntityDescription(
key=PET_DETECTION_TYPE,
- cmd_id=33,
translation_key="pet",
value=lambda api, ch: api.ai_detected(ch, PET_DETECTION_TYPE),
supported=lambda api, ch: (
@@ -79,21 +74,18 @@ BINARY_PUSH_SENSORS = (
),
ReolinkBinarySensorEntityDescription(
key=PET_DETECTION_TYPE,
- cmd_id=33,
translation_key="animal",
value=lambda api, ch: api.ai_detected(ch, PET_DETECTION_TYPE),
supported=lambda api, ch: api.supported(ch, "ai_animal"),
),
ReolinkBinarySensorEntityDescription(
key=PACKAGE_DETECTION_TYPE,
- cmd_id=33,
translation_key="package",
value=lambda api, ch: api.ai_detected(ch, PACKAGE_DETECTION_TYPE),
supported=lambda api, ch: api.ai_supported(ch, PACKAGE_DETECTION_TYPE),
),
ReolinkBinarySensorEntityDescription(
key="visitor",
- cmd_id=33,
translation_key="visitor",
value=lambda api, ch: api.visitor_detected(ch),
supported=lambda api, ch: api.is_doorbell(ch),
diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py
index 0b1ed7b4b15..bf58646536f 100644
--- a/homeassistant/components/reolink/config_flow.py
+++ b/homeassistant/components/reolink/config_flow.py
@@ -7,12 +7,7 @@ import logging
from typing import Any
from reolink_aio.api import ALLOWED_SPECIAL_CHARS
-from reolink_aio.exceptions import (
- ApiError,
- CredentialsInvalidError,
- LoginFirmwareError,
- ReolinkError,
-)
+from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError
import voluptuous as vol
from homeassistant.components import dhcp
@@ -54,6 +49,10 @@ DEFAULT_OPTIONS = {CONF_PROTOCOL: DEFAULT_PROTOCOL}
class ReolinkOptionsFlowHandler(OptionsFlow):
"""Handle Reolink options."""
+ def __init__(self, config_entry: ReolinkConfigEntry) -> None:
+ """Initialize ReolinkOptionsFlowHandler."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -108,7 +107,7 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ReolinkConfigEntry,
) -> ReolinkOptionsFlowHandler:
"""Options callback for Reolink."""
- return ReolinkOptionsFlowHandler()
+ return ReolinkOptionsFlowHandler(config_entry)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
@@ -234,15 +233,6 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN):
placeholders["special_chars"] = ALLOWED_SPECIAL_CHARS
except CredentialsInvalidError:
errors[CONF_PASSWORD] = "invalid_auth"
- except LoginFirmwareError:
- errors["base"] = "update_needed"
- placeholders["current_firmware"] = host.api.sw_version
- placeholders["needed_firmware"] = (
- host.api.sw_version_required.version_string
- )
- placeholders["download_center_url"] = (
- "https://reolink.com/download-center"
- )
except ApiError as err:
placeholders["error"] = str(err)
errors[CONF_HOST] = "api_error"
diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py
index 6101eee8a4c..d0a8f6dfc8d 100644
--- a/homeassistant/components/reolink/entity.py
+++ b/homeassistant/components/reolink/entity.py
@@ -7,7 +7,6 @@ from dataclasses import dataclass
from reolink_aio.api import DUAL_LENS_MODELS, Chime, Host
-from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import (
@@ -24,7 +23,6 @@ class ReolinkEntityDescription(EntityDescription):
"""A class that describes entities for Reolink."""
cmd_key: str | None = None
- cmd_id: int | None = None
@dataclass(frozen=True, kw_only=True)
@@ -92,35 +90,18 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None]
"""Return True if entity is available."""
return self._host.api.session_active and super().available
- @callback
- def _push_callback(self) -> None:
- """Handle incoming TCP push event."""
- self.async_write_ha_state()
-
- def register_callback(self, unique_id: str, cmd_id: int) -> None:
- """Register callback for TCP push events."""
- self._host.api.baichuan.register_callback( # pragma: no cover
- unique_id, self._push_callback, cmd_id
- )
-
async def async_added_to_hass(self) -> None:
"""Entity created."""
await super().async_added_to_hass()
cmd_key = self.entity_description.cmd_key
- cmd_id = self.entity_description.cmd_id
if cmd_key is not None:
self._host.async_register_update_cmd(cmd_key)
- if cmd_id is not None and self._attr_unique_id is not None:
- self.register_callback(self._attr_unique_id, cmd_id)
async def async_will_remove_from_hass(self) -> None:
"""Entity removed."""
cmd_key = self.entity_description.cmd_key
- cmd_id = self.entity_description.cmd_id
if cmd_key is not None:
self._host.async_unregister_update_cmd(cmd_key)
- if cmd_id is not None and self._attr_unique_id is not None:
- self._host.api.baichuan.unregister_callback(self._attr_unique_id)
await super().async_will_remove_from_hass()
@@ -179,12 +160,6 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
"""Return True if entity is available."""
return super().available and self._host.api.camera_online(self._channel)
- def register_callback(self, unique_id: str, cmd_id) -> None:
- """Register callback for TCP push events."""
- self._host.api.baichuan.register_callback(
- unique_id, self._push_callback, cmd_id, self._channel
- )
-
async def async_added_to_hass(self) -> None:
"""Entity created."""
await super().async_added_to_hass()
diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py
index 336876d4c4f..a90b9314440 100644
--- a/homeassistant/components/reolink/host.py
+++ b/homeassistant/components/reolink/host.py
@@ -41,7 +41,6 @@ from .exceptions import (
)
DEFAULT_TIMEOUT = 30
-FIRST_TCP_PUSH_TIMEOUT = 10
FIRST_ONVIF_TIMEOUT = 10
FIRST_ONVIF_LONG_POLL_TIMEOUT = 90
SUBSCRIPTION_RENEW_THRESHOLD = 300
@@ -106,7 +105,6 @@ class ReolinkHost:
self._long_poll_received: bool = False
self._long_poll_error: bool = False
self._cancel_poll: CALLBACK_TYPE | None = None
- self._cancel_tcp_push_check: CALLBACK_TYPE | None = None
self._cancel_onvif_check: CALLBACK_TYPE | None = None
self._cancel_long_poll_check: CALLBACK_TYPE | None = None
self._poll_job = HassJob(self._async_poll_all_motion, cancel_on_shutdown=True)
@@ -222,14 +220,49 @@ class ReolinkHost:
else:
self._unique_id = format_mac(self._api.mac_address)
- try:
- await self._api.baichuan.subscribe_events()
- except ReolinkError:
- await self._async_check_tcp_push()
- else:
- self._cancel_tcp_push_check = async_call_later(
- self._hass, FIRST_TCP_PUSH_TIMEOUT, self._async_check_tcp_push
+ if self._onvif_push_supported:
+ try:
+ await self.subscribe()
+ except ReolinkError:
+ self._onvif_push_supported = False
+ self.unregister_webhook()
+ await self._api.unsubscribe()
+ else:
+ if self._api.supported(None, "initial_ONVIF_state"):
+ _LOGGER.debug(
+ "Waiting for initial ONVIF state on webhook '%s'",
+ self._webhook_url,
+ )
+ else:
+ _LOGGER.debug(
+ "Camera model %s most likely does not push its initial state"
+ " upon ONVIF subscription, do not check",
+ self._api.model,
+ )
+ self._cancel_onvif_check = async_call_later(
+ self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif
+ )
+ if not self._onvif_push_supported:
+ _LOGGER.debug(
+ "Camera model %s does not support ONVIF push, using ONVIF long polling instead",
+ self._api.model,
)
+ try:
+ await self._async_start_long_polling(initial=True)
+ except NotSupportedError:
+ _LOGGER.debug(
+ "Camera model %s does not support ONVIF long polling, using fast polling instead",
+ self._api.model,
+ )
+ self._onvif_long_poll_supported = False
+ await self._api.unsubscribe()
+ await self._async_poll_all_motion()
+ else:
+ self._cancel_long_poll_check = async_call_later(
+ self._hass,
+ FIRST_ONVIF_LONG_POLL_TIMEOUT,
+ self._async_check_onvif_long_poll,
+ )
ch_list: list[int | None] = [None]
if self._api.is_nvr:
@@ -261,67 +294,6 @@ class ReolinkHost:
else:
ir.async_delete_issue(self._hass, DOMAIN, f"firmware_update_{key}")
- async def _async_check_tcp_push(self, *_) -> None:
- """Check the TCP push subscription."""
- if self._api.baichuan.events_active:
- ir.async_delete_issue(self._hass, DOMAIN, "webhook_url")
- self._cancel_tcp_push_check = None
- return
-
- _LOGGER.debug(
- "Reolink %s, did not receive initial TCP push event after %i seconds",
- self._api.nvr_name,
- FIRST_TCP_PUSH_TIMEOUT,
- )
-
- if self._onvif_push_supported:
- try:
- await self.subscribe()
- except ReolinkError:
- self._onvif_push_supported = False
- self.unregister_webhook()
- await self._api.unsubscribe()
- else:
- if self._api.supported(None, "initial_ONVIF_state"):
- _LOGGER.debug(
- "Waiting for initial ONVIF state on webhook '%s'",
- self._webhook_url,
- )
- else:
- _LOGGER.debug(
- "Camera model %s most likely does not push its initial state"
- " upon ONVIF subscription, do not check",
- self._api.model,
- )
- self._cancel_onvif_check = async_call_later(
- self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif
- )
-
- # start long polling if ONVIF push failed immediately
- if not self._onvif_push_supported:
- _LOGGER.debug(
- "Camera model %s does not support ONVIF push, using ONVIF long polling instead",
- self._api.model,
- )
- try:
- await self._async_start_long_polling(initial=True)
- except NotSupportedError:
- _LOGGER.debug(
- "Camera model %s does not support ONVIF long polling, using fast polling instead",
- self._api.model,
- )
- self._onvif_long_poll_supported = False
- await self._api.unsubscribe()
- await self._async_poll_all_motion()
- else:
- self._cancel_long_poll_check = async_call_later(
- self._hass,
- FIRST_ONVIF_LONG_POLL_TIMEOUT,
- self._async_check_onvif_long_poll,
- )
-
- self._cancel_tcp_push_check = None
-
async def _async_check_onvif(self, *_) -> None:
"""Check the ONVIF subscription."""
if self._webhook_reachable:
@@ -419,16 +391,6 @@ class ReolinkHost:
async def disconnect(self) -> None:
"""Disconnect from the API, so the connection will be released."""
- try:
- await self._api.baichuan.unsubscribe_events()
- except ReolinkError as err:
- _LOGGER.error(
- "Reolink error while unsubscribing Baichuan from host %s:%s: %s",
- self._api.host,
- self._api.port,
- err,
- )
-
try:
await self._api.unsubscribe()
except ReolinkError as err:
@@ -499,9 +461,6 @@ class ReolinkHost:
if self._cancel_poll is not None:
self._cancel_poll()
self._cancel_poll = None
- if self._cancel_tcp_push_check is not None:
- self._cancel_tcp_push_check()
- self._cancel_tcp_push_check = None
if self._cancel_onvif_check is not None:
self._cancel_onvif_check()
self._cancel_onvif_check = None
@@ -535,13 +494,8 @@ class ReolinkHost:
async def renew(self) -> None:
"""Renew the subscription of motion events (lease time is 15 minutes)."""
- if self._api.baichuan.events_active and self._api.subscribed(SubType.push):
- # TCP push active, unsubscribe from ONVIF push because not needed
- self.unregister_webhook()
- await self._api.unsubscribe()
-
try:
- if self._onvif_push_supported and not self._api.baichuan.events_active:
+ if self._onvif_push_supported:
await self._renew(SubType.push)
if self._onvif_long_poll_supported and self._long_poll_task is not None:
@@ -654,8 +608,7 @@ class ReolinkHost:
"""Use ONVIF long polling to immediately receive events."""
# This task will be cancelled once _async_stop_long_polling is called
while True:
- if self._api.baichuan.events_active or self._webhook_reachable:
- # TCP push or ONVIF push working, stop long polling
+ if self._webhook_reachable:
self._long_poll_task = None
await self._async_stop_long_polling()
return
@@ -689,12 +642,8 @@ class ReolinkHost:
async def _async_poll_all_motion(self, *_) -> None:
"""Poll motion and AI states until the first ONVIF push is received."""
- if (
- self._api.baichuan.events_active
- or self._webhook_reachable
- or self._long_poll_received
- ):
- # TCP push, ONVIF push or long polling is working, stop fast polling
+ if self._webhook_reachable or self._long_poll_received:
+ # ONVIF push or long polling is working, stop fast polling
self._cancel_poll = None
return
@@ -798,8 +747,6 @@ class ReolinkHost:
@property
def event_connection(self) -> str:
"""Type of connection to receive events."""
- if self._api.baichuan.events_active:
- return "TCP push"
if self._webhook_reachable:
return "ONVIF push"
if self._long_poll_received:
diff --git a/homeassistant/components/reolink/icons.json b/homeassistant/components/reolink/icons.json
index d333a8a0201..5815e165607 100644
--- a/homeassistant/components/reolink/icons.json
+++ b/homeassistant/components/reolink/icons.json
@@ -246,12 +246,6 @@
"off": "mdi:music-note-off"
}
},
- "vehicle_tone": {
- "default": "mdi:music-note",
- "state": {
- "off": "mdi:music-note-off"
- }
- },
"visitor_tone": {
"default": "mdi:music-note",
"state": {
@@ -267,10 +261,7 @@
},
"sensor": {
"ptz_pan_position": {
- "default": "mdi:pan-horizontal"
- },
- "ptz_tilt_position": {
- "default": "mdi:pan-vertical"
+ "default": "mdi:pan"
},
"battery_temperature": {
"default": "mdi:thermometer"
diff --git a/homeassistant/components/reolink/light.py b/homeassistant/components/reolink/light.py
index 0f239a30813..d545a878068 100644
--- a/homeassistant/components/reolink/light.py
+++ b/homeassistant/components/reolink/light.py
@@ -57,7 +57,6 @@ LIGHT_ENTITIES = (
ReolinkLightEntityDescription(
key="floodlight",
cmd_key="GetWhiteLed",
- cmd_id=291,
translation_key="floodlight",
supported=lambda api, ch: api.supported(ch, "floodLight"),
is_on_fn=lambda api, ch: api.whiteled_state(ch),
diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json
index 7921bdb6ed5..9e05cf7431e 100644
--- a/homeassistant/components/reolink/manifest.json
+++ b/homeassistant/components/reolink/manifest.json
@@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push",
"loggers": ["reolink_aio"],
- "requirements": ["reolink-aio==0.11.1"]
+ "requirements": ["reolink-aio==0.9.11"]
}
diff --git a/homeassistant/components/reolink/select.py b/homeassistant/components/reolink/select.py
index a444997a907..b4175d41069 100644
--- a/homeassistant/components/reolink/select.py
+++ b/homeassistant/components/reolink/select.py
@@ -197,16 +197,6 @@ CHIME_SELECT_ENTITIES = (
value=lambda chime: ChimeToneEnum(chime.tone("people")).name,
method=lambda chime, name: chime.set_tone("people", ChimeToneEnum[name].value),
),
- ReolinkChimeSelectEntityDescription(
- key="vehicle_tone",
- cmd_key="GetDingDongCfg",
- translation_key="vehicle_tone",
- entity_category=EntityCategory.CONFIG,
- get_options=[method.name for method in ChimeToneEnum],
- supported=lambda chime: "vehicle" in chime.chime_event_types,
- value=lambda chime: ChimeToneEnum(chime.tone("vehicle")).name,
- method=lambda chime, name: chime.set_tone("vehicle", ChimeToneEnum[name].value),
- ),
ReolinkChimeSelectEntityDescription(
key="visitor_tone",
cmd_key="GetDingDongCfg",
@@ -282,7 +272,7 @@ class ReolinkSelectEntity(ReolinkChannelCoordinatorEntity, SelectEntity):
try:
option = self.entity_description.value(self._host.api, self._channel)
- except (ValueError, KeyError):
+ except ValueError:
if self._log_error:
_LOGGER.exception("Reolink '%s' has an unknown value", self.name)
self._log_error = False
@@ -324,7 +314,7 @@ class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity):
"""Return the current option."""
try:
option = self.entity_description.value(self._chime)
- except (ValueError, KeyError):
+ except ValueError:
if self._log_error:
_LOGGER.exception("Reolink '%s' has an unknown value", self.name)
self._log_error = False
diff --git a/homeassistant/components/reolink/sensor.py b/homeassistant/components/reolink/sensor.py
index 80e58c3d5c2..c2fc815235e 100644
--- a/homeassistant/components/reolink/sensor.py
+++ b/homeassistant/components/reolink/sensor.py
@@ -58,16 +58,7 @@ SENSORS = (
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
value=lambda api, ch: api.ptz_pan_position(ch),
- supported=lambda api, ch: api.supported(ch, "ptz_pan_position"),
- ),
- ReolinkSensorEntityDescription(
- key="ptz_tilt_position",
- cmd_key="GetPtzCurPos",
- translation_key="ptz_tilt_position",
- state_class=SensorStateClass.MEASUREMENT,
- entity_category=EntityCategory.DIAGNOSTIC,
- value=lambda api, ch: api.ptz_tilt_position(ch),
- supported=lambda api, ch: api.supported(ch, "ptz_tilt_position"),
+ supported=lambda api, ch: api.supported(ch, "ptz_position"),
),
ReolinkSensorEntityDescription(
key="battery_percent",
diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json
index 1d699b7b658..4ec4dcffdfd 100644
--- a/homeassistant/components/reolink/strings.json
+++ b/homeassistant/components/reolink/strings.json
@@ -31,7 +31,6 @@
"not_admin": "User needs to be admin, user \"{username}\" has authorisation level \"{userlevel}\"",
"password_incompatible": "Password contains incompatible special character, only these characters are allowed: a-z, A-Z, 0-9 or {special_chars}",
"unknown": "[%key:common::config_flow::error::unknown%]",
- "update_needed": "Failed to login because of outdated firmware, please update the firmware to version {needed_firmware} using the Reolink Download Center: {download_center_url}, currently version {current_firmware} is installed",
"webhook_exception": "Home Assistant URL is not available, go to Settings > System > Network > Home Assistant URL and correct the URLs, see {more_info}"
},
"abort": {
@@ -606,22 +605,6 @@
"waybackhome": "[%key:component::reolink::entity::select::motion_tone::state::waybackhome%]"
}
},
- "vehicle_tone": {
- "name": "Vehicle ringtone",
- "state": {
- "off": "[%key:common::state::off%]",
- "citybird": "[%key:component::reolink::entity::select::motion_tone::state::citybird%]",
- "originaltune": "[%key:component::reolink::entity::select::motion_tone::state::originaltune%]",
- "pianokey": "[%key:component::reolink::entity::select::motion_tone::state::pianokey%]",
- "loop": "[%key:component::reolink::entity::select::motion_tone::state::loop%]",
- "attraction": "[%key:component::reolink::entity::select::motion_tone::state::attraction%]",
- "hophop": "[%key:component::reolink::entity::select::motion_tone::state::hophop%]",
- "goodday": "[%key:component::reolink::entity::select::motion_tone::state::goodday%]",
- "operetta": "[%key:component::reolink::entity::select::motion_tone::state::operetta%]",
- "moonlight": "[%key:component::reolink::entity::select::motion_tone::state::moonlight%]",
- "waybackhome": "[%key:component::reolink::entity::select::motion_tone::state::waybackhome%]"
- }
- },
"visitor_tone": {
"name": "Visitor ringtone",
"state": {
@@ -665,9 +648,6 @@
"ptz_pan_position": {
"name": "PTZ pan position"
},
- "ptz_tilt_position": {
- "name": "PTZ tilt position"
- },
"battery_temperature": {
"name": "Battery temperature"
},
diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py
index 5695e51933e..59239ad6744 100644
--- a/homeassistant/components/rest/__init__.py
+++ b/homeassistant/components/rest/__init__.py
@@ -180,7 +180,6 @@ def _rest_coordinator(
return DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=None,
name="rest data",
update_method=update_method,
update_interval=update_interval,
diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py
index 89632ac50b3..68b7847423c 100644
--- a/homeassistant/components/rflink/sensor.py
+++ b/homeassistant/components/rflink/sensor.py
@@ -71,8 +71,6 @@ SENSOR_TYPES = (
native_unit_of_measurement=UnitOfPressure.HPA,
),
SensorEntityDescription(
- # Rflink devices reports ok/low so device class can’t be used
- # It should be migrated to a binary sensor
key="battery",
name="Battery",
icon="mdi:battery",
diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py
index 866d9ecb1bb..ceb9bea4661 100644
--- a/homeassistant/components/rfxtrx/config_flow.py
+++ b/homeassistant/components/rfxtrx/config_flow.py
@@ -87,8 +87,9 @@ class RfxtrxOptionsFlow(OptionsFlow):
_device_registry: dr.DeviceRegistry
_device_entries: list[dr.DeviceEntry]
- def __init__(self) -> None:
+ def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize rfxtrx options flow."""
+ self._config_entry = config_entry
self._global_options: dict[str, Any] = {}
self._selected_device: dict[str, Any] = {}
self._selected_device_entry_id: str | None = None
@@ -119,7 +120,9 @@ class RfxtrxOptionsFlow(OptionsFlow):
event_code = device_data["event_code"]
assert event_code
self._selected_device_event_code = event_code
- self._selected_device = self.config_entry.data[CONF_DEVICES][event_code]
+ self._selected_device = self._config_entry.data[CONF_DEVICES][
+ event_code
+ ]
self._selected_device_object = get_rfx_object(event_code)
return await self.async_step_set_device_options()
if CONF_EVENT_CODE in user_input:
@@ -145,7 +148,7 @@ class RfxtrxOptionsFlow(OptionsFlow):
device_registry = dr.async_get(self.hass)
device_entries = dr.async_entries_for_config_entry(
- device_registry, self.config_entry.entry_id
+ device_registry, self._config_entry.entry_id
)
self._device_registry = device_registry
self._device_entries = device_entries
@@ -159,11 +162,11 @@ class RfxtrxOptionsFlow(OptionsFlow):
options = {
vol.Optional(
CONF_AUTOMATIC_ADD,
- default=self.config_entry.data[CONF_AUTOMATIC_ADD],
+ default=self._config_entry.data[CONF_AUTOMATIC_ADD],
): bool,
vol.Optional(
CONF_PROTOCOLS,
- default=self.config_entry.data.get(CONF_PROTOCOLS) or [],
+ default=self._config_entry.data.get(CONF_PROTOCOLS) or [],
): cv.multi_select(RECV_MODES),
vol.Optional(CONF_EVENT_CODE): str,
vol.Optional(CONF_DEVICE): vol.In(configure_devices),
@@ -422,7 +425,7 @@ class RfxtrxOptionsFlow(OptionsFlow):
def _can_add_device(self, new_rfx_obj: rfxtrxmod.RFXtrxEvent) -> bool:
"""Check if device does not already exist."""
new_device_id = get_device_id(new_rfx_obj.device)
- for packet_id, entity_info in self.config_entry.data[CONF_DEVICES].items():
+ for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items():
rfx_obj = get_rfx_object(packet_id)
assert rfx_obj
@@ -465,7 +468,7 @@ class RfxtrxOptionsFlow(OptionsFlow):
assert entry
device_id = get_device_tuple_from_identifiers(entry.identifiers)
assert device_id
- for packet_id, entity_info in self.config_entry.data[CONF_DEVICES].items():
+ for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items():
if tuple(entity_info.get(CONF_DEVICE_ID)) == device_id:
event_code = cast(str, packet_id)
break
@@ -478,8 +481,8 @@ class RfxtrxOptionsFlow(OptionsFlow):
devices: dict[str, Any] | None = None,
) -> None:
"""Update data in ConfigEntry."""
- entry_data = self.config_entry.data.copy()
- entry_data[CONF_DEVICES] = copy.deepcopy(self.config_entry.data[CONF_DEVICES])
+ entry_data = self._config_entry.data.copy()
+ entry_data[CONF_DEVICES] = copy.deepcopy(self._config_entry.data[CONF_DEVICES])
if global_options:
entry_data.update(global_options)
if devices:
@@ -491,9 +494,9 @@ class RfxtrxOptionsFlow(OptionsFlow):
entry_data[CONF_DEVICES].pop(event_code, None)
else:
entry_data[CONF_DEVICES][event_code] = options
- self.hass.config_entries.async_update_entry(self.config_entry, data=entry_data)
+ self.hass.config_entries.async_update_entry(self._config_entry, data=entry_data)
self.hass.async_create_task(
- self.hass.config_entries.async_reload(self.config_entry.entry_id)
+ self.hass.config_entries.async_reload(self._config_entry.entry_id)
)
@@ -634,11 +637,9 @@ class RfxtrxConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> RfxtrxOptionsFlow:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
- return RfxtrxOptionsFlow()
+ return RfxtrxOptionsFlow(config_entry)
def _test_transport(host: str | None, port: int | None, device: str | None) -> bool:
diff --git a/homeassistant/components/rhasspy/config_flow.py b/homeassistant/components/rhasspy/config_flow.py
index ea79f6b8845..114d74d4d05 100644
--- a/homeassistant/components/rhasspy/config_flow.py
+++ b/homeassistant/components/rhasspy/config_flow.py
@@ -20,6 +20,9 @@ class RhasspyConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
if user_input is None:
return self.async_show_form(step_id="user", data_schema=vol.Schema({}))
diff --git a/homeassistant/components/rhasspy/manifest.json b/homeassistant/components/rhasspy/manifest.json
index f3496f7eeab..2675935618c 100644
--- a/homeassistant/components/rhasspy/manifest.json
+++ b/homeassistant/components/rhasspy/manifest.json
@@ -5,6 +5,5 @@
"config_flow": true,
"dependencies": ["intent"],
"documentation": "https://www.home-assistant.io/integrations/rhasspy",
- "iot_class": "local_push",
- "single_config_entry": true
+ "iot_class": "local_push"
}
diff --git a/homeassistant/components/rhasspy/strings.json b/homeassistant/components/rhasspy/strings.json
index 3d574d30117..4d2111ebd8a 100644
--- a/homeassistant/components/rhasspy/strings.json
+++ b/homeassistant/components/rhasspy/strings.json
@@ -4,6 +4,9 @@
"user": {
"description": "Do you want to enable Rhasspy support?"
}
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}
diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py
index b2340b34556..c1042a9546d 100644
--- a/homeassistant/components/ring/__init__.py
+++ b/homeassistant/components/ring/__init__.py
@@ -10,9 +10,13 @@ import uuid
from ring_doorbell import Auth, Ring, RingDevices
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import APPLICATION_NAME, CONF_DEVICE_ID, CONF_TOKEN
+from homeassistant.const import APPLICATION_NAME, CONF_TOKEN
from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers import device_registry as dr, entity_registry as er
+from homeassistant.helpers import (
+ device_registry as dr,
+ entity_registry as er,
+ instance_id,
+)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_LISTEN_CREDENTIALS, DOMAIN, PLATFORMS
@@ -34,12 +38,18 @@ class RingData:
type RingConfigEntry = ConfigEntry[RingData]
-def get_auth_user_agent() -> str:
- """Return user-agent for Auth instantiation.
+async def get_auth_agent_id(hass: HomeAssistant) -> tuple[str, str]:
+ """Return user-agent and hardware id for Auth instantiation.
user_agent will be the display name in the ring.com authorised devices.
+ hardware_id will uniquely describe the authorised HA device.
"""
- return f"{APPLICATION_NAME}/{DOMAIN}-integration"
+ user_agent = f"{APPLICATION_NAME}/{DOMAIN}-integration"
+
+ # Generate a new uuid from the instance_uuid to keep the HA one private
+ instance_uuid = uuid.UUID(hex=await instance_id.async_get(hass))
+ hardware_id = str(uuid.uuid5(instance_uuid, user_agent))
+ return user_agent, hardware_id
async def async_setup_entry(hass: HomeAssistant, entry: RingConfigEntry) -> bool:
@@ -59,13 +69,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: RingConfigEntry) -> bool
data={**entry.data, CONF_LISTEN_CREDENTIALS: token},
)
- user_agent = get_auth_user_agent()
+ user_agent, hardware_id = await get_auth_agent_id(hass)
client_session = async_get_clientsession(hass)
auth = Auth(
user_agent,
entry.data[CONF_TOKEN],
token_updater,
- hardware_id=entry.data[CONF_DEVICE_ID],
+ hardware_id=hardware_id,
http_client_session=client_session,
)
ring = Ring(auth)
@@ -128,25 +138,3 @@ async def _migrate_old_unique_ids(hass: HomeAssistant, entry_id: str) -> None:
return None
await er.async_migrate_entries(hass, entry_id, _async_migrator)
-
-
-async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
- """Migrate old config entry."""
- entry_version = entry.version
- entry_minor_version = entry.minor_version
-
- new_minor_version = 2
- if entry_version == 1 and entry_minor_version == 1:
- _LOGGER.debug(
- "Migrating from version %s.%s", entry_version, entry_minor_version
- )
- hardware_id = str(uuid.uuid4())
- hass.config_entries.async_update_entry(
- entry,
- data={**entry.data, CONF_DEVICE_ID: hardware_id},
- minor_version=new_minor_version,
- )
- _LOGGER.debug(
- "Migration to version %s.%s complete", entry_version, new_minor_version
- )
- return True
diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py
index a1024186349..aa78164eb6d 100644
--- a/homeassistant/components/ring/config_flow.py
+++ b/homeassistant/components/ring/config_flow.py
@@ -3,32 +3,20 @@
from collections.abc import Mapping
import logging
from typing import Any
-import uuid
from ring_doorbell import Auth, AuthenticationError, Requires2FAError
import voluptuous as vol
from homeassistant.components import dhcp
-from homeassistant.config_entries import (
- SOURCE_REAUTH,
- SOURCE_RECONFIGURE,
- ConfigFlow,
- ConfigFlowResult,
-)
-from homeassistant.const import (
- CONF_DEVICE_ID,
- CONF_NAME,
- CONF_PASSWORD,
- CONF_TOKEN,
- CONF_USERNAME,
-)
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
+from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.device_registry as dr
-from . import get_auth_user_agent
-from .const import CONF_2FA, CONF_CONFIG_ENTRY_MINOR_VERSION, DOMAIN
+from . import get_auth_agent_id
+from .const import CONF_2FA, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -37,17 +25,13 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
)
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
-STEP_RECONFIGURE_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
-
UNKNOWN_RING_ACCOUNT = "unknown_ring_account"
-async def validate_input(
- hass: HomeAssistant, hardware_id: str, data: dict[str, str]
-) -> dict[str, Any]:
+async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, Any]:
"""Validate the user input allows us to connect."""
- user_agent = get_auth_user_agent()
+ user_agent, hardware_id = await get_auth_agent_id(hass)
auth = Auth(
user_agent,
http_client_session=async_get_clientsession(hass),
@@ -72,10 +56,9 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Ring."""
VERSION = 1
- MINOR_VERSION = CONF_CONFIG_ENTRY_MINOR_VERSION
user_pass: dict[str, Any] = {}
- hardware_id: str | None = None
+ reauth_entry: ConfigEntry | None = None
async def async_step_dhcp(
self, discovery_info: dhcp.DhcpServiceInfo
@@ -104,10 +87,8 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_USERNAME])
self._abort_if_unique_id_configured()
- if not self.hardware_id:
- self.hardware_id = str(uuid.uuid4())
try:
- token = await validate_input(self.hass, self.hardware_id, user_input)
+ token = await validate_input(self.hass, user_input)
except Require2FA:
self.user_pass = user_input
@@ -120,11 +101,7 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
else:
return self.async_create_entry(
title=user_input[CONF_USERNAME],
- data={
- CONF_DEVICE_ID: self.hardware_id,
- CONF_USERNAME: user_input[CONF_USERNAME],
- CONF_TOKEN: token,
- },
+ data={CONF_USERNAME: user_input[CONF_USERNAME], CONF_TOKEN: token},
)
return self.async_show_form(
@@ -136,16 +113,11 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle 2fa step."""
if user_input:
- if self.source == SOURCE_REAUTH:
+ if self.reauth_entry:
return await self.async_step_reauth_confirm(
{**self.user_pass, **user_input}
)
- if self.source == SOURCE_RECONFIGURE:
- return await self.async_step_reconfigure(
- {**self.user_pass, **user_input}
- )
-
return await self.async_step_user({**self.user_pass, **user_input})
return self.async_show_form(
@@ -157,6 +129,9 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -164,17 +139,12 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
+ assert self.reauth_entry is not None
- reauth_entry = self._get_reauth_entry()
if user_input:
- user_input[CONF_USERNAME] = reauth_entry.data[CONF_USERNAME]
- # Reauth will use the same hardware id and re-authorise an existing
- # authorised device.
- if not self.hardware_id:
- self.hardware_id = reauth_entry.data[CONF_DEVICE_ID]
- assert self.hardware_id
+ user_input[CONF_USERNAME] = self.reauth_entry.data[CONF_USERNAME]
try:
- token = await validate_input(self.hass, self.hardware_id, user_input)
+ token = await validate_input(self.hass, user_input)
except Require2FA:
self.user_pass = user_input
return await self.async_step_2fa()
@@ -187,59 +157,19 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
data = {
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_TOKEN: token,
- CONF_DEVICE_ID: self.hardware_id,
}
- return self.async_update_reload_and_abort(reauth_entry, data=data)
+ self.hass.config_entries.async_update_entry(
+ self.reauth_entry, data=data
+ )
+ await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_REAUTH_DATA_SCHEMA,
errors=errors,
description_placeholders={
- CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
- CONF_NAME: reauth_entry.data[CONF_USERNAME],
- },
- )
-
- async def async_step_reconfigure(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Trigger a reconfiguration flow."""
- errors: dict[str, str] = {}
- reconfigure_entry = self._get_reconfigure_entry()
- username = reconfigure_entry.data[CONF_USERNAME]
- await self.async_set_unique_id(username)
- if user_input:
- user_input[CONF_USERNAME] = username
- # Reconfigure will generate a new hardware id and create a new
- # authorised device at ring.com.
- if not self.hardware_id:
- self.hardware_id = str(uuid.uuid4())
- try:
- assert self.hardware_id
- token = await validate_input(self.hass, self.hardware_id, user_input)
- except Require2FA:
- self.user_pass = user_input
- return await self.async_step_2fa()
- except InvalidAuth:
- errors["base"] = "invalid_auth"
- except Exception:
- _LOGGER.exception("Unexpected exception")
- errors["base"] = "unknown"
- else:
- data = {
- CONF_USERNAME: username,
- CONF_TOKEN: token,
- CONF_DEVICE_ID: self.hardware_id,
- }
- return self.async_update_reload_and_abort(reconfigure_entry, data=data)
-
- return self.async_show_form(
- step_id="reconfigure",
- data_schema=STEP_RECONFIGURE_DATA_SCHEMA,
- errors=errors,
- description_placeholders={
- CONF_USERNAME: username,
+ CONF_USERNAME: self.reauth_entry.data[CONF_USERNAME]
},
)
diff --git a/homeassistant/components/ring/const.py b/homeassistant/components/ring/const.py
index 9595241ebb1..24801045b17 100644
--- a/homeassistant/components/ring/const.py
+++ b/homeassistant/components/ring/const.py
@@ -3,7 +3,6 @@
from __future__ import annotations
from datetime import timedelta
-from typing import Final
from homeassistant.const import Platform
@@ -32,5 +31,3 @@ SCAN_INTERVAL = timedelta(minutes=1)
CONF_2FA = "2fa"
CONF_LISTEN_CREDENTIALS = "listen_token"
-
-CONF_CONFIG_ENTRY_MINOR_VERSION: Final = 2
diff --git a/homeassistant/components/ring/event.py b/homeassistant/components/ring/event.py
index 71a4bc8aea5..e6d9d25542f 100644
--- a/homeassistant/components/ring/event.py
+++ b/homeassistant/components/ring/event.py
@@ -96,7 +96,7 @@ class RingEvent(RingBaseEntity[RingListenCoordinator, RingDeviceT], EventEntity)
@callback
def _handle_coordinator_update(self) -> None:
- if (alert := self._get_coordinator_alert()) and not alert.is_update:
+ if alert := self._get_coordinator_alert():
self._async_handle_event(alert.kind)
super()._handle_coordinator_update()
diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json
index e431c680081..7eff30c18cb 100644
--- a/homeassistant/components/ring/manifest.json
+++ b/homeassistant/components/ring/manifest.json
@@ -30,5 +30,5 @@
"iot_class": "cloud_polling",
"loggers": ["ring_doorbell"],
"quality_scale": "silver",
- "requirements": ["ring-doorbell==0.9.12"]
+ "requirements": ["ring-doorbell==0.9.7"]
}
diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json
index 0887e4112c6..5d282fae1b2 100644
--- a/homeassistant/components/ring/strings.json
+++ b/homeassistant/components/ring/strings.json
@@ -20,13 +20,6 @@
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
- },
- "reconfigure": {
- "title": "Reconfigure Ring Integration",
- "description": "Will create a new Authorized Device for {username} at ring.com",
- "data": {
- "password": "[%key:common::config_flow::data::password%]"
- }
}
},
"error": {
@@ -35,8 +28,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"entity": {
diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py
index b1eae8fd917..08dee936d37 100644
--- a/homeassistant/components/risco/alarm_control_panel.py
+++ b/homeassistant/components/risco/alarm_control_panel.py
@@ -12,11 +12,19 @@ from pyrisco.local.partition import Partition as LocalPartition
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_PIN
+from homeassistant.const import (
+ CONF_PIN,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -40,10 +48,10 @@ from .entity import RiscoCloudEntity
_LOGGER = logging.getLogger(__name__)
STATES_TO_SUPPORTED_FEATURES = {
- AlarmControlPanelState.ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY,
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
- AlarmControlPanelState.ARMED_HOME: AlarmControlPanelEntityFeature.ARM_HOME,
- AlarmControlPanelState.ARMED_NIGHT: AlarmControlPanelEntityFeature.ARM_NIGHT,
+ STATE_ALARM_ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME: AlarmControlPanelEntityFeature.ARM_HOME,
+ STATE_ALARM_ARMED_NIGHT: AlarmControlPanelEntityFeature.ARM_NIGHT,
}
@@ -108,14 +116,14 @@ class RiscoAlarm(AlarmControlPanelEntity):
self._attr_supported_features |= STATES_TO_SUPPORTED_FEATURES[state]
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the device."""
if self._partition.triggered:
- return AlarmControlPanelState.TRIGGERED
+ return STATE_ALARM_TRIGGERED
if self._partition.arming:
- return AlarmControlPanelState.ARMING
+ return STATE_ALARM_ARMING
if self._partition.disarmed:
- return AlarmControlPanelState.DISARMED
+ return STATE_ALARM_DISARMED
if self._partition.armed:
return self._risco_to_ha[RISCO_ARM]
if self._partition.partially_armed:
@@ -140,21 +148,21 @@ class RiscoAlarm(AlarmControlPanelEntity):
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
- await self._arm(AlarmControlPanelState.ARMED_HOME, code)
+ await self._arm(STATE_ALARM_ARMED_HOME, code)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
- await self._arm(AlarmControlPanelState.ARMED_AWAY, code)
+ await self._arm(STATE_ALARM_ARMED_AWAY, code)
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
- await self._arm(AlarmControlPanelState.ARMED_NIGHT, code)
+ await self._arm(STATE_ALARM_ARMED_NIGHT, code)
async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
"""Send arm custom bypass command."""
- await self._arm(AlarmControlPanelState.ARMED_CUSTOM_BYPASS, code)
+ await self._arm(STATE_ALARM_ARMED_CUSTOM_BYPASS, code)
- async def _arm(self, mode: AlarmControlPanelState, code: str | None) -> None:
+ async def _arm(self, mode: str, code: str | None) -> None:
if self.code_arm_required and not self._validate_code(code):
_LOGGER.warning("Wrong code entered for %s", mode)
return
diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py
index f7365d35414..735880df09b 100644
--- a/homeassistant/components/risco/config_flow.py
+++ b/homeassistant/components/risco/config_flow.py
@@ -9,7 +9,6 @@ from typing import Any
from pyrisco import CannotConnectError, RiscoCloud, RiscoLocal, UnauthorizedError
import voluptuous as vol
-from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
@@ -24,6 +23,10 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL,
CONF_TYPE,
CONF_USERNAME,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -61,10 +64,10 @@ LOCAL_SCHEMA = vol.Schema(
}
)
HA_STATES = [
- AlarmControlPanelState.ARMED_AWAY.value,
- AlarmControlPanelState.ARMED_HOME.value,
- AlarmControlPanelState.ARMED_NIGHT.value,
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS.value,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
]
@@ -220,6 +223,7 @@ class RiscoOptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize."""
+ self.config_entry = config_entry
self._data = {**DEFAULT_OPTIONS, **config_entry.options}
def _options_schema(self) -> vol.Schema:
diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py
index 078e26c43b5..f1240a704de 100644
--- a/homeassistant/components/risco/const.py
+++ b/homeassistant/components/risco/const.py
@@ -1,7 +1,10 @@
"""Constants for the Risco integration."""
-from homeassistant.components.alarm_control_panel import AlarmControlPanelState
-from homeassistant.const import CONF_SCAN_INTERVAL
+from homeassistant.const import (
+ CONF_SCAN_INTERVAL,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+)
DOMAIN = "risco"
@@ -30,18 +33,16 @@ RISCO_ARM = "arm"
RISCO_PARTIAL_ARM = "partial_arm"
RISCO_STATES = [RISCO_ARM, RISCO_PARTIAL_ARM, *RISCO_GROUPS]
-DEFAULT_RISCO_GROUPS_TO_HA = {
- group: AlarmControlPanelState.ARMED_HOME for group in RISCO_GROUPS
-}
+DEFAULT_RISCO_GROUPS_TO_HA = {group: STATE_ALARM_ARMED_HOME for group in RISCO_GROUPS}
DEFAULT_RISCO_STATES_TO_HA = {
- RISCO_ARM: AlarmControlPanelState.ARMED_AWAY,
- RISCO_PARTIAL_ARM: AlarmControlPanelState.ARMED_HOME,
+ RISCO_ARM: STATE_ALARM_ARMED_AWAY,
+ RISCO_PARTIAL_ARM: STATE_ALARM_ARMED_HOME,
**DEFAULT_RISCO_GROUPS_TO_HA,
}
DEFAULT_HA_STATES_TO_RISCO = {
- AlarmControlPanelState.ARMED_AWAY: RISCO_ARM,
- AlarmControlPanelState.ARMED_HOME: RISCO_PARTIAL_ARM,
+ STATE_ALARM_ARMED_AWAY: RISCO_ARM,
+ STATE_ALARM_ARMED_HOME: RISCO_PARTIAL_ARM,
}
DEFAULT_OPTIONS = {
diff --git a/homeassistant/components/risco/strings.json b/homeassistant/components/risco/strings.json
index 86d131b4f80..e35b13394cb 100644
--- a/homeassistant/components/risco/strings.json
+++ b/homeassistant/components/risco/strings.json
@@ -28,8 +28,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py
index d1cbccc6b05..bb42c0bd080 100644
--- a/homeassistant/components/roborock/__init__.py
+++ b/homeassistant/components/roborock/__init__.py
@@ -169,7 +169,7 @@ async def setup_device_v1(
) -> RoborockDataUpdateCoordinator | None:
"""Set up a device Coordinator."""
mqtt_client = await hass.async_add_executor_job(
- RoborockMqttClientV1, user_data, DeviceData(device, product_info.model)
+ RoborockMqttClientV1, user_data, DeviceData(device, product_info.name)
)
try:
networking = await mqtt_client.get_networking()
diff --git a/homeassistant/components/roborock/config_flow.py b/homeassistant/components/roborock/config_flow.py
index 200614b024e..c6dee7ce4ed 100644
--- a/homeassistant/components/roborock/config_flow.py
+++ b/homeassistant/components/roborock/config_flow.py
@@ -3,7 +3,6 @@
from __future__ import annotations
from collections.abc import Mapping
-from copy import deepcopy
import logging
from typing import Any
@@ -20,11 +19,11 @@ from roborock.web_api import RoborockApiClient
import voluptuous as vol
from homeassistant.config_entries import (
- SOURCE_REAUTH,
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_USERNAME
from homeassistant.core import callback
@@ -45,6 +44,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Roborock."""
VERSION = 1
+ reauth_entry: ConfigEntry | None = None
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -116,12 +116,11 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
- if self.source == SOURCE_REAUTH:
- reauth_entry = self._get_reauth_entry()
+ if self.reauth_entry is not None:
self.hass.config_entries.async_update_entry(
- reauth_entry,
+ self.reauth_entry,
data={
- **reauth_entry.data,
+ **self.reauth_entry.data,
CONF_USER_DATA: login_data.as_dict(),
},
)
@@ -141,6 +140,9 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
self._username = entry_data[CONF_USERNAME]
assert self._username
self._client = RoborockApiClient(self._username)
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -171,18 +173,14 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
- ) -> RoborockOptionsFlowHandler:
+ ) -> OptionsFlow:
"""Create the options flow."""
return RoborockOptionsFlowHandler(config_entry)
-class RoborockOptionsFlowHandler(OptionsFlow):
+class RoborockOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle an option flow for Roborock."""
- def __init__(self, config_entry: ConfigEntry) -> None:
- """Initialize options flow."""
- self.options = deepcopy(dict(config_entry.options))
-
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py
index fe592074f71..20bc50f9855 100644
--- a/homeassistant/components/roborock/coordinator.py
+++ b/homeassistant/components/roborock/coordinator.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+import asyncio
from datetime import timedelta
import logging
@@ -106,12 +107,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
async def _async_update_data(self) -> DeviceProp:
"""Update data via library."""
try:
- # Update device props and standard api information
- await self._update_device_prop()
- # Set the new map id from the updated device props
+ await asyncio.gather(*(self._update_device_prop(), self.get_rooms()))
self._set_current_map()
- # Get the rooms for that map id.
- await self.get_rooms()
except RoborockException as ex:
raise UpdateFailed(ex) from ex
return self.roborock_device_info.props
diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json
index c305e4710fc..3bb3b9b2046 100644
--- a/homeassistant/components/roborock/manifest.json
+++ b/homeassistant/components/roborock/manifest.json
@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": [
- "python-roborock==2.7.2",
+ "python-roborock==2.6.0",
"vacuum-map-parser-roborock==0.1.2"
]
}
diff --git a/homeassistant/components/roborock/select.py b/homeassistant/components/roborock/select.py
index 73cb95d2d7c..3dfe0e72a7b 100644
--- a/homeassistant/components/roborock/select.py
+++ b/homeassistant/components/roborock/select.py
@@ -135,9 +135,6 @@ class RoborockCurrentMapSelectEntity(RoborockCoordinatedEntityV1, SelectEntity):
RoborockCommand.LOAD_MULTI_MAP,
[map_id],
)
- # Update the current map id manually so that nothing gets broken
- # if another service hits the api.
- self.coordinator.current_map = map_id
# We need to wait after updating the map
# so that other commands will be executed correctly.
await asyncio.sleep(MAP_SLEEP)
@@ -151,9 +148,6 @@ class RoborockCurrentMapSelectEntity(RoborockCoordinatedEntityV1, SelectEntity):
@property
def current_option(self) -> str | None:
"""Get the current status of the select entity from device_status."""
- if (
- (current_map := self.coordinator.current_map) is not None
- and current_map in self.coordinator.maps
- ): # 63 means it is searching for a map.
+ if (current_map := self.coordinator.current_map) is not None:
return self.coordinator.maps[current_map].name
return None
diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py
index b318a91e4c7..7515f375054 100644
--- a/homeassistant/components/roku/__init__.py
+++ b/homeassistant/components/roku/__init__.py
@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
-from .const import CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID, DOMAIN
+from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
PLATFORMS = [
@@ -24,12 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device_id = entry.entry_id
coordinator = RokuDataUpdateCoordinator(
- hass,
- host=entry.data[CONF_HOST],
- device_id=device_id,
- play_media_app_id=entry.options.get(
- CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID
- ),
+ hass, host=entry.data[CONF_HOST], device_id=device_id
)
await coordinator.async_config_entry_first_refresh()
@@ -37,8 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
- entry.async_on_unload(entry.add_update_listener(async_reload_entry))
-
return True
@@ -47,8 +40,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
-
-
-async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
- """Reload the config entry when it changed."""
- await hass.config_entries.async_reload(entry.entry_id)
diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py
index 18e3b3ed68a..7757cc53e1c 100644
--- a/homeassistant/components/roku/config_flow.py
+++ b/homeassistant/components/roku/config_flow.py
@@ -10,17 +10,12 @@ from rokuecp import Roku, RokuError
import voluptuous as vol
from homeassistant.components import ssdp, zeroconf
-from homeassistant.config_entries import (
- ConfigEntry,
- ConfigFlow,
- ConfigFlowResult,
- OptionsFlow,
-)
+from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID, DOMAIN
+from .const import DOMAIN
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
@@ -160,36 +155,3 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
title=self.discovery_info[CONF_NAME],
data=self.discovery_info,
)
-
- @staticmethod
- @callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> RokuOptionsFlowHandler:
- """Create the options flow."""
- return RokuOptionsFlowHandler()
-
-
-class RokuOptionsFlowHandler(OptionsFlow):
- """Handle Roku options."""
-
- async def async_step_init(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Manage Roku options."""
- if user_input is not None:
- return self.async_create_entry(title="", data=user_input)
-
- return self.async_show_form(
- step_id="init",
- data_schema=vol.Schema(
- {
- vol.Optional(
- CONF_PLAY_MEDIA_APP_ID,
- default=self.config_entry.options.get(
- CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID
- ),
- ): str,
- }
- ),
- )
diff --git a/homeassistant/components/roku/const.py b/homeassistant/components/roku/const.py
index f0c7d4e2537..ab633a4044c 100644
--- a/homeassistant/components/roku/const.py
+++ b/homeassistant/components/roku/const.py
@@ -15,9 +15,3 @@ DEFAULT_PORT = 8060
# Services
SERVICE_SEARCH = "search"
-
-# Config
-CONF_PLAY_MEDIA_APP_ID = "play_media_app_id"
-
-# Defaults
-DEFAULT_PLAY_MEDIA_APP_ID = "15985"
diff --git a/homeassistant/components/roku/coordinator.py b/homeassistant/components/roku/coordinator.py
index 7900669d02f..303d0e91a36 100644
--- a/homeassistant/components/roku/coordinator.py
+++ b/homeassistant/components/roku/coordinator.py
@@ -29,12 +29,15 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]):
roku: Roku
def __init__(
- self, hass: HomeAssistant, *, host: str, device_id: str, play_media_app_id: str
+ self,
+ hass: HomeAssistant,
+ *,
+ host: str,
+ device_id: str,
) -> None:
"""Initialize global Roku data updater."""
self.device_id = device_id
self.roku = Roku(host=host, session=async_get_clientsession(hass))
- self.play_media_app_id = play_media_app_id
self.full_update_interval = timedelta(minutes=15)
self.last_full_update = None
diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py
index 35f01553cdd..5b15253068e 100644
--- a/homeassistant/components/roku/media_player.py
+++ b/homeassistant/components/roku/media_player.py
@@ -445,25 +445,17 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
if attr in extra
}
- params = {"u": media_id, "t": "a", **params}
+ params = {"t": "a", **params}
- await self.coordinator.roku.launch(
- self.coordinator.play_media_app_id,
- params,
- )
+ await self.coordinator.roku.play_on_roku(media_id, params)
elif media_type in {MediaType.URL, MediaType.VIDEO}:
params = {
param: extra[attr]
for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items()
if attr in extra
}
- params["u"] = media_id
- params["t"] = "v"
- await self.coordinator.roku.launch(
- self.coordinator.play_media_app_id,
- params,
- )
+ await self.coordinator.roku.play_on_roku(media_id, params)
else:
_LOGGER.error("Media type %s is not supported", original_media_type)
return
diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json
index 9d657be6d61..9eef366163e 100644
--- a/homeassistant/components/roku/strings.json
+++ b/homeassistant/components/roku/strings.json
@@ -24,18 +24,6 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
- "options": {
- "step": {
- "init": {
- "data": {
- "play_media_app_id": "Play Media Roku Application ID"
- },
- "data_description": {
- "play_media_app_id": "The application ID to use when launching media playback. Must support the PlayOnRoku API."
- }
- }
- }
- },
"entity": {
"binary_sensor": {
"headphones_connected": {
diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py
index e48d2d91139..d690bcce978 100644
--- a/homeassistant/components/roomba/config_flow.py
+++ b/homeassistant/components/roomba/config_flow.py
@@ -16,7 +16,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD
from homeassistant.core import HomeAssistant, callback
@@ -57,7 +57,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
address=data[CONF_HOST],
blid=data[CONF_BLID],
password=data[CONF_PASSWORD],
- continuous=True,
+ continuous=False,
delay=data[CONF_DELAY],
)
)
@@ -92,7 +92,7 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> RoombaOptionsFlowHandler:
"""Get the options flow for this handler."""
- return RoombaOptionsFlowHandler()
+ return RoombaOptionsFlowHandler(config_entry)
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
@@ -300,7 +300,7 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN):
)
-class RoombaOptionsFlowHandler(OptionsFlow):
+class RoombaOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle options."""
async def async_step_init(
@@ -310,18 +310,17 @@ class RoombaOptionsFlowHandler(OptionsFlow):
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
- options = self.config_entry.options
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_CONTINUOUS,
- default=options.get(CONF_CONTINUOUS, DEFAULT_CONTINUOUS),
+ default=self.options.get(CONF_CONTINUOUS, DEFAULT_CONTINUOUS),
): bool,
vol.Optional(
CONF_DELAY,
- default=options.get(CONF_DELAY, DEFAULT_DELAY),
+ default=self.options.get(CONF_DELAY, DEFAULT_DELAY),
): int,
}
),
diff --git a/homeassistant/components/roomba/const.py b/homeassistant/components/roomba/const.py
index 7f1e3b8e1ee..331c0900682 100644
--- a/homeassistant/components/roomba/const.py
+++ b/homeassistant/components/roomba/const.py
@@ -9,5 +9,5 @@ CONF_CONTINUOUS = "continuous"
CONF_BLID = "blid"
DEFAULT_CERT = "/etc/ssl/certs/ca-certificates.crt"
DEFAULT_CONTINUOUS = True
-DEFAULT_DELAY = 30
+DEFAULT_DELAY = 1
ROOMBA_SESSION = "roomba_session"
diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json
index edb317f9752..a697680b379 100644
--- a/homeassistant/components/roomba/manifest.json
+++ b/homeassistant/components/roomba/manifest.json
@@ -1,7 +1,7 @@
{
"domain": "roomba",
"name": "iRobot Roomba and Braava",
- "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn", "@Orhideous"],
+ "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn", "@Xitee1", "@Orhideous"],
"config_flow": true,
"dhcp": [
{
diff --git a/homeassistant/components/rova/strings.json b/homeassistant/components/rova/strings.json
index 3b89fc789ee..864989b90db 100644
--- a/homeassistant/components/rova/strings.json
+++ b/homeassistant/components/rova/strings.json
@@ -12,8 +12,7 @@
},
"error": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
- "invalid_rova_area": "Rova does not collect at this address",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ "invalid_rova_area": "Rova does not collect at this address"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py
index 0151a92856d..c44bb65d79a 100644
--- a/homeassistant/components/rpi_power/config_flow.py
+++ b/homeassistant/components/rpi_power/config_flow.py
@@ -37,6 +37,8 @@ class RPiPowerFlow(DiscoveryFlowHandler[Awaitable[bool]], domain=DOMAIN):
self, data: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by onboarding."""
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
has_devices = await self._discovery_function(self.hass)
if not has_devices:
diff --git a/homeassistant/components/rpi_power/manifest.json b/homeassistant/components/rpi_power/manifest.json
index d5704f61564..7da5897c00d 100644
--- a/homeassistant/components/rpi_power/manifest.json
+++ b/homeassistant/components/rpi_power/manifest.json
@@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/rpi_power",
"iot_class": "local_polling",
"loggers": ["rpi_bad_power"],
- "requirements": ["rpi-bad-power==0.1.0"],
- "single_config_entry": true
+ "requirements": ["rpi-bad-power==0.1.0"]
}
diff --git a/homeassistant/components/rpi_power/strings.json b/homeassistant/components/rpi_power/strings.json
index 796a973335b..9a46ca1e10e 100644
--- a/homeassistant/components/rpi_power/strings.json
+++ b/homeassistant/components/rpi_power/strings.json
@@ -7,6 +7,7 @@
}
},
"abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"no_devices_found": "Can't find the system class needed for this component, make sure that your kernel is recent and the hardware is supported"
}
}
diff --git a/homeassistant/components/rtsp_to_webrtc/__init__.py b/homeassistant/components/rtsp_to_webrtc/__init__.py
index 59b8077e398..948ba8929fc 100644
--- a/homeassistant/components/rtsp_to_webrtc/__init__.py
+++ b/homeassistant/components/rtsp_to_webrtc/__init__.py
@@ -24,11 +24,11 @@ import logging
from rtsp_to_webrtc.client import get_adaptive_client
from rtsp_to_webrtc.exceptions import ClientError, ResponseError
from rtsp_to_webrtc.interface import WebRTCClientInterface
-from webrtc_models import RTCIceServer
from homeassistant.components import camera
+from homeassistant.components.camera.webrtc import RTCIceServer, register_ice_server
from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -59,11 +59,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN][CONF_STUN_SERVER] = entry.options.get(CONF_STUN_SERVER)
if server := entry.options.get(CONF_STUN_SERVER):
- @callback
- def get_servers() -> list[RTCIceServer]:
- return [RTCIceServer(urls=[server])]
+ async def get_server() -> RTCIceServer:
+ return RTCIceServer(urls=[server])
- entry.async_on_unload(camera.async_register_ice_servers(hass, get_servers))
+ entry.async_on_unload(register_ice_server(hass, get_server))
async def async_offer_for_stream_source(
stream_source: str,
diff --git a/homeassistant/components/rtsp_to_webrtc/config_flow.py b/homeassistant/components/rtsp_to_webrtc/config_flow.py
index 22502659757..adab1a456d0 100644
--- a/homeassistant/components/rtsp_to_webrtc/config_flow.py
+++ b/homeassistant/components/rtsp_to_webrtc/config_flow.py
@@ -9,6 +9,7 @@ from urllib.parse import urlparse
import rtsp_to_webrtc
import voluptuous as vol
+from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
@@ -18,7 +19,6 @@ from homeassistant.config_entries import (
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from . import CONF_STUN_SERVER, DATA_SERVER_URL, DOMAIN
@@ -119,12 +119,16 @@ class RTSPToWebRTCConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Create an options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""RTSPtoWeb Options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py
index 0743b19bdaf..fdfacfc73a7 100644
--- a/homeassistant/components/ruckus_unleashed/config_flow.py
+++ b/homeassistant/components/ruckus_unleashed/config_flow.py
@@ -8,7 +8,7 @@ from aioruckus import AjaxSession, SystemStat
from aioruckus.exceptions import AuthenticationError, SchemaError
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -64,6 +64,8 @@ class RuckusConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ _reauth_entry: ConfigEntry | None = None
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -80,24 +82,27 @@ class RuckusConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
- await self.async_set_unique_id(info[KEY_SYS_SERIAL])
- if self.source != SOURCE_REAUTH:
+ if self._reauth_entry is None:
+ await self.async_set_unique_id(info[KEY_SYS_SERIAL])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=info[KEY_SYS_TITLE], data=user_input
)
- reauth_entry = self._get_reauth_entry()
- if info[KEY_SYS_SERIAL] == reauth_entry.unique_id:
- return self.async_update_reload_and_abort(
- reauth_entry, data=user_input
+ if info[KEY_SYS_SERIAL] == self._reauth_entry.unique_id:
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry, data=user_input
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(
+ self._reauth_entry.entry_id
+ )
+ )
+ return self.async_abort(reason="reauth_successful")
errors["base"] = "invalid_host"
- data_schema = DATA_SCHEMA
- if self.source == SOURCE_REAUTH:
- data_schema = self.add_suggested_values_to_schema(
- data_schema, self._get_reauth_entry().data
- )
+ data_schema = self.add_suggested_values_to_schema(
+ DATA_SCHEMA, self._reauth_entry.data if self._reauth_entry else {}
+ )
return self.async_show_form(
step_id="user", data_schema=data_schema, errors=errors
)
@@ -106,6 +111,9 @@ class RuckusConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_user()
diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json
index 8d56f3a5563..2066b65221e 100644
--- a/homeassistant/components/ruckus_unleashed/manifest.json
+++ b/homeassistant/components/ruckus_unleashed/manifest.json
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioruckus"],
- "requirements": ["aioruckus==0.42"]
+ "requirements": ["aioruckus==0.41"]
}
diff --git a/homeassistant/components/russound_rio/__init__.py b/homeassistant/components/russound_rio/__init__.py
index 784629ea0bc..ba53f6794e3 100644
--- a/homeassistant/components/russound_rio/__init__.py
+++ b/homeassistant/components/russound_rio/__init__.py
@@ -11,7 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
-from .const import CONNECT_TIMEOUT, DOMAIN, RUSSOUND_RIO_EXCEPTIONS
+from .const import CONNECT_TIMEOUT, RUSSOUND_RIO_EXCEPTIONS
PLATFORMS = [Platform.MEDIA_PLAYER]
@@ -43,14 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) ->
async with asyncio.timeout(CONNECT_TIMEOUT):
await client.connect()
except RUSSOUND_RIO_EXCEPTIONS as err:
- raise ConfigEntryNotReady(
- translation_domain=DOMAIN,
- translation_key="entry_cannot_connect",
- translation_placeholders={
- "host": host,
- "port": port,
- },
- ) from err
+ raise ConfigEntryNotReady(f"Error while connecting to {host}:{port}") from err
entry.runtime_data = client
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
diff --git a/homeassistant/components/russound_rio/const.py b/homeassistant/components/russound_rio/const.py
index af52e89d399..1b38dc8ce5c 100644
--- a/homeassistant/components/russound_rio/const.py
+++ b/homeassistant/components/russound_rio/const.py
@@ -17,7 +17,7 @@ RUSSOUND_RIO_EXCEPTIONS = (
)
-CONNECT_TIMEOUT = 15
+CONNECT_TIMEOUT = 5
MP_FEATURES_BY_FLAG = {
FeatureFlag.COMMANDS_ZONE_MUTE_OFF_ON: MediaPlayerEntityFeature.VOLUME_MUTE
diff --git a/homeassistant/components/russound_rio/entity.py b/homeassistant/components/russound_rio/entity.py
index 9790ff43e68..23b196ecb2f 100644
--- a/homeassistant/components/russound_rio/entity.py
+++ b/homeassistant/components/russound_rio/entity.py
@@ -26,12 +26,7 @@ def command[_EntityT: RussoundBaseEntity, **_P](
await func(self, *args, **kwargs)
except RUSSOUND_RIO_EXCEPTIONS as exc:
raise HomeAssistantError(
- translation_domain=DOMAIN,
- translation_key="command_error",
- translation_placeholders={
- "function_name": func.__name__,
- "entity_id": self.entity_id,
- },
+ f"Error executing {func.__name__} on entity {self.entity_id},"
) from exc
return decorator
@@ -96,4 +91,6 @@ class RussoundBaseEntity(Entity):
async def async_will_remove_from_hass(self) -> None:
"""Remove callbacks."""
- self._client.unregister_state_update_callbacks(self._state_update_callback)
+ await self._client.unregister_state_update_callbacks(
+ self._state_update_callback
+ )
diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json
index ab77ca3ab6a..96fc0fb53db 100644
--- a/homeassistant/components/russound_rio/manifest.json
+++ b/homeassistant/components/russound_rio/manifest.json
@@ -7,5 +7,5 @@
"iot_class": "local_push",
"loggers": ["aiorussound"],
"quality_scale": "silver",
- "requirements": ["aiorussound==4.1.0"]
+ "requirements": ["aiorussound==4.0.5"]
}
diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py
index 45818d3e25b..316e4d2be7c 100644
--- a/homeassistant/components/russound_rio/media_player.py
+++ b/homeassistant/components/russound_rio/media_player.py
@@ -5,7 +5,7 @@ from __future__ import annotations
import logging
from aiorussound import Controller
-from aiorussound.models import PlayStatus, Source
+from aiorussound.models import Source
from aiorussound.rio import ZoneControlSurface
from homeassistant.components.media_player import (
@@ -132,18 +132,11 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
def state(self) -> MediaPlayerState | None:
"""Return the state of the device."""
status = self._zone.status
- play_status = self._source.play_status
- if not status:
+ if status == "ON":
+ return MediaPlayerState.ON
+ if status == "OFF":
return MediaPlayerState.OFF
- if play_status == PlayStatus.PLAYING:
- return MediaPlayerState.PLAYING
- if play_status == PlayStatus.PAUSED:
- return MediaPlayerState.PAUSED
- if play_status == PlayStatus.TRANSITIONING:
- return MediaPlayerState.BUFFERING
- if play_status == PlayStatus.STOPPED:
- return MediaPlayerState.IDLE
- return MediaPlayerState.ON
+ return None
@property
def source(self):
@@ -182,7 +175,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
Value is returned based on a range (0..50).
Therefore float divide by 50 to get to the required range.
"""
- return self._zone.volume / 50.0
+ return float(self._zone.volume or "0") / 50.0
@command
async def async_turn_off(self) -> None:
diff --git a/homeassistant/components/russound_rio/strings.json b/homeassistant/components/russound_rio/strings.json
index b8c29c08301..c105dcafae2 100644
--- a/homeassistant/components/russound_rio/strings.json
+++ b/homeassistant/components/russound_rio/strings.json
@@ -33,13 +33,5 @@
"title": "[%key:component::russound_rio::issues::deprecated_yaml_import_issue_cannot_connect::title%]",
"description": "[%key:component::russound_rio::issues::deprecated_yaml_import_issue_cannot_connect::description%]"
}
- },
- "exceptions": {
- "entry_cannot_connect": {
- "message": "Error while connecting to {host}:{port}"
- },
- "command_error": {
- "message": "Error executing {function_name} on entity {entity_id}"
- }
}
}
diff --git a/homeassistant/components/rympro/config_flow.py b/homeassistant/components/rympro/config_flow.py
index 1d5d8a9e79d..be35c48ac5b 100644
--- a/homeassistant/components/rympro/config_flow.py
+++ b/homeassistant/components/rympro/config_flow.py
@@ -9,7 +9,7 @@ from typing import Any
from pyrympro import CannotConnectError, RymPro, UnauthorizedError
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN, CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -46,6 +46,10 @@ class RymproConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ def __init__(self) -> None:
+ """Init the config flow."""
+ self._reauth_entry: ConfigEntry | None = None
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -70,17 +74,19 @@ class RymproConfigFlow(ConfigFlow, domain=DOMAIN):
title = user_input[CONF_EMAIL]
data = {**user_input, **info}
- if self.source != SOURCE_REAUTH:
+ if not self._reauth_entry:
await self.async_set_unique_id(info[CONF_UNIQUE_ID])
self._abort_if_unique_id_configured()
return self.async_create_entry(title=title, data=data)
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry,
title=title,
data=data,
unique_id=info[CONF_UNIQUE_ID],
)
+ await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
@@ -90,4 +96,7 @@ class RymproConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_user()
diff --git a/homeassistant/components/rympro/strings.json b/homeassistant/components/rympro/strings.json
index 2c1e2ad93c9..c58bf5b93ba 100644
--- a/homeassistant/components/rympro/strings.json
+++ b/homeassistant/components/rympro/strings.json
@@ -14,8 +14,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"entity": {
diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py
index 6d4e491b839..b43b8abea65 100644
--- a/homeassistant/components/samsungtv/__init__.py
+++ b/homeassistant/components/samsungtv/__init__.py
@@ -10,7 +10,7 @@ from urllib.parse import urlparse
import getmac
from homeassistant.components import ssdp
-from homeassistant.config_entries import ConfigEntry
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
@@ -36,6 +36,7 @@ from .const import (
CONF_SESSION_ID,
CONF_SSDP_MAIN_TV_AGENT_LOCATION,
CONF_SSDP_RENDERING_CONTROL_LOCATION,
+ DOMAIN,
ENTRY_RELOAD_COOLDOWN,
LEGACY_PORT,
LOGGER,
@@ -134,7 +135,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: SamsungTVConfigEntry) ->
def _access_denied() -> None:
"""Access denied callback."""
LOGGER.debug("Access denied in getting remote object")
- entry.async_start_reauth(hass)
+ hass.create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={
+ "source": SOURCE_REAUTH,
+ "entry_id": entry.entry_id,
+ },
+ data=entry.data,
+ )
+ )
bridge.register_reauth_callback(_access_denied)
diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py
index 837651f9900..9d2ecefd442 100644
--- a/homeassistant/components/samsungtv/config_flow.py
+++ b/homeassistant/components/samsungtv/config_flow.py
@@ -105,6 +105,7 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize flow."""
+ self._reauth_entry: ConfigEntry | None = None
self._host: str = ""
self._mac: str | None = None
self._udn: str | None = None
@@ -528,6 +529,9 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
if entry_data.get(CONF_MODEL) and entry_data.get(CONF_NAME):
self._title = f"{entry_data[CONF_NAME]} ({entry_data[CONF_MODEL]})"
else:
@@ -539,23 +543,22 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm reauth."""
errors = {}
-
- reauth_entry = self._get_reauth_entry()
- method = reauth_entry.data[CONF_METHOD]
+ assert self._reauth_entry
+ method = self._reauth_entry.data[CONF_METHOD]
if user_input is not None:
if method == METHOD_ENCRYPTED_WEBSOCKET:
return await self.async_step_reauth_confirm_encrypted()
bridge = SamsungTVBridge.get_bridge(
self.hass,
method,
- reauth_entry.data[CONF_HOST],
+ self._reauth_entry.data[CONF_HOST],
)
result = await bridge.async_try_connect()
if result == RESULT_SUCCESS:
- new_data = dict(reauth_entry.data)
+ new_data = dict(self._reauth_entry.data)
new_data[CONF_TOKEN] = bridge.token
return self.async_update_reload_and_abort(
- reauth_entry,
+ self._reauth_entry,
data=new_data,
)
if result not in (RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT):
@@ -584,9 +587,8 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm reauth (encrypted method)."""
errors = {}
-
- reauth_entry = self._get_reauth_entry()
- await self._async_start_encrypted_pairing(reauth_entry.data[CONF_HOST])
+ assert self._reauth_entry
+ await self._async_start_encrypted_pairing(self._reauth_entry.data[CONF_HOST])
assert self._authenticator is not None
if user_input is not None:
@@ -596,8 +598,9 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN):
and (session_id := await self._authenticator.get_session_id_and_close())
):
return self.async_update_reload_and_abort(
- reauth_entry,
- data_updates={
+ self._reauth_entry,
+ data={
+ **self._reauth_entry.data,
CONF_TOKEN: token,
CONF_SESSION_ID: session_id,
},
diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py
index 39c0d6b876d..f9e261b25b1 100644
--- a/homeassistant/components/satel_integra/alarm_control_panel.py
+++ b/homeassistant/components/satel_integra/alarm_control_panel.py
@@ -11,9 +11,15 @@ from satel_integra.satel_integra import AlarmState
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -61,6 +67,7 @@ class SatelIntegraAlarmPanel(AlarmControlPanelEntity):
_attr_code_format = CodeFormat.NUMBER
_attr_should_poll = False
+ _attr_state: str | None
_attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
@@ -88,8 +95,8 @@ class SatelIntegraAlarmPanel(AlarmControlPanelEntity):
"""Handle alarm status update."""
state = self._read_alarm_state()
_LOGGER.debug("Got status update, current status: %s", state)
- if state != self._attr_alarm_state:
- self._attr_alarm_state = state
+ if state != self._attr_state:
+ self._attr_state = state
self.async_write_ha_state()
else:
_LOGGER.debug("Ignoring alarm status message, same state")
@@ -98,28 +105,22 @@ class SatelIntegraAlarmPanel(AlarmControlPanelEntity):
"""Read current status of the alarm and translate it into HA status."""
# Default - disarmed:
- hass_alarm_status = AlarmControlPanelState.DISARMED
+ hass_alarm_status = STATE_ALARM_DISARMED
if not self._satel.connected:
return None
state_map = OrderedDict(
[
- (AlarmState.TRIGGERED, AlarmControlPanelState.TRIGGERED),
- (AlarmState.TRIGGERED_FIRE, AlarmControlPanelState.TRIGGERED),
- (AlarmState.ENTRY_TIME, AlarmControlPanelState.PENDING),
- (AlarmState.ARMED_MODE3, AlarmControlPanelState.ARMED_HOME),
- (AlarmState.ARMED_MODE2, AlarmControlPanelState.ARMED_HOME),
- (AlarmState.ARMED_MODE1, AlarmControlPanelState.ARMED_HOME),
- (AlarmState.ARMED_MODE0, AlarmControlPanelState.ARMED_AWAY),
- (
- AlarmState.EXIT_COUNTDOWN_OVER_10,
- AlarmControlPanelState.PENDING,
- ),
- (
- AlarmState.EXIT_COUNTDOWN_UNDER_10,
- AlarmControlPanelState.PENDING,
- ),
+ (AlarmState.TRIGGERED, STATE_ALARM_TRIGGERED),
+ (AlarmState.TRIGGERED_FIRE, STATE_ALARM_TRIGGERED),
+ (AlarmState.ENTRY_TIME, STATE_ALARM_PENDING),
+ (AlarmState.ARMED_MODE3, STATE_ALARM_ARMED_HOME),
+ (AlarmState.ARMED_MODE2, STATE_ALARM_ARMED_HOME),
+ (AlarmState.ARMED_MODE1, STATE_ALARM_ARMED_HOME),
+ (AlarmState.ARMED_MODE0, STATE_ALARM_ARMED_AWAY),
+ (AlarmState.EXIT_COUNTDOWN_OVER_10, STATE_ALARM_PENDING),
+ (AlarmState.EXIT_COUNTDOWN_UNDER_10, STATE_ALARM_PENDING),
]
)
_LOGGER.debug("State map of Satel: %s", self._satel.partition_states)
@@ -140,11 +141,9 @@ class SatelIntegraAlarmPanel(AlarmControlPanelEntity):
_LOGGER.debug("Code was empty or None")
return
- clear_alarm_necessary = (
- self._attr_alarm_state == AlarmControlPanelState.TRIGGERED
- )
+ clear_alarm_necessary = self._attr_state == STATE_ALARM_TRIGGERED
- _LOGGER.debug("Disarming, self._attr_alarm_state: %s", self._attr_alarm_state)
+ _LOGGER.debug("Disarming, self._attr_state: %s", self._attr_state)
await self._satel.disarm(code, [self._partition_id])
diff --git a/homeassistant/components/schlage/__init__.py b/homeassistant/components/schlage/__init__.py
index e9fb24f1309..1c3ad547f3d 100644
--- a/homeassistant/components/schlage/__init__.py
+++ b/homeassistant/components/schlage/__init__.py
@@ -16,7 +16,6 @@ from .coordinator import SchlageDataUpdateCoordinator
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.LOCK,
- Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]
diff --git a/homeassistant/components/schlage/config_flow.py b/homeassistant/components/schlage/config_flow.py
index f359f7dda71..a6104702396 100644
--- a/homeassistant/components/schlage/config_flow.py
+++ b/homeassistant/components/schlage/config_flow.py
@@ -9,7 +9,7 @@ import pyschlage
from pyschlage.exceptions import NotAuthorizedError
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import DOMAIN, LOGGER
@@ -25,13 +25,15 @@ class SchlageConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ reauth_entry: ConfigEntry | None = None
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
if user_input is None:
return self._show_user_form({})
- username = user_input[CONF_USERNAME].lower()
+ username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
user_id, errors = await self.hass.async_add_executor_job(
_authenticate, username, password
@@ -40,13 +42,7 @@ class SchlageConfigFlow(ConfigFlow, domain=DOMAIN):
return self._show_user_form(errors)
await self.async_set_unique_id(user_id)
- return self.async_create_entry(
- title=username,
- data={
- CONF_USERNAME: username,
- CONF_PASSWORD: password,
- },
- )
+ return self.async_create_entry(title=username, data=user_input)
def _show_user_form(self, errors: dict[str, str]) -> ConfigFlowResult:
"""Show the user form."""
@@ -58,17 +54,20 @@ class SchlageConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
+ assert self.reauth_entry is not None
if user_input is None:
return self._show_reauth_form({})
- reauth_entry = self._get_reauth_entry()
- username = reauth_entry.data[CONF_USERNAME]
+ username = self.reauth_entry.data[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
user_id, errors = await self.hass.async_add_executor_job(
_authenticate, username, password
@@ -76,14 +75,16 @@ class SchlageConfigFlow(ConfigFlow, domain=DOMAIN):
if user_id is None:
return self._show_reauth_form(errors)
- await self.async_set_unique_id(user_id)
- self._abort_if_unique_id_mismatch(reason="wrong_account")
+ if self.reauth_entry.unique_id != user_id:
+ return self.async_abort(reason="wrong_account")
data = {
CONF_USERNAME: username,
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
- return self.async_update_reload_and_abort(reauth_entry, data=data)
+ self.hass.config_entries.async_update_entry(self.reauth_entry, data=data)
+ await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
def _show_reauth_form(self, errors: dict[str, str]) -> ConfigFlowResult:
"""Show the reauth form."""
diff --git a/homeassistant/components/schlage/select.py b/homeassistant/components/schlage/select.py
deleted file mode 100644
index 6d93eccaa85..00000000000
--- a/homeassistant/components/schlage/select.py
+++ /dev/null
@@ -1,78 +0,0 @@
-"""Platform for Schlage select integration."""
-
-from __future__ import annotations
-
-from homeassistant.components.select import SelectEntity, SelectEntityDescription
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import EntityCategory
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from .const import DOMAIN
-from .coordinator import LockData, SchlageDataUpdateCoordinator
-from .entity import SchlageEntity
-
-_DESCRIPTIONS = (
- SelectEntityDescription(
- key="auto_lock_time",
- translation_key="auto_lock_time",
- entity_category=EntityCategory.CONFIG,
- # valid values are from Schlage UI and validated by pyschlage
- options=[
- "0",
- "15",
- "30",
- "60",
- "120",
- "240",
- "300",
- ],
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up selects based on a config entry."""
- coordinator: SchlageDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
-
- def _add_new_locks(locks: dict[str, LockData]) -> None:
- async_add_entities(
- SchlageSelect(
- coordinator=coordinator,
- description=description,
- device_id=device_id,
- )
- for device_id in locks
- for description in _DESCRIPTIONS
- )
-
- _add_new_locks(coordinator.data.locks)
- coordinator.new_locks_callbacks.append(_add_new_locks)
-
-
-class SchlageSelect(SchlageEntity, SelectEntity):
- """Schlage select entity."""
-
- def __init__(
- self,
- coordinator: SchlageDataUpdateCoordinator,
- description: SelectEntityDescription,
- device_id: str,
- ) -> None:
- """Initialize a SchlageSelect."""
- super().__init__(coordinator, device_id)
- self.entity_description = description
- self._attr_unique_id = f"{device_id}_{self.entity_description.key}"
-
- @property
- def current_option(self) -> str:
- """Return the current option."""
- return str(self._lock_data.lock.auto_lock_time)
-
- def select_option(self, option: str) -> None:
- """Set the current option."""
- self._lock.set_auto_lock_time(int(option))
diff --git a/homeassistant/components/schlage/strings.json b/homeassistant/components/schlage/strings.json
index 5c8cd0826a9..721d9e80286 100644
--- a/homeassistant/components/schlage/strings.json
+++ b/homeassistant/components/schlage/strings.json
@@ -31,20 +31,6 @@
"name": "Keypad disabled"
}
},
- "select": {
- "auto_lock_time": {
- "name": "Auto-Lock time",
- "state": {
- "0": "Disabled",
- "15": "15 seconds",
- "30": "30 seconds",
- "60": "1 minute",
- "120": "2 minutes",
- "240": "4 minutes",
- "300": "5 minutes"
- }
- }
- },
"switch": {
"beeper": {
"name": "Keypress Beep"
diff --git a/homeassistant/components/scrape/__init__.py b/homeassistant/components/scrape/__init__.py
index ff991c5f348..16220d5c567 100644
--- a/homeassistant/components/scrape/__init__.py
+++ b/homeassistant/components/scrape/__init__.py
@@ -72,7 +72,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
scan_interval: timedelta = resource_config.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
)
- coordinator = ScrapeCoordinator(hass, None, rest, scan_interval)
+ coordinator = ScrapeCoordinator(hass, rest, scan_interval)
sensors: list[ConfigType] = resource_config.get(SENSOR_DOMAIN, [])
if sensors:
@@ -100,7 +100,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ScrapeConfigEntry) -> bo
coordinator = ScrapeCoordinator(
hass,
- entry,
rest,
DEFAULT_SCAN_INTERVAL,
)
diff --git a/homeassistant/components/scrape/coordinator.py b/homeassistant/components/scrape/coordinator.py
index b5cabc6b94e..74fd510ac94 100644
--- a/homeassistant/components/scrape/coordinator.py
+++ b/homeassistant/components/scrape/coordinator.py
@@ -8,7 +8,6 @@ import logging
from bs4 import BeautifulSoup
from homeassistant.components.rest import RestData
-from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -19,17 +18,12 @@ class ScrapeCoordinator(DataUpdateCoordinator[BeautifulSoup]):
"""Scrape Coordinator."""
def __init__(
- self,
- hass: HomeAssistant,
- config_entry: ConfigEntry | None,
- rest: RestData,
- update_interval: timedelta,
+ self, hass: HomeAssistant, rest: RestData, update_interval: timedelta
) -> None:
"""Initialize Scrape coordinator."""
super().__init__(
hass,
_LOGGER,
- config_entry=config_entry,
name="Scrape Coordinator",
update_interval=update_interval,
)
diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py
index 19db89dc03d..4a46756cf2f 100644
--- a/homeassistant/components/screenlogic/config_flow.py
+++ b/homeassistant/components/screenlogic/config_flow.py
@@ -81,7 +81,7 @@ class ScreenlogicConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> ScreenLogicOptionsFlowHandler:
"""Get the options flow for ScreenLogic."""
- return ScreenLogicOptionsFlowHandler()
+ return ScreenLogicOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -192,6 +192,10 @@ class ScreenlogicConfigFlow(ConfigFlow, domain=DOMAIN):
class ScreenLogicOptionsFlowHandler(OptionsFlow):
"""Handles the options for the ScreenLogic integration."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Init the screen logic options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(self, user_input=None) -> ConfigFlowResult:
"""Manage the options."""
if user_input is not None:
diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py
index e919d48e96d..58e993ad6e0 100644
--- a/homeassistant/components/sense/__init__.py
+++ b/homeassistant/components/sense/__init__.py
@@ -1,8 +1,10 @@
"""Support for monitoring a Sense energy sensor."""
from dataclasses import dataclass
+from datetime import timedelta
from functools import partial
import logging
+from typing import Any
from sense_energy import (
ASyncSenseable,
@@ -11,18 +13,26 @@ from sense_energy import (
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_TIMEOUT, Platform
-from homeassistant.core import HomeAssistant
+from homeassistant.const import (
+ CONF_EMAIL,
+ CONF_TIMEOUT,
+ EVENT_HOMEASSISTANT_STOP,
+ Platform,
+)
+from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
ACTIVE_UPDATE_RATE,
SENSE_CONNECT_EXCEPTIONS,
+ SENSE_DEVICE_UPDATE,
SENSE_TIMEOUT_EXCEPTIONS,
SENSE_WEBSOCKET_EXCEPTIONS,
)
-from .coordinator import SenseRealtimeCoordinator, SenseTrendCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -30,19 +40,37 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
type SenseConfigEntry = ConfigEntry[SenseData]
+class SenseDevicesData:
+ """Data for each sense device."""
+
+ def __init__(self):
+ """Create."""
+ self._data_by_device = {}
+
+ def set_devices_data(self, devices):
+ """Store a device update."""
+ self._data_by_device = {device["id"]: device for device in devices}
+
+ def get_device_by_id(self, sense_device_id):
+ """Get the latest device data."""
+ return self._data_by_device.get(sense_device_id)
+
+
@dataclass(kw_only=True, slots=True)
class SenseData:
"""Sense data type."""
data: ASyncSenseable
- trends: SenseTrendCoordinator
- rt: SenseRealtimeCoordinator
+ device_data: SenseDevicesData
+ trends: DataUpdateCoordinator[None]
+ discovered: list[dict[str, Any]]
async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> bool:
"""Set up Sense from a config entry."""
entry_data = entry.data
+ email = entry_data[CONF_EMAIL]
timeout = entry_data[CONF_TIMEOUT]
access_token = entry_data.get("access_token", "")
@@ -80,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> boo
raise ConfigEntryNotReady(str(err)) from err
try:
- await gateway.fetch_devices()
+ sense_discovered_devices = await gateway.get_discovered_device_data()
await gateway.update_realtime()
except SENSE_TIMEOUT_EXCEPTIONS as err:
raise ConfigEntryNotReady(
@@ -89,8 +117,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> boo
except SENSE_WEBSOCKET_EXCEPTIONS as err:
raise ConfigEntryNotReady(str(err) or "Error during realtime update") from err
- trends_coordinator = SenseTrendCoordinator(hass, gateway)
- realtime_coordinator = SenseRealtimeCoordinator(hass, gateway)
+ async def _async_update_trend():
+ """Update the trend data."""
+ try:
+ await gateway.update_trend_data()
+ except (SenseAuthenticationException, SenseMFARequiredException) as err:
+ _LOGGER.warning("Sense authentication expired")
+ raise ConfigEntryAuthFailed(err) from err
+ except SENSE_CONNECT_EXCEPTIONS as err:
+ raise UpdateFailed(err) from err
+
+ trends_coordinator: DataUpdateCoordinator[None] = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name=f"Sense Trends {email}",
+ update_method=_async_update_trend,
+ update_interval=timedelta(seconds=300),
+ )
+ # Start out as unavailable so we do not report 0 data
+ # until the update happens
+ trends_coordinator.last_update_success = False
# This can take longer than 60s and we already know
# sense is online since get_discovered_device_data was
@@ -100,19 +146,45 @@ async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> boo
trends_coordinator.async_request_refresh(),
"sense.trends-coordinator-refresh",
)
- entry.async_create_background_task(
- hass,
- realtime_coordinator.async_request_refresh(),
- "sense.realtime-coordinator-refresh",
- )
entry.runtime_data = SenseData(
data=gateway,
+ device_data=SenseDevicesData(),
trends=trends_coordinator,
- rt=realtime_coordinator,
+ discovered=sense_discovered_devices,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+
+ async def async_sense_update(_):
+ """Retrieve latest state."""
+ try:
+ await gateway.update_realtime()
+ except SENSE_TIMEOUT_EXCEPTIONS as ex:
+ _LOGGER.error("Timeout retrieving data: %s", ex)
+ except SENSE_WEBSOCKET_EXCEPTIONS as ex:
+ _LOGGER.error("Failed to update data: %s", ex)
+
+ data = gateway.get_realtime()
+ if "devices" in data:
+ entry.runtime_data.device_data.set_devices_data(data["devices"])
+ async_dispatcher_send(hass, f"{SENSE_DEVICE_UPDATE}-{gateway.sense_monitor_id}")
+
+ remove_update_callback = async_track_time_interval(
+ hass, async_sense_update, timedelta(seconds=ACTIVE_UPDATE_RATE)
+ )
+
+ @callback
+ def _remove_update_callback_at_stop(event):
+ remove_update_callback()
+
+ entry.async_on_unload(remove_update_callback)
+ entry.async_on_unload(
+ hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP, _remove_update_callback_at_stop
+ )
+ )
+
return True
diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py
index d06b3a62937..8317f8458b3 100644
--- a/homeassistant/components/sense/binary_sensor.py
+++ b/homeassistant/components/sense/binary_sensor.py
@@ -2,20 +2,17 @@
import logging
-from sense_energy.sense_api import SenseDevice
-
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
-from homeassistant.core import HomeAssistant
+from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import SenseConfigEntry
-from .const import DOMAIN
-from .coordinator import SenseRealtimeCoordinator
-from .entity import SenseDeviceEntity
+from .const import ATTRIBUTION, DOMAIN, MDI_ICONS, SENSE_DEVICE_UPDATE
_LOGGER = logging.getLogger(__name__)
@@ -27,11 +24,13 @@ async def async_setup_entry(
) -> None:
"""Set up the Sense binary sensor."""
sense_monitor_id = config_entry.runtime_data.data.sense_monitor_id
- realtime_coordinator = config_entry.runtime_data.rt
+ sense_devices = config_entry.runtime_data.discovered
+ device_data = config_entry.runtime_data.device_data
devices = [
- SenseBinarySensor(device, realtime_coordinator, sense_monitor_id)
- for device in config_entry.runtime_data.data.devices
+ SenseDevice(device_data, device, sense_monitor_id)
+ for device in sense_devices
+ if device["tags"]["DeviceListAllowed"] == "true"
]
await _migrate_old_unique_ids(hass, devices)
@@ -39,46 +38,65 @@ async def async_setup_entry(
async_add_entities(devices)
-class SenseBinarySensor(SenseDeviceEntity, BinarySensorEntity):
- """Implementation of a Sense energy device binary sensor."""
-
- _attr_device_class = BinarySensorDeviceClass.POWER
-
- def __init__(
- self,
- device: SenseDevice,
- coordinator: SenseRealtimeCoordinator,
- sense_monitor_id: str,
- ) -> None:
- """Initialize the Sense binary sensor."""
- super().__init__(device, coordinator, sense_monitor_id, device.id)
- self._id = device.id
-
- @property
- def old_unique_id(self) -> str:
- """Return the old not so unique id of the binary sensor."""
- return self._id
-
- @property
- def is_on(self) -> bool:
- """Return the state of the sensor."""
- return self._device.is_on
-
-
-async def _migrate_old_unique_ids(
- hass: HomeAssistant, devices: list[SenseBinarySensor]
-) -> None:
+async def _migrate_old_unique_ids(hass, devices):
registry = er.async_get(hass)
for device in devices:
# Migration of old not so unique ids
old_entity_id = registry.async_get_entity_id(
"binary_sensor", DOMAIN, device.old_unique_id
)
- updated_id = device.unique_id
- if old_entity_id is not None and updated_id is not None:
+ if old_entity_id is not None:
_LOGGER.debug(
"Migrating unique_id from [%s] to [%s]",
device.old_unique_id,
device.unique_id,
)
- registry.async_update_entity(old_entity_id, new_unique_id=updated_id)
+ registry.async_update_entity(old_entity_id, new_unique_id=device.unique_id)
+
+
+def sense_to_mdi(sense_icon):
+ """Convert sense icon to mdi icon."""
+ return f"mdi:{MDI_ICONS.get(sense_icon, "power-plug")}"
+
+
+class SenseDevice(BinarySensorEntity):
+ """Implementation of a Sense energy device binary sensor."""
+
+ _attr_attribution = ATTRIBUTION
+ _attr_should_poll = False
+ _attr_available = False
+ _attr_device_class = BinarySensorDeviceClass.POWER
+
+ def __init__(self, sense_devices_data, device, sense_monitor_id):
+ """Initialize the Sense binary sensor."""
+ self._attr_name = device["name"]
+ self._id = device["id"]
+ self._sense_monitor_id = sense_monitor_id
+ self._attr_unique_id = f"{sense_monitor_id}-{self._id}"
+ self._attr_icon = sense_to_mdi(device["icon"])
+ self._sense_devices_data = sense_devices_data
+
+ @property
+ def old_unique_id(self):
+ """Return the old not so unique id of the binary sensor."""
+ return self._id
+
+ async def async_added_to_hass(self) -> None:
+ """Register callbacks."""
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
+ self._async_update_from_data,
+ )
+ )
+
+ @callback
+ def _async_update_from_data(self):
+ """Get the latest data, update state. Must not do I/O."""
+ new_state = bool(self._sense_devices_data.get_device_by_id(self._id))
+ if self._attr_available and self._attr_is_on == new_state:
+ return
+ self._attr_available = True
+ self._attr_is_on = new_state
+ self.async_write_ha_state()
diff --git a/homeassistant/components/sense/const.py b/homeassistant/components/sense/const.py
index b23117c977d..5e944c18d8d 100644
--- a/homeassistant/components/sense/const.py
+++ b/homeassistant/components/sense/const.py
@@ -11,7 +11,6 @@ from sense_energy import (
DOMAIN = "sense"
DEFAULT_TIMEOUT = 30
ACTIVE_UPDATE_RATE = 60
-TREND_UPDATE_RATE = 300
DEFAULT_NAME = "Sense"
SENSE_DEVICE_UPDATE = "sense_devices_update"
@@ -20,7 +19,7 @@ ACTIVE_TYPE = "active"
ATTRIBUTION = "Data provided by Sense.com"
-CONSUMPTION_NAME = "Energy"
+CONSUMPTION_NAME = "Usage"
CONSUMPTION_ID = "usage"
PRODUCTION_NAME = "Production"
PRODUCTION_ID = "production"
diff --git a/homeassistant/components/sense/coordinator.py b/homeassistant/components/sense/coordinator.py
deleted file mode 100644
index c0029cd79ea..00000000000
--- a/homeassistant/components/sense/coordinator.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""Sense Coordinators."""
-
-from datetime import timedelta
-import logging
-
-from sense_energy import (
- ASyncSenseable,
- SenseAuthenticationException,
- SenseMFARequiredException,
-)
-
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryAuthFailed
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-
-from .const import (
- ACTIVE_UPDATE_RATE,
- SENSE_CONNECT_EXCEPTIONS,
- SENSE_TIMEOUT_EXCEPTIONS,
- SENSE_WEBSOCKET_EXCEPTIONS,
- TREND_UPDATE_RATE,
-)
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class SenseCoordinator(DataUpdateCoordinator[None]):
- """Sense Trend Coordinator."""
-
- def __init__(
- self, hass: HomeAssistant, gateway: ASyncSenseable, name: str, update: int
- ) -> None:
- """Initialize."""
- super().__init__(
- hass,
- logger=_LOGGER,
- name=f"Sense {name} {gateway.sense_monitor_id}",
- update_interval=timedelta(seconds=update),
- )
- self._gateway = gateway
- self.last_update_success = False
-
-
-class SenseTrendCoordinator(SenseCoordinator):
- """Sense Trend Coordinator."""
-
- def __init__(self, hass: HomeAssistant, gateway: ASyncSenseable) -> None:
- """Initialize."""
- super().__init__(hass, gateway, "Trends", TREND_UPDATE_RATE)
-
- async def _async_update_data(self) -> None:
- """Update the trend data."""
- try:
- await self._gateway.update_trend_data()
- except (SenseAuthenticationException, SenseMFARequiredException) as err:
- _LOGGER.warning("Sense authentication expired")
- raise ConfigEntryAuthFailed(err) from err
- except SENSE_CONNECT_EXCEPTIONS as err:
- raise UpdateFailed(err) from err
-
-
-class SenseRealtimeCoordinator(SenseCoordinator):
- """Sense Realtime Coordinator."""
-
- def __init__(self, hass: HomeAssistant, gateway: ASyncSenseable) -> None:
- """Initialize."""
- super().__init__(hass, gateway, "Realtime", ACTIVE_UPDATE_RATE)
-
- async def _async_update_data(self) -> None:
- """Retrieve latest state."""
- try:
- await self._gateway.update_realtime()
- except SENSE_TIMEOUT_EXCEPTIONS as ex:
- _LOGGER.error("Timeout retrieving data: %s", ex)
- except SENSE_WEBSOCKET_EXCEPTIONS as ex:
- _LOGGER.error("Failed to update data: %s", ex)
diff --git a/homeassistant/components/sense/entity.py b/homeassistant/components/sense/entity.py
deleted file mode 100644
index 248be53ceb7..00000000000
--- a/homeassistant/components/sense/entity.py
+++ /dev/null
@@ -1,71 +0,0 @@
-"""Base entities for Sense energy."""
-
-from sense_energy import ASyncSenseable
-from sense_energy.sense_api import SenseDevice
-
-from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from .const import ATTRIBUTION, DOMAIN, MDI_ICONS
-from .coordinator import SenseCoordinator
-
-
-def sense_to_mdi(sense_icon: str) -> str:
- """Convert sense icon to mdi icon."""
- return f"mdi:{MDI_ICONS.get(sense_icon, "power-plug")}"
-
-
-class SenseEntity(CoordinatorEntity[SenseCoordinator]):
- """Base implementation of a Sense sensor."""
-
- _attr_attribution = ATTRIBUTION
- _attr_should_poll = False
- _attr_has_entity_name = True
-
- def __init__(
- self,
- gateway: ASyncSenseable,
- coordinator: SenseCoordinator,
- sense_monitor_id: str,
- unique_id: str,
- ) -> None:
- """Initialize the Sense sensor."""
- super().__init__(coordinator)
- self._attr_unique_id = f"{sense_monitor_id}-{unique_id}"
- self._gateway = gateway
- self._attr_device_info = DeviceInfo(
- name=f"Sense {sense_monitor_id}",
- identifiers={(DOMAIN, sense_monitor_id)},
- model="Sense",
- manufacturer="Sense Labs, Inc.",
- configuration_url="https://home.sense.com",
- )
-
-
-class SenseDeviceEntity(CoordinatorEntity[SenseCoordinator]):
- """Base implementation of a Sense sensor."""
-
- _attr_attribution = ATTRIBUTION
- _attr_should_poll = False
- _attr_has_entity_name = True
-
- def __init__(
- self,
- device: SenseDevice,
- coordinator: SenseCoordinator,
- sense_monitor_id: str,
- unique_id: str,
- ) -> None:
- """Initialize the Sense sensor."""
- super().__init__(coordinator)
- self._attr_unique_id = f"{sense_monitor_id}-{unique_id}"
- self._device = device
- self._attr_icon = sense_to_mdi(device.icon)
- self._attr_device_info = DeviceInfo(
- name=device.name,
- identifiers={(DOMAIN, f"{sense_monitor_id}:{device.id}")},
- model="Sense",
- manufacturer="Sense Labs, Inc.",
- configuration_url="https://home.sense.com",
- via_device=(DOMAIN, sense_monitor_id),
- )
diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json
index df2317c3a6c..116b714ba82 100644
--- a/homeassistant/components/sense/manifest.json
+++ b/homeassistant/components/sense/manifest.json
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/sense",
"iot_class": "cloud_polling",
"loggers": ["sense_energy"],
- "requirements": ["sense-energy==0.13.3"]
+ "requirements": ["sense-energy==0.12.4"]
}
diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py
index 2f5c82675d5..bc9dd470f5e 100644
--- a/homeassistant/components/sense/sensor.py
+++ b/homeassistant/components/sense/sensor.py
@@ -1,10 +1,5 @@
"""Support for monitoring a Sense energy sensor."""
-from datetime import datetime
-
-from sense_energy import ASyncSenseable, Scale
-from sense_energy.sense_api import SenseDevice
-
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
@@ -16,37 +11,55 @@ from homeassistant.const import (
UnitOfEnergy,
UnitOfPower,
)
-from homeassistant.core import HomeAssistant
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.device_registry import DeviceInfo
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import SenseConfigEntry
from .const import (
+ ACTIVE_NAME,
ACTIVE_TYPE,
+ ATTRIBUTION,
CONSUMPTION_ID,
CONSUMPTION_NAME,
+ DOMAIN,
FROM_GRID_ID,
FROM_GRID_NAME,
+ MDI_ICONS,
NET_PRODUCTION_ID,
NET_PRODUCTION_NAME,
PRODUCTION_ID,
PRODUCTION_NAME,
PRODUCTION_PCT_ID,
PRODUCTION_PCT_NAME,
+ SENSE_DEVICE_UPDATE,
SOLAR_POWERED_ID,
SOLAR_POWERED_NAME,
TO_GRID_ID,
TO_GRID_NAME,
)
-from .coordinator import SenseRealtimeCoordinator, SenseTrendCoordinator
-from .entity import SenseDeviceEntity, SenseEntity
+
+
+class SensorConfig:
+ """Data structure holding sensor configuration."""
+
+ def __init__(self, name, sensor_type):
+ """Sensor name and type to pass to API."""
+ self.name = name
+ self.sensor_type = sensor_type
+
+
+# Sensor types/ranges
+ACTIVE_SENSOR_TYPE = SensorConfig(ACTIVE_NAME, ACTIVE_TYPE)
# Sensor types/ranges
TRENDS_SENSOR_TYPES = {
- Scale.DAY: "Daily",
- Scale.WEEK: "Weekly",
- Scale.MONTH: "Monthly",
- Scale.YEAR: "Yearly",
- Scale.CYCLE: "Bill",
+ "daily": SensorConfig("Daily", "DAY"),
+ "weekly": SensorConfig("Weekly", "WEEK"),
+ "monthly": SensorConfig("Monthly", "MONTH"),
+ "yearly": SensorConfig("Yearly", "YEAR"),
}
# Production/consumption variants
@@ -63,6 +76,11 @@ TREND_SENSOR_VARIANTS = [
]
+def sense_to_mdi(sense_icon):
+ """Convert sense icon to mdi icon."""
+ return f"mdi:{MDI_ICONS.get(sense_icon, 'power-plug')}"
+
+
async def async_setup_entry(
hass: HomeAssistant,
config_entry: SenseConfigEntry,
@@ -71,46 +89,58 @@ async def async_setup_entry(
"""Set up the Sense sensor."""
data = config_entry.runtime_data.data
trends_coordinator = config_entry.runtime_data.trends
- realtime_coordinator = config_entry.runtime_data.rt
# Request only in case it takes longer
# than 60s
await trends_coordinator.async_request_refresh()
sense_monitor_id = data.sense_monitor_id
+ sense_devices = config_entry.runtime_data.discovered
+ device_data = config_entry.runtime_data.device_data
- entities: list[SensorEntity] = []
-
- for device in config_entry.runtime_data.data.devices:
- entities.append(
- SenseDevicePowerSensor(device, sense_monitor_id, realtime_coordinator)
- )
- entities.extend(
- SenseDeviceEnergySensor(device, scale, trends_coordinator, sense_monitor_id)
- for scale in Scale
- )
+ entities: list[SensorEntity] = [
+ SenseEnergyDevice(device_data, device, sense_monitor_id)
+ for device in sense_devices
+ if device["tags"]["DeviceListAllowed"] == "true"
+ ]
for variant_id, variant_name in SENSOR_VARIANTS:
+ name = ACTIVE_SENSOR_TYPE.name
+ sensor_type = ACTIVE_SENSOR_TYPE.sensor_type
+
+ unique_id = f"{sense_monitor_id}-active-{variant_id}"
entities.append(
- SensePowerSensor(
- data, sense_monitor_id, variant_id, variant_name, realtime_coordinator
+ SenseActiveSensor(
+ data,
+ name,
+ sensor_type,
+ sense_monitor_id,
+ variant_id,
+ variant_name,
+ unique_id,
)
)
entities.extend(
- SenseVoltageSensor(data, i, sense_monitor_id, realtime_coordinator)
+ SenseVoltageSensor(data, i, sense_monitor_id)
for i in range(len(data.active_voltage))
)
- for scale in Scale:
+ for type_id, typ in TRENDS_SENSOR_TYPES.items():
for variant_id, variant_name in TREND_SENSOR_VARIANTS:
+ name = typ.name
+ sensor_type = typ.sensor_type
+
+ unique_id = f"{sense_monitor_id}-{type_id}-{variant_id}"
entities.append(
SenseTrendsSensor(
data,
- scale,
+ name,
+ sensor_type,
variant_id,
variant_name,
trends_coordinator,
+ unique_id,
sense_monitor_id,
)
)
@@ -118,89 +148,131 @@ async def async_setup_entry(
async_add_entities(entities)
-class SensePowerSensor(SenseEntity, SensorEntity):
+class SenseActiveSensor(SensorEntity):
"""Implementation of a Sense energy sensor."""
_attr_device_class = SensorDeviceClass.POWER
_attr_native_unit_of_measurement = UnitOfPower.WATT
+ _attr_attribution = ATTRIBUTION
+ _attr_should_poll = False
+ _attr_available = False
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(
self,
- gateway: ASyncSenseable,
- sense_monitor_id: str,
- variant_id: str,
- variant_name: str,
- realtime_coordinator: SenseRealtimeCoordinator,
- ) -> None:
+ data,
+ name,
+ sensor_type,
+ sense_monitor_id,
+ variant_id,
+ variant_name,
+ unique_id,
+ ):
"""Initialize the Sense sensor."""
- super().__init__(
- gateway,
- realtime_coordinator,
- sense_monitor_id,
- f"{ACTIVE_TYPE}-{variant_id}",
- )
- self._attr_name = variant_name
+ self._attr_name = f"{name} {variant_name}"
+ self._attr_unique_id = unique_id
+ self._data = data
+ self._sense_monitor_id = sense_monitor_id
+ self._sensor_type = sensor_type
self._variant_id = variant_id
+ self._variant_name = variant_name
- @property
- def native_value(self) -> float:
- """Return the state of the sensor."""
- return round(
- self._gateway.active_solar_power
- if self._variant_id == PRODUCTION_ID
- else self._gateway.active_power
+ async def async_added_to_hass(self) -> None:
+ """Register callbacks."""
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
+ self._async_update_from_data,
+ )
)
+ @callback
+ def _async_update_from_data(self):
+ """Update the sensor from the data. Must not do I/O."""
+ new_state = round(
+ self._data.active_solar_power
+ if self._variant_id == PRODUCTION_ID
+ else self._data.active_power
+ )
+ if self._attr_available and self._attr_native_value == new_state:
+ return
+ self._attr_native_value = new_state
+ self._attr_available = True
+ self.async_write_ha_state()
-class SenseVoltageSensor(SenseEntity, SensorEntity):
+
+class SenseVoltageSensor(SensorEntity):
"""Implementation of a Sense energy voltage sensor."""
_attr_device_class = SensorDeviceClass.VOLTAGE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
+ _attr_attribution = ATTRIBUTION
+ _attr_should_poll = False
+ _attr_available = False
def __init__(
self,
- gateway: ASyncSenseable,
- index: int,
- sense_monitor_id: str,
- realtime_coordinator: SenseRealtimeCoordinator,
- ) -> None:
+ data,
+ index,
+ sense_monitor_id,
+ ):
"""Initialize the Sense sensor."""
- super().__init__(
- gateway, realtime_coordinator, sense_monitor_id, f"L{index + 1}"
- )
- self._attr_name = f"L{index + 1} Voltage"
+ line_num = index + 1
+ self._attr_name = f"L{line_num} Voltage"
+ self._attr_unique_id = f"{sense_monitor_id}-L{line_num}"
+ self._data = data
+ self._sense_monitor_id = sense_monitor_id
self._voltage_index = index
- @property
- def native_value(self) -> float:
- """Return the state of the sensor."""
- return round(self._gateway.active_voltage[self._voltage_index], 1)
+ async def async_added_to_hass(self) -> None:
+ """Register callbacks."""
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
+ self._async_update_from_data,
+ )
+ )
+
+ @callback
+ def _async_update_from_data(self):
+ """Update the sensor from the data. Must not do I/O."""
+ new_state = round(self._data.active_voltage[self._voltage_index], 1)
+ if self._attr_available and self._attr_native_value == new_state:
+ return
+ self._attr_available = True
+ self._attr_native_value = new_state
+ self.async_write_ha_state()
-class SenseTrendsSensor(SenseEntity, SensorEntity):
+class SenseTrendsSensor(CoordinatorEntity, SensorEntity):
"""Implementation of a Sense energy sensor."""
+ _attr_device_class = SensorDeviceClass.ENERGY
+ _attr_state_class = SensorStateClass.TOTAL
+ _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
+ _attr_attribution = ATTRIBUTION
+ _attr_should_poll = False
+
def __init__(
self,
- gateway: ASyncSenseable,
- scale: Scale,
- variant_id: str,
- variant_name: str,
- trends_coordinator: SenseTrendCoordinator,
- sense_monitor_id: str,
- ) -> None:
+ data,
+ name,
+ sensor_type,
+ variant_id,
+ variant_name,
+ trends_coordinator,
+ unique_id,
+ sense_monitor_id,
+ ):
"""Initialize the Sense sensor."""
- super().__init__(
- gateway,
- trends_coordinator,
- sense_monitor_id,
- f"{TRENDS_SENSOR_TYPES[scale].lower()}-{variant_id}",
- )
- self._attr_name = f"{TRENDS_SENSOR_TYPES[scale]} {variant_name}"
- self._scale = scale
+ super().__init__(trends_coordinator)
+ self._attr_name = f"{name} {variant_name}"
+ self._attr_unique_id = unique_id
+ self._data = data
+ self._sensor_type = sensor_type
self._variant_id = variant_id
self._had_any_update = False
if variant_id in [PRODUCTION_PCT_ID, SOLAR_POWERED_ID]:
@@ -208,75 +280,66 @@ class SenseTrendsSensor(SenseEntity, SensorEntity):
self._attr_entity_registry_enabled_default = False
self._attr_state_class = None
self._attr_device_class = None
- else:
- self._attr_device_class = SensorDeviceClass.ENERGY
- self._attr_state_class = SensorStateClass.TOTAL
- self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
+ self._attr_device_info = DeviceInfo(
+ name=f"Sense {sense_monitor_id}",
+ identifiers={(DOMAIN, sense_monitor_id)},
+ model="Sense",
+ manufacturer="Sense Labs, Inc.",
+ configuration_url="https://home.sense.com",
+ )
@property
- def native_value(self) -> float:
+ def native_value(self):
"""Return the state of the sensor."""
- return round(self._gateway.get_stat(self._scale, self._variant_id), 1)
+ return round(self._data.get_trend(self._sensor_type, self._variant_id), 1)
@property
- def last_reset(self) -> datetime | None:
+ def last_reset(self):
"""Return the time when the sensor was last reset, if any."""
if self._attr_state_class == SensorStateClass.TOTAL:
- return self._gateway.trend_start(self._scale)
+ return self._data.trend_start(self._sensor_type)
return None
-class SenseDevicePowerSensor(SenseDeviceEntity, SensorEntity):
+class SenseEnergyDevice(SensorEntity):
"""Implementation of a Sense energy device."""
+ _attr_available = False
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = UnitOfPower.WATT
+ _attr_attribution = ATTRIBUTION
_attr_device_class = SensorDeviceClass.POWER
+ _attr_should_poll = False
- def __init__(
- self,
- device: SenseDevice,
- sense_monitor_id: str,
- coordinator: SenseRealtimeCoordinator,
- ) -> None:
- """Initialize the Sense device sensor."""
- super().__init__(
- device, coordinator, sense_monitor_id, f"{device.id}-{CONSUMPTION_ID}"
+ def __init__(self, sense_devices_data, device, sense_monitor_id):
+ """Initialize the Sense binary sensor."""
+ self._attr_name = f"{device['name']} {CONSUMPTION_NAME}"
+ self._id = device["id"]
+ self._sense_monitor_id = sense_monitor_id
+ self._attr_unique_id = f"{sense_monitor_id}-{self._id}-{CONSUMPTION_ID}"
+ self._attr_icon = sense_to_mdi(device["icon"])
+ self._sense_devices_data = sense_devices_data
+
+ async def async_added_to_hass(self) -> None:
+ """Register callbacks."""
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
+ self._async_update_from_data,
+ )
)
- @property
- def native_value(self) -> float:
- """Return the state of the sensor."""
- return self._device.power_w
-
-
-class SenseDeviceEnergySensor(SenseDeviceEntity, SensorEntity):
- """Implementation of a Sense device energy sensor."""
-
- _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
- _attr_state_class = SensorStateClass.TOTAL_INCREASING
- _attr_device_class = SensorDeviceClass.ENERGY
-
- def __init__(
- self,
- device: SenseDevice,
- scale: Scale,
- coordinator: SenseTrendCoordinator,
- sense_monitor_id: str,
- ) -> None:
- """Initialize the Sense device sensor."""
- super().__init__(
- device,
- coordinator,
- sense_monitor_id,
- f"{device.id}-{TRENDS_SENSOR_TYPES[scale].lower()}-energy",
- )
- self._attr_translation_key = f"{TRENDS_SENSOR_TYPES[scale].lower()}_energy"
- self._attr_suggested_display_precision = 2
- self._scale = scale
- self._device = device
-
- @property
- def native_value(self) -> float:
- """Return the state of the sensor."""
- return self._device.energy_kwh[self._scale]
+ @callback
+ def _async_update_from_data(self):
+ """Get the latest data, update state. Must not do I/O."""
+ device_data = self._sense_devices_data.get_device_by_id(self._id)
+ if not device_data or "w" not in device_data:
+ new_state = 0
+ else:
+ new_state = int(device_data["w"])
+ if self._attr_available and self._attr_native_value == new_state:
+ return
+ self._attr_native_value = new_state
+ self._attr_available = True
+ self.async_write_ha_state()
diff --git a/homeassistant/components/sense/strings.json b/homeassistant/components/sense/strings.json
index 4579c84f050..a519155bee1 100644
--- a/homeassistant/components/sense/strings.json
+++ b/homeassistant/components/sense/strings.json
@@ -32,24 +32,5 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
- },
- "entity": {
- "sensor": {
- "daily_energy": {
- "name": "Daily energy"
- },
- "weekly_energy": {
- "name": "Weekly energy"
- },
- "monthly_energy": {
- "name": "Monthly energy"
- },
- "yearly_energy": {
- "name": "Yearly energy"
- },
- "bill_energy": {
- "name": "Bill energy"
- }
- }
}
}
diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py
index b8b1029f141..667f96fe1c2 100644
--- a/homeassistant/components/sensibo/config_flow.py
+++ b/homeassistant/components/sensibo/config_flow.py
@@ -8,9 +8,8 @@ from typing import Any
from pysensibo.exceptions import AuthenticationError
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY
-from homeassistant.core import HomeAssistant
from homeassistant.helpers.selector import TextSelector
from .const import DEFAULT_NAME, DOMAIN
@@ -23,34 +22,19 @@ DATA_SCHEMA = vol.Schema(
)
-async def validate_api(
- hass: HomeAssistant, api_key: str
-) -> tuple[str | None, dict[str, str]]:
- """Validate the API key."""
- errors: dict[str, str] = {}
- username: str | None = None
- try:
- username = await async_validate_api(hass, api_key)
- except AuthenticationError:
- errors["base"] = "invalid_auth"
- except ConnectionError:
- errors["base"] = "cannot_connect"
- except NoDevicesError:
- errors["base"] = "no_devices"
- except NoUsernameError:
- errors["base"] = "no_username"
- return (username, errors)
-
-
class SensiboConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Sensibo integration."""
VERSION = 2
+ entry: ConfigEntry | None
+
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication with Sensibo."""
+
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -61,13 +45,24 @@ class SensiboConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input:
api_key = user_input[CONF_API_KEY]
- username, errors = await validate_api(self.hass, api_key)
- if username:
- reauth_entry = self._get_reauth_entry()
- if username == reauth_entry.unique_id:
+ try:
+ username = await async_validate_api(self.hass, api_key)
+ except AuthenticationError:
+ errors["base"] = "invalid_auth"
+ except ConnectionError:
+ errors["base"] = "cannot_connect"
+ except NoDevicesError:
+ errors["base"] = "no_devices"
+ except NoUsernameError:
+ errors["base"] = "no_username"
+ else:
+ assert self.entry is not None
+
+ if username == self.entry.unique_id:
return self.async_update_reload_and_abort(
- reauth_entry,
- data_updates={
+ self.entry,
+ data={
+ **self.entry.data,
CONF_API_KEY: api_key,
},
)
@@ -79,32 +74,6 @@ class SensiboConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
- async def async_step_reconfigure(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Reconfigure Sensibo."""
- errors: dict[str, str] = {}
-
- if user_input:
- api_key = user_input[CONF_API_KEY]
- username, errors = await validate_api(self.hass, api_key)
- if username:
- reconfigure_entry = self._get_reconfigure_entry()
- if username == reconfigure_entry.unique_id:
- return self.async_update_reload_and_abort(
- reconfigure_entry,
- data_updates={
- CONF_API_KEY: api_key,
- },
- )
- errors["base"] = "incorrect_api_key"
-
- return self.async_show_form(
- step_id="reconfigure",
- data_schema=DATA_SCHEMA,
- errors=errors,
- )
-
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -114,8 +83,17 @@ class SensiboConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input:
api_key = user_input[CONF_API_KEY]
- username, errors = await validate_api(self.hass, api_key)
- if username:
+ try:
+ username = await async_validate_api(self.hass, api_key)
+ except AuthenticationError:
+ errors["base"] = "invalid_auth"
+ except ConnectionError:
+ errors["base"] = "cannot_connect"
+ except NoDevicesError:
+ errors["base"] = "no_devices"
+ except NoUsernameError:
+ errors["base"] = "no_username"
+ else:
await self.async_set_unique_id(username)
self._abort_if_unique_id_configured()
diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json
index bec402bee18..60a32028017 100644
--- a/homeassistant/components/sensibo/strings.json
+++ b/homeassistant/components/sensibo/strings.json
@@ -2,8 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -28,14 +27,6 @@
"data_description": {
"api_key": "[%key:component::sensibo::config::step::user::data_description::api_key%]"
}
- },
- "reconfigure": {
- "data": {
- "api_key": "[%key:common::config_flow::data::api_key%]"
- },
- "data_description": {
- "api_key": "[%key:component::sensibo::config::step::user::data_description::api_key%]"
- }
}
}
},
diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py
index f4573f873a2..da0b48a23a0 100644
--- a/homeassistant/components/sensor/const.py
+++ b/homeassistant/components/sensor/const.py
@@ -17,7 +17,6 @@ from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfApparentPower,
- UnitOfBloodGlucoseConcentration,
UnitOfConductivity,
UnitOfDataRate,
UnitOfElectricCurrent,
@@ -48,7 +47,6 @@ from homeassistant.helpers.deprecation import (
)
from homeassistant.util.unit_conversion import (
BaseUnitConverter,
- BloodGlucoseConcentrationConverter,
ConductivityConverter,
DataRateConverter,
DistanceConverter,
@@ -129,12 +127,6 @@ class SensorDeviceClass(StrEnum):
Unit of measurement: `%`
"""
- BLOOD_GLUCOSE_CONCENTRATION = "blood_glucose_concentration"
- """Blood glucose concentration.
-
- Unit of measurement: `mg/dL`, `mmol/L`
- """
-
CO = "carbon_monoxide"
"""Carbon Monoxide gas concentration.
@@ -190,7 +182,7 @@ class SensorDeviceClass(StrEnum):
Use this device class for sensors measuring energy consumption, for example
electric energy consumption.
- Unit of measurement: `J`, `kJ`, `MJ`, `GJ`, `Wh`, `kWh`, `MWh`, `GWh`, `TWh`, `cal`, `kcal`, `Mcal`, `Gcal`
+ Unit of measurement: `J`, `kJ`, `MJ`, `GJ`, `Wh`, `kWh`, `MWh`, `cal`, `kcal`, `Mcal`, `Gcal`
"""
ENERGY_STORAGE = "energy_storage"
@@ -199,7 +191,7 @@ class SensorDeviceClass(StrEnum):
Use this device class for sensors measuring stored energy, for example the amount
of electric energy currently stored in a battery or the capacity of a battery.
- Unit of measurement: `Wh`, `kWh`, `MWh`, `GWh`, `TWh`, `MJ`, `GJ`
+ Unit of measurement: `Wh`, `kWh`, `MWh`, `MJ`, `GJ`
"""
FREQUENCY = "frequency"
@@ -307,7 +299,7 @@ class SensorDeviceClass(StrEnum):
POWER = "power"
"""Power.
- Unit of measurement: `W`, `kW`, `MW`, `GW`, `TW`
+ Unit of measurement: `W`, `kW`
"""
PRECIPITATION = "precipitation"
@@ -501,7 +493,6 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass]
UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = {
SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter,
- SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: BloodGlucoseConcentrationConverter,
SensorDeviceClass.CONDUCTIVITY: ConductivityConverter,
SensorDeviceClass.CURRENT: ElectricCurrentConverter,
SensorDeviceClass.DATA_RATE: DataRateConverter,
@@ -533,7 +524,6 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.AQI: {None},
SensorDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure),
SensorDeviceClass.BATTERY: {PERCENTAGE},
- SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),
SensorDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION},
SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION},
SensorDeviceClass.CONDUCTIVITY: set(UnitOfConductivity),
@@ -609,7 +599,6 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = {
SensorDeviceClass.AQI: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.ATMOSPHERIC_PRESSURE: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.BATTERY: {SensorStateClass.MEASUREMENT},
- SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.CO: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.CO2: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.CONDUCTIVITY: {SensorStateClass.MEASUREMENT},
diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py
index 56ecb36adb3..f2b51899312 100644
--- a/homeassistant/components/sensor/device_condition.py
+++ b/homeassistant/components/sensor/device_condition.py
@@ -37,7 +37,6 @@ CONF_IS_APPARENT_POWER = "is_apparent_power"
CONF_IS_AQI = "is_aqi"
CONF_IS_ATMOSPHERIC_PRESSURE = "is_atmospheric_pressure"
CONF_IS_BATTERY_LEVEL = "is_battery_level"
-CONF_IS_BLOOD_GLUCOSE_CONCENTRATION = "is_blood_glucose_concentration"
CONF_IS_CO = "is_carbon_monoxide"
CONF_IS_CO2 = "is_carbon_dioxide"
CONF_IS_CONDUCTIVITY = "is_conductivity"
@@ -88,9 +87,6 @@ ENTITY_CONDITIONS = {
SensorDeviceClass.AQI: [{CONF_TYPE: CONF_IS_AQI}],
SensorDeviceClass.ATMOSPHERIC_PRESSURE: [{CONF_TYPE: CONF_IS_ATMOSPHERIC_PRESSURE}],
SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}],
- SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: [
- {CONF_TYPE: CONF_IS_BLOOD_GLUCOSE_CONCENTRATION}
- ],
SensorDeviceClass.CO: [{CONF_TYPE: CONF_IS_CO}],
SensorDeviceClass.CO2: [{CONF_TYPE: CONF_IS_CO2}],
SensorDeviceClass.CONDUCTIVITY: [{CONF_TYPE: CONF_IS_CONDUCTIVITY}],
@@ -155,7 +151,6 @@ CONDITION_SCHEMA = vol.All(
CONF_IS_AQI,
CONF_IS_ATMOSPHERIC_PRESSURE,
CONF_IS_BATTERY_LEVEL,
- CONF_IS_BLOOD_GLUCOSE_CONCENTRATION,
CONF_IS_CO,
CONF_IS_CO2,
CONF_IS_CONDUCTIVITY,
diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py
index ffee10d9f40..b07b3fac11e 100644
--- a/homeassistant/components/sensor/device_trigger.py
+++ b/homeassistant/components/sensor/device_trigger.py
@@ -36,7 +36,6 @@ CONF_APPARENT_POWER = "apparent_power"
CONF_AQI = "aqi"
CONF_ATMOSPHERIC_PRESSURE = "atmospheric_pressure"
CONF_BATTERY_LEVEL = "battery_level"
-CONF_BLOOD_GLUCOSE_CONCENTRATION = "blood_glucose_concentration"
CONF_CO = "carbon_monoxide"
CONF_CO2 = "carbon_dioxide"
CONF_CONDUCTIVITY = "conductivity"
@@ -87,9 +86,6 @@ ENTITY_TRIGGERS = {
SensorDeviceClass.AQI: [{CONF_TYPE: CONF_AQI}],
SensorDeviceClass.ATMOSPHERIC_PRESSURE: [{CONF_TYPE: CONF_ATMOSPHERIC_PRESSURE}],
SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}],
- SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: [
- {CONF_TYPE: CONF_BLOOD_GLUCOSE_CONCENTRATION}
- ],
SensorDeviceClass.CO: [{CONF_TYPE: CONF_CO}],
SensorDeviceClass.CO2: [{CONF_TYPE: CONF_CO2}],
SensorDeviceClass.CONDUCTIVITY: [{CONF_TYPE: CONF_CONDUCTIVITY}],
@@ -155,7 +151,6 @@ TRIGGER_SCHEMA = vol.All(
CONF_AQI,
CONF_ATMOSPHERIC_PRESSURE,
CONF_BATTERY_LEVEL,
- CONF_BLOOD_GLUCOSE_CONCENTRATION,
CONF_CO,
CONF_CO2,
CONF_CONDUCTIVITY,
diff --git a/homeassistant/components/sensor/icons.json b/homeassistant/components/sensor/icons.json
index ea4c902e665..6132fcbc1e9 100644
--- a/homeassistant/components/sensor/icons.json
+++ b/homeassistant/components/sensor/icons.json
@@ -12,9 +12,6 @@
"atmospheric_pressure": {
"default": "mdi:thermometer-lines"
},
- "blood_glucose_concentration": {
- "default": "mdi:spoon-sugar"
- },
"carbon_dioxide": {
"default": "mdi:molecule-co2"
},
diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json
index 6d529e72c3b..71bead342c4 100644
--- a/homeassistant/components/sensor/strings.json
+++ b/homeassistant/components/sensor/strings.json
@@ -6,7 +6,6 @@
"is_aqi": "Current {entity_name} air quality index",
"is_atmospheric_pressure": "Current {entity_name} atmospheric pressure",
"is_battery_level": "Current {entity_name} battery level",
- "is_blood_glucose_concentration": "Current {entity_name} blood glucose concentration",
"is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level",
"is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level",
"is_conductivity": "Current {entity_name} conductivity",
@@ -57,7 +56,6 @@
"aqi": "{entity_name} air quality index changes",
"atmospheric_pressure": "{entity_name} atmospheric pressure changes",
"battery_level": "{entity_name} battery level changes",
- "blood_glucose_concentration": "{entity_name} blood glucose concentration changes",
"carbon_monoxide": "{entity_name} carbon monoxide concentration changes",
"carbon_dioxide": "{entity_name} carbon dioxide concentration changes",
"conductivity": "{entity_name} conductivity changes",
@@ -151,9 +149,6 @@
"battery": {
"name": "Battery"
},
- "blood_glucose_concentration": {
- "name": "Blood glucose concentration"
- },
"carbon_monoxide": {
"name": "Carbon monoxide"
},
diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json
index 7729a67d7a1..0222a1c2884 100644
--- a/homeassistant/components/sensorpush/manifest.json
+++ b/homeassistant/components/sensorpush/manifest.json
@@ -17,5 +17,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/sensorpush",
"iot_class": "local_push",
- "requirements": ["sensorpush-ble==1.7.1"]
+ "requirements": ["sensorpush-ble==1.6.2"]
}
diff --git a/homeassistant/components/sentry/config_flow.py b/homeassistant/components/sentry/config_flow.py
index 2fead7c27cd..59cd1f3f0e9 100644
--- a/homeassistant/components/sentry/config_flow.py
+++ b/homeassistant/components/sentry/config_flow.py
@@ -49,7 +49,7 @@ class SentryConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> SentryOptionsFlow:
"""Get the options flow for this handler."""
- return SentryOptionsFlow()
+ return SentryOptionsFlow(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -78,6 +78,10 @@ class SentryConfigFlow(ConfigFlow, domain=DOMAIN):
class SentryOptionsFlow(OptionsFlow):
"""Handle Sentry options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize Sentry options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/senz/__init__.py b/homeassistant/components/senz/__init__.py
index c3238f7355f..bd4dfae4571 100644
--- a/homeassistant/components/senz/__init__.py
+++ b/homeassistant/components/senz/__init__.py
@@ -60,7 +60,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator: SENZDataUpdateCoordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=account.username,
update_interval=UPDATE_INTERVAL,
update_method=update_thermostats,
diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json
index af00a1fdfed..2f39644d6d3 100644
--- a/homeassistant/components/seven_segments/manifest.json
+++ b/homeassistant/components/seven_segments/manifest.json
@@ -4,5 +4,5 @@
"codeowners": ["@fabaff"],
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
"iot_class": "local_polling",
- "requirements": ["Pillow==11.0.0"]
+ "requirements": ["Pillow==10.4.0"]
}
diff --git a/homeassistant/components/seventeentrack/services.py b/homeassistant/components/seventeentrack/services.py
index 54c23e6d619..0833bc0a97b 100644
--- a/homeassistant/components/seventeentrack/services.py
+++ b/homeassistant/components/seventeentrack/services.py
@@ -1,8 +1,8 @@
"""Services for the seventeentrack integration."""
-from typing import Any, Final
+from typing import Final
-from pyseventeentrack.package import PACKAGE_STATUS_MAP, Package
+from pyseventeentrack.package import PACKAGE_STATUS_MAP
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@@ -81,7 +81,18 @@ def setup_services(hass: HomeAssistant) -> None:
return {
"packages": [
- package_to_dict(package)
+ {
+ ATTR_DESTINATION_COUNTRY: package.destination_country,
+ ATTR_ORIGIN_COUNTRY: package.origin_country,
+ ATTR_PACKAGE_TYPE: package.package_type,
+ ATTR_TRACKING_INFO_LANGUAGE: package.tracking_info_language,
+ ATTR_TRACKING_NUMBER: package.tracking_number,
+ ATTR_LOCATION: package.location,
+ ATTR_STATUS: package.status,
+ ATTR_TIMESTAMP: package.timestamp.isoformat(),
+ ATTR_INFO_TEXT: package.info_text,
+ ATTR_FRIENDLY_NAME: package.friendly_name,
+ }
for package in live_packages
if slugify(package.status) in package_states or package_states == []
]
@@ -99,22 +110,6 @@ def setup_services(hass: HomeAssistant) -> None:
await seventeen_coordinator.client.profile.archive_package(tracking_number)
- def package_to_dict(package: Package) -> dict[str, Any]:
- result = {
- ATTR_DESTINATION_COUNTRY: package.destination_country,
- ATTR_ORIGIN_COUNTRY: package.origin_country,
- ATTR_PACKAGE_TYPE: package.package_type,
- ATTR_TRACKING_INFO_LANGUAGE: package.tracking_info_language,
- ATTR_TRACKING_NUMBER: package.tracking_number,
- ATTR_LOCATION: package.location,
- ATTR_STATUS: package.status,
- ATTR_INFO_TEXT: package.info_text,
- ATTR_FRIENDLY_NAME: package.friendly_name,
- }
- if timestamp := package.timestamp:
- result[ATTR_TIMESTAMP] = timestamp.isoformat()
- return result
-
async def _validate_service(config_entry_id):
entry: ConfigEntry | None = hass.config_entries.async_get_entry(config_entry_id)
if not entry:
diff --git a/homeassistant/components/sfr_box/config_flow.py b/homeassistant/components/sfr_box/config_flow.py
index 629f6ad291f..a4f14e59069 100644
--- a/homeassistant/components/sfr_box/config_flow.py
+++ b/homeassistant/components/sfr_box/config_flow.py
@@ -9,7 +9,7 @@ from sfrbox_api.bridge import SFRBox
from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import selector
from homeassistant.helpers.httpx_client import get_async_client
@@ -37,6 +37,7 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
_box: SFRBox
_config: dict[str, Any] = {}
+ _reauth_entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, str] | None = None
@@ -87,16 +88,19 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN):
except SFRBoxAuthenticationError:
errors["base"] = "invalid_auth"
else:
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=user_input
+ if reauth_entry := self._reauth_entry:
+ data = {**reauth_entry.data, **user_input}
+ self.hass.config_entries.async_update_entry(reauth_entry, data=data)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(reauth_entry.entry_id)
)
+ return self.async_abort(reason="reauth_successful")
self._config.update(user_input)
return self.async_create_entry(title="SFR Box", data=self._config)
suggested_values: Mapping[str, Any] | None = user_input
- if self.source == SOURCE_REAUTH and not suggested_values:
- suggested_values = self._get_reauth_entry().data
+ if self._reauth_entry and not suggested_values:
+ suggested_values = self._reauth_entry.data
data_schema = self.add_suggested_values_to_schema(AUTH_SCHEMA, suggested_values)
return self.async_show_form(
@@ -113,5 +117,8 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle failed credentials."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
self._box = SFRBox(ip=entry_data[CONF_HOST], client=get_async_client(self.hass))
return await self.async_step_auth()
diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py
index 1daa4710f30..83caaeb4776 100644
--- a/homeassistant/components/shelly/config_flow.py
+++ b/homeassistant/components/shelly/config_flow.py
@@ -146,6 +146,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
port: int = DEFAULT_HTTP_PORT
info: dict[str, Any] = {}
device_info: dict[str, Any] = {}
+ entry: ConfigEntry
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -355,6 +356,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self.entry = self._get_reauth_entry()
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -362,9 +364,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
- reauth_entry = self._get_reauth_entry()
- host = reauth_entry.data[CONF_HOST]
- port = get_http_port(reauth_entry.data)
+ host = self.entry.data[CONF_HOST]
+ port = get_http_port(self.entry.data)
if user_input is not None:
try:
@@ -372,7 +373,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
except (DeviceConnectionError, InvalidAuthError):
return self.async_abort(reason="reauth_unsuccessful")
- if get_device_entry_gen(reauth_entry) != 1:
+ if get_device_entry_gen(self.entry) != 1:
user_input[CONF_USERNAME] = "admin"
try:
await validate_input(self.hass, host, port, info, user_input)
@@ -380,10 +381,10 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="reauth_unsuccessful")
return self.async_update_reload_and_abort(
- reauth_entry, data_updates=user_input
+ self.entry, data={**self.entry.data, **user_input}
)
- if get_device_entry_gen(reauth_entry) in BLOCK_GENERATIONS:
+ if get_device_entry_gen(self.entry) in BLOCK_GENERATIONS:
schema = {
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
@@ -399,12 +400,19 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle a reconfiguration flow initialized by the user."""
+ self.entry = self._get_reconfigure_entry()
+ self.host = self.entry.data[CONF_HOST]
+ self.port = self.entry.data.get(CONF_PORT, DEFAULT_HTTP_PORT)
+
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfiguration flow initialized by the user."""
errors = {}
- reconfigure_entry = self._get_reconfigure_entry()
- self.host = reconfigure_entry.data[CONF_HOST]
- self.port = reconfigure_entry.data.get(CONF_PORT, DEFAULT_HTTP_PORT)
if user_input is not None:
host = user_input[CONF_HOST]
@@ -416,23 +424,23 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
except CustomPortNotSupported:
errors["base"] = "custom_port_not_supported"
else:
- await self.async_set_unique_id(info[CONF_MAC])
- self._abort_if_unique_id_mismatch(reason="another_device")
+ if info[CONF_MAC] != self.entry.unique_id:
+ return self.async_abort(reason="another_device")
- return self.async_update_reload_and_abort(
- reconfigure_entry,
- data_updates={CONF_HOST: host, CONF_PORT: port},
- )
+ data = {**self.entry.data, CONF_HOST: host, CONF_PORT: port}
+ self.hass.config_entries.async_update_entry(self.entry, data=data)
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
+ return self.async_abort(reason="reconfigure_successful")
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST, default=self.host): str,
vol.Required(CONF_PORT, default=self.port): vol.Coerce(int),
}
),
- description_placeholders={"device_name": reconfigure_entry.title},
+ description_placeholders={"device_name": self.entry.title},
errors=errors,
)
@@ -444,7 +452,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
@classmethod
@callback
@@ -460,6 +468,10 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(OptionsFlow):
"""Handle the option flow for shelly."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py
index a66fbb20f48..6332e139244 100644
--- a/homeassistant/components/shelly/coordinator.py
+++ b/homeassistant/components/shelly/coordinator.py
@@ -603,7 +603,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
async def _async_update_data(self) -> None:
"""Fetch data."""
- if self.update_sleep_period() or self.hass.is_stopping:
+ if self.update_sleep_period():
return
if self.sleep_period:
diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json
index 38437fb2137..9530771c8f0 100644
--- a/homeassistant/components/shelly/manifest.json
+++ b/homeassistant/components/shelly/manifest.json
@@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["aioshelly"],
"quality_scale": "platinum",
- "requirements": ["aioshelly==12.0.1"],
+ "requirements": ["aioshelly==12.0.0"],
"zeroconf": [
{
"type": "_http._tcp.local.",
diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json
index 342a7418b2a..f76319eb08c 100644
--- a/homeassistant/components/shelly/strings.json
+++ b/homeassistant/components/shelly/strings.json
@@ -28,7 +28,7 @@
"confirm_discovery": {
"description": "Do you want to set up the {model} at {host}?\n\nBattery-powered devices that are password protected must be woken up before continuing with setting up.\nBattery-powered devices that are not password protected will be added when the device wakes up, you can now manually wake the device up using a button on it or wait for the next data update from the device."
},
- "reconfigure": {
+ "reconfigure_confirm": {
"description": "Update configuration for {device_name}.\n\nBefore setup, battery-powered devices must be woken up, you can now wake the device up using a button on it.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py
index 134704cb0ff..5ec223f53ad 100644
--- a/homeassistant/components/shelly/switch.py
+++ b/homeassistant/components/shelly/switch.py
@@ -66,13 +66,6 @@ RPC_VIRTUAL_SWITCH = RpcSwitchDescription(
sub_key="value",
)
-RPC_SCRIPT_SWITCH = RpcSwitchDescription(
- key="script",
- sub_key="running",
- entity_registry_enabled_default=False,
- entity_category=EntityCategory.CONFIG,
-)
-
async def async_setup_entry(
hass: HomeAssistant,
@@ -183,14 +176,6 @@ def async_setup_rpc_entry(
RpcVirtualSwitch,
)
- async_setup_rpc_attribute_entities(
- hass,
- config_entry,
- async_add_entities,
- {"script": RPC_SCRIPT_SWITCH},
- RpcScriptSwitch,
- )
-
# the user can remove virtual components from the device configuration, so we need
# to remove orphaned entities
virtual_switch_ids = get_virtual_component_ids(
@@ -205,17 +190,6 @@ def async_setup_rpc_entry(
"boolean",
)
- # if the script is removed, from the device configuration, we need
- # to remove orphaned entities
- async_remove_orphaned_entities(
- hass,
- config_entry.entry_id,
- coordinator.mac,
- SWITCH_PLATFORM,
- coordinator.device.status,
- "script",
- )
-
if not switch_ids:
return
@@ -343,23 +317,3 @@ class RpcVirtualSwitch(ShellyRpcAttributeEntity, SwitchEntity):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off relay."""
await self.call_rpc("Boolean.Set", {"id": self._id, "value": False})
-
-
-class RpcScriptSwitch(ShellyRpcAttributeEntity, SwitchEntity):
- """Entity that controls a script component on RPC based Shelly devices."""
-
- entity_description: RpcSwitchDescription
- _attr_has_entity_name = True
-
- @property
- def is_on(self) -> bool:
- """If switch is on."""
- return bool(self.status["running"])
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn on relay."""
- await self.call_rpc("Script.Start", {"id": self._id})
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn off relay."""
- await self.call_rpc("Script.Stop", {"id": self._id})
diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py
index f22547acf50..fb586ae8b85 100644
--- a/homeassistant/components/shelly/update.py
+++ b/homeassistant/components/shelly/update.py
@@ -238,8 +238,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
) -> None:
"""Initialize update entity."""
super().__init__(coordinator, key, attribute, description)
- self._ota_in_progress = False
- self._ota_progress_percentage: int | None = None
+ self._ota_in_progress: bool | int = False
self._attr_release_url = get_release_url(
coordinator.device.gen, coordinator.model, description.beta
)
@@ -257,12 +256,11 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
if self.in_progress is not False:
event_type = event["event"]
if event_type == OTA_BEGIN:
- self._ota_progress_percentage = 0
+ self._ota_in_progress = 0
elif event_type == OTA_PROGRESS:
- self._ota_progress_percentage = event["progress_percent"]
+ self._ota_in_progress = event["progress_percent"]
elif event_type in (OTA_ERROR, OTA_SUCCESS):
self._ota_in_progress = False
- self._ota_progress_percentage = None
self.async_write_ha_state()
@property
@@ -280,15 +278,10 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
return self.installed_version
@property
- def in_progress(self) -> bool:
+ def in_progress(self) -> bool | int:
"""Update installation in progress."""
return self._ota_in_progress
- @property
- def update_percentage(self) -> int | None:
- """Update installation progress."""
- return self._ota_progress_percentage
-
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
@@ -317,7 +310,6 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
await self.coordinator.async_shutdown_device_and_start_reauth()
else:
self._ota_in_progress = True
- self._ota_progress_percentage = None
LOGGER.debug("OTA update call for %s successful", self.coordinator.name)
diff --git a/homeassistant/components/shopping_list/intent.py b/homeassistant/components/shopping_list/intent.py
index 1a6370f4168..84ea3971293 100644
--- a/homeassistant/components/shopping_list/intent.py
+++ b/homeassistant/components/shopping_list/intent.py
@@ -29,7 +29,7 @@ class AddItemIntent(intent.IntentHandler):
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent."""
slots = self.async_validate_slots(intent_obj.slots)
- item = slots["item"]["value"].strip()
+ item = slots["item"]["value"]
await intent_obj.hass.data[DOMAIN].async_add(item)
response = intent_obj.create_response()
diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py
index 7ea878f538d..2b2a32ca67d 100644
--- a/homeassistant/components/sia/alarm_control_panel.py
+++ b/homeassistant/components/sia/alarm_control_panel.py
@@ -4,19 +4,25 @@ from __future__ import annotations
from dataclasses import dataclass
import logging
-from typing import TYPE_CHECKING
from pysiaalarm import SIAEvent
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityDescription,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+ STATE_UNAVAILABLE,
+)
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import StateType
from .const import CONF_ACCOUNT, CONF_ACCOUNTS, CONF_ZONES, KEY_ALARM, PREVIOUS_STATE
from .entity import SIABaseEntity, SIAEntityDescription
@@ -35,32 +41,31 @@ class SIAAlarmControlPanelEntityDescription(
ENTITY_DESCRIPTION_ALARM = SIAAlarmControlPanelEntityDescription(
key=KEY_ALARM,
code_consequences={
- "PA": AlarmControlPanelState.TRIGGERED,
- "JA": AlarmControlPanelState.TRIGGERED,
- "TA": AlarmControlPanelState.TRIGGERED,
- "BA": AlarmControlPanelState.TRIGGERED,
- "HA": AlarmControlPanelState.TRIGGERED,
- "CA": AlarmControlPanelState.ARMED_AWAY,
- "CB": AlarmControlPanelState.ARMED_AWAY,
- "CG": AlarmControlPanelState.ARMED_AWAY,
- "CL": AlarmControlPanelState.ARMED_AWAY,
- "CP": AlarmControlPanelState.ARMED_AWAY,
- "CQ": AlarmControlPanelState.ARMED_AWAY,
- "CS": AlarmControlPanelState.ARMED_AWAY,
- "CF": AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
- "NP": AlarmControlPanelState.DISARMED,
- "NO": AlarmControlPanelState.DISARMED,
- "OA": AlarmControlPanelState.DISARMED,
- "OB": AlarmControlPanelState.DISARMED,
- "OG": AlarmControlPanelState.DISARMED,
- "OP": AlarmControlPanelState.DISARMED,
- "OQ": AlarmControlPanelState.DISARMED,
- "OR": AlarmControlPanelState.DISARMED,
- "OS": AlarmControlPanelState.DISARMED,
- "NC": AlarmControlPanelState.ARMED_NIGHT,
- "NL": AlarmControlPanelState.ARMED_NIGHT,
- "NE": AlarmControlPanelState.ARMED_NIGHT,
- "NF": AlarmControlPanelState.ARMED_NIGHT,
+ "PA": STATE_ALARM_TRIGGERED,
+ "JA": STATE_ALARM_TRIGGERED,
+ "TA": STATE_ALARM_TRIGGERED,
+ "BA": STATE_ALARM_TRIGGERED,
+ "CA": STATE_ALARM_ARMED_AWAY,
+ "CB": STATE_ALARM_ARMED_AWAY,
+ "CG": STATE_ALARM_ARMED_AWAY,
+ "CL": STATE_ALARM_ARMED_AWAY,
+ "CP": STATE_ALARM_ARMED_AWAY,
+ "CQ": STATE_ALARM_ARMED_AWAY,
+ "CS": STATE_ALARM_ARMED_AWAY,
+ "CF": STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ "NP": STATE_ALARM_DISARMED,
+ "NO": STATE_ALARM_DISARMED,
+ "OA": STATE_ALARM_DISARMED,
+ "OB": STATE_ALARM_DISARMED,
+ "OG": STATE_ALARM_DISARMED,
+ "OP": STATE_ALARM_DISARMED,
+ "OQ": STATE_ALARM_DISARMED,
+ "OR": STATE_ALARM_DISARMED,
+ "OS": STATE_ALARM_DISARMED,
+ "NC": STATE_ALARM_ARMED_NIGHT,
+ "NL": STATE_ALARM_ARMED_NIGHT,
+ "NE": STATE_ALARM_ARMED_NIGHT,
+ "NF": STATE_ALARM_ARMED_NIGHT,
"BR": PREVIOUS_STATE,
},
)
@@ -104,17 +109,13 @@ class SIAAlarmControlPanel(SIABaseEntity, AlarmControlPanelEntity):
entity_description,
)
- self._attr_alarm_state: AlarmControlPanelState | None = None
- self._old_state: AlarmControlPanelState | None = None
+ self._attr_state: StateType = None
+ self._old_state: StateType = None
def handle_last_state(self, last_state: State | None) -> None:
"""Handle the last state."""
- self._attr_alarm_state = None
- if last_state is not None and last_state.state not in (
- STATE_UNAVAILABLE,
- STATE_UNKNOWN,
- ):
- self._attr_alarm_state = AlarmControlPanelState(last_state.state)
+ if last_state is not None:
+ self._attr_state = last_state.state
if self.state == STATE_UNAVAILABLE:
self._attr_available = False
@@ -131,7 +132,5 @@ class SIAAlarmControlPanel(SIABaseEntity, AlarmControlPanelEntity):
_LOGGER.debug("New state will be %s", new_state)
if new_state == PREVIOUS_STATE:
new_state = self._old_state
- if TYPE_CHECKING:
- assert isinstance(new_state, AlarmControlPanelState)
- self._attr_alarm_state, self._old_state = new_state, self._attr_alarm_state
+ self._attr_state, self._old_state = new_state, self._attr_state
return True
diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py
index a23978145e7..cb451133d41 100644
--- a/homeassistant/components/sia/config_flow.py
+++ b/homeassistant/components/sia/config_flow.py
@@ -181,6 +181,7 @@ class SIAOptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize SIA options flow."""
+ self.config_entry = config_entry
self.options = deepcopy(dict(config_entry.options))
self.hub: SIAHub | None = None
self.accounts_todo: list = []
diff --git a/homeassistant/components/sia/entity.py b/homeassistant/components/sia/entity.py
index 48af8e0beb4..aecac2b540b 100644
--- a/homeassistant/components/sia/entity.py
+++ b/homeassistant/components/sia/entity.py
@@ -8,7 +8,6 @@ import logging
from pysiaalarm import SIAEvent
-from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT
from homeassistant.core import CALLBACK_TYPE, State, callback
@@ -41,7 +40,7 @@ _LOGGER = logging.getLogger(__name__)
class SIARequiredKeysMixin:
"""Required keys for SIA entities."""
- code_consequences: dict[str, StateType | bool | AlarmControlPanelState]
+ code_consequences: dict[str, StateType | bool]
@dataclass(frozen=True)
diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json
index 7d08367cf7d..875c98acb6d 100644
--- a/homeassistant/components/sighthound/manifest.json
+++ b/homeassistant/components/sighthound/manifest.json
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/sighthound",
"iot_class": "cloud_polling",
"loggers": ["simplehound"],
- "requirements": ["Pillow==11.0.0", "simplehound==0.3"]
+ "requirements": ["Pillow==10.4.0", "simplehound==0.3"]
}
diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py
index 18f2d8ddcd5..478e5784e19 100644
--- a/homeassistant/components/simplisafe/alarm_control_panel.py
+++ b/homeassistant/components/simplisafe/alarm_control_panel.py
@@ -26,9 +26,16 @@ from simplipy.websocket import (
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -58,33 +65,33 @@ ATTR_WALL_POWER_LEVEL = "wall_power_level"
ATTR_WIFI_STRENGTH = "wifi_strength"
STATE_MAP_FROM_REST_API = {
- SystemStates.ALARM: AlarmControlPanelState.TRIGGERED,
- SystemStates.ALARM_COUNT: AlarmControlPanelState.PENDING,
- SystemStates.AWAY: AlarmControlPanelState.ARMED_AWAY,
- SystemStates.AWAY_COUNT: AlarmControlPanelState.ARMING,
- SystemStates.ENTRY_DELAY: AlarmControlPanelState.PENDING,
- SystemStates.EXIT_DELAY: AlarmControlPanelState.ARMING,
- SystemStates.HOME: AlarmControlPanelState.ARMED_HOME,
- SystemStates.HOME_COUNT: AlarmControlPanelState.ARMING,
- SystemStates.OFF: AlarmControlPanelState.DISARMED,
- SystemStates.TEST: AlarmControlPanelState.DISARMED,
+ SystemStates.ALARM: STATE_ALARM_TRIGGERED,
+ SystemStates.ALARM_COUNT: STATE_ALARM_PENDING,
+ SystemStates.AWAY: STATE_ALARM_ARMED_AWAY,
+ SystemStates.AWAY_COUNT: STATE_ALARM_ARMING,
+ SystemStates.ENTRY_DELAY: STATE_ALARM_PENDING,
+ SystemStates.EXIT_DELAY: STATE_ALARM_ARMING,
+ SystemStates.HOME: STATE_ALARM_ARMED_HOME,
+ SystemStates.HOME_COUNT: STATE_ALARM_ARMING,
+ SystemStates.OFF: STATE_ALARM_DISARMED,
+ SystemStates.TEST: STATE_ALARM_DISARMED,
}
STATE_MAP_FROM_WEBSOCKET_EVENT = {
- EVENT_ALARM_CANCELED: AlarmControlPanelState.DISARMED,
- EVENT_ALARM_TRIGGERED: AlarmControlPanelState.TRIGGERED,
- EVENT_ARMED_AWAY: AlarmControlPanelState.ARMED_AWAY,
- EVENT_ARMED_AWAY_BY_KEYPAD: AlarmControlPanelState.ARMED_AWAY,
- EVENT_ARMED_AWAY_BY_REMOTE: AlarmControlPanelState.ARMED_AWAY,
- EVENT_ARMED_HOME: AlarmControlPanelState.ARMED_HOME,
- EVENT_AWAY_EXIT_DELAY_BY_KEYPAD: AlarmControlPanelState.ARMING,
- EVENT_AWAY_EXIT_DELAY_BY_REMOTE: AlarmControlPanelState.ARMING,
- EVENT_DISARMED_BY_KEYPAD: AlarmControlPanelState.DISARMED,
- EVENT_DISARMED_BY_REMOTE: AlarmControlPanelState.DISARMED,
- EVENT_ENTRY_DELAY: AlarmControlPanelState.PENDING,
- EVENT_HOME_EXIT_DELAY: AlarmControlPanelState.ARMING,
- EVENT_SECRET_ALERT_TRIGGERED: AlarmControlPanelState.TRIGGERED,
- EVENT_USER_INITIATED_TEST: AlarmControlPanelState.DISARMED,
+ EVENT_ALARM_CANCELED: STATE_ALARM_DISARMED,
+ EVENT_ALARM_TRIGGERED: STATE_ALARM_TRIGGERED,
+ EVENT_ARMED_AWAY: STATE_ALARM_ARMED_AWAY,
+ EVENT_ARMED_AWAY_BY_KEYPAD: STATE_ALARM_ARMED_AWAY,
+ EVENT_ARMED_AWAY_BY_REMOTE: STATE_ALARM_ARMED_AWAY,
+ EVENT_ARMED_HOME: STATE_ALARM_ARMED_HOME,
+ EVENT_AWAY_EXIT_DELAY_BY_KEYPAD: STATE_ALARM_ARMING,
+ EVENT_AWAY_EXIT_DELAY_BY_REMOTE: STATE_ALARM_ARMING,
+ EVENT_DISARMED_BY_KEYPAD: STATE_ALARM_DISARMED,
+ EVENT_DISARMED_BY_REMOTE: STATE_ALARM_DISARMED,
+ EVENT_ENTRY_DELAY: STATE_ALARM_PENDING,
+ EVENT_HOME_EXIT_DELAY: STATE_ALARM_ARMING,
+ EVENT_SECRET_ALERT_TRIGGERED: STATE_ALARM_TRIGGERED,
+ EVENT_USER_INITIATED_TEST: STATE_ALARM_DISARMED,
}
WEBSOCKET_EVENTS_TO_LISTEN_FOR = (
@@ -138,9 +145,9 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
def _set_state_from_system_data(self) -> None:
"""Set the state based on the latest REST API data."""
if self._system.alarm_going_off:
- self._attr_alarm_state = AlarmControlPanelState.TRIGGERED
+ self._attr_state = STATE_ALARM_TRIGGERED
elif state := STATE_MAP_FROM_REST_API.get(self._system.state):
- self._attr_alarm_state = state
+ self._attr_state = state
self.async_reset_error_count()
else:
LOGGER.warning("Unexpected system state (REST API): %s", self._system.state)
@@ -155,7 +162,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
f'Error while disarming "{self._system.system_id}": {err}'
) from err
- self._attr_alarm_state = AlarmControlPanelState.DISARMED
+ self._attr_state = STATE_ALARM_DISARMED
self.async_write_ha_state()
async def async_alarm_arm_home(self, code: str | None = None) -> None:
@@ -167,7 +174,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
f'Error while arming (home) "{self._system.system_id}": {err}'
) from err
- self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
+ self._attr_state = STATE_ALARM_ARMED_HOME
self.async_write_ha_state()
async def async_alarm_arm_away(self, code: str | None = None) -> None:
@@ -179,7 +186,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
f'Error while arming (away) "{self._system.system_id}": {err}'
) from err
- self._attr_alarm_state = AlarmControlPanelState.ARMING
+ self._attr_state = STATE_ALARM_ARMING
self.async_write_ha_state()
@callback
@@ -223,7 +230,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
assert event.event_type
if state := STATE_MAP_FROM_WEBSOCKET_EVENT.get(event.event_type):
- self._attr_alarm_state = state
+ self._attr_state = state
self.async_reset_error_count()
else:
LOGGER.error("Unknown alarm websocket event: %s", event.event_type)
diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py
index 68974fe118f..6fdbd351a29 100644
--- a/homeassistant/components/simplisafe/config_flow.py
+++ b/homeassistant/components/simplisafe/config_flow.py
@@ -67,7 +67,7 @@ class SimpliSafeFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> SimpliSafeOptionsFlowHandler:
"""Define the config flow to handle options."""
- return SimpliSafeOptionsFlowHandler()
+ return SimpliSafeOptionsFlowHandler(config_entry)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
@@ -153,6 +153,10 @@ class SimpliSafeFlowHandler(ConfigFlow, domain=DOMAIN):
class SimpliSafeOptionsFlowHandler(OptionsFlow):
"""Handle a SimpliSafe options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/sky_remote/__init__.py b/homeassistant/components/sky_remote/__init__.py
deleted file mode 100644
index 4daad78c558..00000000000
--- a/homeassistant/components/sky_remote/__init__.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""The Sky Remote Control integration."""
-
-import logging
-
-from skyboxremote import RemoteControl, SkyBoxConnectionError
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST, CONF_PORT, Platform
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
-
-PLATFORMS = [Platform.REMOTE]
-
-_LOGGER = logging.getLogger(__name__)
-
-
-type SkyRemoteConfigEntry = ConfigEntry[RemoteControl]
-
-
-async def async_setup_entry(hass: HomeAssistant, entry: SkyRemoteConfigEntry) -> bool:
- """Set up Sky remote."""
- host = entry.data[CONF_HOST]
- port = entry.data[CONF_PORT]
-
- _LOGGER.debug("Setting up Host: %s, Port: %s", host, port)
- remote = RemoteControl(host, port)
- try:
- await remote.check_connectable()
- except SkyBoxConnectionError as e:
- raise ConfigEntryNotReady from e
-
- entry.runtime_data = remote
- await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
- return True
-
-
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
- """Unload a config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/sky_remote/config_flow.py b/homeassistant/components/sky_remote/config_flow.py
deleted file mode 100644
index a55dfb2a52b..00000000000
--- a/homeassistant/components/sky_remote/config_flow.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""Config flow for sky_remote."""
-
-import logging
-from typing import Any
-
-from skyboxremote import RemoteControl, SkyBoxConnectionError
-import voluptuous as vol
-
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_HOST, CONF_PORT
-import homeassistant.helpers.config_validation as cv
-
-from .const import DEFAULT_PORT, DOMAIN, LEGACY_PORT
-
-DATA_SCHEMA = vol.Schema(
- {
- vol.Required(CONF_HOST): cv.string,
- }
-)
-
-
-async def async_find_box_port(host: str) -> int:
- """Find port box uses for communication."""
- logging.debug("Attempting to find port to connect to %s on", host)
- remote = RemoteControl(host, DEFAULT_PORT)
- try:
- await remote.check_connectable()
- except SkyBoxConnectionError:
- # Try legacy port if the default one failed
- remote = RemoteControl(host, LEGACY_PORT)
- await remote.check_connectable()
- return LEGACY_PORT
- return DEFAULT_PORT
-
-
-class SkyRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
- """Handle a config flow for Sky Remote."""
-
- VERSION = 1
- MINOR_VERSION = 1
-
- async def async_step_user(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle the user step."""
-
- errors: dict[str, str] = {}
- if user_input is not None:
- logging.debug("user_input: %s", user_input)
- self._async_abort_entries_match(user_input)
- try:
- port = await async_find_box_port(user_input[CONF_HOST])
- except SkyBoxConnectionError:
- logging.exception("while finding port of skybox")
- errors["base"] = "cannot_connect"
- else:
- return self.async_create_entry(
- title=user_input[CONF_HOST],
- data={**user_input, CONF_PORT: port},
- )
-
- return self.async_show_form(
- step_id="user", data_schema=DATA_SCHEMA, errors=errors
- )
diff --git a/homeassistant/components/sky_remote/const.py b/homeassistant/components/sky_remote/const.py
deleted file mode 100644
index e67744a741b..00000000000
--- a/homeassistant/components/sky_remote/const.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""Constants."""
-
-DOMAIN = "sky_remote"
-
-DEFAULT_PORT = 49160
-LEGACY_PORT = 5900
diff --git a/homeassistant/components/sky_remote/manifest.json b/homeassistant/components/sky_remote/manifest.json
deleted file mode 100644
index b00ff309b10..00000000000
--- a/homeassistant/components/sky_remote/manifest.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "domain": "sky_remote",
- "name": "Sky Remote Control",
- "codeowners": ["@dunnmj", "@saty9"],
- "config_flow": true,
- "documentation": "https://www.home-assistant.io/integrations/sky_remote",
- "integration_type": "device",
- "iot_class": "assumed_state",
- "requirements": ["skyboxremote==0.0.6"]
-}
diff --git a/homeassistant/components/sky_remote/remote.py b/homeassistant/components/sky_remote/remote.py
deleted file mode 100644
index 05a464f73a6..00000000000
--- a/homeassistant/components/sky_remote/remote.py
+++ /dev/null
@@ -1,70 +0,0 @@
-"""Home Assistant integration to control a sky box using the remote platform."""
-
-from collections.abc import Iterable
-import logging
-from typing import Any
-
-from skyboxremote import VALID_KEYS, RemoteControl
-
-from homeassistant.components.remote import RemoteEntity
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ServiceValidationError
-from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import SkyRemoteConfigEntry
-from .const import DOMAIN
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config: SkyRemoteConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the Sky remote platform."""
- async_add_entities(
- [SkyRemote(config.runtime_data, config.entry_id)],
- True,
- )
-
-
-class SkyRemote(RemoteEntity):
- """Representation of a Sky Remote."""
-
- _attr_has_entity_name = True
- _attr_name = None
-
- def __init__(self, remote: RemoteControl, unique_id: str) -> None:
- """Initialize the Sky Remote."""
- self._remote = remote
- self._attr_unique_id = unique_id
- self._attr_device_info = DeviceInfo(
- identifiers={(DOMAIN, unique_id)},
- manufacturer="SKY",
- model="Sky Box",
- name=remote.host,
- )
-
- def turn_on(self, activity: str | None = None, **kwargs: Any) -> None:
- """Send the power on command."""
- self.send_command(["sky"])
-
- def turn_off(self, activity: str | None = None, **kwargs: Any) -> None:
- """Send the power command."""
- self.send_command(["power"])
-
- def send_command(self, command: Iterable[str], **kwargs: Any) -> None:
- """Send a list of commands to the device."""
- for cmd in command:
- if cmd not in VALID_KEYS:
- raise ServiceValidationError(
- f"{cmd} is not in Valid Keys: {VALID_KEYS}"
- )
- try:
- self._remote.send_keys(command)
- except ValueError as err:
- _LOGGER.error("Invalid command: %s. Error: %s", command, err)
- return
- _LOGGER.debug("Successfully sent command %s", command)
diff --git a/homeassistant/components/sky_remote/strings.json b/homeassistant/components/sky_remote/strings.json
deleted file mode 100644
index af794490c43..00000000000
--- a/homeassistant/components/sky_remote/strings.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "config": {
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
- },
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
- },
- "step": {
- "user": {
- "title": "Add Sky Remote",
- "data": {
- "host": "[%key:common::config_flow::data::host%]"
- },
- "data_description": {
- "host": "Hostname or IP address of your Sky device"
- }
- }
- }
- }
-}
diff --git a/homeassistant/components/skybell/config_flow.py b/homeassistant/components/skybell/config_flow.py
index a32441f4cf8..385f3dc39d7 100644
--- a/homeassistant/components/skybell/config_flow.py
+++ b/homeassistant/components/skybell/config_flow.py
@@ -34,11 +34,16 @@ class SkybellFlowHandler(ConfigFlow, domain=DOMAIN):
errors = {}
if user_input:
password = user_input[CONF_PASSWORD]
- _, error = await self._async_validate_input(self.reauth_email, password)
- if error is None:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=user_input
- )
+ entry_id = self.context["entry_id"]
+ if entry := self.hass.config_entries.async_get_entry(entry_id):
+ _, error = await self._async_validate_input(self.reauth_email, password)
+ if error is None:
+ self.hass.config_entries.async_update_entry(
+ entry,
+ data=entry.data | user_input,
+ )
+ await self.hass.config_entries.async_reload(entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
errors["base"] = error
return self.async_show_form(
diff --git a/homeassistant/components/sleepiq/config_flow.py b/homeassistant/components/sleepiq/config_flow.py
index 0a473404eb9..26f3672d588 100644
--- a/homeassistant/components/sleepiq/config_flow.py
+++ b/homeassistant/components/sleepiq/config_flow.py
@@ -9,7 +9,7 @@ from typing import Any
from asyncsleepiq import AsyncSleepIQ, SleepIQLoginException, SleepIQTimeoutException
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -24,6 +24,10 @@ class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ def __init__(self) -> None:
+ """Initialize the config flow."""
+ self._reauth_entry: ConfigEntry | None = None
+
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Import a SleepIQ account as a config entry.
@@ -80,6 +84,9 @@ class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -87,16 +94,19 @@ class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm reauth."""
errors: dict[str, str] = {}
-
- reauth_entry = self._get_reauth_entry()
+ assert self._reauth_entry is not None
if user_input is not None:
data = {
- CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
+ CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
if not (error := await try_connection(self.hass, data)):
- return self.async_update_reload_and_abort(reauth_entry, data=data)
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry, data=data
+ )
+ await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
errors["base"] = error
return self.async_show_form(
@@ -104,7 +114,7 @@ class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
errors=errors,
description_placeholders={
- CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
+ CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME],
},
)
diff --git a/homeassistant/components/slide/manifest.json b/homeassistant/components/slide/manifest.json
index 111bc9bd7a9..bb25e10658a 100644
--- a/homeassistant/components/slide/manifest.json
+++ b/homeassistant/components/slide/manifest.json
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/slide",
"iot_class": "cloud_polling",
"loggers": ["goslideapi"],
- "requirements": ["goslide-api==0.7.0"]
+ "requirements": ["goslide-api==0.5.1"]
}
diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py
index 37fb4d72284..d8a7929ae79 100644
--- a/homeassistant/components/sma/__init__.py
+++ b/homeassistant/components/sma/__init__.py
@@ -92,7 +92,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name="sma",
update_method=async_update_data,
update_interval=interval,
diff --git a/homeassistant/components/smappee/strings.json b/homeassistant/components/smappee/strings.json
index 2966b5cd753..2bdbf0dabe8 100644
--- a/homeassistant/components/smappee/strings.json
+++ b/homeassistant/components/smappee/strings.json
@@ -23,7 +23,6 @@
}
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
"already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py
index 1cd7df68e91..c6e466392f0 100644
--- a/homeassistant/components/smart_meter_texas/__init__.py
+++ b/homeassistant/components/smart_meter_texas/__init__.py
@@ -64,7 +64,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name="Smart Meter Texas",
update_method=async_update_data,
update_interval=SCAN_INTERVAL,
diff --git a/homeassistant/components/smarttub/config_flow.py b/homeassistant/components/smarttub/config_flow.py
index cf96d7082a1..5caff953d6d 100644
--- a/homeassistant/components/smarttub/config_flow.py
+++ b/homeassistant/components/smarttub/config_flow.py
@@ -3,12 +3,12 @@
from __future__ import annotations
from collections.abc import Mapping
-from typing import Any
+from typing import TYPE_CHECKING, Any
from smarttub import LoginFailed
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from .const import DOMAIN
@@ -24,6 +24,12 @@ class SmartTubConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ def __init__(self) -> None:
+ """Instantiate config flow."""
+ super().__init__()
+ self._reauth_input: Mapping[str, Any] | None = None
+ self._reauth_entry: ConfigEntry | None = None
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -42,17 +48,24 @@ class SmartTubConfigFlow(ConfigFlow, domain=DOMAIN):
else:
await self.async_set_unique_id(account.id)
- if self.source != SOURCE_REAUTH:
+ if self._reauth_input is None:
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_EMAIL], data=user_input
)
# this is a reauth attempt
- self._abort_if_unique_id_mismatch(reason="already_configured")
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=user_input
+ if TYPE_CHECKING:
+ assert self._reauth_entry
+ if self._reauth_entry.unique_id != self.unique_id:
+ # there is a config entry matching this account,
+ # but it is not the one we were trying to reauth
+ return self.async_abort(reason="already_configured")
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry, data=user_input
)
+ await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
@@ -62,6 +75,10 @@ class SmartTubConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Get new credentials if the current ones don't work anymore."""
+ self._reauth_input = entry_data
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -69,12 +86,13 @@ class SmartTubConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
if user_input is None:
+ if TYPE_CHECKING:
+ assert self._reauth_input is not None
# same as DATA_SCHEMA but with default email
data_schema = vol.Schema(
{
vol.Required(
- CONF_EMAIL,
- default=self._get_reauth_entry().data.get(CONF_EMAIL),
+ CONF_EMAIL, default=self._reauth_input.get(CONF_EMAIL)
): str,
vol.Required(CONF_PASSWORD): str,
}
diff --git a/homeassistant/components/smarty/__init__.py b/homeassistant/components/smarty/__init__.py
index 0d043804c3d..17c4bd0a26a 100644
--- a/homeassistant/components/smarty/__init__.py
+++ b/homeassistant/components/smarty/__init__.py
@@ -1,20 +1,23 @@
"""Support to control a Salda Smarty XP/XV ventilation unit."""
+from datetime import timedelta
import ipaddress
import logging
+from pysmarty2 import Smarty
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST, CONF_NAME, Platform
-from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
-from homeassistant.data_entry_flow import FlowResultType
-from homeassistant.helpers import issue_registry as ir
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import dispatcher_send
+from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType
-from .const import DOMAIN
-from .coordinator import SmartyConfigEntry, SmartyCoordinator
+DOMAIN = "smarty"
+DATA_SMARTY = "smarty"
+SMARTY_NAME = "Smarty"
_LOGGER = logging.getLogger(__name__)
@@ -23,84 +26,48 @@ CONFIG_SCHEMA = vol.Schema(
DOMAIN: vol.Schema(
{
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
- vol.Optional(CONF_NAME, default="Smarty"): cv.string,
+ vol.Optional(CONF_NAME, default=SMARTY_NAME): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
-PLATFORMS = [
- Platform.BINARY_SENSOR,
- Platform.BUTTON,
- Platform.FAN,
- Platform.SENSOR,
- Platform.SWITCH,
-]
+RPM = "rpm"
+SIGNAL_UPDATE_SMARTY = "smarty_update"
-async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
- """Create a smarty system."""
- if config := hass_config.get(DOMAIN):
- hass.async_create_task(_async_import(hass, config))
- return True
-
-
-async def _async_import(hass: HomeAssistant, config: ConfigType) -> None:
+def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the smarty environment."""
- if not hass.config_entries.async_entries(DOMAIN):
- # Start import flow
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_IMPORT}, data=config
- )
- if result["type"] == FlowResultType.ABORT:
- ir.async_create_issue(
- hass,
- DOMAIN,
- f"deprecated_yaml_import_issue_{result['reason']}",
- breaks_in_ha_version="2025.5.0",
- is_fixable=False,
- issue_domain=DOMAIN,
- severity=ir.IssueSeverity.WARNING,
- translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
- translation_placeholders={
- "domain": DOMAIN,
- "integration_title": "Smarty",
- },
- )
- return
+ conf = config[DOMAIN]
- ir.async_create_issue(
- hass,
- HOMEASSISTANT_DOMAIN,
- f"deprecated_yaml_{DOMAIN}",
- breaks_in_ha_version="2025.5.0",
- is_fixable=False,
- issue_domain=DOMAIN,
- severity=ir.IssueSeverity.WARNING,
- translation_key="deprecated_yaml",
- translation_placeholders={
- "domain": DOMAIN,
- "integration_title": "Smarty",
- },
- )
+ host = conf[CONF_HOST]
+ name = conf[CONF_NAME]
+ _LOGGER.debug("Name: %s, host: %s", name, host)
-async def async_setup_entry(hass: HomeAssistant, entry: SmartyConfigEntry) -> bool:
- """Set up the Smarty environment from a config entry."""
+ smarty = Smarty(host=host)
- coordinator = SmartyCoordinator(hass)
+ hass.data[DOMAIN] = {"api": smarty, "name": name}
- await coordinator.async_config_entry_first_refresh()
+ # Initial update
+ smarty.update()
- entry.runtime_data = coordinator
+ # Load platforms
+ discovery.load_platform(hass, Platform.FAN, DOMAIN, {}, config)
+ discovery.load_platform(hass, Platform.SENSOR, DOMAIN, {}, config)
+ discovery.load_platform(hass, Platform.BINARY_SENSOR, DOMAIN, {}, config)
- await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+ def poll_device_update(event_time):
+ """Update Smarty device."""
+ _LOGGER.debug("Updating Smarty device")
+ if smarty.update():
+ _LOGGER.debug("Update success")
+ dispatcher_send(hass, SIGNAL_UPDATE_SMARTY)
+ else:
+ _LOGGER.debug("Update failed")
+
+ track_time_interval(hass, poll_device_update, timedelta(seconds=30))
return True
-
-
-async def async_unload_entry(hass: HomeAssistant, entry: SmartyConfigEntry) -> bool:
- """Unload a config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py
index 213cb00d47c..b31c51244b8 100644
--- a/homeassistant/components/smarty/binary_sensor.py
+++ b/homeassistant/components/smarty/binary_sensor.py
@@ -2,8 +2,6 @@
from __future__ import annotations
-from collections.abc import Callable
-from dataclasses import dataclass
import logging
from pysmarty2 import Smarty
@@ -11,77 +9,104 @@ from pysmarty2 import Smarty
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
- BinarySensorEntityDescription,
)
-from homeassistant.core import HomeAssistant
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
-from .coordinator import SmartyConfigEntry, SmartyCoordinator
-from .entity import SmartyEntity
+from . import DOMAIN, SIGNAL_UPDATE_SMARTY
_LOGGER = logging.getLogger(__name__)
-@dataclass(frozen=True, kw_only=True)
-class SmartyBinarySensorEntityDescription(BinarySensorEntityDescription):
- """Class describing Smarty binary sensor entities."""
-
- value_fn: Callable[[Smarty], bool]
-
-
-ENTITIES: tuple[SmartyBinarySensorEntityDescription, ...] = (
- SmartyBinarySensorEntityDescription(
- key="alarm",
- translation_key="alarm",
- device_class=BinarySensorDeviceClass.PROBLEM,
- value_fn=lambda smarty: smarty.alarm,
- ),
- SmartyBinarySensorEntityDescription(
- key="warning",
- translation_key="warning",
- device_class=BinarySensorDeviceClass.PROBLEM,
- value_fn=lambda smarty: smarty.warning,
- ),
- SmartyBinarySensorEntityDescription(
- key="boost",
- translation_key="boost_state",
- value_fn=lambda smarty: smarty.boost,
- ),
-)
-
-
-async def async_setup_entry(
+async def async_setup_platform(
hass: HomeAssistant,
- entry: SmartyConfigEntry,
+ config: ConfigType,
async_add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Smarty Binary Sensor Platform."""
+ smarty: Smarty = hass.data[DOMAIN]["api"]
+ name: str = hass.data[DOMAIN]["name"]
- coordinator = entry.runtime_data
+ sensors = [
+ AlarmSensor(name, smarty),
+ WarningSensor(name, smarty),
+ BoostSensor(name, smarty),
+ ]
- async_add_entities(
- SmartyBinarySensor(coordinator, description) for description in ENTITIES
- )
+ async_add_entities(sensors, True)
-class SmartyBinarySensor(SmartyEntity, BinarySensorEntity):
+class SmartyBinarySensor(BinarySensorEntity):
"""Representation of a Smarty Binary Sensor."""
- entity_description: SmartyBinarySensorEntityDescription
+ _attr_should_poll = False
def __init__(
self,
- coordinator: SmartyCoordinator,
- entity_description: SmartyBinarySensorEntityDescription,
+ name: str,
+ device_class: BinarySensorDeviceClass | None,
+ smarty: Smarty,
) -> None:
"""Initialize the entity."""
- super().__init__(coordinator)
- self.entity_description = entity_description
- self._attr_unique_id = (
- f"{coordinator.config_entry.entry_id}_{entity_description.key}"
+ self._attr_name = name
+ self._attr_device_class = device_class
+ self._smarty = smarty
+
+ async def async_added_to_hass(self) -> None:
+ """Call to update."""
+ async_dispatcher_connect(self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback)
+
+ @callback
+ def _update_callback(self) -> None:
+ """Call update method."""
+ self.async_schedule_update_ha_state(True)
+
+
+class BoostSensor(SmartyBinarySensor):
+ """Boost State Binary Sensor."""
+
+ def __init__(self, name: str, smarty: Smarty) -> None:
+ """Alarm Sensor Init."""
+ super().__init__(name=f"{name} Boost State", device_class=None, smarty=smarty)
+
+ def update(self) -> None:
+ """Update state."""
+ _LOGGER.debug("Updating sensor %s", self._attr_name)
+ self._attr_is_on = self._smarty.boost
+
+
+class AlarmSensor(SmartyBinarySensor):
+ """Alarm Binary Sensor."""
+
+ def __init__(self, name: str, smarty: Smarty) -> None:
+ """Alarm Sensor Init."""
+ super().__init__(
+ name=f"{name} Alarm",
+ device_class=BinarySensorDeviceClass.PROBLEM,
+ smarty=smarty,
)
- @property
- def is_on(self) -> bool:
- """Return the state of the binary sensor."""
- return self.entity_description.value_fn(self.coordinator.client)
+ def update(self) -> None:
+ """Update state."""
+ _LOGGER.debug("Updating sensor %s", self._attr_name)
+ self._attr_is_on = self._smarty.alarm
+
+
+class WarningSensor(SmartyBinarySensor):
+ """Warning Sensor."""
+
+ def __init__(self, name: str, smarty: Smarty) -> None:
+ """Warning Sensor Init."""
+ super().__init__(
+ name=f"{name} Warning",
+ device_class=BinarySensorDeviceClass.PROBLEM,
+ smarty=smarty,
+ )
+
+ def update(self) -> None:
+ """Update state."""
+ _LOGGER.debug("Updating sensor %s", self._attr_name)
+ self._attr_is_on = self._smarty.warning
diff --git a/homeassistant/components/smarty/button.py b/homeassistant/components/smarty/button.py
deleted file mode 100644
index b8e31cf6fc8..00000000000
--- a/homeassistant/components/smarty/button.py
+++ /dev/null
@@ -1,74 +0,0 @@
-"""Platform to control a Salda Smarty XP/XV ventilation unit."""
-
-from __future__ import annotations
-
-from collections.abc import Callable
-from dataclasses import dataclass
-import logging
-from typing import Any
-
-from pysmarty2 import Smarty
-
-from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from .coordinator import SmartyConfigEntry, SmartyCoordinator
-from .entity import SmartyEntity
-
-_LOGGER = logging.getLogger(__name__)
-
-
-@dataclass(frozen=True, kw_only=True)
-class SmartyButtonDescription(ButtonEntityDescription):
- """Class describing Smarty button."""
-
- press_fn: Callable[[Smarty], bool | None]
-
-
-ENTITIES: tuple[SmartyButtonDescription, ...] = (
- SmartyButtonDescription(
- key="reset_filters_timer",
- translation_key="reset_filters_timer",
- press_fn=lambda smarty: smarty.reset_filters_timer(),
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: SmartyConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the Smarty Button Platform."""
-
- coordinator = entry.runtime_data
-
- async_add_entities(
- SmartyButton(coordinator, description) for description in ENTITIES
- )
-
-
-class SmartyButton(SmartyEntity, ButtonEntity):
- """Representation of a Smarty Button."""
-
- entity_description: SmartyButtonDescription
-
- def __init__(
- self,
- coordinator: SmartyCoordinator,
- entity_description: SmartyButtonDescription,
- ) -> None:
- """Initialize the entity."""
- super().__init__(coordinator)
- self.entity_description = entity_description
- self._attr_unique_id = (
- f"{coordinator.config_entry.entry_id}_{entity_description.key}"
- )
-
- async def async_press(self, **kwargs: Any) -> None:
- """Press the button."""
- await self.hass.async_add_executor_job(
- self.entity_description.press_fn, self.coordinator.client
- )
- await self.coordinator.async_refresh()
diff --git a/homeassistant/components/smarty/config_flow.py b/homeassistant/components/smarty/config_flow.py
deleted file mode 100644
index 9a55356a990..00000000000
--- a/homeassistant/components/smarty/config_flow.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""Config flow for Smarty integration."""
-
-from typing import Any
-
-from pysmarty2 import Smarty
-import voluptuous as vol
-
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_HOST, CONF_NAME
-
-from .const import DOMAIN
-
-
-class SmartyConfigFlow(ConfigFlow, domain=DOMAIN):
- """Smarty config flow."""
-
- def _test_connection(self, host: str) -> str | None:
- """Test the connection to the Smarty API."""
- smarty = Smarty(host=host)
- try:
- if smarty.update():
- return None
- except Exception: # noqa: BLE001
- return "unknown"
- else:
- return "cannot_connect"
-
- async def async_step_user(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle a flow initialized by the user."""
- errors: dict[str, str] = {}
-
- if user_input is not None:
- self._async_abort_entries_match(user_input)
- error = await self.hass.async_add_executor_job(
- self._test_connection, user_input[CONF_HOST]
- )
- if not error:
- return self.async_create_entry(
- title=user_input[CONF_HOST], data=user_input
- )
- errors["base"] = error
- return self.async_show_form(
- step_id="user",
- data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
- errors=errors,
- )
-
- async def async_step_import(
- self, import_config: dict[str, Any]
- ) -> ConfigFlowResult:
- """Handle a flow initialized by import."""
- error = await self.hass.async_add_executor_job(
- self._test_connection, import_config[CONF_HOST]
- )
- if not error:
- return self.async_create_entry(
- title=import_config[CONF_NAME],
- data={CONF_HOST: import_config[CONF_HOST]},
- )
- return self.async_abort(reason=error)
diff --git a/homeassistant/components/smarty/const.py b/homeassistant/components/smarty/const.py
deleted file mode 100644
index 926c4233750..00000000000
--- a/homeassistant/components/smarty/const.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""Constants for the Smarty component."""
-
-DOMAIN = "smarty"
diff --git a/homeassistant/components/smarty/coordinator.py b/homeassistant/components/smarty/coordinator.py
deleted file mode 100644
index d7f3e2452d1..00000000000
--- a/homeassistant/components/smarty/coordinator.py
+++ /dev/null
@@ -1,44 +0,0 @@
-"""Smarty Coordinator."""
-
-from datetime import timedelta
-import logging
-
-from pysmarty2 import Smarty
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-
-_LOGGER = logging.getLogger(__name__)
-
-type SmartyConfigEntry = ConfigEntry[SmartyCoordinator]
-
-
-class SmartyCoordinator(DataUpdateCoordinator[None]):
- """Smarty Coordinator."""
-
- config_entry: SmartyConfigEntry
- software_version: str
- configuration_version: str
-
- def __init__(self, hass: HomeAssistant) -> None:
- """Initialize."""
- super().__init__(
- hass,
- logger=_LOGGER,
- name="Smarty",
- update_interval=timedelta(seconds=30),
- )
- self.client = Smarty(host=self.config_entry.data[CONF_HOST])
-
- async def _async_setup(self) -> None:
- if not await self.hass.async_add_executor_job(self.client.update):
- raise UpdateFailed("Failed to update Smarty data")
- self.software_version = self.client.get_software_version()
- self.configuration_version = self.client.get_configuration_version()
-
- async def _async_update_data(self) -> None:
- """Fetch data from Smarty."""
- if not await self.hass.async_add_executor_job(self.client.update):
- raise UpdateFailed("Failed to update Smarty data")
diff --git a/homeassistant/components/smarty/entity.py b/homeassistant/components/smarty/entity.py
deleted file mode 100644
index d26b56d489f..00000000000
--- a/homeassistant/components/smarty/entity.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""Smarty Entity class."""
-
-from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from . import DOMAIN
-from .coordinator import SmartyCoordinator
-
-
-class SmartyEntity(CoordinatorEntity[SmartyCoordinator]):
- """Representation of a Smarty Entity."""
-
- _attr_has_entity_name = True
-
- def __init__(self, coordinator: SmartyCoordinator) -> None:
- """Initialize the entity."""
- super().__init__(coordinator)
- self._attr_device_info = DeviceInfo(
- identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
- manufacturer="Salda",
- sw_version=self.coordinator.software_version,
- hw_version=self.coordinator.configuration_version,
- )
diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py
index 378585a33e1..a2d72250197 100644
--- a/homeassistant/components/smarty/fan.py
+++ b/homeassistant/components/smarty/fan.py
@@ -6,19 +6,21 @@ import logging
import math
from typing import Any
+from pysmarty2 import Smarty
+
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.percentage import (
percentage_to_ranged_value,
ranged_value_to_percentage,
)
from homeassistant.util.scaling import int_states_in_range
-from . import SmartyConfigEntry
-from .coordinator import SmartyCoordinator
-from .entity import SmartyEntity
+from . import DOMAIN, SIGNAL_UPDATE_SMARTY
_LOGGER = logging.getLogger(__name__)
@@ -26,23 +28,24 @@ DEFAULT_ON_PERCENTAGE = 66
SPEED_RANGE = (1, 3) # off is not included
-async def async_setup_entry(
+async def async_setup_platform(
hass: HomeAssistant,
- entry: SmartyConfigEntry,
+ config: ConfigType,
async_add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Smarty Fan Platform."""
+ smarty: Smarty = hass.data[DOMAIN]["api"]
+ name: str = hass.data[DOMAIN]["name"]
- coordinator = entry.runtime_data
-
- async_add_entities([SmartyFan(coordinator)])
+ async_add_entities([SmartyFan(name, smarty)], True)
-class SmartyFan(SmartyEntity, FanEntity):
+class SmartyFan(FanEntity):
"""Representation of a Smarty Fan."""
- _attr_name = None
- _attr_translation_key = "fan"
+ _attr_icon = "mdi:air-conditioner"
+ _attr_should_poll = False
_attr_supported_features = (
FanEntityFeature.SET_SPEED
| FanEntityFeature.TURN_OFF
@@ -50,12 +53,11 @@ class SmartyFan(SmartyEntity, FanEntity):
)
_enable_turn_on_off_backwards_compatibility = False
- def __init__(self, coordinator: SmartyCoordinator) -> None:
+ def __init__(self, name, smarty):
"""Initialize the entity."""
- super().__init__(coordinator)
+ self._attr_name = name
self._smarty_fan_speed = 0
- self._smarty = coordinator.client
- self._attr_unique_id = coordinator.config_entry.entry_id
+ self._smarty = smarty
@property
def is_on(self) -> bool:
@@ -109,8 +111,17 @@ class SmartyFan(SmartyEntity, FanEntity):
self._smarty_fan_speed = 0
self.schedule_update_ha_state()
+ async def async_added_to_hass(self) -> None:
+ """Call to update fan."""
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback
+ )
+ )
+
@callback
- def _handle_coordinator_update(self) -> None:
+ def _update_callback(self) -> None:
"""Call update method."""
+ _LOGGER.debug("Updating state")
self._smarty_fan_speed = self._smarty.fan_speed
- super()._handle_coordinator_update()
+ self.async_write_ha_state()
diff --git a/homeassistant/components/smarty/icons.json b/homeassistant/components/smarty/icons.json
deleted file mode 100644
index 97e74199f0a..00000000000
--- a/homeassistant/components/smarty/icons.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "entity": {
- "fan": {
- "fan": {
- "default": "mdi:air-conditioner"
- }
- }
- }
-}
diff --git a/homeassistant/components/smarty/manifest.json b/homeassistant/components/smarty/manifest.json
index ca3133d8add..b83319b6744 100644
--- a/homeassistant/components/smarty/manifest.json
+++ b/homeassistant/components/smarty/manifest.json
@@ -2,7 +2,6 @@
"domain": "smarty",
"name": "Salda Smarty",
"codeowners": ["@z0mbieprocess"],
- "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/smarty",
"integration_type": "hub",
"iot_class": "local_polling",
diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py
index 9d847003a59..3c6873611b4 100644
--- a/homeassistant/components/smarty/sensor.py
+++ b/homeassistant/components/smarty/sensor.py
@@ -2,118 +2,182 @@
from __future__ import annotations
-from collections.abc import Callable
-from dataclasses import dataclass
-from datetime import datetime, timedelta
+import datetime as dt
import logging
from pysmarty2 import Smarty
-from homeassistant.components.sensor import (
- SensorDeviceClass,
- SensorEntity,
- SensorEntityDescription,
-)
-from homeassistant.const import REVOLUTIONS_PER_MINUTE, UnitOfTemperature
-from homeassistant.core import HomeAssistant
+from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
+from homeassistant.const import UnitOfTemperature
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.dt as dt_util
-from .coordinator import SmartyConfigEntry, SmartyCoordinator
-from .entity import SmartyEntity
+from . import DOMAIN, SIGNAL_UPDATE_SMARTY
_LOGGER = logging.getLogger(__name__)
-def get_filter_days_left(smarty: Smarty) -> datetime | None:
- """Return the date when the filter needs to be replaced."""
- if (days_left := smarty.filter_timer) is not None:
- return dt_util.now() + timedelta(days=days_left)
- return None
-
-
-@dataclass(frozen=True, kw_only=True)
-class SmartySensorDescription(SensorEntityDescription):
- """Class describing Smarty sensor."""
-
- value_fn: Callable[[Smarty], float | datetime | None]
-
-
-ENTITIES: tuple[SmartySensorDescription, ...] = (
- SmartySensorDescription(
- key="supply_air_temperature",
- translation_key="supply_air_temperature",
- device_class=SensorDeviceClass.TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- value_fn=lambda smarty: smarty.supply_air_temperature,
- ),
- SmartySensorDescription(
- key="extract_air_temperature",
- translation_key="extract_air_temperature",
- device_class=SensorDeviceClass.TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- value_fn=lambda smarty: smarty.extract_air_temperature,
- ),
- SmartySensorDescription(
- key="outdoor_air_temperature",
- translation_key="outdoor_air_temperature",
- device_class=SensorDeviceClass.TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- value_fn=lambda smarty: smarty.outdoor_air_temperature,
- ),
- SmartySensorDescription(
- key="supply_fan_speed",
- translation_key="supply_fan_speed",
- native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
- value_fn=lambda smarty: smarty.supply_fan_speed,
- ),
- SmartySensorDescription(
- key="extract_fan_speed",
- translation_key="extract_fan_speed",
- native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
- value_fn=lambda smarty: smarty.extract_fan_speed,
- ),
- SmartySensorDescription(
- key="filter_days_left",
- translation_key="filter_days_left",
- device_class=SensorDeviceClass.TIMESTAMP,
- value_fn=get_filter_days_left,
- ),
-)
-
-
-async def async_setup_entry(
+async def async_setup_platform(
hass: HomeAssistant,
- entry: SmartyConfigEntry,
+ config: ConfigType,
async_add_entities: AddEntitiesCallback,
+ discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Smarty Sensor Platform."""
+ smarty: Smarty = hass.data[DOMAIN]["api"]
+ name: str = hass.data[DOMAIN]["name"]
- coordinator = entry.runtime_data
+ sensors = [
+ SupplyAirTemperatureSensor(name, smarty),
+ ExtractAirTemperatureSensor(name, smarty),
+ OutdoorAirTemperatureSensor(name, smarty),
+ SupplyFanSpeedSensor(name, smarty),
+ ExtractFanSpeedSensor(name, smarty),
+ FilterDaysLeftSensor(name, smarty),
+ ]
- async_add_entities(
- SmartySensor(coordinator, description) for description in ENTITIES
- )
+ async_add_entities(sensors, True)
-class SmartySensor(SmartyEntity, SensorEntity):
+class SmartySensor(SensorEntity):
"""Representation of a Smarty Sensor."""
- entity_description: SmartySensorDescription
+ _attr_should_poll = False
def __init__(
self,
- coordinator: SmartyCoordinator,
- entity_description: SmartySensorDescription,
+ name: str,
+ device_class: SensorDeviceClass | None,
+ smarty: Smarty,
+ unit_of_measurement: str | None,
) -> None:
"""Initialize the entity."""
- super().__init__(coordinator)
- self.entity_description = entity_description
- self._attr_unique_id = (
- f"{coordinator.config_entry.entry_id}_{entity_description.key}"
+ self._attr_name = name
+ self._attr_native_value = None
+ self._attr_device_class = device_class
+ self._attr_native_unit_of_measurement = unit_of_measurement
+ self._smarty = smarty
+
+ async def async_added_to_hass(self) -> None:
+ """Call to update."""
+ async_dispatcher_connect(self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback)
+
+ @callback
+ def _update_callback(self) -> None:
+ """Call update method."""
+ self.async_schedule_update_ha_state(True)
+
+
+class SupplyAirTemperatureSensor(SmartySensor):
+ """Supply Air Temperature Sensor."""
+
+ def __init__(self, name: str, smarty: Smarty) -> None:
+ """Supply Air Temperature Init."""
+ super().__init__(
+ name=f"{name} Supply Air Temperature",
+ device_class=SensorDeviceClass.TEMPERATURE,
+ unit_of_measurement=UnitOfTemperature.CELSIUS,
+ smarty=smarty,
)
- @property
- def native_value(self) -> float | datetime | None:
- """Return the state of the sensor."""
- return self.entity_description.value_fn(self.coordinator.client)
+ def update(self) -> None:
+ """Update state."""
+ _LOGGER.debug("Updating sensor %s", self._attr_name)
+ self._attr_native_value = self._smarty.supply_air_temperature
+
+
+class ExtractAirTemperatureSensor(SmartySensor):
+ """Extract Air Temperature Sensor."""
+
+ def __init__(self, name: str, smarty: Smarty) -> None:
+ """Supply Air Temperature Init."""
+ super().__init__(
+ name=f"{name} Extract Air Temperature",
+ device_class=SensorDeviceClass.TEMPERATURE,
+ unit_of_measurement=UnitOfTemperature.CELSIUS,
+ smarty=smarty,
+ )
+
+ def update(self) -> None:
+ """Update state."""
+ _LOGGER.debug("Updating sensor %s", self._attr_name)
+ self._attr_native_value = self._smarty.extract_air_temperature
+
+
+class OutdoorAirTemperatureSensor(SmartySensor):
+ """Extract Air Temperature Sensor."""
+
+ def __init__(self, name: str, smarty: Smarty) -> None:
+ """Outdoor Air Temperature Init."""
+ super().__init__(
+ name=f"{name} Outdoor Air Temperature",
+ device_class=SensorDeviceClass.TEMPERATURE,
+ unit_of_measurement=UnitOfTemperature.CELSIUS,
+ smarty=smarty,
+ )
+
+ def update(self) -> None:
+ """Update state."""
+ _LOGGER.debug("Updating sensor %s", self._attr_name)
+ self._attr_native_value = self._smarty.outdoor_air_temperature
+
+
+class SupplyFanSpeedSensor(SmartySensor):
+ """Supply Fan Speed RPM."""
+
+ def __init__(self, name: str, smarty: Smarty) -> None:
+ """Supply Fan Speed RPM Init."""
+ super().__init__(
+ name=f"{name} Supply Fan Speed",
+ device_class=None,
+ unit_of_measurement=None,
+ smarty=smarty,
+ )
+
+ def update(self) -> None:
+ """Update state."""
+ _LOGGER.debug("Updating sensor %s", self._attr_name)
+ self._attr_native_value = self._smarty.supply_fan_speed
+
+
+class ExtractFanSpeedSensor(SmartySensor):
+ """Extract Fan Speed RPM."""
+
+ def __init__(self, name: str, smarty: Smarty) -> None:
+ """Extract Fan Speed RPM Init."""
+ super().__init__(
+ name=f"{name} Extract Fan Speed",
+ device_class=None,
+ unit_of_measurement=None,
+ smarty=smarty,
+ )
+
+ def update(self) -> None:
+ """Update state."""
+ _LOGGER.debug("Updating sensor %s", self._attr_name)
+ self._attr_native_value = self._smarty.extract_fan_speed
+
+
+class FilterDaysLeftSensor(SmartySensor):
+ """Filter Days Left."""
+
+ def __init__(self, name: str, smarty: Smarty) -> None:
+ """Filter Days Left Init."""
+ super().__init__(
+ name=f"{name} Filter Days Left",
+ device_class=SensorDeviceClass.TIMESTAMP,
+ unit_of_measurement=None,
+ smarty=smarty,
+ )
+ self._days_left = 91
+
+ def update(self) -> None:
+ """Update state."""
+ _LOGGER.debug("Updating sensor %s", self._attr_name)
+ days_left = self._smarty.filter_timer
+ if days_left is not None and days_left != self._days_left:
+ self._attr_native_value = dt_util.now() + dt.timedelta(days=days_left)
+ self._days_left = days_left
diff --git a/homeassistant/components/smarty/strings.json b/homeassistant/components/smarty/strings.json
deleted file mode 100644
index 341a300a26e..00000000000
--- a/homeassistant/components/smarty/strings.json
+++ /dev/null
@@ -1,80 +0,0 @@
-{
- "config": {
- "step": {
- "user": {
- "data": {
- "host": "[%key:common::config_flow::data::host%]"
- },
- "data_description": {
- "host": "The hostname or IP address of the Smarty device"
- }
- }
- },
- "error": {
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- },
- "abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "unknown": "[%key:common::config_flow::error::unknown%]"
- }
- },
- "issues": {
- "deprecated_yaml_import_issue_unknown": {
- "title": "YAML import failed with unknown error",
- "description": "Configuring {integration_title} using YAML is being removed but there was an unknown error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
- },
- "deprecated_yaml_import_issue_auth_error": {
- "title": "YAML import failed due to an authentication error",
- "description": "Configuring {integration_title} using YAML is being removed but there was an authentication error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
- },
- "deprecated_yaml_import_issue_cannot_connect": {
- "title": "YAML import failed due to a connection error",
- "description": "Configuring {integration_title} using YAML is being removed but there was a connect error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
- }
- },
- "entity": {
- "binary_sensor": {
- "alarm": {
- "name": "Alarm"
- },
- "warning": {
- "name": "Warning"
- },
- "boost_state": {
- "name": "Boost state"
- }
- },
- "button": {
- "reset_filters_timer": {
- "name": "Reset filters timer"
- }
- },
- "sensor": {
- "supply_air_temperature": {
- "name": "Supply air temperature"
- },
- "extract_air_temperature": {
- "name": "Extract air temperature"
- },
- "outdoor_air_temperature": {
- "name": "Outdoor air temperature"
- },
- "supply_fan_speed": {
- "name": "Supply fan speed"
- },
- "extract_fan_speed": {
- "name": "Extract fan speed"
- },
- "filter_days_left": {
- "name": "Filter days left"
- }
- },
- "switch": {
- "boost": {
- "name": "Boost"
- }
- }
- }
-}
diff --git a/homeassistant/components/smarty/switch.py b/homeassistant/components/smarty/switch.py
deleted file mode 100644
index bf5fe80db44..00000000000
--- a/homeassistant/components/smarty/switch.py
+++ /dev/null
@@ -1,90 +0,0 @@
-"""Platform to control a Salda Smarty XP/XV ventilation unit."""
-
-from __future__ import annotations
-
-from collections.abc import Callable
-from dataclasses import dataclass
-import logging
-from typing import Any
-
-from pysmarty2 import Smarty
-
-from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from .coordinator import SmartyConfigEntry, SmartyCoordinator
-from .entity import SmartyEntity
-
-_LOGGER = logging.getLogger(__name__)
-
-
-@dataclass(frozen=True, kw_only=True)
-class SmartySwitchDescription(SwitchEntityDescription):
- """Class describing Smarty switch."""
-
- is_on_fn: Callable[[Smarty], bool]
- turn_on_fn: Callable[[Smarty], bool | None]
- turn_off_fn: Callable[[Smarty], bool | None]
-
-
-ENTITIES: tuple[SmartySwitchDescription, ...] = (
- SmartySwitchDescription(
- key="boost",
- translation_key="boost",
- is_on_fn=lambda smarty: smarty.boost,
- turn_on_fn=lambda smarty: smarty.enable_boost(),
- turn_off_fn=lambda smarty: smarty.disable_boost(),
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: SmartyConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the Smarty Switch Platform."""
-
- coordinator = entry.runtime_data
-
- async_add_entities(
- SmartySwitch(coordinator, description) for description in ENTITIES
- )
-
-
-class SmartySwitch(SmartyEntity, SwitchEntity):
- """Representation of a Smarty Switch."""
-
- entity_description: SmartySwitchDescription
-
- def __init__(
- self,
- coordinator: SmartyCoordinator,
- entity_description: SmartySwitchDescription,
- ) -> None:
- """Initialize the entity."""
- super().__init__(coordinator)
- self.entity_description = entity_description
- self._attr_unique_id = (
- f"{coordinator.config_entry.entry_id}_{entity_description.key}"
- )
-
- @property
- def is_on(self) -> bool:
- """Return the state of the switch."""
- return self.entity_description.is_on_fn(self.coordinator.client)
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn the switch on."""
- await self.hass.async_add_executor_job(
- self.entity_description.turn_on_fn, self.coordinator.client
- )
- await self.coordinator.async_refresh()
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn the switch off."""
- await self.hass.async_add_executor_job(
- self.entity_description.turn_off_fn, self.coordinator.client
- )
- await self.coordinator.async_refresh()
diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py
index 2992b176f24..6ce7964a1d6 100644
--- a/homeassistant/components/smhi/config_flow.py
+++ b/homeassistant/components/smhi/config_flow.py
@@ -82,6 +82,12 @@ class SmhiFlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle a reconfiguration flow initialized by the user."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfiguration flow initialized by the user."""
errors: dict[str, str] = {}
@@ -126,5 +132,5 @@ class SmhiFlowHandler(ConfigFlow, domain=DOMAIN):
reconfigure_entry.data,
)
return self.async_show_form(
- step_id="reconfigure", data_schema=schema, errors=errors
+ step_id="reconfigure_confirm", data_schema=schema, errors=errors
)
diff --git a/homeassistant/components/smhi/strings.json b/homeassistant/components/smhi/strings.json
index 3d2a790e6b6..e78fee64a2b 100644
--- a/homeassistant/components/smhi/strings.json
+++ b/homeassistant/components/smhi/strings.json
@@ -12,7 +12,7 @@
"longitude": "[%key:common::config_flow::data::longitude%]"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"title": "Reconfigure your location in Sweden",
"data": {
"latitude": "[%key:common::config_flow::data::latitude%]",
diff --git a/homeassistant/components/smlight/config_flow.py b/homeassistant/components/smlight/config_flow.py
index 32efc729dc2..0e5b0f49d7b 100644
--- a/homeassistant/components/smlight/config_flow.py
+++ b/homeassistant/components/smlight/config_flow.py
@@ -15,6 +15,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNA
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
+from . import SmConfigEntry
from .const import DOMAIN
STEP_USER_DATA_SCHEMA = vol.Schema(
@@ -38,6 +39,7 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN):
"""Initialize the config flow."""
self.client: Api2
self.host: str | None = None
+ self._reauth_entry: SmConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -138,6 +140,9 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle reauth when API Authentication failed."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
host = entry_data[CONF_HOST]
self.client = Api2(host, session=async_get_clientsession(self.hass))
self.host = host
@@ -159,8 +164,11 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN):
except SmlightConnectionError:
return self.async_abort(reason="cannot_connect")
else:
+ assert self._reauth_entry is not None
+
return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=user_input
+ self._reauth_entry,
+ data={**self._reauth_entry.data, **user_input},
)
return self.async_show_form(
diff --git a/homeassistant/components/smlight/update.py b/homeassistant/components/smlight/update.py
index 147b1d766ef..cb28a197860 100644
--- a/homeassistant/components/smlight/update.py
+++ b/homeassistant/components/smlight/update.py
@@ -23,7 +23,6 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import SmConfigEntry
-from .const import LOGGER
from .coordinator import SmFirmwareUpdateCoordinator, SmFwData
from .entity import SmEntity
@@ -154,21 +153,19 @@ class SmUpdateEntity(SmEntity, UpdateEntity):
"""Update install progress on event."""
progress = int(progress.data)
- self._attr_update_percentage = progress
- self.async_write_ha_state()
+ if progress > 1:
+ self._attr_in_progress = progress
+ self.async_write_ha_state()
def _update_done(self) -> None:
"""Handle cleanup for update done."""
self._finished_event.set()
+ self.coordinator.in_progress = False
for remove_cb in self._unload:
remove_cb()
self._unload.clear()
- self._attr_in_progress = False
- self._attr_update_percentage = None
- self.async_write_ha_state()
-
@callback
def _update_finished(self, event: MessageEvent) -> None:
"""Handle event for update finished."""
@@ -178,7 +175,7 @@ class SmUpdateEntity(SmEntity, UpdateEntity):
@callback
def _update_failed(self, event: MessageEvent) -> None:
self._update_done()
- self.coordinator.in_progress = False
+
raise HomeAssistantError(f"Update failed for {self.name}")
async def async_install(
@@ -189,7 +186,6 @@ class SmUpdateEntity(SmEntity, UpdateEntity):
if not self.coordinator.in_progress and self._firmware:
self.coordinator.in_progress = True
self._attr_in_progress = True
- self._attr_update_percentage = None
self.register_callbacks()
await self.coordinator.client.fw_update(self._firmware)
@@ -197,20 +193,5 @@ class SmUpdateEntity(SmEntity, UpdateEntity):
# block until update finished event received
await self._finished_event.wait()
- # allow time for SLZB-06 to reboot before updating coordinator data
- try:
- async with asyncio.timeout(180):
- while (
- self.coordinator.in_progress
- and self.installed_version != self._firmware.ver
- ):
- await self.coordinator.async_refresh()
- await asyncio.sleep(1)
- except TimeoutError:
- LOGGER.warning(
- "Timeout waiting for %s to reboot after update",
- self.coordinator.data.info.hostname,
- )
-
- self.coordinator.in_progress = False
+ await self.coordinator.async_refresh()
self._finished_event.clear()
diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py
index a61f825aa5e..e90b5986596 100644
--- a/homeassistant/components/solarlog/config_flow.py
+++ b/homeassistant/components/solarlog/config_flow.py
@@ -137,6 +137,12 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle a reconfiguration flow initialized by the user."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfiguration flow initialized by the user."""
reconfigure_entry = self._get_reconfigure_entry()
@@ -158,7 +164,7 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=vol.Schema(
{
vol.Optional(
diff --git a/homeassistant/components/solarlog/coordinator.py b/homeassistant/components/solarlog/coordinator.py
index 5fdf89c9e74..51199ab7051 100644
--- a/homeassistant/components/solarlog/coordinator.py
+++ b/homeassistant/components/solarlog/coordinator.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-from collections.abc import Callable
from datetime import timedelta
import logging
from typing import TYPE_CHECKING
@@ -19,11 +18,7 @@ from solarlog_cli.solarlog_models import SolarlogData
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
-import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-from homeassistant.util import slugify
-
-from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -40,9 +35,6 @@ class SolarLogCoordinator(DataUpdateCoordinator[SolarlogData]):
hass, _LOGGER, name="SolarLog", update_interval=timedelta(seconds=60)
)
- self.new_device_callbacks: list[Callable[[int], None]] = []
- self._devices_last_update: set[tuple[int, str]] = set()
-
host_entry = entry.data[CONF_HOST]
password = entry.data.get("password", "")
@@ -65,8 +57,7 @@ class SolarLogCoordinator(DataUpdateCoordinator[SolarlogData]):
_LOGGER.debug("Start async_setup")
logged_in = False
if self.solarlog.password != "":
- if logged_in := await self.renew_authentication():
- await self.solarlog.test_extended_data_available()
+ logged_in = await self.renew_authentication()
if logged_in or await self.solarlog.test_extended_data_available():
device_list = await self.solarlog.update_device_list()
self.solarlog.set_enabled_devices({key: True for key in device_list})
@@ -93,55 +84,8 @@ class SolarLogCoordinator(DataUpdateCoordinator[SolarlogData]):
_LOGGER.debug("Data successfully updated")
- if self.solarlog.extended_data:
- self._async_add_remove_devices(data)
- _LOGGER.debug("Add_remove_devices finished")
-
return data
- def _async_add_remove_devices(self, data: SolarlogData) -> None:
- """Add new devices, remove non-existing devices."""
- if (
- current_devices := {
- (k, self.solarlog.device_name(k)) for k in data.inverter_data
- }
- ) == self._devices_last_update:
- return
-
- # remove old devices
- if removed_devices := self._devices_last_update - current_devices:
- _LOGGER.debug("Removed device(s): %s", ", ".join(map(str, removed_devices)))
- device_registry = dr.async_get(self.hass)
-
- for removed_device in removed_devices:
- device_name = ""
- for did, dn in self._devices_last_update:
- if did == removed_device[0]:
- device_name = dn
- break
- if device := device_registry.async_get_device(
- identifiers={
- (
- DOMAIN,
- f"{self.unique_id}_{slugify(device_name)}",
- )
- }
- ):
- device_registry.async_update_device(
- device_id=device.id,
- remove_config_entry_id=self.unique_id,
- )
- _LOGGER.debug("Device removed from device registry: %s", device.id)
-
- # add new devices
- if new_devices := current_devices - self._devices_last_update:
- _LOGGER.debug("New device(s) found: %s", ", ".join(map(str, new_devices)))
- for device_id in new_devices:
- for callback in self.new_device_callbacks:
- callback(device_id[0])
-
- self._devices_last_update = current_devices
-
async def renew_authentication(self) -> bool:
"""Renew access token for SolarLog API."""
logged_in = False
diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json
index 9f80b749d08..274c97c76b5 100644
--- a/homeassistant/components/solarlog/manifest.json
+++ b/homeassistant/components/solarlog/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/solarlog",
"iot_class": "local_polling",
"loggers": ["solarlog_cli"],
- "requirements": ["solarlog_cli==0.3.2"]
+ "requirements": ["solarlog_cli==0.3.1"]
}
diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py
index bcff5d57e1b..91e18da1cb2 100644
--- a/homeassistant/components/solarlog/sensor.py
+++ b/homeassistant/components/solarlog/sensor.py
@@ -87,7 +87,6 @@ SOLARLOG_SENSOR_TYPES: tuple[SolarLogCoordinatorSensorEntityDescription, ...] =
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
- state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=3,
value_fn=lambda data: data.yield_day,
),
@@ -106,7 +105,6 @@ SOLARLOG_SENSOR_TYPES: tuple[SolarLogCoordinatorSensorEntityDescription, ...] =
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
- state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=3,
value_fn=lambda data: data.yield_month,
),
@@ -116,7 +114,6 @@ SOLARLOG_SENSOR_TYPES: tuple[SolarLogCoordinatorSensorEntityDescription, ...] =
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
- state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda data: data.yield_year,
),
SolarLogCoordinatorSensorEntityDescription(
@@ -143,7 +140,6 @@ SOLARLOG_SENSOR_TYPES: tuple[SolarLogCoordinatorSensorEntityDescription, ...] =
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
- state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=3,
value_fn=lambda data: data.consumption_day,
),
@@ -162,7 +158,6 @@ SOLARLOG_SENSOR_TYPES: tuple[SolarLogCoordinatorSensorEntityDescription, ...] =
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
- state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=3,
value_fn=lambda data: data.consumption_month,
),
@@ -172,7 +167,6 @@ SOLARLOG_SENSOR_TYPES: tuple[SolarLogCoordinatorSensorEntityDescription, ...] =
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
- state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=3,
value_fn=lambda data: data.consumption_year,
),
@@ -199,7 +193,6 @@ SOLARLOG_SENSOR_TYPES: tuple[SolarLogCoordinatorSensorEntityDescription, ...] =
translation_key="total_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
- state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.total_power,
),
SolarLogCoordinatorSensorEntityDescription(
@@ -254,9 +247,7 @@ INVERTER_SENSOR_TYPES: tuple[SolarLogInverterSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
- value_fn=(
- lambda inverter: None if inverter is None else inverter.current_power
- ),
+ value_fn=lambda inverter: inverter.current_power,
),
SolarLogInverterSensorEntityDescription(
key="consumption_year",
@@ -264,10 +255,11 @@ INVERTER_SENSOR_TYPES: tuple[SolarLogInverterSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
- state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=3,
value_fn=(
- lambda inverter: None if inverter is None else inverter.consumption_year
+ lambda inverter: None
+ if inverter.consumption_year is None
+ else inverter.consumption_year
),
),
)
@@ -297,14 +289,6 @@ async def async_setup_entry(
async_add_entities(entities)
- def _async_add_new_device(device_id: int) -> None:
- async_add_entities(
- SolarLogInverterSensor(coordinator, sensor, device_id)
- for sensor in INVERTER_SENSOR_TYPES
- )
-
- coordinator.new_device_callbacks.append(_async_add_new_device)
-
class SolarLogCoordinatorSensor(SolarLogCoordinatorEntity, SensorEntity):
"""Represents a SolarLog sensor."""
diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json
index 723af6cb277..89c41194859 100644
--- a/homeassistant/components/solarlog/strings.json
+++ b/homeassistant/components/solarlog/strings.json
@@ -29,7 +29,7 @@
"password": "[%key:common::config_flow::data::password%]"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"title": "Configure SolarLog",
"data": {
"has_password": "[%key:component::solarlog::config::step::user::data::has_password%]",
diff --git a/homeassistant/components/solax/__init__.py b/homeassistant/components/solax/__init__.py
index 3b9df623559..253f3b55e0a 100644
--- a/homeassistant/components/solax/__init__.py
+++ b/homeassistant/components/solax/__init__.py
@@ -54,7 +54,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: SolaxConfigEntry) -> boo
coordinator = SolaxDataUpdateCoordinator(
hass,
logger=_LOGGER,
- config_entry=entry,
name=f"solax {entry.title}",
update_interval=SCAN_INTERVAL,
update_method=_async_update,
diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py
index c2d85160175..705db43362e 100644
--- a/homeassistant/components/somfy_mylink/config_flow.py
+++ b/homeassistant/components/somfy_mylink/config_flow.py
@@ -130,6 +130,7 @@ class OptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
+ self.config_entry = config_entry
self.options = deepcopy(dict(config_entry.options))
self._target_id: str | None = None
diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py
index e1cedba10e7..84bae85571e 100644
--- a/homeassistant/components/sonarr/config_flow.py
+++ b/homeassistant/components/sonarr/config_flow.py
@@ -13,7 +13,6 @@ import voluptuous as vol
import yarl
from homeassistant.config_entries import (
- SOURCE_REAUTH,
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
@@ -59,16 +58,22 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 2
+ def __init__(self) -> None:
+ """Initialize the flow."""
+ self.entry: ConfigEntry | None = None
+
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> SonarrOptionsFlowHandler:
"""Get the options flow for this handler."""
- return SonarrOptionsFlowHandler()
+ return SonarrOptionsFlowHandler(config_entry)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -76,11 +81,10 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
if user_input is None:
+ assert self.entry is not None
return self.async_show_form(
step_id="reauth_confirm",
- description_placeholders={
- "url": self._get_reauth_entry().data[CONF_URL]
- },
+ description_placeholders={"url": self.entry.data[CONF_URL]},
errors={},
)
@@ -93,15 +97,8 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
errors = {}
if user_input is not None:
- # aiopyarr defaults to the service port if one isn't given
- # this is counter to standard practice where http = 80
- # and https = 443.
- if CONF_URL in user_input:
- url = yarl.URL(user_input[CONF_URL])
- user_input[CONF_URL] = f"{url.scheme}://{url.host}:{url.port}{url.path}"
-
- if self.source == SOURCE_REAUTH:
- user_input = {**self._get_reauth_entry().data, **user_input}
+ if self.entry:
+ user_input = {**self.entry.data, **user_input}
if CONF_VERIFY_SSL not in user_input:
user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL
@@ -116,10 +113,8 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
else:
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=user_input
- )
+ if self.entry:
+ return await self._async_reauth_update_entry(user_input)
parsed = yarl.URL(user_input[CONF_URL])
@@ -134,9 +129,19 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
+ async def _async_reauth_update_entry(
+ self, data: dict[str, Any]
+ ) -> ConfigFlowResult:
+ """Update existing config entry."""
+ assert self.entry is not None
+ self.hass.config_entries.async_update_entry(self.entry, data=data)
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
+
+ return self.async_abort(reason="reauth_successful")
+
def _get_user_data_schema(self) -> dict[vol.Marker, type]:
"""Get the data schema to display user form."""
- if self.source == SOURCE_REAUTH:
+ if self.entry:
return {vol.Required(CONF_API_KEY): str}
data_schema: dict[vol.Marker, type] = {
@@ -155,6 +160,10 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
class SonarrOptionsFlowHandler(OptionsFlow):
"""Handle Sonarr client options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, int] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json
index 76a7d0bfa91..d6c5eb298d8 100644
--- a/homeassistant/components/sonos/manifest.json
+++ b/homeassistant/components/sonos/manifest.json
@@ -8,7 +8,7 @@
"documentation": "https://www.home-assistant.io/integrations/sonos",
"iot_class": "local_push",
"loggers": ["soco"],
- "requirements": ["soco==0.30.6", "sonos-websocket==0.1.3"],
+ "requirements": ["soco==0.30.4", "sonos-websocket==0.1.3"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py
index 44e0572c9e9..7e584ff5e63 100644
--- a/homeassistant/components/spc/alarm_control_panel.py
+++ b/homeassistant/components/spc/alarm_control_panel.py
@@ -9,7 +9,13 @@ from pyspcwebgw.const import AreaMode
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
+)
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -19,17 +25,17 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DATA_API, SIGNAL_UPDATE_ALARM
-def _get_alarm_state(area: Area) -> AlarmControlPanelState | None:
+def _get_alarm_state(area: Area) -> str | None:
"""Get the alarm state."""
if area.verified_alarm:
- return AlarmControlPanelState.TRIGGERED
+ return STATE_ALARM_TRIGGERED
mode_to_state = {
- AreaMode.UNSET: AlarmControlPanelState.DISARMED,
- AreaMode.PART_SET_A: AlarmControlPanelState.ARMED_HOME,
- AreaMode.PART_SET_B: AlarmControlPanelState.ARMED_NIGHT,
- AreaMode.FULL_SET: AlarmControlPanelState.ARMED_AWAY,
+ AreaMode.UNSET: STATE_ALARM_DISARMED,
+ AreaMode.PART_SET_A: STATE_ALARM_ARMED_HOME,
+ AreaMode.PART_SET_B: STATE_ALARM_ARMED_NIGHT,
+ AreaMode.FULL_SET: STATE_ALARM_ARMED_AWAY,
}
return mode_to_state.get(area.mode)
@@ -85,7 +91,7 @@ class SpcAlarm(AlarmControlPanelEntity):
return self._area.last_changed_by
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the device."""
return _get_alarm_state(self._area)
diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py
index e4c51ab7aa0..aed1cce33db 100644
--- a/homeassistant/components/speedtestdotnet/__init__.py
+++ b/homeassistant/components/speedtestdotnet/__init__.py
@@ -6,7 +6,7 @@ from functools import partial
import speedtest
-from homeassistant.config_entries import ConfigEntry, ConfigEntryState
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
@@ -35,10 +35,7 @@ async def async_setup_entry(
async def _async_finish_startup(hass: HomeAssistant) -> None:
"""Run this only when HA has finished its startup."""
- if config_entry.state is ConfigEntryState.LOADED:
- await coordinator.async_refresh()
- else:
- await coordinator.async_config_entry_first_refresh()
+ await coordinator.async_config_entry_first_refresh()
# Don't start a speedtest during startup
async_at_started(hass, _async_finish_startup)
diff --git a/homeassistant/components/speedtestdotnet/config_flow.py b/homeassistant/components/speedtestdotnet/config_flow.py
index 3bfd4eb6e4a..dc64448bbef 100644
--- a/homeassistant/components/speedtestdotnet/config_flow.py
+++ b/homeassistant/components/speedtestdotnet/config_flow.py
@@ -30,7 +30,7 @@ class SpeedTestFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: SpeedTestConfigEntry,
) -> SpeedTestOptionsFlowHandler:
"""Get the options flow for this handler."""
- return SpeedTestOptionsFlowHandler()
+ return SpeedTestOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -48,8 +48,9 @@ class SpeedTestFlowHandler(ConfigFlow, domain=DOMAIN):
class SpeedTestOptionsFlowHandler(OptionsFlow):
"""Handle SpeedTest options."""
- def __init__(self) -> None:
+ def __init__(self, config_entry: SpeedTestConfigEntry) -> None:
"""Initialize options flow."""
+ self.config_entry = config_entry
self._servers: dict = {}
async def async_step_init(
diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py
index cfcc9011b37..4a0409df383 100644
--- a/homeassistant/components/spotify/__init__.py
+++ b/homeassistant/components/spotify/__init__.py
@@ -3,16 +3,16 @@
from __future__ import annotations
from datetime import timedelta
-from typing import TYPE_CHECKING
+from typing import Any
import aiohttp
-from spotifyaio import Device, SpotifyClient, SpotifyConnectionError
+import requests
+from spotipy import Spotify, SpotifyException
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_ACCESS_TOKEN, Platform
+from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
@@ -21,7 +21,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .browse_media import async_browse_media
from .const import DOMAIN, LOGGER, SPOTIFY_SCOPES
-from .coordinator import SpotifyConfigEntry, SpotifyCoordinator
+from .coordinator import SpotifyCoordinator
from .models import SpotifyData
from .util import (
is_spotify_media_type,
@@ -29,7 +29,7 @@ from .util import (
spotify_uri_from_media_browser_url,
)
-PLATFORMS = [Platform.MEDIA_PLAYER, Platform.SENSOR]
+PLATFORMS = [Platform.MEDIA_PLAYER]
__all__ = [
"async_browse_media",
@@ -40,6 +40,9 @@ __all__ = [
]
+type SpotifyConfigEntry = ConfigEntry[SpotifyData]
+
+
async def async_setup_entry(hass: HomeAssistant, entry: SpotifyConfigEntry) -> bool:
"""Set up Spotify from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)
@@ -50,36 +53,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: SpotifyConfigEntry) -> b
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
- spotify = SpotifyClient(async_get_clientsession(hass))
+ spotify = Spotify(auth=session.token["access_token"])
- spotify.authenticate(session.token[CONF_ACCESS_TOKEN])
-
- async def _refresh_token() -> str:
- await session.async_ensure_token_valid()
- token = session.token[CONF_ACCESS_TOKEN]
- if TYPE_CHECKING:
- assert isinstance(token, str)
- return token
-
- spotify.refresh_token_function = _refresh_token
-
- coordinator = SpotifyCoordinator(hass, spotify)
+ coordinator = SpotifyCoordinator(hass, spotify, session)
await coordinator.async_config_entry_first_refresh()
- async def _update_devices() -> list[Device]:
+ async def _update_devices() -> list[dict[str, Any]]:
+ if not session.valid_token:
+ await session.async_ensure_token_valid()
+ await hass.async_add_executor_job(
+ spotify.set_auth, session.token["access_token"]
+ )
+
try:
- return await spotify.get_devices()
- except SpotifyConnectionError as err:
+ devices: dict[str, Any] | None = await hass.async_add_executor_job(
+ spotify.devices
+ )
+ except (requests.RequestException, SpotifyException) as err:
raise UpdateFailed from err
- device_coordinator: DataUpdateCoordinator[list[Device]] = DataUpdateCoordinator(
- hass,
- LOGGER,
- name=f"{entry.title} Devices",
- config_entry=entry,
- update_interval=timedelta(minutes=5),
- update_method=_update_devices,
+ if devices is None:
+ return []
+
+ return devices.get("devices", [])
+
+ device_coordinator: DataUpdateCoordinator[list[dict[str, Any]]] = (
+ DataUpdateCoordinator(
+ hass,
+ LOGGER,
+ name=f"{entry.title} Devices",
+ update_interval=timedelta(minutes=5),
+ update_method=_update_devices,
+ )
)
await device_coordinator.async_config_entry_first_refresh()
diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py
index 403ec608a7c..58b14e1183a 100644
--- a/homeassistant/components/spotify/browse_media.py
+++ b/homeassistant/components/spotify/browse_media.py
@@ -3,17 +3,11 @@
from __future__ import annotations
from enum import StrEnum
+from functools import partial
import logging
-from typing import TYPE_CHECKING, Any, TypedDict
+from typing import Any
-from spotifyaio import (
- Artist,
- BasePlaylist,
- SimplifiedAlbum,
- SimplifiedTrack,
- SpotifyClient,
- Track,
-)
+from spotipy import Spotify
import yarl
from homeassistant.components.media_player import (
@@ -24,6 +18,7 @@ from homeassistant.components.media_player import (
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
+from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from .const import DOMAIN, MEDIA_PLAYER_PREFIX, MEDIA_TYPE_SHOW, PLAYABLE_MEDIA_TYPES
from .util import fetch_image_url
@@ -34,62 +29,6 @@ BROWSE_LIMIT = 48
_LOGGER = logging.getLogger(__name__)
-class ItemPayload(TypedDict):
- """TypedDict for item payload."""
-
- name: str
- type: str
- uri: str
- id: str | None
- thumbnail: str | None
-
-
-def _get_artist_item_payload(artist: Artist) -> ItemPayload:
- return {
- "id": artist.artist_id,
- "name": artist.name,
- "type": MediaType.ARTIST,
- "uri": artist.uri,
- "thumbnail": fetch_image_url(artist.images),
- }
-
-
-def _get_album_item_payload(album: SimplifiedAlbum) -> ItemPayload:
- return {
- "id": album.album_id,
- "name": album.name,
- "type": MediaType.ALBUM,
- "uri": album.uri,
- "thumbnail": fetch_image_url(album.images),
- }
-
-
-def _get_playlist_item_payload(playlist: BasePlaylist) -> ItemPayload:
- return {
- "id": playlist.playlist_id,
- "name": playlist.name,
- "type": MediaType.PLAYLIST,
- "uri": playlist.uri,
- "thumbnail": fetch_image_url(playlist.images),
- }
-
-
-def _get_track_item_payload(
- track: SimplifiedTrack, show_thumbnails: bool = True
-) -> ItemPayload:
- return {
- "id": track.track_id,
- "name": track.name,
- "type": MediaType.TRACK,
- "uri": track.uri,
- "thumbnail": (
- fetch_image_url(track.album.images)
- if show_thumbnails and isinstance(track, Track)
- else None
- ),
- }
-
-
class BrowsableMedia(StrEnum):
"""Enum of browsable media."""
@@ -253,12 +192,14 @@ async def async_browse_media(
result = await async_browse_media_internal(
hass,
info.coordinator.client,
+ info.session,
+ info.coordinator.current_user,
media_content_type,
media_content_id,
can_play_artist=can_play_artist,
)
- # Build new URLs with config entry specifiers
+ # Build new URLs with config entry specifyers
result.media_content_id = str(parsed_url.with_name(result.media_content_id))
if result.children:
for child in result.children:
@@ -268,7 +209,9 @@ async def async_browse_media(
async def async_browse_media_internal(
hass: HomeAssistant,
- spotify: SpotifyClient,
+ spotify: Spotify,
+ session: OAuth2Session,
+ current_user: dict[str, Any],
media_content_type: str | None,
media_content_id: str | None,
*,
@@ -276,7 +219,15 @@ async def async_browse_media_internal(
) -> BrowseMedia:
"""Browse spotify media."""
if media_content_type in (None, f"{MEDIA_PLAYER_PREFIX}library"):
- return await library_payload(can_play_artist=can_play_artist)
+ return await hass.async_add_executor_job(
+ partial(library_payload, can_play_artist=can_play_artist)
+ )
+
+ if not session.valid_token:
+ await session.async_ensure_token_valid()
+ await hass.async_add_executor_job(
+ spotify.set_auth, session.token["access_token"]
+ )
# Strip prefix
if media_content_type:
@@ -286,18 +237,23 @@ async def async_browse_media_internal(
"media_content_type": media_content_type,
"media_content_id": media_content_id,
}
- response = await build_item_response(
- spotify,
- payload,
- can_play_artist=can_play_artist,
+ response = await hass.async_add_executor_job(
+ partial(
+ build_item_response,
+ spotify,
+ current_user,
+ payload,
+ can_play_artist=can_play_artist,
+ )
)
if response is None:
raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}")
return response
-async def build_item_response( # noqa: C901
- spotify: SpotifyClient,
+def build_item_response( # noqa: C901
+ spotify: Spotify,
+ user: dict[str, Any],
payload: dict[str, str | None],
*,
can_play_artist: bool,
@@ -309,119 +265,80 @@ async def build_item_response( # noqa: C901
if media_content_type is None or media_content_id is None:
return None
- title: str | None = None
- image: str | None = None
- items: list[ItemPayload] = []
+ title = None
+ image = None
+ media: dict[str, Any] | None = None
+ items = []
if media_content_type == BrowsableMedia.CURRENT_USER_PLAYLISTS:
- if playlists := await spotify.get_playlists_for_current_user():
- items = [_get_playlist_item_payload(playlist) for playlist in playlists]
+ if media := spotify.current_user_playlists(limit=BROWSE_LIMIT):
+ items = media.get("items", [])
elif media_content_type == BrowsableMedia.CURRENT_USER_FOLLOWED_ARTISTS:
- if artists := await spotify.get_followed_artists():
- items = [_get_artist_item_payload(artist) for artist in artists]
+ if media := spotify.current_user_followed_artists(limit=BROWSE_LIMIT):
+ items = media.get("artists", {}).get("items", [])
elif media_content_type == BrowsableMedia.CURRENT_USER_SAVED_ALBUMS:
- if saved_albums := await spotify.get_saved_albums():
- items = [
- _get_album_item_payload(saved_album.album)
- for saved_album in saved_albums
- ]
+ if media := spotify.current_user_saved_albums(limit=BROWSE_LIMIT):
+ items = [item["album"] for item in media.get("items", [])]
elif media_content_type == BrowsableMedia.CURRENT_USER_SAVED_TRACKS:
- if saved_tracks := await spotify.get_saved_tracks():
- items = [
- _get_track_item_payload(saved_track.track)
- for saved_track in saved_tracks
- ]
+ if media := spotify.current_user_saved_tracks(limit=BROWSE_LIMIT):
+ items = [item["track"] for item in media.get("items", [])]
elif media_content_type == BrowsableMedia.CURRENT_USER_SAVED_SHOWS:
- if saved_shows := await spotify.get_saved_shows():
- items = [
- {
- "id": saved_show.show.show_id,
- "name": saved_show.show.name,
- "type": MEDIA_TYPE_SHOW,
- "uri": saved_show.show.uri,
- "thumbnail": fetch_image_url(saved_show.show.images),
- }
- for saved_show in saved_shows
- ]
+ if media := spotify.current_user_saved_shows(limit=BROWSE_LIMIT):
+ items = [item["show"] for item in media.get("items", [])]
elif media_content_type == BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED:
- if recently_played_tracks := await spotify.get_recently_played_tracks():
- items = [
- _get_track_item_payload(item.track) for item in recently_played_tracks
- ]
+ if media := spotify.current_user_recently_played(limit=BROWSE_LIMIT):
+ items = [item["track"] for item in media.get("items", [])]
elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_ARTISTS:
- if top_artists := await spotify.get_top_artists():
- items = [_get_artist_item_payload(artist) for artist in top_artists]
+ if media := spotify.current_user_top_artists(limit=BROWSE_LIMIT):
+ items = media.get("items", [])
elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS:
- if top_tracks := await spotify.get_top_tracks():
- items = [_get_track_item_payload(track) for track in top_tracks]
+ if media := spotify.current_user_top_tracks(limit=BROWSE_LIMIT):
+ items = media.get("items", [])
elif media_content_type == BrowsableMedia.FEATURED_PLAYLISTS:
- if featured_playlists := await spotify.get_featured_playlists():
- items = [
- _get_playlist_item_payload(playlist) for playlist in featured_playlists
- ]
+ if media := spotify.featured_playlists(
+ country=user["country"], limit=BROWSE_LIMIT
+ ):
+ items = media.get("playlists", {}).get("items", [])
elif media_content_type == BrowsableMedia.CATEGORIES:
- if categories := await spotify.get_categories():
- items = [
- {
- "id": category.category_id,
- "name": category.name,
- "type": "category_playlists",
- "uri": category.category_id,
- "thumbnail": category.icons[0].url if category.icons else None,
- }
- for category in categories
- ]
+ if media := spotify.categories(country=user["country"], limit=BROWSE_LIMIT):
+ items = media.get("categories", {}).get("items", [])
elif media_content_type == "category_playlists":
if (
- playlists := await spotify.get_category_playlists(
- category_id=media_content_id
+ media := spotify.category_playlists(
+ category_id=media_content_id,
+ country=user["country"],
+ limit=BROWSE_LIMIT,
)
- ) and (category := await spotify.get_category(media_content_id)):
- title = category.name
- image = category.icons[0].url if category.icons else None
- items = [_get_playlist_item_payload(playlist) for playlist in playlists]
+ ) and (category := spotify.category(media_content_id, country=user["country"])):
+ title = category.get("name")
+ image = fetch_image_url(category, key="icons")
+ items = media.get("playlists", {}).get("items", [])
elif media_content_type == BrowsableMedia.NEW_RELEASES:
- if new_releases := await spotify.get_new_releases():
- items = [_get_album_item_payload(album) for album in new_releases]
+ if media := spotify.new_releases(country=user["country"], limit=BROWSE_LIMIT):
+ items = media.get("albums", {}).get("items", [])
elif media_content_type == MediaType.PLAYLIST:
- if playlist := await spotify.get_playlist(media_content_id):
- title = playlist.name
- image = playlist.images[0].url if playlist.images else None
- items = [
- _get_track_item_payload(playlist_track.track)
- for playlist_track in playlist.tracks.items
- ]
+ if media := spotify.playlist(media_content_id):
+ items = [item["track"] for item in media.get("tracks", {}).get("items", [])]
elif media_content_type == MediaType.ALBUM:
- if album := await spotify.get_album(media_content_id):
- title = album.name
- image = album.images[0].url if album.images else None
- items = [
- _get_track_item_payload(track, show_thumbnails=False)
- for track in album.tracks
- ]
+ if media := spotify.album(media_content_id):
+ items = media.get("tracks", {}).get("items", [])
elif media_content_type == MediaType.ARTIST:
- if (artist_albums := await spotify.get_artist_albums(media_content_id)) and (
- artist := await spotify.get_artist(media_content_id)
+ if (media := spotify.artist_albums(media_content_id, limit=BROWSE_LIMIT)) and (
+ artist := spotify.artist(media_content_id)
):
- title = artist.name
- image = artist.images[0].url if artist.images else None
- items = [_get_album_item_payload(album) for album in artist_albums]
+ title = artist.get("name")
+ image = fetch_image_url(artist)
+ items = media.get("items", [])
elif media_content_type == MEDIA_TYPE_SHOW:
- if (show_episodes := await spotify.get_show_episodes(media_content_id)) and (
- show := await spotify.get_show(media_content_id)
+ if (media := spotify.show_episodes(media_content_id, limit=BROWSE_LIMIT)) and (
+ show := spotify.show(media_content_id)
):
- title = show.name
- image = show.images[0].url if show.images else None
- items = [
- {
- "id": episode.episode_id,
- "name": episode.name,
- "type": MediaType.EPISODE,
- "uri": episode.uri,
- "thumbnail": fetch_image_url(episode.images),
- }
- for episode in show_episodes
- ]
+ title = show.get("name")
+ image = fetch_image_url(show)
+ items = media.get("items", [])
+
+ if media is None:
+ return None
try:
media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type]
@@ -442,7 +359,9 @@ async def build_item_response( # noqa: C901
media_item.children = []
for item in items:
- if (item_id := item["id"]) is None:
+ try:
+ item_id = item["id"]
+ except KeyError:
_LOGGER.debug("Missing ID for media item: %s", item)
continue
media_item.children.append(
@@ -453,21 +372,21 @@ async def build_item_response( # noqa: C901
media_class=MediaClass.PLAYLIST,
media_content_id=item_id,
media_content_type=f"{MEDIA_PLAYER_PREFIX}category_playlists",
- thumbnail=item["thumbnail"],
- title=item["name"],
+ thumbnail=fetch_image_url(item, key="icons"),
+ title=item.get("name"),
)
)
return media_item
if title is None:
title = LIBRARY_MAP.get(media_content_id, "Unknown")
+ if "name" in media:
+ title = media["name"]
can_play = media_content_type in PLAYABLE_MEDIA_TYPES and (
media_content_type != MediaType.ARTIST or can_play_artist
)
- if TYPE_CHECKING:
- assert title
browse_media = BrowseMedia(
can_expand=True,
can_play=can_play,
@@ -488,16 +407,23 @@ async def build_item_response( # noqa: C901
except (MissingMediaInformation, UnknownMediaType):
continue
+ if "images" in media:
+ browse_media.thumbnail = fetch_image_url(media)
+
return browse_media
-def item_payload(item: ItemPayload, *, can_play_artist: bool) -> BrowseMedia:
+def item_payload(item: dict[str, Any], *, can_play_artist: bool) -> BrowseMedia:
"""Create response payload for a single media item.
Used by async_browse_media.
"""
- media_type = item["type"]
- media_id = item["uri"]
+ try:
+ media_type = item["type"]
+ media_id = item["uri"]
+ except KeyError as err:
+ _LOGGER.debug("Missing type or URI for media item: %s", item)
+ raise MissingMediaInformation from err
try:
media_class = CONTENT_TYPE_MEDIA_CLASS[media_type]
@@ -514,19 +440,25 @@ def item_payload(item: ItemPayload, *, can_play_artist: bool) -> BrowseMedia:
media_type != MediaType.ARTIST or can_play_artist
)
- return BrowseMedia(
+ browse_media = BrowseMedia(
can_expand=can_expand,
can_play=can_play,
children_media_class=media_class["children"],
media_class=media_class["parent"],
media_content_id=media_id,
media_content_type=f"{MEDIA_PLAYER_PREFIX}{media_type}",
- title=item["name"],
- thumbnail=item["thumbnail"],
+ title=item.get("name", "Unknown"),
)
+ if "images" in item:
+ browse_media.thumbnail = fetch_image_url(item)
+ elif MediaType.ALBUM in item:
+ browse_media.thumbnail = fetch_image_url(item[MediaType.ALBUM])
-async def library_payload(*, can_play_artist: bool) -> BrowseMedia:
+ return browse_media
+
+
+def library_payload(*, can_play_artist: bool) -> BrowseMedia:
"""Create response payload to describe contents of a specific library.
Used by async_browse_media.
@@ -542,16 +474,10 @@ async def library_payload(*, can_play_artist: bool) -> BrowseMedia:
)
browse_media.children = []
- for item_type, item_name in LIBRARY_MAP.items():
+ for item in [{"name": n, "type": t} for t, n in LIBRARY_MAP.items()]:
browse_media.children.append(
item_payload(
- {
- "name": item_name,
- "type": item_type,
- "uri": item_type,
- "id": None,
- "thumbnail": None,
- },
+ {"name": item["name"], "type": item["type"], "uri": item["type"]},
can_play_artist=can_play_artist,
)
)
diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py
index d99fa7793df..58342ba368f 100644
--- a/homeassistant/components/spotify/config_flow.py
+++ b/homeassistant/components/spotify/config_flow.py
@@ -6,12 +6,10 @@ from collections.abc import Mapping
import logging
from typing import Any
-from spotifyaio import SpotifyClient
+from spotipy import Spotify
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
-from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, CONF_TOKEN
from homeassistant.helpers import config_entry_oauth2_flow
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, SPOTIFY_SCOPES
@@ -36,24 +34,27 @@ class SpotifyFlowHandler(
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create an entry for Spotify."""
- spotify = SpotifyClient(async_get_clientsession(self.hass))
- spotify.authenticate(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
+ spotify = Spotify(auth=data["token"]["access_token"])
try:
- current_user = await spotify.get_current_user()
+ current_user = await self.hass.async_add_executor_job(spotify.current_user)
except Exception: # noqa: BLE001
return self.async_abort(reason="connection_error")
- name = current_user.display_name
+ name = data["id"] = current_user["id"]
- await self.async_set_unique_id(current_user.user_id)
+ if current_user.get("display_name"):
+ name = current_user["display_name"]
+ data["name"] = name
+
+ await self.async_set_unique_id(current_user["id"])
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch(reason="reauth_account_mismatch")
return self.async_update_reload_and_abort(
self._get_reauth_entry(), title=name, data=data
)
- return self.async_create_entry(title=name, data={**data, CONF_NAME: name})
+ return self.async_create_entry(title=name, data=data)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
diff --git a/homeassistant/components/spotify/coordinator.py b/homeassistant/components/spotify/coordinator.py
index 9e62d5f137e..72efdefa7a5 100644
--- a/homeassistant/components/spotify/coordinator.py
+++ b/homeassistant/components/spotify/coordinator.py
@@ -3,59 +3,44 @@
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
-from typing import TYPE_CHECKING
+from typing import Any
-from spotifyaio import (
- ContextType,
- ItemType,
- PlaybackState,
- Playlist,
- SpotifyClient,
- SpotifyConnectionError,
- UserProfile,
-)
-from spotifyaio.models import AudioFeatures
+from spotipy import Spotify, SpotifyException
-from homeassistant.config_entries import ConfigEntry
+from homeassistant.components.media_player import MediaType
from homeassistant.core import HomeAssistant
+from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import homeassistant.util.dt as dt_util
from .const import DOMAIN
-if TYPE_CHECKING:
- from .models import SpotifyData
-
_LOGGER = logging.getLogger(__name__)
-type SpotifyConfigEntry = ConfigEntry[SpotifyData]
-
-
@dataclass
class SpotifyCoordinatorData:
"""Class to hold Spotify data."""
- current_playback: PlaybackState | None
+ current_playback: dict[str, Any]
position_updated_at: datetime | None
- playlist: Playlist | None
- audio_features: AudioFeatures | None
- dj_playlist: bool = False
+ playlist: dict[str, Any] | None
# This is a minimal representation of the DJ playlist that Spotify now offers
-# The DJ is not fully integrated with the playlist API, so we need to guard
-# against trying to fetch it as a regular playlist
-SPOTIFY_DJ_PLAYLIST_URI = "spotify:playlist:37i9dQZF1EYkqdzj48dyYq"
+# The DJ is not fully integrated with the playlist API, so needs to have the
+# playlist response mocked in order to maintain functionality
+SPOTIFY_DJ_PLAYLIST = {"uri": "spotify:playlist:37i9dQZF1EYkqdzj48dyYq", "name": "DJ"}
class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]):
"""Class to manage fetching Spotify data."""
- current_user: UserProfile
- config_entry: SpotifyConfigEntry
+ current_user: dict[str, Any]
- def __init__(self, hass: HomeAssistant, client: SpotifyClient) -> None:
+ def __init__(
+ self, hass: HomeAssistant, client: Spotify, session: OAuth2Session
+ ) -> None:
"""Initialize."""
super().__init__(
hass,
@@ -64,70 +49,65 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]):
update_interval=timedelta(seconds=30),
)
self.client = client
- self._playlist: Playlist | None = None
- self._currently_loaded_track: str | None = None
+ self._playlist: dict[str, Any] | None = None
+ self.session = session
async def _async_setup(self) -> None:
"""Set up the coordinator."""
try:
- self.current_user = await self.client.get_current_user()
- except SpotifyConnectionError as err:
+ self.current_user = await self.hass.async_add_executor_job(self.client.me)
+ except SpotifyException as err:
raise UpdateFailed("Error communicating with Spotify API") from err
+ if not self.current_user:
+ raise UpdateFailed("Could not retrieve user")
async def _async_update_data(self) -> SpotifyCoordinatorData:
- try:
- current = await self.client.get_playback()
- except SpotifyConnectionError as err:
- raise UpdateFailed("Error communicating with Spotify API") from err
- if not current:
- return SpotifyCoordinatorData(
- current_playback=None,
- position_updated_at=None,
- playlist=None,
- audio_features=None,
+ if not self.session.valid_token:
+ await self.session.async_ensure_token_valid()
+ await self.hass.async_add_executor_job(
+ self.client.set_auth, self.session.token["access_token"]
)
+ return await self.hass.async_add_executor_job(self._sync_update_data)
+
+ def _sync_update_data(self) -> SpotifyCoordinatorData:
+ current = self.client.current_playback(additional_types=[MediaType.EPISODE])
+ currently_playing = current or {}
# Record the last updated time, because Spotify's timestamp property is unreliable
# and doesn't actually return the fetch time as is mentioned in the API description
- position_updated_at = dt_util.utcnow()
+ position_updated_at = dt_util.utcnow() if current is not None else None
- audio_features: AudioFeatures | None = None
- if (item := current.item) is not None and item.type == ItemType.TRACK:
- if item.uri != self._currently_loaded_track:
- try:
- audio_features = await self.client.get_audio_features(item.uri)
- except SpotifyConnectionError:
- _LOGGER.debug(
- "Unable to load audio features for track '%s'. "
- "Continuing without audio features",
- item.uri,
- )
- audio_features = None
+ context = currently_playing.get("context") or {}
+
+ # For some users in some cases, the uri is formed like
+ # "spotify:user:{name}:playlist:{id}" and spotipy wants
+ # the type to be playlist.
+ uri = context.get("uri")
+ if uri is not None:
+ parts = uri.split(":")
+ if len(parts) == 5 and parts[1] == "user" and parts[3] == "playlist":
+ uri = ":".join([parts[0], parts[3], parts[4]])
+
+ if context and (self._playlist is None or self._playlist["uri"] != uri):
+ self._playlist = None
+ if context["type"] == MediaType.PLAYLIST:
+ # The Spotify API does not currently support doing a lookup for
+ # the DJ playlist,so just use the minimal mock playlist object
+ if uri == SPOTIFY_DJ_PLAYLIST["uri"]:
+ self._playlist = SPOTIFY_DJ_PLAYLIST
else:
- self._currently_loaded_track = item.uri
- else:
- audio_features = self.data.audio_features
- dj_playlist = False
- if (context := current.context) is not None:
- if self._playlist is None or self._playlist.uri != context.uri:
- self._playlist = None
- if context.uri == SPOTIFY_DJ_PLAYLIST_URI:
- dj_playlist = True
- elif context.context_type == ContextType.PLAYLIST:
# Make sure any playlist lookups don't break the current
# playback state update
try:
- self._playlist = await self.client.get_playlist(context.uri)
- except SpotifyConnectionError:
+ self._playlist = self.client.playlist(uri)
+ except SpotifyException:
_LOGGER.debug(
"Unable to load spotify playlist '%s'. "
"Continuing without playlist data",
- context.uri,
+ uri,
)
self._playlist = None
return SpotifyCoordinatorData(
- current_playback=current,
+ current_playback=currently_playing,
position_updated_at=position_updated_at,
playlist=self._playlist,
- audio_features=audio_features,
- dj_playlist=dj_playlist,
)
diff --git a/homeassistant/components/spotify/diagnostics.py b/homeassistant/components/spotify/diagnostics.py
deleted file mode 100644
index 82ce40eb22a..00000000000
--- a/homeassistant/components/spotify/diagnostics.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""Diagnostics support for Spotify."""
-
-from __future__ import annotations
-
-from dataclasses import asdict
-from typing import Any
-
-from homeassistant.core import HomeAssistant
-
-from .coordinator import SpotifyConfigEntry
-
-
-async def async_get_config_entry_diagnostics(
- hass: HomeAssistant, entry: SpotifyConfigEntry
-) -> dict[str, Any]:
- """Return diagnostics for a config entry."""
-
- return {
- "playback": asdict(entry.runtime_data.coordinator.data),
- "devices": [asdict(dev) for dev in entry.runtime_data.devices.data],
- }
diff --git a/homeassistant/components/spotify/entity.py b/homeassistant/components/spotify/entity.py
deleted file mode 100644
index 6ab82977089..00000000000
--- a/homeassistant/components/spotify/entity.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""Base entity for Spotify."""
-
-from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-
-from .const import DOMAIN
-from .coordinator import SpotifyCoordinator
-
-
-class SpotifyEntity(CoordinatorEntity[SpotifyCoordinator]):
- """Defines a base Spotify entity."""
-
- _attr_has_entity_name = True
-
- def __init__(self, coordinator: SpotifyCoordinator) -> None:
- """Initialize the Spotify entity."""
- super().__init__(coordinator)
- self._attr_device_info = DeviceInfo(
- identifiers={(DOMAIN, coordinator.current_user.user_id)},
- manufacturer="Spotify AB",
- model=f"Spotify {coordinator.current_user.product}",
- name=f"Spotify {coordinator.config_entry.title}",
- entry_type=DeviceEntryType.SERVICE,
- configuration_url="https://open.spotify.com",
- )
diff --git a/homeassistant/components/spotify/icons.json b/homeassistant/components/spotify/icons.json
index e1b08127e43..00c63141eae 100644
--- a/homeassistant/components/spotify/icons.json
+++ b/homeassistant/components/spotify/icons.json
@@ -4,41 +4,6 @@
"spotify": {
"default": "mdi:spotify"
}
- },
- "sensor": {
- "song_tempo": {
- "default": "mdi:metronome"
- },
- "danceability": {
- "default": "mdi:dance-ballroom"
- },
- "energy": {
- "default": "mdi:lightning-bolt"
- },
- "mode": {
- "default": "mdi:music"
- },
- "speechiness": {
- "default": "mdi:speaker-message"
- },
- "acousticness": {
- "default": "mdi:guitar-acoustic"
- },
- "instrumentalness": {
- "default": "mdi:guitar-electric"
- },
- "valence": {
- "default": "mdi:emoticon-happy"
- },
- "liveness": {
- "default": "mdi:music-note"
- },
- "time_signature": {
- "default": "mdi:music-clef-treble"
- },
- "key": {
- "default": "mdi:music-clef-treble"
- }
}
}
}
diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json
index 8f8f7e0d588..84f2bc102e3 100644
--- a/homeassistant/components/spotify/manifest.json
+++ b/homeassistant/components/spotify/manifest.json
@@ -9,6 +9,6 @@
"iot_class": "cloud_polling",
"loggers": ["spotipy"],
"quality_scale": "silver",
- "requirements": ["spotifyaio==0.8.8"],
+ "requirements": ["spotipy==2.23.0"],
"zeroconf": ["_spotify-connect._tcp.local."]
}
diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py
index 7687936fe4c..ad27e2919b2 100644
--- a/homeassistant/components/spotify/media_player.py
+++ b/homeassistant/components/spotify/media_player.py
@@ -2,22 +2,14 @@
from __future__ import annotations
-import asyncio
-from collections.abc import Awaitable, Callable, Coroutine
+from collections.abc import Callable
import datetime as dt
+from datetime import timedelta
import logging
-from typing import TYPE_CHECKING, Any, Concatenate
+from typing import Any, Concatenate
-from spotifyaio import (
- Device,
- Episode,
- Item,
- ItemType,
- PlaybackState,
- ProductType,
- RepeatMode as SpotifyRepeatMode,
- Track,
-)
+import requests
+from spotipy import SpotifyException
from yarl import URL
from homeassistant.components.media_player import (
@@ -30,17 +22,26 @@ from homeassistant.components.media_player import (
MediaType,
RepeatMode,
)
+from homeassistant.const import CONF_ID
from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
+from . import SpotifyConfigEntry
from .browse_media import async_browse_media_internal
-from .const import MEDIA_PLAYER_PREFIX, PLAYABLE_MEDIA_TYPES
-from .coordinator import SpotifyConfigEntry, SpotifyCoordinator
-from .entity import SpotifyEntity
+from .const import DOMAIN, MEDIA_PLAYER_PREFIX, PLAYABLE_MEDIA_TYPES
+from .coordinator import SpotifyCoordinator
+from .util import fetch_image_url
_LOGGER = logging.getLogger(__name__)
+SCAN_INTERVAL = timedelta(seconds=30)
+
SUPPORT_SPOTIFY = (
MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.NEXT_TRACK
@@ -56,15 +57,14 @@ SUPPORT_SPOTIFY = (
)
REPEAT_MODE_MAPPING_TO_HA = {
- SpotifyRepeatMode.CONTEXT: RepeatMode.ALL,
- SpotifyRepeatMode.OFF: RepeatMode.OFF,
- SpotifyRepeatMode.TRACK: RepeatMode.ONE,
+ "context": RepeatMode.ALL,
+ "off": RepeatMode.OFF,
+ "track": RepeatMode.ONE,
}
REPEAT_MODE_MAPPING_TO_SPOTIFY = {
value: key for key, value in REPEAT_MODE_MAPPING_TO_HA.items()
}
-AFTER_REQUEST_SLEEP = 1
async def async_setup_entry(
@@ -74,43 +74,47 @@ async def async_setup_entry(
) -> None:
"""Set up Spotify based on a config entry."""
data = entry.runtime_data
- assert entry.unique_id is not None
spotify = SpotifyMediaPlayer(
data.coordinator,
data.devices,
+ entry.data[CONF_ID],
+ entry.title,
)
async_add_entities([spotify])
-def ensure_item[_R](
- func: Callable[[SpotifyMediaPlayer, Item], _R],
-) -> Callable[[SpotifyMediaPlayer], _R | None]:
- """Ensure that the currently playing item is available."""
+def spotify_exception_handler[_SpotifyMediaPlayerT: SpotifyMediaPlayer, **_P, _R](
+ func: Callable[Concatenate[_SpotifyMediaPlayerT, _P], _R],
+) -> Callable[Concatenate[_SpotifyMediaPlayerT, _P], _R | None]:
+ """Decorate Spotify calls to handle Spotify exception.
- def wrapper(self: SpotifyMediaPlayer) -> _R | None:
- if not self.currently_playing or not self.currently_playing.item:
+ A decorator that wraps the passed in function, catches Spotify errors,
+ aiohttp exceptions and handles the availability of the media player.
+ """
+
+ def wrapper(
+ self: _SpotifyMediaPlayerT, *args: _P.args, **kwargs: _P.kwargs
+ ) -> _R | None:
+ try:
+ result = func(self, *args, **kwargs)
+ except requests.RequestException:
+ self._attr_available = False
return None
- return func(self, self.currently_playing.item)
+ except SpotifyException as exc:
+ self._attr_available = False
+ if exc.reason == "NO_ACTIVE_DEVICE":
+ raise HomeAssistantError("No active playback device found") from None
+ raise HomeAssistantError(f"Spotify error: {exc.reason}") from exc
+ self._attr_available = True
+ return result
return wrapper
-def async_refresh_after[_T: SpotifyEntity, **_P](
- func: Callable[Concatenate[_T, _P], Awaitable[None]],
-) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
- """Define a wrapper to yield and refresh after."""
-
- async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
- await func(self, *args, **kwargs)
- await asyncio.sleep(AFTER_REQUEST_SLEEP)
- await self.coordinator.async_refresh()
-
- return _async_wrap
-
-
-class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
+class SpotifyMediaPlayer(CoordinatorEntity[SpotifyCoordinator], MediaPlayerEntity):
"""Representation of a Spotify controller."""
+ _attr_has_entity_name = True
_attr_media_image_remotely_accessible = False
_attr_name = None
_attr_translation_key = "spotify"
@@ -118,24 +122,38 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
def __init__(
self,
coordinator: SpotifyCoordinator,
- device_coordinator: DataUpdateCoordinator[list[Device]],
+ device_coordinator: DataUpdateCoordinator[list[dict[str, Any]]],
+ user_id: str,
+ name: str,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self.devices = device_coordinator
- self._attr_unique_id = coordinator.current_user.user_id
+
+ self._attr_unique_id = user_id
+
+ self._attr_device_info = DeviceInfo(
+ identifiers={(DOMAIN, user_id)},
+ manufacturer="Spotify AB",
+ model=f"Spotify {coordinator.current_user['product']}",
+ name=f"Spotify {name}",
+ entry_type=DeviceEntryType.SERVICE,
+ configuration_url="https://open.spotify.com",
+ )
@property
- def currently_playing(self) -> PlaybackState | None:
+ def currently_playing(self) -> dict[str, Any]:
"""Return the current playback."""
return self.coordinator.data.current_playback
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Return the supported features."""
- if self.coordinator.current_user.product != ProductType.PREMIUM:
+ if self.coordinator.current_user["product"] != "premium":
return MediaPlayerEntityFeature(0)
- if not self.currently_playing or self.currently_playing.device.is_restricted:
+ if not self.currently_playing or self.currently_playing.get("device", {}).get(
+ "is_restricted"
+ ):
return MediaPlayerEntityFeature.SELECT_SOURCE
return SUPPORT_SPOTIFY
@@ -144,7 +162,7 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
"""Return the playback state."""
if not self.currently_playing:
return MediaPlayerState.IDLE
- if self.currently_playing.is_playing:
+ if self.currently_playing["is_playing"]:
return MediaPlayerState.PLAYING
return MediaPlayerState.PAUSED
@@ -153,32 +171,41 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
"""Return the device volume."""
if not self.currently_playing:
return None
- return self.currently_playing.device.volume_percent / 100
+ return self.currently_playing.get("device", {}).get("volume_percent", 0) / 100
@property
- @ensure_item
- def media_content_id(self, item: Item) -> str: # noqa: PLR0206
+ def media_content_id(self) -> str | None:
"""Return the media URL."""
- return item.uri
+ if not self.currently_playing:
+ return None
+ item = self.currently_playing.get("item") or {}
+ return item.get("uri")
@property
- @ensure_item
- def media_content_type(self, item: Item) -> str: # noqa: PLR0206
+ def media_content_type(self) -> str | None:
"""Return the media type."""
- return MediaType.PODCAST if item.type == ItemType.EPISODE else MediaType.MUSIC
+ if not self.currently_playing:
+ return None
+ item = self.currently_playing.get("item") or {}
+ is_episode = item.get("type") == MediaType.EPISODE
+ return MediaType.PODCAST if is_episode else MediaType.MUSIC
@property
- @ensure_item
- def media_duration(self, item: Item) -> int: # noqa: PLR0206
+ def media_duration(self) -> int | None:
"""Duration of current playing media in seconds."""
- return round(item.duration_ms / 1000)
+ if self.currently_playing is None or self.currently_playing.get("item") is None:
+ return None
+ return self.currently_playing["item"]["duration_ms"] / 1000
@property
def media_position(self) -> int | None:
"""Position of current playing media in seconds."""
- if not self.currently_playing or self.currently_playing.progress_ms is None:
+ if (
+ not self.currently_playing
+ or self.currently_playing.get("progress_ms") is None
+ ):
return None
- return round(self.currently_playing.progress_ms / 1000)
+ return self.currently_playing["progress_ms"] / 1000
@property
def media_position_updated_at(self) -> dt.datetime | None:
@@ -188,132 +215,131 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
return self.coordinator.data.position_updated_at
@property
- @ensure_item
- def media_image_url(self, item: Item) -> str | None: # noqa: PLR0206
+ def media_image_url(self) -> str | None:
"""Return the media image URL."""
- if item.type == ItemType.EPISODE:
- if TYPE_CHECKING:
- assert isinstance(item, Episode)
- if item.images:
- return item.images[0].url
- if item.show and item.show.images:
- return item.show.images[0].url
+ if not self.currently_playing or self.currently_playing.get("item") is None:
return None
- if TYPE_CHECKING:
- assert isinstance(item, Track)
- if not item.album.images:
+
+ item = self.currently_playing["item"]
+ if item["type"] == MediaType.EPISODE:
+ if item["images"]:
+ return fetch_image_url(item)
+ if item["show"]["images"]:
+ return fetch_image_url(item["show"])
return None
- return item.album.images[0].url
+
+ if not item["album"]["images"]:
+ return None
+ return fetch_image_url(item["album"])
@property
- @ensure_item
- def media_title(self, item: Item) -> str: # noqa: PLR0206
+ def media_title(self) -> str | None:
"""Return the media title."""
- return item.name
-
- @property
- @ensure_item
- def media_artist(self, item: Item) -> str: # noqa: PLR0206
- """Return the media artist."""
- if item.type == ItemType.EPISODE:
- if TYPE_CHECKING:
- assert isinstance(item, Episode)
- return item.show.publisher
-
- if TYPE_CHECKING:
- assert isinstance(item, Track)
- return ", ".join(artist.name for artist in item.artists)
-
- @property
- @ensure_item
- def media_album_name(self, item: Item) -> str: # noqa: PLR0206
- """Return the media album."""
- if item.type == ItemType.EPISODE:
- if TYPE_CHECKING:
- assert isinstance(item, Episode)
- return item.show.name
-
- if TYPE_CHECKING:
- assert isinstance(item, Track)
- return item.album.name
-
- @property
- @ensure_item
- def media_track(self, item: Item) -> int | None: # noqa: PLR0206
- """Track number of current playing media, music track only."""
- if item.type == ItemType.EPISODE:
+ if not self.currently_playing:
return None
- if TYPE_CHECKING:
- assert isinstance(item, Track)
- return item.track_number
+ item = self.currently_playing.get("item") or {}
+ return item.get("name")
@property
- def media_playlist(self) -> str | None:
+ def media_artist(self) -> str | None:
+ """Return the media artist."""
+ if not self.currently_playing or self.currently_playing.get("item") is None:
+ return None
+
+ item = self.currently_playing["item"]
+ if item["type"] == MediaType.EPISODE:
+ return item["show"]["publisher"]
+
+ return ", ".join(artist["name"] for artist in item["artists"])
+
+ @property
+ def media_album_name(self) -> str | None:
+ """Return the media album."""
+ if not self.currently_playing or self.currently_playing.get("item") is None:
+ return None
+
+ item = self.currently_playing["item"]
+ if item["type"] == MediaType.EPISODE:
+ return item["show"]["name"]
+
+ return item["album"]["name"]
+
+ @property
+ def media_track(self) -> int | None:
+ """Track number of current playing media, music track only."""
+ if not self.currently_playing:
+ return None
+ item = self.currently_playing.get("item") or {}
+ return item.get("track_number")
+
+ @property
+ def media_playlist(self):
"""Title of Playlist currently playing."""
- if self.coordinator.data.dj_playlist:
- return "DJ"
if self.coordinator.data.playlist is None:
return None
- return self.coordinator.data.playlist.name
+ return self.coordinator.data.playlist["name"]
@property
def source(self) -> str | None:
"""Return the current playback device."""
if not self.currently_playing:
return None
- return self.currently_playing.device.name
+ return self.currently_playing.get("device", {}).get("name")
@property
def source_list(self) -> list[str] | None:
"""Return a list of source devices."""
- return [device.name for device in self.devices.data]
+ return [device["name"] for device in self.devices.data]
@property
def shuffle(self) -> bool | None:
"""Shuffling state."""
if not self.currently_playing:
return None
- return self.currently_playing.shuffle
+ return self.currently_playing.get("shuffle_state")
@property
def repeat(self) -> RepeatMode | None:
"""Return current repeat mode."""
- if not self.currently_playing:
+ if (
+ not self.currently_playing
+ or (repeat_state := self.currently_playing.get("repeat_state")) is None
+ ):
return None
- return REPEAT_MODE_MAPPING_TO_HA.get(self.currently_playing.repeat_mode)
+ return REPEAT_MODE_MAPPING_TO_HA.get(repeat_state)
- @async_refresh_after
- async def async_set_volume_level(self, volume: float) -> None:
+ @spotify_exception_handler
+ def set_volume_level(self, volume: float) -> None:
"""Set the volume level."""
- await self.coordinator.client.set_volume(int(volume * 100))
+ self.coordinator.client.volume(int(volume * 100))
- @async_refresh_after
- async def async_media_play(self) -> None:
+ @spotify_exception_handler
+ def media_play(self) -> None:
"""Start or resume playback."""
- await self.coordinator.client.start_playback()
+ self.coordinator.client.start_playback()
- @async_refresh_after
- async def async_media_pause(self) -> None:
+ @spotify_exception_handler
+ def media_pause(self) -> None:
"""Pause playback."""
- await self.coordinator.client.pause_playback()
+ self.coordinator.client.pause_playback()
- @async_refresh_after
- async def async_media_previous_track(self) -> None:
+ @spotify_exception_handler
+ def media_previous_track(self) -> None:
"""Skip to previous track."""
- await self.coordinator.client.previous_track()
+ self.coordinator.client.previous_track()
- @async_refresh_after
- async def async_media_next_track(self) -> None:
+ @spotify_exception_handler
+ def media_next_track(self) -> None:
"""Skip to next track."""
- await self.coordinator.client.next_track()
+ self.coordinator.client.next_track()
- @async_refresh_after
- async def async_media_seek(self, position: float) -> None:
+ @spotify_exception_handler
+ def media_seek(self, position: float) -> None:
"""Send seek command."""
- await self.coordinator.client.seek_track(int(position * 1000))
+ self.coordinator.client.seek_track(int(position * 1000))
- @async_refresh_after
- async def async_play_media(
+ @spotify_exception_handler
+ def play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
) -> None:
"""Play media."""
@@ -337,8 +363,12 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
_LOGGER.error("Media type %s is not supported", media_type)
return
- if not self.currently_playing and self.devices.data:
- kwargs["device_id"] = self.devices.data[0].device_id
+ if (
+ self.currently_playing
+ and not self.currently_playing.get("device")
+ and self.devices.data
+ ):
+ kwargs["device_id"] = self.devices.data[0].get("id")
if enqueue == MediaPlayerEnqueue.ADD:
if media_type not in {
@@ -349,32 +379,32 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
raise ValueError(
f"Media type {media_type} is not supported when enqueue is ADD"
)
- await self.coordinator.client.add_to_queue(
- media_id, kwargs.get("device_id")
- )
+ self.coordinator.client.add_to_queue(media_id, kwargs.get("device_id"))
return
- await self.coordinator.client.start_playback(**kwargs)
+ self.coordinator.client.start_playback(**kwargs)
- @async_refresh_after
- async def async_select_source(self, source: str) -> None:
+ @spotify_exception_handler
+ def select_source(self, source: str) -> None:
"""Select playback device."""
for device in self.devices.data:
- if device.name == source:
- await self.coordinator.client.transfer_playback(device.device_id)
+ if device["name"] == source:
+ self.coordinator.client.transfer_playback(
+ device["id"], self.state == MediaPlayerState.PLAYING
+ )
return
- @async_refresh_after
- async def async_set_shuffle(self, shuffle: bool) -> None:
+ @spotify_exception_handler
+ def set_shuffle(self, shuffle: bool) -> None:
"""Enable/Disable shuffle mode."""
- await self.coordinator.client.set_shuffle(state=shuffle)
+ self.coordinator.client.shuffle(shuffle)
- @async_refresh_after
- async def async_set_repeat(self, repeat: RepeatMode) -> None:
+ @spotify_exception_handler
+ def set_repeat(self, repeat: RepeatMode) -> None:
"""Set repeat mode."""
if repeat not in REPEAT_MODE_MAPPING_TO_SPOTIFY:
raise ValueError(f"Unsupported repeat mode: {repeat}")
- await self.coordinator.client.set_repeat(REPEAT_MODE_MAPPING_TO_SPOTIFY[repeat])
+ self.coordinator.client.repeat(REPEAT_MODE_MAPPING_TO_SPOTIFY[repeat])
async def async_browse_media(
self,
@@ -386,6 +416,8 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
return await async_browse_media_internal(
self.hass,
self.coordinator.client,
+ self.coordinator.session,
+ self.coordinator.current_user,
media_content_type,
media_content_id,
)
diff --git a/homeassistant/components/spotify/models.py b/homeassistant/components/spotify/models.py
index ca323267f79..daeee560d58 100644
--- a/homeassistant/components/spotify/models.py
+++ b/homeassistant/components/spotify/models.py
@@ -1,8 +1,7 @@
"""Models for use in Spotify integration."""
from dataclasses import dataclass
-
-from spotifyaio import Device
+from typing import Any
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@@ -16,4 +15,4 @@ class SpotifyData:
coordinator: SpotifyCoordinator
session: OAuth2Session
- devices: DataUpdateCoordinator[list[Device]]
+ devices: DataUpdateCoordinator[list[dict[str, Any]]]
diff --git a/homeassistant/components/spotify/sensor.py b/homeassistant/components/spotify/sensor.py
deleted file mode 100644
index 3486a911b0d..00000000000
--- a/homeassistant/components/spotify/sensor.py
+++ /dev/null
@@ -1,179 +0,0 @@
-"""Sensor platform for Spotify."""
-
-from collections.abc import Callable
-from dataclasses import dataclass
-
-from spotifyaio.models import AudioFeatures, Key
-
-from homeassistant.components.sensor import (
- SensorDeviceClass,
- SensorEntity,
- SensorEntityDescription,
-)
-from homeassistant.const import PERCENTAGE
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from .coordinator import SpotifyConfigEntry, SpotifyCoordinator
-from .entity import SpotifyEntity
-
-
-@dataclass(frozen=True, kw_only=True)
-class SpotifyAudioFeaturesSensorEntityDescription(SensorEntityDescription):
- """Describes Spotify sensor entity."""
-
- value_fn: Callable[[AudioFeatures], float | str | None]
-
-
-KEYS: dict[Key, str] = {
- Key.C: "C",
- Key.C_SHARP_D_FLAT: "C♯/D♭",
- Key.D: "D",
- Key.D_SHARP_E_FLAT: "D♯/E♭",
- Key.E: "E",
- Key.F: "F",
- Key.F_SHARP_G_FLAT: "F♯/G♭",
- Key.G: "G",
- Key.G_SHARP_A_FLAT: "G♯/A♭",
- Key.A: "A",
- Key.A_SHARP_B_FLAT: "A♯/B♭",
- Key.B: "B",
-}
-
-KEY_OPTIONS = list(KEYS.values())
-
-
-def _get_key(audio_features: AudioFeatures) -> str | None:
- if audio_features.key is None:
- return None
- return KEYS[audio_features.key]
-
-
-AUDIO_FEATURE_SENSORS: tuple[SpotifyAudioFeaturesSensorEntityDescription, ...] = (
- SpotifyAudioFeaturesSensorEntityDescription(
- key="bpm",
- translation_key="song_tempo",
- native_unit_of_measurement="bpm",
- suggested_display_precision=0,
- value_fn=lambda audio_features: audio_features.tempo,
- ),
- SpotifyAudioFeaturesSensorEntityDescription(
- key="danceability",
- translation_key="danceability",
- native_unit_of_measurement=PERCENTAGE,
- suggested_display_precision=0,
- value_fn=lambda audio_features: audio_features.danceability * 100,
- entity_registry_enabled_default=False,
- ),
- SpotifyAudioFeaturesSensorEntityDescription(
- key="energy",
- translation_key="energy",
- native_unit_of_measurement=PERCENTAGE,
- suggested_display_precision=0,
- value_fn=lambda audio_features: audio_features.energy * 100,
- entity_registry_enabled_default=False,
- ),
- SpotifyAudioFeaturesSensorEntityDescription(
- key="mode",
- translation_key="mode",
- device_class=SensorDeviceClass.ENUM,
- options=["major", "minor"],
- value_fn=lambda audio_features: audio_features.mode.name.lower(),
- entity_registry_enabled_default=False,
- ),
- SpotifyAudioFeaturesSensorEntityDescription(
- key="speechiness",
- translation_key="speechiness",
- native_unit_of_measurement=PERCENTAGE,
- suggested_display_precision=0,
- value_fn=lambda audio_features: audio_features.speechiness * 100,
- entity_registry_enabled_default=False,
- ),
- SpotifyAudioFeaturesSensorEntityDescription(
- key="acousticness",
- translation_key="acousticness",
- native_unit_of_measurement=PERCENTAGE,
- suggested_display_precision=0,
- value_fn=lambda audio_features: audio_features.acousticness * 100,
- entity_registry_enabled_default=False,
- ),
- SpotifyAudioFeaturesSensorEntityDescription(
- key="instrumentalness",
- translation_key="instrumentalness",
- native_unit_of_measurement=PERCENTAGE,
- suggested_display_precision=0,
- value_fn=lambda audio_features: audio_features.instrumentalness * 100,
- entity_registry_enabled_default=False,
- ),
- SpotifyAudioFeaturesSensorEntityDescription(
- key="liveness",
- translation_key="liveness",
- native_unit_of_measurement=PERCENTAGE,
- suggested_display_precision=0,
- value_fn=lambda audio_features: audio_features.liveness * 100,
- entity_registry_enabled_default=False,
- ),
- SpotifyAudioFeaturesSensorEntityDescription(
- key="valence",
- translation_key="valence",
- native_unit_of_measurement=PERCENTAGE,
- suggested_display_precision=0,
- value_fn=lambda audio_features: audio_features.valence * 100,
- entity_registry_enabled_default=False,
- ),
- SpotifyAudioFeaturesSensorEntityDescription(
- key="time_signature",
- translation_key="time_signature",
- device_class=SensorDeviceClass.ENUM,
- options=["3/4", "4/4", "5/4", "6/4", "7/4"],
- value_fn=lambda audio_features: f"{audio_features.time_signature}/4",
- entity_registry_enabled_default=False,
- ),
- SpotifyAudioFeaturesSensorEntityDescription(
- key="key",
- translation_key="key",
- device_class=SensorDeviceClass.ENUM,
- options=KEY_OPTIONS,
- value_fn=_get_key,
- entity_registry_enabled_default=False,
- ),
-)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- entry: SpotifyConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up Spotify sensor based on a config entry."""
- coordinator = entry.runtime_data.coordinator
-
- async_add_entities(
- SpotifyAudioFeatureSensor(coordinator, description)
- for description in AUDIO_FEATURE_SENSORS
- )
-
-
-class SpotifyAudioFeatureSensor(SpotifyEntity, SensorEntity):
- """Representation of a Spotify sensor."""
-
- entity_description: SpotifyAudioFeaturesSensorEntityDescription
-
- def __init__(
- self,
- coordinator: SpotifyCoordinator,
- entity_description: SpotifyAudioFeaturesSensorEntityDescription,
- ) -> None:
- """Initialize."""
- super().__init__(coordinator)
- self._attr_unique_id = (
- f"{coordinator.current_user.user_id}_{entity_description.key}"
- )
- self.entity_description = entity_description
-
- @property
- def native_value(self) -> float | str | None:
- """Return the state of the sensor."""
- if (audio_features := self.coordinator.data.audio_features) is None:
- return None
- return self.entity_description.value_fn(audio_features)
diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json
index faf20d740d9..6447e6e6d1b 100644
--- a/homeassistant/components/spotify/strings.json
+++ b/homeassistant/components/spotify/strings.json
@@ -19,8 +19,7 @@
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
- "connection_error": "Could not fetch account information. Is the user registered in the Spotify Developer Dashboard?",
- "missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]"
+ "connection_error": "Could not fetch account information. Is the user registered in the Spotify Developer Dashboard?"
},
"create_entry": {
"default": "Successfully authenticated with Spotify."
@@ -30,46 +29,5 @@
"info": {
"api_endpoint_reachable": "Spotify API endpoint reachable"
}
- },
- "entity": {
- "sensor": {
- "song_tempo": {
- "name": "Song tempo"
- },
- "danceability": {
- "name": "Song danceability"
- },
- "energy": {
- "name": "Song energy"
- },
- "mode": {
- "name": "Song mode",
- "state": {
- "minor": "Minor",
- "major": "Major"
- }
- },
- "speechiness": {
- "name": "Song speechiness"
- },
- "acousticness": {
- "name": "Song acousticness"
- },
- "instrumentalness": {
- "name": "Song instrumentalness"
- },
- "valence": {
- "name": "Song valence"
- },
- "liveness": {
- "name": "Song liveness"
- },
- "time_signature": {
- "name": "Song time signature"
- },
- "key": {
- "name": "Song key"
- }
- }
}
}
diff --git a/homeassistant/components/spotify/system_health.py b/homeassistant/components/spotify/system_health.py
index 5ed6defe090..963c3bfb0ef 100644
--- a/homeassistant/components/spotify/system_health.py
+++ b/homeassistant/components/spotify/system_health.py
@@ -1,7 +1,5 @@
"""Provide info to system health."""
-from typing import Any
-
from homeassistant.components import system_health
from homeassistant.core import HomeAssistant, callback
@@ -14,7 +12,7 @@ def async_register(
register.async_register_info(system_health_info)
-async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
+async def system_health_info(hass):
"""Get info for the info page."""
return {
"api_endpoint_reachable": system_health.async_check_can_reach_url(
diff --git a/homeassistant/components/spotify/util.py b/homeassistant/components/spotify/util.py
index d882e9c58b8..98bce980e5b 100644
--- a/homeassistant/components/spotify/util.py
+++ b/homeassistant/components/spotify/util.py
@@ -2,7 +2,8 @@
from __future__ import annotations
-from spotifyaio import Image
+from typing import Any
+
import yarl
from .const import MEDIA_PLAYER_PREFIX
@@ -18,11 +19,12 @@ def resolve_spotify_media_type(media_content_type: str) -> str:
return media_content_type.removeprefix(MEDIA_PLAYER_PREFIX)
-def fetch_image_url(images: list[Image]) -> str | None:
+def fetch_image_url(item: dict[str, Any], key="images") -> str | None:
"""Fetch image url."""
- if not images:
- return None
- return images[0].url
+ source = item.get(key, [])
+ if isinstance(source, list) and source:
+ return source[0].get("url")
+ return None
def spotify_uri_from_media_browser_url(media_content_id: str) -> str:
diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py
index 4fe04f2401c..5537c7ff3b0 100644
--- a/homeassistant/components/sql/config_flow.py
+++ b/homeassistant/components/sql/config_flow.py
@@ -23,7 +23,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import (
CONF_DEVICE_CLASS,
@@ -144,7 +144,7 @@ class SQLConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> SQLOptionsFlowHandler:
"""Get the options flow for this handler."""
- return SQLOptionsFlowHandler()
+ return SQLOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -209,7 +209,7 @@ class SQLConfigFlow(ConfigFlow, domain=DOMAIN):
)
-class SQLOptionsFlowHandler(OptionsFlow):
+class SQLOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle SQL options."""
async def async_step_init(
@@ -223,7 +223,7 @@ class SQLOptionsFlowHandler(OptionsFlow):
db_url = user_input.get(CONF_DB_URL)
query = user_input[CONF_QUERY]
column = user_input[CONF_COLUMN_NAME]
- name = self.config_entry.options.get(CONF_NAME, self.config_entry.title)
+ name = self.options.get(CONF_NAME, self.config_entry.title)
try:
query = validate_sql_select(query)
@@ -275,7 +275,7 @@ class SQLOptionsFlowHandler(OptionsFlow):
return self.async_show_form(
step_id="init",
data_schema=self.add_suggested_values_to_schema(
- OPTIONS_SCHEMA, user_input or self.config_entry.options
+ OPTIONS_SCHEMA, user_input or self.options
),
errors=errors,
description_placeholders=description_placeholders,
diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py
index f466f3bcb62..c0a5b906474 100644
--- a/homeassistant/components/squeezebox/__init__.py
+++ b/homeassistant/components/squeezebox/__init__.py
@@ -2,10 +2,9 @@
from asyncio import timeout
from dataclasses import dataclass
-from datetime import datetime
import logging
-from pysqueezebox import Player, Server
+from pysqueezebox import Server
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -24,30 +23,20 @@ from homeassistant.helpers.device_registry import (
DeviceEntryType,
format_mac,
)
-from homeassistant.helpers.dispatcher import async_dispatcher_send
-from homeassistant.helpers.event import async_call_later
from .const import (
CONF_HTTPS,
- DISCOVERY_INTERVAL,
DISCOVERY_TASK,
DOMAIN,
- KNOWN_PLAYERS,
- KNOWN_SERVERS,
MANUFACTURER,
SERVER_MODEL,
- SIGNAL_PLAYER_DISCOVERED,
- SIGNAL_PLAYER_REDISCOVERED,
STATUS_API_TIMEOUT,
STATUS_QUERY_LIBRARYNAME,
STATUS_QUERY_MAC,
STATUS_QUERY_UUID,
STATUS_QUERY_VERSION,
)
-from .coordinator import (
- LMSStatusDataUpdateCoordinator,
- SqueezeBoxPlayerUpdateCoordinator,
-)
+from .coordinator import LMSStatusDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -128,55 +117,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
)
_LOGGER.debug("LMS Device %s", device)
- server_coordinator = LMSStatusDataUpdateCoordinator(hass, lms)
+ coordinator = LMSStatusDataUpdateCoordinator(hass, lms)
entry.runtime_data = SqueezeboxData(
- coordinator=server_coordinator,
+ coordinator=coordinator,
server=lms,
)
- # set up player discovery
- known_servers = hass.data.setdefault(DOMAIN, {}).setdefault(KNOWN_SERVERS, {})
- known_players = known_servers.setdefault(lms.uuid, {}).setdefault(KNOWN_PLAYERS, [])
-
- async def _player_discovery(now: datetime | None = None) -> None:
- """Discover squeezebox players by polling server."""
-
- async def _discovered_player(player: Player) -> None:
- """Handle a (re)discovered player."""
- if player.player_id in known_players:
- await player.async_update()
- async_dispatcher_send(
- hass, SIGNAL_PLAYER_REDISCOVERED, player.player_id, player.connected
- )
- else:
- _LOGGER.debug("Adding new entity: %s", player)
- player_coordinator = SqueezeBoxPlayerUpdateCoordinator(
- hass, player, lms.uuid
- )
- known_players.append(player.player_id)
- async_dispatcher_send(
- hass, SIGNAL_PLAYER_DISCOVERED, player_coordinator
- )
-
- if players := await lms.async_get_players():
- for player in players:
- hass.async_create_task(_discovered_player(player))
-
- entry.async_on_unload(
- async_call_later(hass, DISCOVERY_INTERVAL, _player_discovery)
- )
-
- await server_coordinator.async_config_entry_first_refresh()
+ await coordinator.async_config_entry_first_refresh()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
-
- _LOGGER.debug(
- "Adding player discovery job for LMS server: %s", entry.data[CONF_HOST]
- )
- entry.async_create_background_task(
- hass, _player_discovery(), "squeezebox.media_player.player_discovery"
- )
-
return True
diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py
index 4d1c98bc4fc..6c69aa532ec 100644
--- a/homeassistant/components/squeezebox/browse_media.py
+++ b/homeassistant/components/squeezebox/browse_media.py
@@ -18,15 +18,7 @@ from homeassistant.components.media_player import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.network import is_internal_request
-LIBRARY = [
- "Favorites",
- "Artists",
- "Albums",
- "Tracks",
- "Playlists",
- "Genres",
- "New Music",
-]
+LIBRARY = ["Favorites", "Artists", "Albums", "Tracks", "Playlists", "Genres"]
MEDIA_TYPE_TO_SQUEEZEBOX = {
"Favorites": "favorites",
@@ -35,7 +27,6 @@ MEDIA_TYPE_TO_SQUEEZEBOX = {
"Tracks": "titles",
"Playlists": "playlists",
"Genres": "genres",
- "New Music": "new music",
MediaType.ALBUM: "album",
MediaType.ARTIST: "artist",
MediaType.TRACK: "title",
@@ -59,7 +50,6 @@ CONTENT_TYPE_MEDIA_CLASS: dict[str | MediaType, dict[str, MediaClass | None]] =
"Tracks": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK},
"Playlists": {"item": MediaClass.DIRECTORY, "children": MediaClass.PLAYLIST},
"Genres": {"item": MediaClass.DIRECTORY, "children": MediaClass.GENRE},
- "New Music": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM},
MediaType.ALBUM: {"item": MediaClass.ALBUM, "children": MediaClass.TRACK},
MediaType.ARTIST: {"item": MediaClass.ARTIST, "children": MediaClass.ALBUM},
MediaType.TRACK: {"item": MediaClass.TRACK, "children": None},
@@ -78,7 +68,6 @@ CONTENT_TYPE_TO_CHILD_TYPE = {
"Playlists": MediaType.PLAYLIST,
"Genres": MediaType.GENRE,
"Favorites": None, # can only be determined after inspecting the item
- "New Music": MediaType.ALBUM,
}
BROWSE_LIMIT = 1000
diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py
index 8bc33214170..0bf8c24a5d1 100644
--- a/homeassistant/components/squeezebox/const.py
+++ b/homeassistant/components/squeezebox/const.py
@@ -5,7 +5,6 @@ DISCOVERY_TASK = "discovery_task"
DOMAIN = "squeezebox"
DEFAULT_PORT = 9000
KNOWN_PLAYERS = "known_players"
-KNOWN_SERVERS = "known_servers"
MANUFACTURER = "https://lyrion.org/"
PLAYER_DISCOVERY_UNSUB = "player_discovery_unsub"
SENSOR_UPDATE_INTERVAL = 60
@@ -28,7 +27,3 @@ STATUS_QUERY_MAC = "mac"
STATUS_QUERY_UUID = "uuid"
STATUS_QUERY_VERSION = "version"
SQUEEZEBOX_SOURCE_STRINGS = ("source:", "wavin:", "spotify:")
-SIGNAL_PLAYER_DISCOVERED = "squeezebox_player_discovered"
-SIGNAL_PLAYER_REDISCOVERED = "squeezebox_player_rediscovered"
-DISCOVERY_INTERVAL = 60
-PLAYER_UPDATE_INTERVAL = 5
diff --git a/homeassistant/components/squeezebox/coordinator.py b/homeassistant/components/squeezebox/coordinator.py
index f3aacbc9833..0d958399bcb 100644
--- a/homeassistant/components/squeezebox/coordinator.py
+++ b/homeassistant/components/squeezebox/coordinator.py
@@ -1,23 +1,18 @@
"""DataUpdateCoordinator for the Squeezebox integration."""
from asyncio import timeout
-from collections.abc import Callable
from datetime import timedelta
import logging
import re
-from typing import Any
-from pysqueezebox import Player, Server
+from pysqueezebox import Server
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import (
- PLAYER_UPDATE_INTERVAL,
SENSOR_UPDATE_INTERVAL,
- SIGNAL_PLAYER_REDISCOVERED,
STATUS_API_TIMEOUT,
STATUS_SENSOR_LASTSCAN,
STATUS_SENSOR_NEEDSRESTART,
@@ -43,7 +38,7 @@ class LMSStatusDataUpdateCoordinator(DataUpdateCoordinator):
self.newversion_regex = re.compile("<.*$")
async def _async_update_data(self) -> dict:
- """Fetch data from LMS status call.
+ """Fetch data fromn LMS status call.
Then we process only a subset to make then nice for HA
"""
@@ -75,46 +70,3 @@ class LMSStatusDataUpdateCoordinator(DataUpdateCoordinator):
_LOGGER.debug("Processed serverstatus %s=%s", self.lms.name, data)
return data
-
-
-class SqueezeBoxPlayerUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
- """Coordinator for Squeezebox players."""
-
- def __init__(self, hass: HomeAssistant, player: Player, server_uuid: str) -> None:
- """Initialize the coordinator."""
- super().__init__(
- hass,
- _LOGGER,
- name=player.name,
- update_interval=timedelta(seconds=PLAYER_UPDATE_INTERVAL),
- always_update=True,
- )
- self.player = player
- self.available = True
- self._remove_dispatcher: Callable | None = None
- self.server_uuid = server_uuid
-
- async def _async_update_data(self) -> dict[str, Any]:
- """Update Player if available, or listen for rediscovery if not."""
- if self.available:
- # Only update players available at last update, unavailable players are rediscovered instead
- await self.player.async_update()
-
- if self.player.connected is False:
- _LOGGER.debug("Player %s is not available", self.name)
- self.available = False
-
- # start listening for restored players
- self._remove_dispatcher = async_dispatcher_connect(
- self.hass, SIGNAL_PLAYER_REDISCOVERED, self.rediscovered
- )
- return {}
-
- @callback
- def rediscovered(self, unique_id: str, connected: bool) -> None:
- """Make a player available again."""
- if unique_id == self.player.player_id and connected:
- self.available = True
- _LOGGER.debug("Player %s is available again", self.name)
- if self._remove_dispatcher:
- self._remove_dispatcher()
diff --git a/homeassistant/components/squeezebox/icons.json b/homeassistant/components/squeezebox/icons.json
index 29911ddad77..e86016329f5 100644
--- a/homeassistant/components/squeezebox/icons.json
+++ b/homeassistant/components/squeezebox/icons.json
@@ -27,6 +27,12 @@
},
"call_query": {
"service": "mdi:database"
+ },
+ "sync": {
+ "service": "mdi:sync"
+ },
+ "unsync": {
+ "service": "mdi:sync-off"
}
}
}
diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json
index aa595340d56..74b7c1f4800 100644
--- a/homeassistant/components/squeezebox/manifest.json
+++ b/homeassistant/components/squeezebox/manifest.json
@@ -12,5 +12,5 @@
"documentation": "https://www.home-assistant.io/integrations/squeezebox",
"iot_class": "local_polling",
"loggers": ["pysqueezebox"],
- "requirements": ["pysqueezebox==0.10.0"]
+ "requirements": ["pysqueezebox==0.9.3"]
}
diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py
index 19cd1e36910..54cb07cafaf 100644
--- a/homeassistant/components/squeezebox/media_player.py
+++ b/homeassistant/components/squeezebox/media_player.py
@@ -6,9 +6,9 @@ from collections.abc import Callable
from datetime import datetime
import json
import logging
-from typing import TYPE_CHECKING, Any
+from typing import Any
-from pysqueezebox import Server, async_discover
+from pysqueezebox import Player, Server, async_discover
import voluptuous as vol
from homeassistant.components import media_source
@@ -25,53 +25,50 @@ from homeassistant.components.media_player import (
async_process_play_media_url,
)
from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY
-from homeassistant.const import ATTR_COMMAND, CONF_HOST, CONF_PORT, Platform
+from homeassistant.const import ATTR_COMMAND, CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
discovery_flow,
entity_platform,
- entity_registry as er,
)
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.start import async_at_start
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utcnow
+from . import SqueezeboxConfigEntry
from .browse_media import (
build_item_response,
generate_playlist,
library_payload,
media_source_content_filter,
)
-from .const import (
- DISCOVERY_TASK,
- DOMAIN,
- KNOWN_PLAYERS,
- KNOWN_SERVERS,
- SIGNAL_PLAYER_DISCOVERED,
- SQUEEZEBOX_SOURCE_STRINGS,
-)
-from .coordinator import SqueezeBoxPlayerUpdateCoordinator
-
-if TYPE_CHECKING:
- from . import SqueezeboxConfigEntry
+from .const import DISCOVERY_TASK, DOMAIN, KNOWN_PLAYERS, SQUEEZEBOX_SOURCE_STRINGS
SERVICE_CALL_METHOD = "call_method"
SERVICE_CALL_QUERY = "call_query"
ATTR_QUERY_RESULT = "query_result"
+SIGNAL_PLAYER_REDISCOVERED = "squeezebox_player_rediscovered"
+
_LOGGER = logging.getLogger(__name__)
+DISCOVERY_INTERVAL = 60
+
+KNOWN_SERVERS = "known_servers"
ATTR_PARAMETERS = "parameters"
ATTR_OTHER_PLAYER = "other_player"
@@ -115,15 +112,49 @@ async def async_setup_entry(
entry: SqueezeboxConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
- """Set up the Squeezebox media_player platform from a server config entry."""
+ """Set up an player discovery from a config entry."""
+ hass.data.setdefault(DOMAIN, {})
+ known_players = hass.data[DOMAIN].setdefault(KNOWN_PLAYERS, [])
+ lms = entry.runtime_data.server
- # Add media player entities when discovered
- async def _player_discovered(player: SqueezeBoxPlayerUpdateCoordinator) -> None:
- _LOGGER.debug("Setting up media_player entity for player %s", player)
- async_add_entities([SqueezeBoxMediaPlayerEntity(player)])
+ async def _player_discovery(now: datetime | None = None) -> None:
+ """Discover squeezebox players by polling server."""
- entry.async_on_unload(
- async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered)
+ async def _discovered_player(player: Player) -> None:
+ """Handle a (re)discovered player."""
+ entity = next(
+ (
+ known
+ for known in known_players
+ if known.unique_id == player.player_id
+ ),
+ None,
+ )
+ if entity:
+ await player.async_update()
+ async_dispatcher_send(
+ hass, SIGNAL_PLAYER_REDISCOVERED, player.player_id, player.connected
+ )
+
+ if not entity:
+ _LOGGER.debug("Adding new entity: %s", player)
+ entity = SqueezeBoxEntity(player, lms)
+ known_players.append(entity)
+ async_add_entities([entity], True)
+
+ if players := await lms.async_get_players():
+ for player in players:
+ hass.async_create_task(_discovered_player(player))
+
+ entry.async_on_unload(
+ async_call_later(hass, DISCOVERY_INTERVAL, _player_discovery)
+ )
+
+ _LOGGER.debug(
+ "Adding player discovery job for LMS server: %s", entry.data[CONF_HOST]
+ )
+ entry.async_create_background_task(
+ hass, _player_discovery(), "squeezebox.media_player.player_discovery"
)
# Register entity services
@@ -153,10 +184,8 @@ async def async_setup_entry(
entry.async_on_unload(async_at_start(hass, start_server_discovery))
-class SqueezeBoxMediaPlayerEntity(
- CoordinatorEntity[SqueezeBoxPlayerUpdateCoordinator], MediaPlayerEntity
-):
- """Representation of the media player features of a SqueezeBox device.
+class SqueezeBoxEntity(MediaPlayerEntity):
+ """Representation of a SqueezeBox device.
Wraps a pysqueezebox.Player() object.
"""
@@ -183,18 +212,13 @@ class SqueezeBoxMediaPlayerEntity(
_attr_has_entity_name = True
_attr_name = None
_last_update: datetime | None = None
+ _attr_available = True
- def __init__(
- self,
- coordinator: SqueezeBoxPlayerUpdateCoordinator,
- ) -> None:
+ def __init__(self, player: Player, server: Server) -> None:
"""Initialize the SqueezeBox device."""
- super().__init__(coordinator)
- player = coordinator.player
self._player = player
self._query_result: bool | dict = {}
self._remove_dispatcher: Callable | None = None
- self._previous_media_position = 0
self._attr_unique_id = format_mac(player.player_id)
_manufacturer = None
if player.model == "SqueezeLite" or "SqueezePlay" in player.model:
@@ -210,24 +234,11 @@ class SqueezeBoxMediaPlayerEntity(
identifiers={(DOMAIN, self._attr_unique_id)},
name=player.name,
connections={(CONNECTION_NETWORK_MAC, self._attr_unique_id)},
- via_device=(DOMAIN, coordinator.server_uuid),
+ via_device=(DOMAIN, server.uuid),
model=player.model,
manufacturer=_manufacturer,
)
- @callback
- def _handle_coordinator_update(self) -> None:
- """Handle updated data from the coordinator."""
- if self._previous_media_position != self.media_position:
- self._previous_media_position = self.media_position
- self._last_update = utcnow()
- self.async_write_ha_state()
-
- @property
- def available(self) -> bool:
- """Return True if entity is available."""
- return self.coordinator.available and super().available
-
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device-specific attributes."""
@@ -237,6 +248,15 @@ class SqueezeBoxMediaPlayerEntity(
if getattr(self, attr) is not None
}
+ @callback
+ def rediscovered(self, unique_id: str, connected: bool) -> None:
+ """Make a player available again."""
+ if unique_id == self.unique_id and connected:
+ self._attr_available = True
+ _LOGGER.debug("Player %s is available again", self.name)
+ if self._remove_dispatcher:
+ self._remove_dispatcher()
+
@property
def state(self) -> MediaPlayerState | None:
"""Return the state of the device."""
@@ -249,11 +269,26 @@ class SqueezeBoxMediaPlayerEntity(
)
return None
+ async def async_update(self) -> None:
+ """Update the Player() object."""
+ # only update available players, newly available players will be rediscovered and marked available
+ if self._attr_available:
+ last_media_position = self.media_position
+ await self._player.async_update()
+ if self.media_position != last_media_position:
+ self._last_update = utcnow()
+ if self._player.connected is False:
+ _LOGGER.debug("Player %s is not available", self.name)
+ self._attr_available = False
+
+ # start listening for restored players
+ self._remove_dispatcher = async_dispatcher_connect(
+ self.hass, SIGNAL_PLAYER_REDISCOVERED, self.rediscovered
+ )
+
async def async_will_remove_from_hass(self) -> None:
"""Remove from list of known players when removed from hass."""
- known_servers = self.hass.data[DOMAIN][KNOWN_SERVERS]
- known_players = known_servers[self.coordinator.server_uuid][KNOWN_PLAYERS]
- known_players.remove(self.coordinator.player.player_id)
+ self.hass.data[DOMAIN][KNOWN_PLAYERS].remove(self)
@property
def volume_level(self) -> float | None:
@@ -345,15 +380,13 @@ class SqueezeBoxMediaPlayerEntity(
@property
def group_members(self) -> list[str]:
"""List players we are synced with."""
- ent_reg = er.async_get(self.hass)
+ player_ids = {
+ p.unique_id: p.entity_id for p in self.hass.data[DOMAIN][KNOWN_PLAYERS]
+ }
return [
- entity_id
+ player_ids[player]
for player in self._player.sync_group
- if (
- entity_id := ent_reg.async_get_entity_id(
- Platform.MEDIA_PLAYER, DOMAIN, player
- )
- )
+ if player in player_ids
]
@property
@@ -364,68 +397,55 @@ class SqueezeBoxMediaPlayerEntity(
async def async_turn_off(self) -> None:
"""Turn off media player."""
await self._player.async_set_power(False)
- await self.coordinator.async_refresh()
async def async_volume_up(self) -> None:
"""Volume up media player."""
await self._player.async_set_volume("+5")
- await self.coordinator.async_refresh()
async def async_volume_down(self) -> None:
"""Volume down media player."""
await self._player.async_set_volume("-5")
- await self.coordinator.async_refresh()
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
volume_percent = str(int(volume * 100))
await self._player.async_set_volume(volume_percent)
- await self.coordinator.async_refresh()
async def async_mute_volume(self, mute: bool) -> None:
"""Mute (true) or unmute (false) media player."""
await self._player.async_set_muting(mute)
- await self.coordinator.async_refresh()
async def async_media_stop(self) -> None:
"""Send stop command to media player."""
await self._player.async_stop()
- await self.coordinator.async_refresh()
async def async_media_play_pause(self) -> None:
"""Send pause command to media player."""
await self._player.async_toggle_pause()
- await self.coordinator.async_refresh()
async def async_media_play(self) -> None:
"""Send play command to media player."""
await self._player.async_play()
- await self.coordinator.async_refresh()
async def async_media_pause(self) -> None:
"""Send pause command to media player."""
await self._player.async_pause()
- await self.coordinator.async_refresh()
async def async_media_next_track(self) -> None:
"""Send next track command."""
await self._player.async_index("+1")
- await self.coordinator.async_refresh()
async def async_media_previous_track(self) -> None:
"""Send next track command."""
await self._player.async_index("-1")
- await self.coordinator.async_refresh()
async def async_media_seek(self, position: float) -> None:
"""Send seek command."""
await self._player.async_time(position)
- await self.coordinator.async_refresh()
async def async_turn_on(self) -> None:
"""Turn the media player on."""
await self._player.async_set_power(True)
- await self.coordinator.async_refresh()
async def async_play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
@@ -484,7 +504,6 @@ class SqueezeBoxMediaPlayerEntity(
await self._player.async_load_playlist(playlist, cmd)
if index is not None:
await self._player.async_index(index)
- await self.coordinator.async_refresh()
async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set the repeat mode."""
@@ -496,18 +515,15 @@ class SqueezeBoxMediaPlayerEntity(
repeat_mode = "none"
await self._player.async_set_repeat(repeat_mode)
- await self.coordinator.async_refresh()
async def async_set_shuffle(self, shuffle: bool) -> None:
"""Enable/disable shuffle mode."""
shuffle_mode = "song" if shuffle else "none"
await self._player.async_set_shuffle(shuffle_mode)
- await self.coordinator.async_refresh()
async def async_clear_playlist(self) -> None:
"""Send the media player the command for clear playlist."""
await self._player.async_clear_playlist()
- await self.coordinator.async_refresh()
async def async_call_method(
self, command: str, parameters: list[str] | None = None
@@ -535,7 +551,6 @@ class SqueezeBoxMediaPlayerEntity(
all_params.extend(parameters)
self._query_result = await self._player.async_query(*all_params)
_LOGGER.debug("call_query got result %s", self._query_result)
- self.async_write_ha_state()
async def async_join_players(self, group_members: list[str]) -> None:
"""Add other Squeezebox players to this player's sync group.
@@ -543,24 +558,21 @@ class SqueezeBoxMediaPlayerEntity(
If the other player is a member of a sync group, it will leave the current sync group
without asking.
"""
- ent_reg = er.async_get(self.hass)
- for other_player_entity_id in group_members:
- other_player = ent_reg.async_get(other_player_entity_id)
- if other_player is None:
- raise ServiceValidationError(
- f"Could not find player with entity_id {other_player_entity_id}"
- )
- if other_player_id := other_player.unique_id:
+ player_ids = {
+ p.entity_id: p.unique_id for p in self.hass.data[DOMAIN][KNOWN_PLAYERS]
+ }
+
+ for other_player in group_members:
+ if other_player_id := player_ids.get(other_player):
await self._player.async_sync(other_player_id)
else:
raise ServiceValidationError(
- f"Could not join unknown player {other_player_entity_id}"
+ f"Could not join unknown player {other_player}"
)
async def async_unjoin_player(self) -> None:
"""Unsync this Squeezebox player."""
await self._player.async_unsync()
- await self.coordinator.async_refresh()
async def async_browse_media(
self,
diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml
index 07885ae5dd6..90f9bf2d769 100644
--- a/homeassistant/components/squeezebox/services.yaml
+++ b/homeassistant/components/squeezebox/services.yaml
@@ -30,3 +30,19 @@ call_query:
advanced: true
selector:
object:
+sync:
+ target:
+ entity:
+ integration: squeezebox
+ domain: media_player
+ fields:
+ other_player:
+ required: true
+ example: "media_player.living_room"
+ selector:
+ text:
+unsync:
+ target:
+ entity:
+ integration: squeezebox
+ domain: media_player
diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json
index b1b71cd8c1d..1a120ee0567 100644
--- a/homeassistant/components/squeezebox/strings.json
+++ b/homeassistant/components/squeezebox/strings.json
@@ -60,6 +60,20 @@
"description": "[%key:component::squeezebox::services::call_method::fields::parameters::description%]"
}
}
+ },
+ "sync": {
+ "name": "Sync",
+ "description": "Adds another player to this player's sync group. If the other player is already in a sync group, it will leave it.\n.",
+ "fields": {
+ "other_player": {
+ "name": "Other player",
+ "description": "Name of the other Squeezebox player to link."
+ }
+ }
+ },
+ "unsync": {
+ "name": "Unsync",
+ "description": "Removes this player from its sync group."
}
},
"entity": {
diff --git a/homeassistant/components/srp_energy/strings.json b/homeassistant/components/srp_energy/strings.json
index eca4f465435..191d10a70dd 100644
--- a/homeassistant/components/srp_energy/strings.json
+++ b/homeassistant/components/srp_energy/strings.json
@@ -17,8 +17,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
- "unknown": "Unexpected error"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
},
"entity": {
diff --git a/homeassistant/components/statistics/config_flow.py b/homeassistant/components/statistics/config_flow.py
index 4280c92131a..145a7655b36 100644
--- a/homeassistant/components/statistics/config_flow.py
+++ b/homeassistant/components/statistics/config_flow.py
@@ -169,8 +169,8 @@ class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
vol.Required("user_input"): dict,
}
)
-@websocket_api.async_response
-async def ws_start_preview(
+@callback
+def ws_start_preview(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
@@ -234,6 +234,6 @@ async def ws_start_preview(
preview_entity.hass = hass
connection.send_result(msg["id"])
- connection.subscriptions[msg["id"]] = await preview_entity.async_start_preview(
+ connection.subscriptions[msg["id"]] = preview_entity.async_start_preview(
async_preview_updated
)
diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py
index 50d07d4e466..ba98fe3ec6e 100644
--- a/homeassistant/components/statistics/sensor.py
+++ b/homeassistant/components/statistics/sensor.py
@@ -17,7 +17,6 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI
from homeassistant.components.recorder import get_instance, history
from homeassistant.components.sensor import (
DEVICE_CLASS_STATE_CLASSES,
- DEVICE_CLASS_UNITS,
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
@@ -38,7 +37,6 @@ from homeassistant.core import (
CALLBACK_TYPE,
Event,
EventStateChangedData,
- EventStateReportedData,
HomeAssistant,
State,
callback,
@@ -50,9 +48,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import (
async_track_point_in_utc_time,
async_track_state_change_event,
- async_track_state_report_event,
)
from homeassistant.helpers.reload import async_setup_reload_service
+from homeassistant.helpers.start import async_at_start
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from homeassistant.util import dt as dt_util
from homeassistant.util.enum import try_parse_enum
@@ -360,20 +358,23 @@ class StatisticsSensor(SensorEntity):
self.samples_keep_last: bool = samples_keep_last
self._precision: int = precision
self._percentile: int = percentile
- self._attr_available: bool = False
+ self._value: StateType | datetime = None
+ self._unit_of_measurement: str | None = None
+ self._available: bool = False
self.states: deque[float | bool] = deque(maxlen=self._samples_max_buffer_size)
self.ages: deque[datetime] = deque(maxlen=self._samples_max_buffer_size)
self.attributes: dict[str, StateType] = {}
- self._state_characteristic_fn: Callable[[], float | int | datetime | None] = (
+ self._state_characteristic_fn: Callable[[], StateType | datetime] = (
self._callable_characteristic_fn(self._state_characteristic)
)
self._update_listener: CALLBACK_TYPE | None = None
self._preview_callback: Callable[[str, Mapping[str, Any]], None] | None = None
- async def async_start_preview(
+ @callback
+ def async_start_preview(
self,
preview_callback: Callable[[str, Mapping[str, Any]], None],
) -> CALLBACK_TYPE:
@@ -384,22 +385,23 @@ class StatisticsSensor(SensorEntity):
if not self._source_entity_id or (
self._samples_max_buffer_size is None and self._samples_max_age is None
):
- self._attr_available = False
+ self._available = False
calculated_state = self._async_calculate_state()
preview_callback(calculated_state.state, calculated_state.attributes)
return self._call_on_remove_callbacks
self._preview_callback = preview_callback
- await self._async_stats_sensor_startup()
+ self._async_stats_sensor_startup(self.hass)
return self._call_on_remove_callbacks
- def _async_handle_new_state(
+ @callback
+ def _async_stats_sensor_state_listener(
self,
- reported_state: State | None,
+ event: Event[EventStateChangedData],
) -> None:
"""Handle the sensor state changes."""
- if (new_state := reported_state) is None:
+ if (new_state := event.data["new_state"]) is None:
return
self._add_state_to_queue(new_state)
self._async_purge_update_and_schedule()
@@ -412,55 +414,28 @@ class StatisticsSensor(SensorEntity):
self.async_write_ha_state()
@callback
- def _async_stats_sensor_state_change_listener(
- self,
- event: Event[EventStateChangedData],
- ) -> None:
- self._async_handle_new_state(event.data["new_state"])
-
- @callback
- def _async_stats_sensor_state_report_listener(
- self,
- event: Event[EventStateReportedData],
- ) -> None:
- self._async_handle_new_state(event.data["new_state"])
-
- async def _async_stats_sensor_startup(self) -> None:
- """Add listener and get recorded state.
-
- Historical data needs to be loaded from the database first before we
- can start accepting new incoming changes.
- This is needed to ensure that the buffer is properly sorted by time.
- """
+ def _async_stats_sensor_startup(self, _: HomeAssistant) -> None:
+ """Add listener and get recorded state."""
_LOGGER.debug("Startup for %s", self.entity_id)
- if "recorder" in self.hass.config.components:
- await self._initialize_from_database()
self.async_on_remove(
async_track_state_change_event(
self.hass,
[self._source_entity_id],
- self._async_stats_sensor_state_change_listener,
- )
- )
- self.async_on_remove(
- async_track_state_report_event(
- self.hass,
- [self._source_entity_id],
- self._async_stats_sensor_state_report_listener,
+ self._async_stats_sensor_state_listener,
)
)
+ if "recorder" in self.hass.config.components:
+ self.hass.async_create_task(self._initialize_from_database())
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
- await self._async_stats_sensor_startup()
+ self.async_on_remove(
+ async_at_start(self.hass, self._async_stats_sensor_startup)
+ )
def _add_state_to_queue(self, new_state: State) -> None:
"""Add the state to the queue."""
-
- # Attention: it is not safe to store the new_state object,
- # since the "last_reported" value will be updated over time.
- # Here we make a copy the current value, which is okay.
- self._attr_available = new_state.state != STATE_UNAVAILABLE
+ self._available = new_state.state != STATE_UNAVAILABLE
if new_state.state == STATE_UNAVAILABLE:
self.attributes[STAT_SOURCE_VALUE_VALID] = None
return
@@ -474,7 +449,7 @@ class StatisticsSensor(SensorEntity):
self.states.append(new_state.state == "on")
else:
self.states.append(float(new_state.state))
- self.ages.append(new_state.last_reported)
+ self.ages.append(new_state.last_updated)
self.attributes[STAT_SOURCE_VALUE_VALID] = True
except ValueError:
self.attributes[STAT_SOURCE_VALUE_VALID] = False
@@ -485,28 +460,11 @@ class StatisticsSensor(SensorEntity):
)
return
- self._calculate_state_attributes(new_state)
-
- def _calculate_state_attributes(self, new_state: State) -> None:
- """Set the entity state attributes."""
-
- self._attr_native_unit_of_measurement = self._calculate_unit_of_measurement(
- new_state
- )
- self._attr_device_class = self._calculate_device_class(
- new_state, self._attr_native_unit_of_measurement
- )
- self._attr_state_class = self._calculate_state_class(new_state)
-
- def _calculate_unit_of_measurement(self, new_state: State) -> str | None:
- """Return the calculated unit of measurement.
-
- The unit of measurement is that of the source sensor, adjusted based on the
- state characteristics.
- """
+ self._unit_of_measurement = self._derive_unit_of_measurement(new_state)
+ def _derive_unit_of_measurement(self, new_state: State) -> str | None:
base_unit: str | None = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
- unit: str | None = None
+ unit: str | None
if self.is_binary and self._state_characteristic in STATS_BINARY_PERCENTAGE:
unit = PERCENTAGE
elif not base_unit:
@@ -529,61 +487,53 @@ class StatisticsSensor(SensorEntity):
unit = base_unit + "/sample"
elif self._state_characteristic == STAT_CHANGE_SECOND:
unit = base_unit + "/s"
-
return unit
- def _calculate_device_class(
- self, new_state: State, unit: str | None
- ) -> SensorDeviceClass | None:
- """Return the calculated device class.
-
- The device class is calculated based on the state characteristics,
- the source device class and the unit of measurement is
- in the device class units list.
- """
-
- device_class: SensorDeviceClass | None = None
+ @property
+ def device_class(self) -> SensorDeviceClass | None:
+ """Return the class of this device."""
if self._state_characteristic in STATS_DATETIME:
return SensorDeviceClass.TIMESTAMP
if self._state_characteristic in STATS_NUMERIC_RETAIN_UNIT:
- device_class = new_state.attributes.get(ATTR_DEVICE_CLASS)
- if device_class is None:
+ source_state = self.hass.states.get(self._source_entity_id)
+ if source_state is None:
return None
- if (
- sensor_device_class := try_parse_enum(SensorDeviceClass, device_class)
- ) is None:
+ source_device_class = source_state.attributes.get(ATTR_DEVICE_CLASS)
+ if source_device_class is None:
return None
- if (
- sensor_device_class
- and (
- sensor_state_classes := DEVICE_CLASS_STATE_CLASSES.get(
- sensor_device_class
- )
- )
- and sensor_state_classes
- and SensorStateClass.MEASUREMENT not in sensor_state_classes
- ):
+ sensor_device_class = try_parse_enum(SensorDeviceClass, source_device_class)
+ if sensor_device_class is None:
return None
- if device_class not in DEVICE_CLASS_UNITS:
- return None
- if (
- device_class in DEVICE_CLASS_UNITS
- and unit not in DEVICE_CLASS_UNITS[device_class]
- ):
+ sensor_state_classes = DEVICE_CLASS_STATE_CLASSES.get(
+ sensor_device_class, set()
+ )
+ if SensorStateClass.MEASUREMENT not in sensor_state_classes:
return None
+ return sensor_device_class
+ return None
- return device_class
-
- def _calculate_state_class(self, new_state: State) -> SensorStateClass | None:
- """Return the calculated state class.
-
- Will be None if the characteristics is not numerical, otherwise
- SensorStateClass.MEASUREMENT.
- """
+ @property
+ def state_class(self) -> SensorStateClass | None:
+ """Return the state class of this entity."""
if self._state_characteristic in STATS_NOT_A_NUMBER:
return None
return SensorStateClass.MEASUREMENT
+ @property
+ def native_value(self) -> StateType | datetime:
+ """Return the state of the sensor."""
+ return self._value
+
+ @property
+ def native_unit_of_measurement(self) -> str | None:
+ """Return the unit the value is expressed in."""
+ return self._unit_of_measurement
+
+ @property
+ def available(self) -> bool:
+ """Return the availability of the sensor linked to the source sensor."""
+ return self._available
+
@property
def extra_state_attributes(self) -> dict[str, StateType] | None:
"""Return the state attributes of the sensor."""
@@ -727,7 +677,7 @@ class StatisticsSensor(SensorEntity):
):
for state in reversed(states):
self._add_state_to_queue(state)
- self._calculate_state_attributes(state)
+
self._async_purge_update_and_schedule()
# only write state to the state machine if we are not in preview mode
@@ -762,21 +712,19 @@ class StatisticsSensor(SensorEntity):
"""
value = self._state_characteristic_fn()
- _LOGGER.debug(
- "Updating value: states: %s, ages: %s => %s", self.states, self.ages, value
- )
+
if self._state_characteristic not in STATS_NOT_A_NUMBER:
with contextlib.suppress(TypeError):
value = round(cast(float, value), self._precision)
if self._precision == 0:
value = int(value)
- self._attr_native_value = value
+ self._value = value
def _callable_characteristic_fn(
self, characteristic: str
- ) -> Callable[[], float | int | datetime | None]:
+ ) -> Callable[[], StateType | datetime]:
"""Return the function callable of one characteristic function."""
- function: Callable[[], float | int | datetime | None] = getattr(
+ function: Callable[[], StateType | datetime] = getattr(
self,
f"_stat_binary_{characteristic}"
if self.is_binary
@@ -787,8 +735,6 @@ class StatisticsSensor(SensorEntity):
# Statistics for numeric sensor
def _stat_average_linear(self) -> StateType:
- if len(self.states) == 1:
- return self.states[0]
if len(self.states) >= 2:
area: float = 0
for i in range(1, len(self.states)):
@@ -802,8 +748,6 @@ class StatisticsSensor(SensorEntity):
return None
def _stat_average_step(self) -> StateType:
- if len(self.states) == 1:
- return self.states[0]
if len(self.states) >= 2:
area: float = 0
for i in range(1, len(self.states)):
@@ -859,12 +803,12 @@ class StatisticsSensor(SensorEntity):
return None
def _stat_distance_95_percent_of_values(self) -> StateType:
- if len(self.states) >= 1:
+ if len(self.states) >= 2:
return 2 * 1.96 * cast(float, self._stat_standard_deviation())
return None
def _stat_distance_99_percent_of_values(self) -> StateType:
- if len(self.states) >= 1:
+ if len(self.states) >= 2:
return 2 * 2.58 * cast(float, self._stat_standard_deviation())
return None
@@ -891,23 +835,17 @@ class StatisticsSensor(SensorEntity):
return None
def _stat_noisiness(self) -> StateType:
- if len(self.states) == 1:
- return 0.0
if len(self.states) >= 2:
return cast(float, self._stat_sum_differences()) / (len(self.states) - 1)
return None
def _stat_percentile(self) -> StateType:
- if len(self.states) == 1:
- return self.states[0]
if len(self.states) >= 2:
percentiles = statistics.quantiles(self.states, n=100, method="exclusive")
return percentiles[self._percentile - 1]
return None
def _stat_standard_deviation(self) -> StateType:
- if len(self.states) == 1:
- return 0.0
if len(self.states) >= 2:
return statistics.stdev(self.states)
return None
@@ -918,8 +856,6 @@ class StatisticsSensor(SensorEntity):
return None
def _stat_sum_differences(self) -> StateType:
- if len(self.states) == 1:
- return 0.0
if len(self.states) >= 2:
return sum(
abs(j - i)
@@ -928,8 +864,6 @@ class StatisticsSensor(SensorEntity):
return None
def _stat_sum_differences_nonnegative(self) -> StateType:
- if len(self.states) == 1:
- return 0.0
if len(self.states) >= 2:
return sum(
(j - i if j >= i else j - 0)
@@ -951,8 +885,6 @@ class StatisticsSensor(SensorEntity):
return None
def _stat_variance(self) -> StateType:
- if len(self.states) == 1:
- return 0.0
if len(self.states) >= 2:
return statistics.variance(self.states)
return None
@@ -960,8 +892,6 @@ class StatisticsSensor(SensorEntity):
# Statistics for binary sensor
def _stat_binary_average_step(self) -> StateType:
- if len(self.states) == 1:
- return 100.0 * int(self.states[0] is True)
if len(self.states) >= 2:
on_seconds: float = 0
for i in range(1, len(self.states)):
diff --git a/homeassistant/components/statistics/strings.json b/homeassistant/components/statistics/strings.json
index 3e6fec9d986..5f32b203bfd 100644
--- a/homeassistant/components/statistics/strings.json
+++ b/homeassistant/components/statistics/strings.json
@@ -1,5 +1,4 @@
{
- "title": "Statistics",
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
@@ -23,10 +22,10 @@
"state_characteristic": {
"description": "Read the documention for further details on available options and how to use them.",
"data": {
- "state_characteristic": "Statistic characteristic"
+ "state_characteristic": "State_characteristic"
},
"data_description": {
- "state_characteristic": "The statistic characteristic that should be used as the state of the sensor."
+ "state_characteristic": "The characteristic that should be used as the state of the statistics sensor."
}
},
"options": {
diff --git a/homeassistant/components/steam_online/config_flow.py b/homeassistant/components/steam_online/config_flow.py
index 69009fca8c4..4b99bf7738d 100644
--- a/homeassistant/components/steam_online/config_flow.py
+++ b/homeassistant/components/steam_online/config_flow.py
@@ -36,11 +36,15 @@ def validate_input(user_input: dict[str, str]) -> dict[str, str | int]:
class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Steam."""
+ def __init__(self) -> None:
+ """Initialize the flow."""
+ self.entry: SteamConfigEntry | None = None
+
@staticmethod
@callback
def async_get_options_flow(
config_entry: SteamConfigEntry,
- ) -> SteamOptionsFlowHandler:
+ ) -> OptionsFlow:
"""Get the options flow for this handler."""
return SteamOptionsFlowHandler(config_entry)
@@ -49,8 +53,8 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle a flow initiated by the user."""
errors = {}
- if user_input is None and self.source == SOURCE_REAUTH:
- user_input = {CONF_ACCOUNT: self._get_reauth_entry().data[CONF_ACCOUNT]}
+ if user_input is None and self.entry:
+ user_input = {CONF_ACCOUNT: self.entry.data[CONF_ACCOUNT]}
elif user_input is not None:
try:
res = await self.hass.async_add_executor_job(validate_input, user_input)
@@ -98,6 +102,8 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle a reauthorization flow request."""
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -123,6 +129,7 @@ class SteamOptionsFlowHandler(OptionsFlow):
def __init__(self, entry: SteamConfigEntry) -> None:
"""Initialize options flow."""
+ self.entry = entry
self.options = dict(entry.options)
async def async_step_init(
@@ -130,7 +137,7 @@ class SteamOptionsFlowHandler(OptionsFlow):
) -> ConfigFlowResult:
"""Manage Steam options."""
if user_input is not None:
- await self.hass.config_entries.async_unload(self.config_entry.entry_id)
+ await self.hass.config_entries.async_unload(self.entry.entry_id)
for _id in self.options[CONF_ACCOUNTS]:
if _id not in user_input[CONF_ACCOUNTS] and (
entity_id := er.async_get(self.hass).async_get_entity_id(
@@ -145,7 +152,7 @@ class SteamOptionsFlowHandler(OptionsFlow):
if _id in user_input[CONF_ACCOUNTS]
}
}
- await self.hass.config_entries.async_reload(self.config_entry.entry_id)
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_create_entry(title="", data=channel_data)
error = None
try:
@@ -175,9 +182,7 @@ class SteamOptionsFlowHandler(OptionsFlow):
"""Get accounts."""
interface = steam.api.interface("ISteamUser")
try:
- friends = interface.GetFriendList(
- steamid=self.config_entry.data[CONF_ACCOUNT]
- )
+ friends = interface.GetFriendList(steamid=self.entry.data[CONF_ACCOUNT])
_users_str = [user["steamid"] for user in friends["friendslist"]["friends"]]
except steam.api.HTTPError:
return []
diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py
index 66455ffad1a..a2fa065e019 100644
--- a/homeassistant/components/stream/const.py
+++ b/homeassistant/components/stream/const.py
@@ -1,9 +1,5 @@
"""Constants for Stream component."""
-from __future__ import annotations
-
-from typing import Final
-
DOMAIN = "stream"
ATTR_ENDPOINTS = "endpoints"
@@ -15,8 +11,8 @@ RECORDER_PROVIDER = "recorder"
OUTPUT_FORMATS = [HLS_PROVIDER]
-SEGMENT_CONTAINER_FORMAT: Final = "mp4" # format for segments
-RECORDER_CONTAINER_FORMAT: Final = "mp4" # format for recorder output
+SEGMENT_CONTAINER_FORMAT = "mp4" # format for segments
+RECORDER_CONTAINER_FORMAT = "mp4" # format for recorder output
AUDIO_CODECS = {"aac", "mp3"}
FORMAT_CONTENT_TYPE = {HLS_PROVIDER: "application/vnd.apple.mpegurl"}
diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py
index 4184b23b9a0..68c08a4f072 100644
--- a/homeassistant/components/stream/core.py
+++ b/homeassistant/components/stream/core.py
@@ -9,7 +9,7 @@ from dataclasses import dataclass, field
import datetime
from enum import IntEnum
import logging
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, Any
from aiohttp import web
import numpy as np
@@ -27,7 +27,7 @@ from .const import (
)
if TYPE_CHECKING:
- from av import Packet, VideoCodecContext
+ from av import CodecContext, Packet
from homeassistant.components.camera import DynamicStreamSettings
@@ -438,17 +438,17 @@ class KeyFrameConverter:
"""Initialize."""
# Keep import here so that we can import stream integration
- # without installing reqs
+ # without installingreqs
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.camera.img_util import TurboJPEGSingleton
- self._packet: Packet | None = None
+ self._packet: Packet = None
self._event: asyncio.Event = asyncio.Event()
self._hass = hass
self._image: bytes | None = None
self._turbojpeg = TurboJPEGSingleton.instance()
self._lock = asyncio.Lock()
- self._codec_context: VideoCodecContext | None = None
+ self._codec_context: CodecContext | None = None
self._stream_settings = stream_settings
self._dynamic_stream_settings = dynamic_stream_settings
@@ -460,7 +460,7 @@ class KeyFrameConverter:
self._packet = packet
self._hass.loop.call_soon_threadsafe(self._event.set)
- def create_codec_context(self, codec_context: VideoCodecContext) -> None:
+ def create_codec_context(self, codec_context: CodecContext) -> None:
"""Create a codec context to be used for decoding the keyframes.
This is run by the worker thread and will only be called once per worker.
@@ -474,9 +474,7 @@ class KeyFrameConverter:
# pylint: disable-next=import-outside-toplevel
from av import CodecContext
- self._codec_context = cast(
- "VideoCodecContext", CodecContext.create(codec_context.name, "r")
- )
+ self._codec_context = CodecContext.create(codec_context.name, "r")
self._codec_context.extradata = codec_context.extradata
self._codec_context.skip_frame = "NONKEY"
self._codec_context.thread_type = "NONE"
@@ -508,8 +506,9 @@ class KeyFrameConverter:
frames = self._codec_context.decode(None)
break
except EOFError:
- _LOGGER.debug("Codec context needs flushing")
- self._codec_context.flush_buffers()
+ _LOGGER.debug("Codec context needs flushing, attempting to reopen")
+ self._codec_context.close()
+ self._codec_context.open()
else:
_LOGGER.debug("Unable to decode keyframe")
return
diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json
index fdf81d99e65..00387d97b83 100644
--- a/homeassistant/components/stream/manifest.json
+++ b/homeassistant/components/stream/manifest.json
@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
- "requirements": ["PyTurboJPEG==1.7.5", "av==13.1.0", "numpy==2.1.3"]
+ "requirements": ["PyTurboJPEG==1.7.5", "ha-av==10.1.1", "numpy==1.26.4"]
}
diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py
index a24440e6d19..6dfc09891b7 100644
--- a/homeassistant/components/stream/recorder.py
+++ b/homeassistant/components/stream/recorder.py
@@ -9,7 +9,6 @@ import os
from typing import TYPE_CHECKING
import av
-import av.container
from homeassistant.core import HomeAssistant, callback
@@ -106,23 +105,24 @@ class RecorderOutput(StreamOutput):
# Create output on first segment
if not output:
- container_options: dict[str, str] = {
- "video_track_timescale": str(int(1 / source_v.time_base)), # type: ignore[operator]
- "movflags": "frag_keyframe+empty_moov",
- "min_frag_duration": str(self.stream_settings.min_segment_duration),
- }
output = av.open(
self.video_path + ".tmp",
"w",
format=RECORDER_CONTAINER_FORMAT,
- container_options=container_options,
+ container_options={
+ "video_track_timescale": str(int(1 / source_v.time_base)),
+ "movflags": "frag_keyframe+empty_moov",
+ "min_frag_duration": str(
+ self.stream_settings.min_segment_duration
+ ),
+ },
)
# Add output streams if necessary
if not output_v:
output_v = output.add_stream(template=source_v)
context = output_v.codec_context
- context.global_header = True
+ context.flags |= "GLOBAL_HEADER"
if source_a and not output_a:
output_a = output.add_stream(template=source_a)
@@ -132,23 +132,21 @@ class RecorderOutput(StreamOutput):
last_stream_id = segment.stream_id
pts_adjuster["video"] = int(
(running_duration - source.start_time)
- / (av.time_base * source_v.time_base) # type: ignore[operator]
+ / (av.time_base * source_v.time_base)
)
if source_a:
pts_adjuster["audio"] = int(
(running_duration - source.start_time)
- / (av.time_base * source_a.time_base) # type: ignore[operator]
+ / (av.time_base * source_a.time_base)
)
# Remux video
for packet in source.demux():
- if packet.pts is None:
+ if packet.dts is None:
continue
- packet.pts += pts_adjuster[packet.stream.type] # type: ignore[operator]
- packet.dts += pts_adjuster[packet.stream.type] # type: ignore[operator]
- stream = output_v if packet.stream.type == "video" else output_a
- assert stream
- packet.stream = stream
+ packet.pts += pts_adjuster[packet.stream.type]
+ packet.dts += pts_adjuster[packet.stream.type]
+ packet.stream = output_v if packet.stream.type == "video" else output_a
output.mux(packet)
running_duration += source.duration - source.start_time
@@ -171,9 +169,7 @@ class RecorderOutput(StreamOutput):
os.remove(video_path + ".tmp")
def finish_writing(
- segments: deque[Segment],
- output: av.container.OutputContainer | None,
- video_path: str,
+ segments: deque[Segment], output: av.OutputContainer, video_path: str
) -> None:
"""Finish writing output."""
# Should only have 0 or 1 segments, but loop through just in case
diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py
index 8c9bb1b8e9e..0d72a9b0818 100644
--- a/homeassistant/components/stream/worker.py
+++ b/homeassistant/components/stream/worker.py
@@ -13,9 +13,6 @@ from threading import Event
from typing import Any, Self, cast
import av
-import av.audio
-import av.container
-import av.stream
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
@@ -50,10 +47,10 @@ class StreamWorkerError(Exception):
"""An exception thrown while processing a stream."""
-def redact_av_error_string(err: av.FFmpegError) -> str:
+def redact_av_error_string(err: av.AVError) -> str:
"""Return an error string with credentials redacted from the url."""
- parts = [str(err.type), err.strerror] # type: ignore[attr-defined]
- if err.filename:
+ parts = [str(err.type), err.strerror]
+ if err.filename is not None:
parts.append(redact_credentials(err.filename))
return ", ".join(parts)
@@ -126,31 +123,30 @@ class StreamState:
class StreamMuxer:
"""StreamMuxer re-packages video/audio packets for output."""
- _segment_start_dts: int
- _memory_file: BytesIO
- _av_output: av.container.OutputContainer
- _output_video_stream: av.VideoStream
- _output_audio_stream: av.audio.AudioStream | None
- _segment: Segment | None
- # the following 2 member variables are used for Part formation
- _memory_file_pos: int
- _part_start_dts: float
-
def __init__(
self,
hass: HomeAssistant,
- video_stream: av.VideoStream,
- audio_stream: av.audio.AudioStream | None,
- audio_bsf: str | None,
+ video_stream: av.video.VideoStream,
+ audio_stream: av.audio.stream.AudioStream | None,
+ audio_bsf: av.BitStreamFilter | None,
stream_state: StreamState,
stream_settings: StreamSettings,
) -> None:
"""Initialize StreamMuxer."""
self._hass = hass
- self._input_video_stream = video_stream
- self._input_audio_stream = audio_stream
+ self._segment_start_dts: int = cast(int, None)
+ self._memory_file: BytesIO = cast(BytesIO, None)
+ self._av_output: av.container.OutputContainer = None
+ self._input_video_stream: av.video.VideoStream = video_stream
+ self._input_audio_stream: av.audio.stream.AudioStream | None = audio_stream
self._audio_bsf = audio_bsf
- self._audio_bsf_context: av.BitStreamFilterContext | None = None
+ self._audio_bsf_context: av.BitStreamFilterContext = None
+ self._output_video_stream: av.video.VideoStream = None
+ self._output_audio_stream: av.audio.stream.AudioStream | None = None
+ self._segment: Segment | None = None
+ # the following 3 member variables are used for Part formation
+ self._memory_file_pos: int = cast(int, None)
+ self._part_start_dts: int = cast(int, None)
self._part_has_keyframe = False
self._stream_settings = stream_settings
self._stream_state = stream_state
@@ -160,83 +156,83 @@ class StreamMuxer:
self,
memory_file: BytesIO,
sequence: int,
- input_vstream: av.VideoStream,
- input_astream: av.audio.AudioStream | None,
+ input_vstream: av.video.VideoStream,
+ input_astream: av.audio.stream.AudioStream | None,
) -> tuple[
av.container.OutputContainer,
- av.VideoStream,
- av.audio.AudioStream | None,
+ av.video.VideoStream,
+ av.audio.stream.AudioStream | None,
]:
"""Make a new av OutputContainer and add output streams."""
- container_options: dict[str, str] = {
- # Removed skip_sidx - see:
- # https://github.com/home-assistant/core/pull/39970
- # "cmaf" flag replaces several of the movflags used,
- # but too recent to use for now
- "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov",
- # Sometimes the first segment begins with negative timestamps,
- # and this setting just
- # adjusts the timestamps in the output from that segment to start
- # from 0. Helps from having to make some adjustments
- # in test_durations
- "avoid_negative_ts": "make_non_negative",
- "fragment_index": str(sequence + 1),
- "video_track_timescale": str(int(1 / input_vstream.time_base)), # type: ignore[operator]
- # Only do extra fragmenting if we are using ll_hls
- # Let ffmpeg do the work using frag_duration
- # Fragment durations may exceed the 15% allowed variance but it seems ok
- **(
- {
- "movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov",
- # Create a fragment every TARGET_PART_DURATION. The data from
- # each fragment is stored in a "Part" that can be combined with
- # the data from all the other "Part"s, plus an init section,
- # to reconstitute the data in a "Segment".
- #
- # The LL-HLS spec allows for a fragment's duration to be within
- # the range [0.85x,1.0x] of the part target duration. We use the
- # frag_duration option to tell ffmpeg to try to cut the
- # fragments when they reach frag_duration. However,
- # the resulting fragments can have variability in their
- # durations and can end up being too short or too long. With a
- # video track with no audio, the discrete nature of frames means
- # that the frame at the end of a fragment will sometimes extend
- # slightly beyond the desired frag_duration.
- #
- # If there are two tracks, as in the case of a video feed with
- # audio, there is an added wrinkle as the fragment cut seems to
- # be done on the first track that crosses the desired threshold,
- # and cutting on the audio track may also result in a shorter
- # video fragment than desired.
- #
- # Given this, our approach is to give ffmpeg a frag_duration
- # somewhere in the middle of the range, hoping that the parts
- # stay pretty well bounded, and we adjust the part durations
- # a bit in the hls metadata so that everything "looks" ok.
- "frag_duration": str(
- int(self._stream_settings.part_target_duration * 9e5)
- ),
- }
- if self._stream_settings.ll_hls
- else {}
- ),
- }
container = av.open(
memory_file,
mode="w",
format=SEGMENT_CONTAINER_FORMAT,
- container_options=container_options,
+ container_options={
+ # Removed skip_sidx - see:
+ # https://github.com/home-assistant/core/pull/39970
+ # "cmaf" flag replaces several of the movflags used,
+ # but too recent to use for now
+ "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov",
+ # Sometimes the first segment begins with negative timestamps,
+ # and this setting just
+ # adjusts the timestamps in the output from that segment to start
+ # from 0. Helps from having to make some adjustments
+ # in test_durations
+ "avoid_negative_ts": "make_non_negative",
+ "fragment_index": str(sequence + 1),
+ "video_track_timescale": str(int(1 / input_vstream.time_base)),
+ # Only do extra fragmenting if we are using ll_hls
+ # Let ffmpeg do the work using frag_duration
+ # Fragment durations may exceed the 15% allowed variance but it seems ok
+ **(
+ {
+ "movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov",
+ # Create a fragment every TARGET_PART_DURATION. The data from
+ # each fragment is stored in a "Part" that can be combined with
+ # the data from all the other "Part"s, plus an init section,
+ # to reconstitute the data in a "Segment".
+ #
+ # The LL-HLS spec allows for a fragment's duration to be within
+ # the range [0.85x,1.0x] of the part target duration. We use the
+ # frag_duration option to tell ffmpeg to try to cut the
+ # fragments when they reach frag_duration. However,
+ # the resulting fragments can have variability in their
+ # durations and can end up being too short or too long. With a
+ # video track with no audio, the discrete nature of frames means
+ # that the frame at the end of a fragment will sometimes extend
+ # slightly beyond the desired frag_duration.
+ #
+ # If there are two tracks, as in the case of a video feed with
+ # audio, there is an added wrinkle as the fragment cut seems to
+ # be done on the first track that crosses the desired threshold,
+ # and cutting on the audio track may also result in a shorter
+ # video fragment than desired.
+ #
+ # Given this, our approach is to give ffmpeg a frag_duration
+ # somewhere in the middle of the range, hoping that the parts
+ # stay pretty well bounded, and we adjust the part durations
+ # a bit in the hls metadata so that everything "looks" ok.
+ "frag_duration": str(
+ int(self._stream_settings.part_target_duration * 9e5)
+ ),
+ }
+ if self._stream_settings.ll_hls
+ else {}
+ ),
+ },
)
output_vstream = container.add_stream(template=input_vstream)
# Check if audio is requested
output_astream = None
if input_astream:
if self._audio_bsf:
- self._audio_bsf_context = av.BitStreamFilterContext(
- self._audio_bsf, input_astream
- )
- output_astream = container.add_stream(template=input_astream)
- return container, output_vstream, output_astream # type: ignore[return-value]
+ self._audio_bsf_context = self._audio_bsf.create()
+ self._audio_bsf_context.set_input_stream(input_astream)
+ output_astream = container.add_stream(
+ template=self._audio_bsf_context or input_astream
+ )
+ return container, output_vstream, output_astream
def reset(self, video_dts: int) -> None:
"""Initialize a new stream segment."""
@@ -255,7 +251,7 @@ class StreamMuxer:
input_astream=self._input_audio_stream,
)
if self._output_video_stream.name == "hevc":
- self._output_video_stream.codec_context.codec_tag = "hvc1"
+ self._output_video_stream.codec_tag = "hvc1"
def mux_packet(self, packet: av.Packet) -> None:
"""Mux a packet to the appropriate output stream."""
@@ -277,11 +273,11 @@ class StreamMuxer:
self._part_has_keyframe |= packet.is_keyframe
elif packet.stream == self._input_audio_stream:
- assert self._output_audio_stream
if self._audio_bsf_context:
- for audio_packet in self._audio_bsf_context.filter(packet):
- audio_packet.stream = self._output_audio_stream
- self._av_output.mux(audio_packet)
+ self._audio_bsf_context.send(packet)
+ while packet := self._audio_bsf_context.recv():
+ packet.stream = self._output_audio_stream
+ self._av_output.mux(packet)
return
packet.stream = self._output_audio_stream
self._av_output.mux(packet)
@@ -399,7 +395,7 @@ class StreamMuxer:
self._memory_file.close()
-class PeekIterator(Iterator[av.Packet]):
+class PeekIterator(Iterator):
"""An Iterator that may allow multiple passes.
This may be consumed like a normal Iterator, however also supports a
@@ -463,7 +459,7 @@ class TimestampValidator:
"""Validate the packet timestamp based on ordering within the stream."""
# Discard packets missing DTS. Terminate if too many are missing.
if packet.dts is None:
- if self._missing_dts >= MAX_MISSING_DTS: # type: ignore[unreachable]
+ if self._missing_dts >= MAX_MISSING_DTS:
raise StreamWorkerError(
f"No dts in {MAX_MISSING_DTS+1} consecutive packets"
)
@@ -490,7 +486,7 @@ def is_keyframe(packet: av.Packet) -> Any:
def get_audio_bitstream_filter(
packets: Iterator[av.Packet], audio_stream: Any
-) -> str | None:
+) -> av.BitStreamFilterContext | None:
"""Return the aac_adtstoasc bitstream filter if ADTS AAC is detected."""
if not audio_stream:
return None
@@ -507,7 +503,7 @@ def get_audio_bitstream_filter(
_LOGGER.debug(
"ADTS AAC detected. Adding aac_adtstoaac bitstream filter"
)
- return "aac_adtstoasc"
+ return av.BitStreamFilter("aac_adtstoasc")
break
return None
@@ -528,7 +524,7 @@ def stream_worker(
del pyav_options["stimeout"]
try:
container = av.open(source, options=pyav_options, timeout=SOURCE_TIMEOUT)
- except av.FFmpegError as err:
+ except av.AVError as err:
raise StreamWorkerError(
f"Error opening stream ({redact_av_error_string(err)})"
) from err
@@ -545,7 +541,7 @@ def stream_worker(
audio_stream = None
# Some audio streams do not have a profile and throw errors when remuxing
if audio_stream and audio_stream.profile is None:
- audio_stream = None # type: ignore[unreachable]
+ audio_stream = None
# Disable ll-hls for hls inputs
if container.format.name == "hls":
for field in fields(StreamSettings):
@@ -560,8 +556,8 @@ def stream_worker(
stream_state.diagnostics.set_value("audio_codec", audio_stream.name)
dts_validator = TimestampValidator(
- int(1 / video_stream.time_base), # type: ignore[operator]
- int(1 / audio_stream.time_base) if audio_stream else 1, # type: ignore[operator]
+ int(1 / video_stream.time_base),
+ 1 / audio_stream.time_base if audio_stream else 1,
)
container_packets = PeekIterator(
filter(dts_validator.is_valid, container.demux((video_stream, audio_stream)))
@@ -602,7 +598,7 @@ def stream_worker(
except StopIteration as ex:
container.close()
raise StreamEndedError("Stream ended; no additional packets") from ex
- except av.FFmpegError as ex:
+ except av.AVError as ex:
container.close()
raise StreamWorkerError(
f"Error demuxing stream while finding first packet ({redact_av_error_string(ex)})"
@@ -629,7 +625,7 @@ def stream_worker(
raise
except StopIteration as ex:
raise StreamEndedError("Stream ended; no additional packets") from ex
- except av.FFmpegError as ex:
+ except av.AVError as ex:
raise StreamWorkerError(
f"Error demuxing stream ({redact_av_error_string(ex)})"
) from ex
diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py
index 3762b16e58b..db2ee7fdbbc 100644
--- a/homeassistant/components/subaru/__init__.py
+++ b/homeassistant/components/subaru/__init__.py
@@ -85,7 +85,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=COORDINATOR_NAME,
update_method=async_update_data,
update_interval=timedelta(seconds=FETCH_INTERVAL),
diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py
index 0ef4ed29941..3d96a89a14f 100644
--- a/homeassistant/components/subaru/config_flow.py
+++ b/homeassistant/components/subaru/config_flow.py
@@ -106,7 +106,7 @@ class SubaruConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def validate_login_creds(self, data):
"""Validate the user input allows us to connect.
@@ -218,6 +218,10 @@ class SubaruConfigFlow(ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(OptionsFlow):
"""Handle a option flow for Subaru."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json
index 00da729dccd..78625192e4a 100644
--- a/homeassistant/components/subaru/strings.json
+++ b/homeassistant/components/subaru/strings.json
@@ -37,13 +37,13 @@
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"incorrect_pin": "Incorrect PIN",
"bad_pin_format": "PIN should be 4 digits",
+ "two_factor_request_failed": "Request for 2FA code failed, please try again",
"bad_validation_code_format": "Validation code should be 6 digits",
"incorrect_validation_code": "Incorrect validation code"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "two_factor_request_failed": "Request for 2FA code failed, please try again"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"options": {
diff --git a/homeassistant/components/suez_water/__init__.py b/homeassistant/components/suez_water/__init__.py
index 06f503b85c2..f5b2880e011 100644
--- a/homeassistant/components/suez_water/__init__.py
+++ b/homeassistant/components/suez_water/__init__.py
@@ -2,12 +2,15 @@
from __future__ import annotations
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import Platform
-from homeassistant.core import HomeAssistant
+from pysuez import SuezClient
+from pysuez.client import PySuezError
-from .const import DOMAIN
-from .coordinator import SuezWaterCoordinator
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
+
+from .const import CONF_COUNTER_ID, DOMAIN
PLATFORMS: list[Platform] = [Platform.SENSOR]
@@ -15,10 +18,23 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Suez Water from a config entry."""
- coordinator = SuezWaterCoordinator(hass, entry)
- await coordinator.async_config_entry_first_refresh()
+ def get_client() -> SuezClient:
+ try:
+ client = SuezClient(
+ entry.data[CONF_USERNAME],
+ entry.data[CONF_PASSWORD],
+ entry.data[CONF_COUNTER_ID],
+ provider=None,
+ )
+ if not client.check_credentials():
+ raise ConfigEntryError
+ except PySuezError as ex:
+ raise ConfigEntryNotReady from ex
+ return client
- hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
+ hass.data.setdefault(DOMAIN, {})[
+ entry.entry_id
+ ] = await hass.async_add_executor_job(get_client)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
diff --git a/homeassistant/components/suez_water/config_flow.py b/homeassistant/components/suez_water/config_flow.py
index ac09cf4a1d3..28b211dc808 100644
--- a/homeassistant/components/suez_water/config_flow.py
+++ b/homeassistant/components/suez_water/config_flow.py
@@ -5,7 +5,8 @@ from __future__ import annotations
import logging
from typing import Any
-from pysuez import PySuezError, SuezClient
+from pysuez import SuezClient
+from pysuez.client import PySuezError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -20,34 +21,28 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
- vol.Optional(CONF_COUNTER_ID): str,
+ vol.Required(CONF_COUNTER_ID): str,
}
)
-async def validate_input(data: dict[str, Any]) -> None:
+def validate_input(data: dict[str, Any]) -> None:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
try:
- counter_id = data.get(CONF_COUNTER_ID)
client = SuezClient(
data[CONF_USERNAME],
data[CONF_PASSWORD],
- counter_id,
+ data[CONF_COUNTER_ID],
+ provider=None,
)
- if not await client.check_credentials():
+ if not client.check_credentials():
raise InvalidAuth
except PySuezError as ex:
raise CannotConnect from ex
- if counter_id is None:
- try:
- data[CONF_COUNTER_ID] = await client.find_counter()
- except PySuezError as ex:
- raise CounterNotFound from ex
-
class SuezWaterConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Suez Water."""
@@ -63,13 +58,11 @@ class SuezWaterConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(user_input[CONF_USERNAME])
self._abort_if_unique_id_configured()
try:
- await validate_input(user_input)
+ await self.hass.async_add_executor_job(validate_input, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
- except CounterNotFound:
- errors["base"] = "counter_not_found"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
@@ -89,7 +82,3 @@ class CannotConnect(HomeAssistantError):
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
-
-
-class CounterNotFound(HomeAssistantError):
- """Error to indicate we cannot automatically found the counter id."""
diff --git a/homeassistant/components/suez_water/const.py b/homeassistant/components/suez_water/const.py
index cecd779c22c..7afc0d3ce3e 100644
--- a/homeassistant/components/suez_water/const.py
+++ b/homeassistant/components/suez_water/const.py
@@ -1,9 +1,5 @@
"""Constants for the Suez Water integration."""
-from datetime import timedelta
-
DOMAIN = "suez_water"
CONF_COUNTER_ID = "counter_id"
-
-DATA_REFRESH_INTERVAL = timedelta(hours=12)
diff --git a/homeassistant/components/suez_water/coordinator.py b/homeassistant/components/suez_water/coordinator.py
deleted file mode 100644
index 224929c606e..00000000000
--- a/homeassistant/components/suez_water/coordinator.py
+++ /dev/null
@@ -1,88 +0,0 @@
-"""Suez water update coordinator."""
-
-from collections.abc import Mapping
-from dataclasses import dataclass
-from datetime import date
-from typing import Any
-
-from pysuez import PySuezError, SuezClient
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from homeassistant.core import _LOGGER, HomeAssistant
-from homeassistant.exceptions import ConfigEntryError
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-
-from .const import CONF_COUNTER_ID, DATA_REFRESH_INTERVAL, DOMAIN
-
-
-@dataclass
-class SuezWaterAggregatedAttributes:
- """Class containing aggregated sensor extra attributes."""
-
- this_month_consumption: dict[date, float]
- previous_month_consumption: dict[date, float]
- last_year_overall: dict[str, float]
- this_year_overall: dict[str, float]
- history: dict[date, float]
- highest_monthly_consumption: float
-
-
-@dataclass
-class SuezWaterData:
- """Class used to hold all fetch data from suez api."""
-
- aggregated_value: float
- aggregated_attr: Mapping[str, Any]
- price: float
-
-
-class SuezWaterCoordinator(DataUpdateCoordinator[SuezWaterData]):
- """Suez water coordinator."""
-
- _suez_client: SuezClient
- config_entry: ConfigEntry
-
- def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
- """Initialize suez water coordinator."""
- super().__init__(
- hass,
- _LOGGER,
- name=DOMAIN,
- update_interval=DATA_REFRESH_INTERVAL,
- always_update=True,
- config_entry=config_entry,
- )
-
- async def _async_setup(self) -> None:
- self._suez_client = SuezClient(
- username=self.config_entry.data[CONF_USERNAME],
- password=self.config_entry.data[CONF_PASSWORD],
- counter_id=self.config_entry.data[CONF_COUNTER_ID],
- )
- if not await self._suez_client.check_credentials():
- raise ConfigEntryError("Invalid credentials for suez water")
-
- async def _async_update_data(self) -> SuezWaterData:
- """Fetch data from API endpoint."""
- try:
- aggregated = await self._suez_client.fetch_aggregated_data()
- data = SuezWaterData(
- aggregated_value=aggregated.value,
- aggregated_attr={
- "this_month_consumption": aggregated.current_month,
- "previous_month_consumption": aggregated.previous_month,
- "highest_monthly_consumption": aggregated.highest_monthly_consumption,
- "last_year_overall": aggregated.previous_year,
- "this_year_overall": aggregated.current_year,
- "history": aggregated.history,
- },
- price=(await self._suez_client.get_price()).price,
- )
- except PySuezError as err:
- _LOGGER.exception(err)
- raise UpdateFailed(
- f"Suez coordinator error communicating with API: {err}"
- ) from err
- _LOGGER.debug("Successfully fetched suez data")
- return data
diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json
index 5eb05b9acb7..4503d7a1119 100644
--- a/homeassistant/components/suez_water/manifest.json
+++ b/homeassistant/components/suez_water/manifest.json
@@ -1,10 +1,10 @@
{
"domain": "suez_water",
"name": "Suez Water",
- "codeowners": ["@ooii", "@jb101010-2"],
+ "codeowners": ["@ooii"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/suez_water",
"iot_class": "cloud_polling",
"loggers": ["pysuez", "regex"],
- "requirements": ["pysuezV2==1.3.1"]
+ "requirements": ["pysuez==0.2.0"]
}
diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py
index 2ba699a9af1..5b00cbf2dc4 100644
--- a/homeassistant/components/suez_water/sensor.py
+++ b/homeassistant/components/suez_water/sensor.py
@@ -2,53 +2,24 @@
from __future__ import annotations
-from collections.abc import Callable, Mapping
-from dataclasses import dataclass
-from typing import Any
+from datetime import timedelta
+import logging
-from pysuez.const import ATTRIBUTION
+from pysuez import SuezClient
+from pysuez.client import PySuezError
-from homeassistant.components.sensor import (
- SensorDeviceClass,
- SensorEntity,
- SensorEntityDescription,
-)
+from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CURRENCY_EURO, UnitOfVolume
+from homeassistant.const import UnitOfVolume
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONF_COUNTER_ID, DOMAIN
-from .coordinator import SuezWaterCoordinator, SuezWaterData
+_LOGGER = logging.getLogger(__name__)
-@dataclass(frozen=True, kw_only=True)
-class SuezWaterSensorEntityDescription(SensorEntityDescription):
- """Describes Suez water sensor entity."""
-
- value_fn: Callable[[SuezWaterData], float | str | None]
- attr_fn: Callable[[SuezWaterData], Mapping[str, Any] | None] = lambda _: None
-
-
-SENSORS: tuple[SuezWaterSensorEntityDescription, ...] = (
- SuezWaterSensorEntityDescription(
- key="water_usage_yesterday",
- translation_key="water_usage_yesterday",
- native_unit_of_measurement=UnitOfVolume.LITERS,
- device_class=SensorDeviceClass.WATER,
- value_fn=lambda suez_data: suez_data.aggregated_value,
- attr_fn=lambda suez_data: suez_data.aggregated_attr,
- ),
- SuezWaterSensorEntityDescription(
- key="water_price",
- translation_key="water_price",
- native_unit_of_measurement=CURRENCY_EURO,
- device_class=SensorDeviceClass.MONETARY,
- value_fn=lambda suez_data: suez_data.price,
- ),
-)
+SCAN_INTERVAL = timedelta(hours=12)
async def async_setup_entry(
@@ -57,43 +28,68 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Suez Water sensor from a config entry."""
- coordinator = hass.data[DOMAIN][entry.entry_id]
- counter_id = entry.data[CONF_COUNTER_ID]
-
- async_add_entities(
- SuezWaterSensor(coordinator, counter_id, description) for description in SENSORS
- )
+ client = hass.data[DOMAIN][entry.entry_id]
+ async_add_entities([SuezSensor(client, entry.data[CONF_COUNTER_ID])], True)
-class SuezWaterSensor(CoordinatorEntity[SuezWaterCoordinator], SensorEntity):
- """Representation of a Suez water sensor."""
+class SuezSensor(SensorEntity):
+ """Representation of a Sensor."""
_attr_has_entity_name = True
- _attr_attribution = ATTRIBUTION
- entity_description: SuezWaterSensorEntityDescription
+ _attr_translation_key = "water_usage_yesterday"
+ _attr_native_unit_of_measurement = UnitOfVolume.LITERS
+ _attr_device_class = SensorDeviceClass.WATER
- def __init__(
- self,
- coordinator: SuezWaterCoordinator,
- counter_id: int,
- entity_description: SuezWaterSensorEntityDescription,
- ) -> None:
- """Initialize the suez water sensor entity."""
- super().__init__(coordinator)
- self._attr_unique_id = f"{counter_id}_{entity_description.key}"
+ def __init__(self, client: SuezClient, counter_id: int) -> None:
+ """Initialize the data object."""
+ self.client = client
+ self._attr_extra_state_attributes = {}
+ self._attr_unique_id = f"{counter_id}_water_usage_yesterday"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(counter_id))},
entry_type=DeviceEntryType.SERVICE,
manufacturer="Suez",
)
- self.entity_description = entity_description
- @property
- def native_value(self) -> float | str | None:
- """Return the state of the sensor."""
- return self.entity_description.value_fn(self.coordinator.data)
+ def _fetch_data(self) -> None:
+ """Fetch latest data from Suez."""
+ try:
+ self.client.update()
+ # _state holds the volume of consumed water during previous day
+ self._attr_native_value = self.client.state
+ self._attr_available = True
+ self._attr_attribution = self.client.attributes["attribution"]
- @property
- def extra_state_attributes(self) -> Mapping[str, Any] | None:
- """Return extra state of the sensor."""
- return self.entity_description.attr_fn(self.coordinator.data)
+ self._attr_extra_state_attributes["this_month_consumption"] = {}
+ for item in self.client.attributes["thisMonthConsumption"]:
+ self._attr_extra_state_attributes["this_month_consumption"][item] = (
+ self.client.attributes["thisMonthConsumption"][item]
+ )
+ self._attr_extra_state_attributes["previous_month_consumption"] = {}
+ for item in self.client.attributes["previousMonthConsumption"]:
+ self._attr_extra_state_attributes["previous_month_consumption"][
+ item
+ ] = self.client.attributes["previousMonthConsumption"][item]
+ self._attr_extra_state_attributes["highest_monthly_consumption"] = (
+ self.client.attributes["highestMonthlyConsumption"]
+ )
+ self._attr_extra_state_attributes["last_year_overall"] = (
+ self.client.attributes["lastYearOverAll"]
+ )
+ self._attr_extra_state_attributes["this_year_overall"] = (
+ self.client.attributes["thisYearOverAll"]
+ )
+ self._attr_extra_state_attributes["history"] = {}
+ for item in self.client.attributes["history"]:
+ self._attr_extra_state_attributes["history"][item] = (
+ self.client.attributes["history"][item]
+ )
+
+ except PySuezError:
+ self._attr_available = False
+ _LOGGER.warning("Unable to fetch data")
+
+ def update(self) -> None:
+ """Return the latest collected data from Suez."""
+ self._fetch_data()
+ _LOGGER.debug("Suez data state is: %s", self.native_value)
diff --git a/homeassistant/components/suez_water/strings.json b/homeassistant/components/suez_water/strings.json
index 6be2affab97..f9abd70fc19 100644
--- a/homeassistant/components/suez_water/strings.json
+++ b/homeassistant/components/suez_water/strings.json
@@ -12,8 +12,7 @@
"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%]",
- "counter_not_found": "Could not find counter id automatically"
+ "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
@@ -23,9 +22,6 @@
"sensor": {
"water_usage_yesterday": {
"name": "Water usage yesterday"
- },
- "water_price": {
- "name": "Water price"
}
}
}
diff --git a/homeassistant/components/sunweg/config_flow.py b/homeassistant/components/sunweg/config_flow.py
index 24df8c02f55..2b5e49c2cb9 100644
--- a/homeassistant/components/sunweg/config_flow.py
+++ b/homeassistant/components/sunweg/config_flow.py
@@ -124,6 +124,12 @@ class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN):
if conf_result is not None:
return conf_result
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=self.data
- )
+ entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+ if entry is not None:
+ data: Mapping[str, Any] = self.data
+ self.hass.config_entries.async_update_entry(entry, data=data)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(entry.entry_id)
+ )
+
+ return self.async_abort(reason="reauth_successful")
diff --git a/homeassistant/components/sunweg/manifest.json b/homeassistant/components/sunweg/manifest.json
index 3ebe9ef8cb4..998d3610735 100644
--- a/homeassistant/components/sunweg/manifest.json
+++ b/homeassistant/components/sunweg/manifest.json
@@ -3,7 +3,7 @@
"name": "Sun WEG",
"codeowners": ["@rokam"],
"config_flow": true,
- "documentation": "https://www.home-assistant.io/integrations/sunweg",
+ "documentation": "https://www.home-assistant.io/integrations/sunweg/",
"iot_class": "cloud_polling",
"loggers": ["sunweg"],
"requirements": ["sunweg==3.0.2"]
diff --git a/homeassistant/components/sunweg/strings.json b/homeassistant/components/sunweg/strings.json
index 9ab7be053b1..6033bc314bc 100644
--- a/homeassistant/components/sunweg/strings.json
+++ b/homeassistant/components/sunweg/strings.json
@@ -1,7 +1,6 @@
{
"config": {
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_plants": "No plants have been found on this account",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
diff --git a/homeassistant/components/surepetcare/config_flow.py b/homeassistant/components/surepetcare/config_flow.py
index 472d7ac10f0..a993e9a47f1 100644
--- a/homeassistant/components/surepetcare/config_flow.py
+++ b/homeassistant/components/surepetcare/config_flow.py
@@ -10,7 +10,7 @@ import surepy
from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -31,6 +31,8 @@ class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ reauth_entry: ConfigEntry | None = None
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -70,17 +72,20 @@ class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
+ assert self.reauth_entry
errors = {}
- reauth_entry = self._get_reauth_entry()
if user_input is not None:
client = surepy.Surepy(
- reauth_entry.data[CONF_USERNAME],
+ self.reauth_entry.data[CONF_USERNAME],
user_input[CONF_PASSWORD],
auth_token=None,
api_timeout=SURE_API_TIMEOUT,
@@ -97,8 +102,9 @@ class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
- reauth_entry,
- data_updates={
+ self.reauth_entry,
+ data={
+ **self.reauth_entry.data,
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_TOKEN: token,
},
@@ -106,7 +112,9 @@ class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="reauth_confirm",
- description_placeholders={"username": reauth_entry.data[CONF_USERNAME]},
+ description_placeholders={
+ "username": self.reauth_entry.data[CONF_USERNAME]
+ },
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
errors=errors,
)
diff --git a/homeassistant/components/surepetcare/strings.json b/homeassistant/components/surepetcare/strings.json
index 58db669732a..c3b7864f36a 100644
--- a/homeassistant/components/surepetcare/strings.json
+++ b/homeassistant/components/surepetcare/strings.json
@@ -21,8 +21,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
},
"services": {
diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py
index bceac6007a2..dc1d0eb236c 100644
--- a/homeassistant/components/swiss_public_transport/__init__.py
+++ b/homeassistant/components/swiss_public_transport/__init__.py
@@ -8,8 +8,8 @@ from opendata_transport.exceptions import (
OpendataTransportError,
)
+from homeassistant import config_entries, core
from homeassistant.const import Platform
-from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers import (
config_validation as cv,
@@ -20,10 +20,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import CONF_DESTINATION, CONF_START, CONF_VIA, DOMAIN, PLACEHOLDERS
-from .coordinator import (
- SwissPublicTransportConfigEntry,
- SwissPublicTransportDataUpdateCoordinator,
-)
+from .coordinator import SwissPublicTransportDataUpdateCoordinator
from .helper import unique_id_from_config
from .services import setup_services
@@ -35,14 +32,14 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
-async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+async def async_setup(hass: core.HomeAssistant, config: ConfigType) -> bool:
"""Set up the Swiss public transport component."""
setup_services(hass)
return True
async def async_setup_entry(
- hass: HomeAssistant, entry: SwissPublicTransportConfigEntry
+ hass: core.HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Set up Swiss public transport from a config entry."""
config = entry.data
@@ -77,21 +74,24 @@ async def async_setup_entry(
coordinator = SwissPublicTransportDataUpdateCoordinator(hass, opendata)
await coordinator.async_config_entry_first_refresh()
- entry.runtime_data = coordinator
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(
- hass: HomeAssistant, entry: SwissPublicTransportConfigEntry
+ hass: core.HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Unload a config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
async def async_migrate_entry(
- hass: HomeAssistant, config_entry: SwissPublicTransportConfigEntry
+ hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
) -> bool:
"""Migrate config entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
diff --git a/homeassistant/components/swiss_public_transport/coordinator.py b/homeassistant/components/swiss_public_transport/coordinator.py
index e6413e6f772..f91f9a7c768 100644
--- a/homeassistant/components/swiss_public_transport/coordinator.py
+++ b/homeassistant/components/swiss_public_transport/coordinator.py
@@ -16,16 +16,11 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import homeassistant.util.dt as dt_util
-from homeassistant.util.json import JsonValueType
from .const import CONNECTIONS_COUNT, DEFAULT_UPDATE_TIME, DOMAIN
_LOGGER = logging.getLogger(__name__)
-type SwissPublicTransportConfigEntry = ConfigEntry[
- SwissPublicTransportDataUpdateCoordinator
-]
-
class DataConnection(TypedDict):
"""A connection data class."""
@@ -55,7 +50,7 @@ class SwissPublicTransportDataUpdateCoordinator(
):
"""A SwissPublicTransport Data Update Coordinator."""
- config_entry: SwissPublicTransportConfigEntry
+ config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, opendata: OpendataTransport) -> None:
"""Initialize the SwissPublicTransport data coordinator."""
@@ -75,6 +70,13 @@ class SwissPublicTransportDataUpdateCoordinator(
return departure_datetime - dt_util.as_local(dt_util.utcnow())
return None
+ def nth_departure_time(self, i: int) -> datetime | None:
+ """Get nth departure time."""
+ connections = self._opendata.connections
+ if len(connections) > i and connections[i] is not None:
+ return dt_util.parse_datetime(connections[i]["departure"])
+ return None
+
async def _async_update_data(self) -> list[DataConnection]:
return await self.fetch_connections(limit=CONNECTIONS_COUNT)
@@ -94,7 +96,7 @@ class SwissPublicTransportDataUpdateCoordinator(
connections = self._opendata.connections
return [
DataConnection(
- departure=dt_util.parse_datetime(connections[i]["departure"]),
+ departure=self.nth_departure_time(i),
train_number=connections[i]["number"],
platform=connections[i]["platform"],
transfers=connections[i]["transfers"],
@@ -108,23 +110,3 @@ class SwissPublicTransportDataUpdateCoordinator(
for i in range(limit)
if len(connections) > i and connections[i] is not None
]
-
- async def fetch_connections_as_json(self, limit: int) -> list[JsonValueType]:
- """Fetch connections using the opendata api."""
- return [
- {
- "departure": connection["departure"].isoformat()
- if connection["departure"]
- else None,
- "duration": connection["duration"],
- "platform": connection["platform"],
- "remaining_time": connection["remaining_time"],
- "start": connection["start"],
- "destination": connection["destination"],
- "train_number": connection["train_number"],
- "transfers": connection["transfers"],
- "delay": connection["delay"],
- "line": connection["line"],
- }
- for connection in await self.fetch_connections(limit)
- ]
diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py
index 452ec31972f..eb73ce03062 100644
--- a/homeassistant/components/swiss_public_transport/sensor.py
+++ b/homeassistant/components/swiss_public_transport/sensor.py
@@ -8,24 +8,20 @@ from datetime import datetime, timedelta
import logging
from typing import TYPE_CHECKING
+from homeassistant import config_entries, core
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import UnitOfTime
-from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONNECTIONS_COUNT, DOMAIN
-from .coordinator import (
- DataConnection,
- SwissPublicTransportConfigEntry,
- SwissPublicTransportDataUpdateCoordinator,
-)
+from .coordinator import DataConnection, SwissPublicTransportDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -84,18 +80,20 @@ SENSORS: tuple[SwissPublicTransportSensorEntityDescription, ...] = (
async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: SwissPublicTransportConfigEntry,
+ hass: core.HomeAssistant,
+ config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor from a config entry created in the integrations UI."""
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]
+
unique_id = config_entry.unique_id
if TYPE_CHECKING:
assert unique_id
async_add_entities(
- SwissPublicTransportSensor(config_entry.runtime_data, description, unique_id)
+ SwissPublicTransportSensor(coordinator, description, unique_id)
for description in SENSORS
)
diff --git a/homeassistant/components/swiss_public_transport/services.py b/homeassistant/components/swiss_public_transport/services.py
index 3abf1a14b9f..e8b7c6bd458 100644
--- a/homeassistant/components/swiss_public_transport/services.py
+++ b/homeassistant/components/swiss_public_transport/services.py
@@ -2,6 +2,7 @@
import voluptuous as vol
+from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import (
HomeAssistant,
@@ -25,7 +26,6 @@ from .const import (
DOMAIN,
SERVICE_FETCH_CONNECTIONS,
)
-from .coordinator import SwissPublicTransportConfigEntry
SERVICE_FETCH_CONNECTIONS_SCHEMA = vol.Schema(
{
@@ -41,7 +41,7 @@ SERVICE_FETCH_CONNECTIONS_SCHEMA = vol.Schema(
def async_get_entry(
hass: HomeAssistant, config_entry_id: str
-) -> SwissPublicTransportConfigEntry:
+) -> config_entries.ConfigEntry:
"""Get the Swiss public transport config entry."""
if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
raise ServiceValidationError(
@@ -66,12 +66,10 @@ def setup_services(hass: HomeAssistant) -> None:
) -> ServiceResponse:
"""Fetch a set of connections."""
config_entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID])
-
limit = call.data.get(ATTR_LIMIT) or CONNECTIONS_COUNT
+ coordinator = hass.data[DOMAIN][config_entry.entry_id]
try:
- connections = await config_entry.runtime_data.fetch_connections_as_json(
- limit=int(limit)
- )
+ connections = await coordinator.fetch_connections(limit=int(limit))
except UpdateFailed as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py
index c2b4b2ad736..75845d3f3ce 100644
--- a/homeassistant/components/switchbot/__init__.py
+++ b/homeassistant/components/switchbot/__init__.py
@@ -41,7 +41,6 @@ PLATFORMS_BY_TYPE = {
Platform.SENSOR,
],
SupportedModels.HYGROMETER.value: [Platform.SENSOR],
- SupportedModels.HYGROMETER_CO2.value: [Platform.SENSOR],
SupportedModels.CONTACT.value: [Platform.BINARY_SENSOR, Platform.SENSOR],
SupportedModels.MOTION.value: [Platform.BINARY_SENSOR, Platform.SENSOR],
SupportedModels.HUMIDIFIER.value: [Platform.HUMIDIFIER, Platform.SENSOR],
diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py
index a0e45169770..0468db5618a 100644
--- a/homeassistant/components/switchbot/config_flow.py
+++ b/homeassistant/components/switchbot/config_flow.py
@@ -80,7 +80,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> SwitchbotOptionsFlowHandler:
"""Get the options flow for this handler."""
- return SwitchbotOptionsFlowHandler()
+ return SwitchbotOptionsFlowHandler(config_entry)
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -346,6 +346,10 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
class SwitchbotOptionsFlowHandler(OptionsFlow):
"""Handle Switchbot options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py
index 19b264bd46f..bd727edfea4 100644
--- a/homeassistant/components/switchbot/const.py
+++ b/homeassistant/components/switchbot/const.py
@@ -20,7 +20,6 @@ class SupportedModels(StrEnum):
CEILING_LIGHT = "ceiling_light"
CURTAIN = "curtain"
HYGROMETER = "hygrometer"
- HYGROMETER_CO2 = "hygrometer_co2"
LIGHT_STRIP = "light_strip"
CONTACT = "contact"
PLUG = "plug"
@@ -49,8 +48,6 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = {
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
SwitchbotModel.METER: SupportedModels.HYGROMETER,
SwitchbotModel.IO_METER: SupportedModels.HYGROMETER,
- SwitchbotModel.METER_PRO: SupportedModels.HYGROMETER,
- SwitchbotModel.METER_PRO_C: SupportedModels.HYGROMETER_CO2,
SwitchbotModel.CONTACT_SENSOR: SupportedModels.CONTACT,
SwitchbotModel.MOTION_SENSOR: SupportedModels.MOTION,
}
diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json
index 0e369f8ad2d..f97162184c6 100644
--- a/homeassistant/components/switchbot/manifest.json
+++ b/homeassistant/components/switchbot/manifest.json
@@ -39,5 +39,5 @@
"documentation": "https://www.home-assistant.io/integrations/switchbot",
"iot_class": "local_push",
"loggers": ["switchbot"],
- "requirements": ["PySwitchbot==0.51.0"]
+ "requirements": ["PySwitchbot==0.48.2"]
}
diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py
index fd3de3e31e9..e696f21e082 100644
--- a/homeassistant/components/switchbot/sensor.py
+++ b/homeassistant/components/switchbot/sensor.py
@@ -10,7 +10,6 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
- CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
@@ -51,12 +50,6 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = {
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
- "co2": SensorEntityDescription(
- key="co2",
- native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
- state_class=SensorStateClass.MEASUREMENT,
- device_class=SensorDeviceClass.CO2,
- ),
"lightLevel": SensorEntityDescription(
key="lightLevel",
translation_key="light_level",
diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py
index 625b4698301..39a179aaa21 100644
--- a/homeassistant/components/switchbot_cloud/__init__.py
+++ b/homeassistant/components/switchbot_cloud/__init__.py
@@ -17,7 +17,6 @@ from .coordinator import SwitchBotCoordinator
_LOGGER = getLogger(__name__)
PLATFORMS: list[Platform] = [
Platform.CLIMATE,
- Platform.LOCK,
Platform.SENSOR,
Platform.SWITCH,
Platform.VACUUM,
@@ -32,7 +31,6 @@ class SwitchbotDevices:
switches: list[Device | Remote] = field(default_factory=list)
sensors: list[Device] = field(default_factory=list)
vacuums: list[Device] = field(default_factory=list)
- locks: list[Device] = field(default_factory=list)
@dataclass
@@ -85,9 +83,6 @@ def make_device_data(
"Meter",
"MeterPlus",
"WoIOSensor",
- "Hub 2",
- "MeterPro",
- "MeterPro(CO2)",
]:
devices_data.sensors.append(
prepare_device(hass, api, device, coordinators_by_id)
@@ -102,10 +97,6 @@ def make_device_data(
prepare_device(hass, api, device, coordinators_by_id)
)
- if isinstance(device, Device) and device.device_type.startswith("Smart Lock"):
- devices_data.locks.append(
- prepare_device(hass, api, device, coordinators_by_id)
- )
return devices_data
diff --git a/homeassistant/components/switchbot_cloud/lock.py b/homeassistant/components/switchbot_cloud/lock.py
deleted file mode 100644
index 2fbd551b919..00000000000
--- a/homeassistant/components/switchbot_cloud/lock.py
+++ /dev/null
@@ -1,53 +0,0 @@
-"""Support for the Switchbot lock."""
-
-from typing import Any
-
-from switchbot_api import LockCommands
-
-from homeassistant.components.lock import LockEntity
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import SwitchbotCloudData
-from .const import DOMAIN
-from .entity import SwitchBotCloudEntity
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up SwitchBot Cloud entry."""
- data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
- async_add_entities(
- SwitchBotCloudLock(data.api, device, coordinator)
- for device, coordinator in data.devices.locks
- )
-
-
-class SwitchBotCloudLock(SwitchBotCloudEntity, LockEntity):
- """Representation of a SwitchBot lock."""
-
- _attr_name = None
-
- @callback
- def _handle_coordinator_update(self) -> None:
- """Handle updated data from the coordinator."""
- if coord_data := self.coordinator.data:
- self._attr_is_locked = coord_data["lockState"] == "locked"
- self.async_write_ha_state()
-
- async def async_lock(self, **kwargs: Any) -> None:
- """Lock the lock."""
- await self.send_api_command(LockCommands.LOCK)
- self._attr_is_locked = True
- self.async_write_ha_state()
-
- async def async_unlock(self, **kwargs: Any) -> None:
- """Unlock the lock."""
-
- await self.send_api_command(LockCommands.UNLOCK)
- self._attr_is_locked = False
- self.async_write_ha_state()
diff --git a/homeassistant/components/switchbot_cloud/sensor.py b/homeassistant/components/switchbot_cloud/sensor.py
index 90135ad96b3..ac612aea119 100644
--- a/homeassistant/components/switchbot_cloud/sensor.py
+++ b/homeassistant/components/switchbot_cloud/sensor.py
@@ -9,11 +9,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import (
- CONCENTRATION_PARTS_PER_MILLION,
- PERCENTAGE,
- UnitOfTemperature,
-)
+from homeassistant.const import PERCENTAGE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -25,7 +21,6 @@ from .entity import SwitchBotCloudEntity
SENSOR_TYPE_TEMPERATURE = "temperature"
SENSOR_TYPE_HUMIDITY = "humidity"
SENSOR_TYPE_BATTERY = "battery"
-SENSOR_TYPE_CO2 = "CO2"
METER_PLUS_SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
@@ -48,16 +43,6 @@ METER_PLUS_SENSOR_DESCRIPTIONS = (
),
)
-METER_PRO_CO2_SENSOR_DESCRIPTIONS = (
- *METER_PLUS_SENSOR_DESCRIPTIONS,
- SensorEntityDescription(
- key=SENSOR_TYPE_CO2,
- native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
- state_class=SensorStateClass.MEASUREMENT,
- device_class=SensorDeviceClass.CO2,
- ),
-)
-
async def async_setup_entry(
hass: HomeAssistant,
@@ -70,11 +55,7 @@ async def async_setup_entry(
async_add_entities(
SwitchBotCloudSensor(data.api, device, coordinator, description)
for device, coordinator in data.devices.sensors
- for description in (
- METER_PRO_CO2_SENSOR_DESCRIPTIONS
- if device.device_type == "MeterPro(CO2)"
- else METER_PLUS_SENSOR_DESCRIPTIONS
- )
+ for description in METER_PLUS_SENSOR_DESCRIPTIONS
)
diff --git a/homeassistant/components/switcher_kis/config_flow.py b/homeassistant/components/switcher_kis/config_flow.py
index e6c2e8e8589..e34961ebf6c 100644
--- a/homeassistant/components/switcher_kis/config_flow.py
+++ b/homeassistant/components/switcher_kis/config_flow.py
@@ -10,7 +10,7 @@ from aioswitcher.bridge import SwitcherBase
from aioswitcher.device.tools import validate_token
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_TOKEN, CONF_USERNAME
from .const import DOMAIN
@@ -32,6 +32,7 @@ class SwitcherFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ entry: ConfigEntry | None = None
username: str | None = None
token: str | None = None
discovered_devices: dict[str, SwitcherBase] = {}
@@ -81,6 +82,7 @@ class SwitcherFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -88,6 +90,7 @@ class SwitcherFlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
+ assert self.entry is not None
if user_input is not None:
token_is_valid = await validate_token(
@@ -95,7 +98,7 @@ class SwitcherFlowHandler(ConfigFlow, domain=DOMAIN):
)
if token_is_valid:
return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=user_input
+ self.entry, data={**self.entry.data, **user_input}
)
errors["base"] = "invalid_auth"
diff --git a/homeassistant/components/switcher_kis/coordinator.py b/homeassistant/components/switcher_kis/coordinator.py
index 118c86b8d78..d292e9f8f39 100644
--- a/homeassistant/components/switcher_kis/coordinator.py
+++ b/homeassistant/components/switcher_kis/coordinator.py
@@ -23,8 +23,6 @@ class SwitcherDataUpdateCoordinator(
):
"""Switcher device data update coordinator."""
- config_entry: ConfigEntry
-
def __init__(
self,
hass: HomeAssistant,
@@ -35,10 +33,10 @@ class SwitcherDataUpdateCoordinator(
super().__init__(
hass,
_LOGGER,
- config_entry=entry,
name=device.name,
update_interval=timedelta(seconds=MAX_UPDATE_INTERVAL_SEC),
)
+ self.entry = entry
self.data = device
self.token = entry.data.get(CONF_TOKEN)
@@ -69,7 +67,7 @@ class SwitcherDataUpdateCoordinator(
"""Set up the coordinator."""
dev_reg = dr.async_get(self.hass)
dev_reg.async_get_or_create(
- config_entry_id=self.config_entry.entry_id,
+ config_entry_id=self.entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)},
identifiers={(DOMAIN, self.device_id)},
manufacturer="Switcher",
diff --git a/homeassistant/components/switcher_kis/cover.py b/homeassistant/components/switcher_kis/cover.py
index dc3b6d96aed..d81611b1629 100644
--- a/homeassistant/components/switcher_kis/cover.py
+++ b/homeassistant/components/switcher_kis/cover.py
@@ -40,31 +40,21 @@ async def async_setup_entry(
@callback
def async_add_cover(coordinator: SwitcherDataUpdateCoordinator) -> None:
"""Add cover from Switcher device."""
- entities: list[CoverEntity] = []
-
if coordinator.data.device_type.category in (
DeviceCategory.SHUTTER,
DeviceCategory.SINGLE_SHUTTER_DUAL_LIGHT,
- DeviceCategory.DUAL_SHUTTER_SINGLE_LIGHT,
):
- number_of_covers = len(cast(SwitcherShutter, coordinator.data).position)
- if number_of_covers == 1:
- entities.append(SwitcherSingleCoverEntity(coordinator, 0))
- else:
- entities.extend(
- SwitcherMultiCoverEntity(coordinator, i)
- for i in range(number_of_covers)
- )
- async_add_entities(entities)
+ async_add_entities([SwitcherCoverEntity(coordinator, 0)])
config_entry.async_on_unload(
async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_cover)
)
-class SwitcherBaseCoverEntity(SwitcherEntity, CoverEntity):
+class SwitcherCoverEntity(SwitcherEntity, CoverEntity):
"""Representation of a Switcher cover entity."""
+ _attr_name = None
_attr_device_class = CoverDeviceClass.SHUTTER
_attr_supported_features = (
CoverEntityFeature.OPEN
@@ -72,7 +62,19 @@ class SwitcherBaseCoverEntity(SwitcherEntity, CoverEntity):
| CoverEntityFeature.SET_POSITION
| CoverEntityFeature.STOP
)
- _cover_id: int
+
+ def __init__(
+ self,
+ coordinator: SwitcherDataUpdateCoordinator,
+ cover_id: int | None = None,
+ ) -> None:
+ """Initialize the entity."""
+ super().__init__(coordinator)
+ self._cover_id = cover_id
+
+ self._attr_unique_id = f"{coordinator.device_id}-{coordinator.mac_address}"
+
+ self._update_data()
@callback
def _handle_coordinator_update(self) -> None:
@@ -83,14 +85,10 @@ class SwitcherBaseCoverEntity(SwitcherEntity, CoverEntity):
def _update_data(self) -> None:
"""Update data from device."""
data = cast(SwitcherShutter, self.coordinator.data)
- self._attr_current_cover_position = data.position[self._cover_id]
- self._attr_is_closed = data.position[self._cover_id] == 0
- self._attr_is_closing = (
- data.direction[self._cover_id] == ShutterDirection.SHUTTER_DOWN
- )
- self._attr_is_opening = (
- data.direction[self._cover_id] == ShutterDirection.SHUTTER_UP
- )
+ self._attr_current_cover_position = data.position
+ self._attr_is_closed = data.position == 0
+ self._attr_is_closing = data.direction == ShutterDirection.SHUTTER_DOWN
+ self._attr_is_opening = data.direction == ShutterDirection.SHUTTER_UP
async def _async_call_api(self, api: str, *args: Any) -> None:
"""Call Switcher API."""
@@ -135,44 +133,3 @@ class SwitcherBaseCoverEntity(SwitcherEntity, CoverEntity):
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
await self._async_call_api(API_STOP, self._cover_id)
-
-
-class SwitcherSingleCoverEntity(SwitcherBaseCoverEntity):
- """Representation of a Switcher single cover entity."""
-
- _attr_name = None
-
- def __init__(
- self,
- coordinator: SwitcherDataUpdateCoordinator,
- cover_id: int,
- ) -> None:
- """Initialize the entity."""
- super().__init__(coordinator)
- self._cover_id = cover_id
-
- self._attr_unique_id = f"{coordinator.device_id}-{coordinator.mac_address}"
-
- self._update_data()
-
-
-class SwitcherMultiCoverEntity(SwitcherBaseCoverEntity):
- """Representation of a Switcher multiple cover entity."""
-
- _attr_translation_key = "cover"
-
- def __init__(
- self,
- coordinator: SwitcherDataUpdateCoordinator,
- cover_id: int,
- ) -> None:
- """Initialize the entity."""
- super().__init__(coordinator)
- self._cover_id = cover_id
-
- self._attr_translation_placeholders = {"cover_id": str(cover_id + 1)}
- self._attr_unique_id = (
- f"{coordinator.device_id}-{coordinator.mac_address}-{cover_id}"
- )
-
- self._update_data()
diff --git a/homeassistant/components/switcher_kis/light.py b/homeassistant/components/switcher_kis/light.py
index bd87176bcf0..d3e8d52bc00 100644
--- a/homeassistant/components/switcher_kis/light.py
+++ b/homeassistant/components/switcher_kis/light.py
@@ -6,7 +6,11 @@ import logging
from typing import Any, cast
from aioswitcher.api import SwitcherBaseResponse, SwitcherType2Api
-from aioswitcher.device import DeviceCategory, DeviceState, SwitcherLight
+from aioswitcher.device import (
+ DeviceCategory,
+ DeviceState,
+ SwitcherSingleShutterDualLight,
+)
from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
@@ -34,35 +38,42 @@ async def async_setup_entry(
@callback
def async_add_light(coordinator: SwitcherDataUpdateCoordinator) -> None:
"""Add light from Switcher device."""
- entities: list[LightEntity] = []
-
- if coordinator.data.device_type.category in (
- DeviceCategory.SINGLE_SHUTTER_DUAL_LIGHT,
- DeviceCategory.DUAL_SHUTTER_SINGLE_LIGHT,
- DeviceCategory.LIGHT,
+ if (
+ coordinator.data.device_type.category
+ == DeviceCategory.SINGLE_SHUTTER_DUAL_LIGHT
):
- number_of_lights = len(cast(SwitcherLight, coordinator.data).light)
- if number_of_lights == 1:
- entities.append(SwitcherSingleLightEntity(coordinator, 0))
- else:
- entities.extend(
- SwitcherMultiLightEntity(coordinator, i)
- for i in range(number_of_lights)
- )
- async_add_entities(entities)
+ async_add_entities(
+ [
+ SwitcherLightEntity(coordinator, 0),
+ SwitcherLightEntity(coordinator, 1),
+ ]
+ )
config_entry.async_on_unload(
async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_light)
)
-class SwitcherBaseLightEntity(SwitcherEntity, LightEntity):
+class SwitcherLightEntity(SwitcherEntity, LightEntity):
"""Representation of a Switcher light entity."""
_attr_color_mode = ColorMode.ONOFF
_attr_supported_color_modes = {ColorMode.ONOFF}
- control_result: bool | None = None
- _light_id: int
+ _attr_translation_key = "light"
+
+ def __init__(
+ self, coordinator: SwitcherDataUpdateCoordinator, light_id: int
+ ) -> None:
+ """Initialize the entity."""
+ super().__init__(coordinator)
+ self._light_id = light_id
+ self.control_result: bool | None = None
+
+ # Entity class attributes
+ self._attr_translation_placeholders = {"light_id": str(light_id + 1)}
+ self._attr_unique_id = (
+ f"{coordinator.device_id}-{coordinator.mac_address}-{light_id}"
+ )
@callback
def _handle_coordinator_update(self) -> None:
@@ -76,8 +87,8 @@ class SwitcherBaseLightEntity(SwitcherEntity, LightEntity):
if self.control_result is not None:
return self.control_result
- data = cast(SwitcherLight, self.coordinator.data)
- return bool(data.light[self._light_id] == DeviceState.ON)
+ data = cast(SwitcherSingleShutterDualLight, self.coordinator.data)
+ return bool(data.lights[self._light_id] == DeviceState.ON)
async def _async_call_api(self, api: str, *args: Any) -> None:
"""Call Switcher API."""
@@ -116,44 +127,3 @@ class SwitcherBaseLightEntity(SwitcherEntity, LightEntity):
await self._async_call_api(API_SET_LIGHT, DeviceState.OFF, self._light_id)
self.control_result = False
self.async_write_ha_state()
-
-
-class SwitcherSingleLightEntity(SwitcherBaseLightEntity):
- """Representation of a Switcher single light entity."""
-
- _attr_name = None
-
- def __init__(
- self,
- coordinator: SwitcherDataUpdateCoordinator,
- light_id: int,
- ) -> None:
- """Initialize the entity."""
- super().__init__(coordinator)
- self._light_id = light_id
- self.control_result: bool | None = None
-
- # Entity class attributes
- self._attr_unique_id = f"{coordinator.device_id}-{coordinator.mac_address}"
-
-
-class SwitcherMultiLightEntity(SwitcherBaseLightEntity):
- """Representation of a Switcher multiple light entity."""
-
- _attr_translation_key = "light"
-
- def __init__(
- self,
- coordinator: SwitcherDataUpdateCoordinator,
- light_id: int,
- ) -> None:
- """Initialize the entity."""
- super().__init__(coordinator)
- self._light_id = light_id
- self.control_result: bool | None = None
-
- # Entity class attributes
- self._attr_translation_placeholders = {"light_id": str(light_id + 1)}
- self._attr_unique_id = (
- f"{coordinator.device_id}-{coordinator.mac_address}-{light_id}"
- )
diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json
index 4a50d992d6d..902316f374e 100644
--- a/homeassistant/components/switcher_kis/manifest.json
+++ b/homeassistant/components/switcher_kis/manifest.json
@@ -7,6 +7,6 @@
"iot_class": "local_push",
"loggers": ["aioswitcher"],
"quality_scale": "platinum",
- "requirements": ["aioswitcher==4.4.0"],
+ "requirements": ["aioswitcher==4.0.3"],
"single_config_entry": true
}
diff --git a/homeassistant/components/switcher_kis/strings.json b/homeassistant/components/switcher_kis/strings.json
index 798a43c981c..68f9f9d590c 100644
--- a/homeassistant/components/switcher_kis/strings.json
+++ b/homeassistant/components/switcher_kis/strings.json
@@ -43,11 +43,6 @@
"name": "Vertical swing off"
}
},
- "cover": {
- "cover": {
- "name": "Cover {cover_id}"
- }
- },
"light": {
"light": {
"name": "Light {light_id}"
diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py
index 2817f4c21ce..b3d1230fdfe 100644
--- a/homeassistant/components/syncthru/__init__.py
+++ b/homeassistant/components/syncthru/__init__.py
@@ -52,7 +52,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator[SyncThru](
hass,
_LOGGER,
- config_entry=entry,
name=DOMAIN,
update_method=async_update_data,
update_interval=timedelta(seconds=30),
diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py
index 918a24035f8..70ab13c5c09 100644
--- a/homeassistant/components/synology_dsm/config_flow.py
+++ b/homeassistant/components/synology_dsm/config_flow.py
@@ -118,7 +118,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> SynologyDSMOptionsFlowHandler:
"""Get the options flow for this handler."""
- return SynologyDSMOptionsFlowHandler()
+ return SynologyDSMOptionsFlowHandler(config_entry)
def __init__(self) -> None:
"""Initialize the synology_dsm config flow."""
@@ -376,6 +376,10 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
class SynologyDSMOptionsFlowHandler(OptionsFlow):
"""Handle a option flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json
index 4c6ae0653d3..236f25bb1ed 100644
--- a/homeassistant/components/systemmonitor/manifest.json
+++ b/homeassistant/components/systemmonitor/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/systemmonitor",
"iot_class": "local_push",
"loggers": ["psutil"],
- "requirements": ["psutil-home-assistant==0.0.1", "psutil==6.1.0"]
+ "requirements": ["psutil-home-assistant==0.0.1", "psutil==6.0.0"]
}
diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py
index c7bb7684901..c8839b3a919 100644
--- a/homeassistant/components/tado/config_flow.py
+++ b/homeassistant/components/tado/config_flow.py
@@ -117,6 +117,12 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle a reconfiguration flow initialized by the user."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfiguration flow initialized by the user."""
errors: dict[str, str] = {}
@@ -142,7 +148,7 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
@@ -160,12 +166,16 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow):
"""Handle an option flow for Tado."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json
index 652d51f0261..b0c00c888b7 100644
--- a/homeassistant/components/tado/manifest.json
+++ b/homeassistant/components/tado/manifest.json
@@ -14,5 +14,5 @@
},
"iot_class": "cloud_polling",
"loggers": ["PyTado"],
- "requirements": ["python-tado==0.17.7"]
+ "requirements": ["python-tado==0.17.6"]
}
diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json
index 8124570f9c9..39453cb5fe1 100644
--- a/homeassistant/components/tado/strings.json
+++ b/homeassistant/components/tado/strings.json
@@ -12,7 +12,7 @@
},
"title": "Connect to your Tado account"
},
- "reconfigure": {
+ "reconfigure_confirm": {
"title": "Reconfigure your Tado",
"description": "Reconfigure the entry, for your account: `{username}`.",
"data": {
diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py
index 95efae3d386..0462c5bec34 100644
--- a/homeassistant/components/tag/__init__.py
+++ b/homeassistant/components/tag/__init__.py
@@ -84,9 +84,7 @@ def _create_entry(
original_name=f"{DEFAULT_NAME} {tag_id}",
suggested_object_id=slugify(name) if name else tag_id,
)
- if name:
- return entity_registry.async_update_entity(entry.entity_id, name=name)
- return entry
+ return entity_registry.async_update_entity(entry.entity_id, name=name)
class TagStore(Store[collection.SerializedStorageCollection]):
diff --git a/homeassistant/components/tailscale/config_flow.py b/homeassistant/components/tailscale/config_flow.py
index ab57e9eadc6..c5888e64f71 100644
--- a/homeassistant/components/tailscale/config_flow.py
+++ b/homeassistant/components/tailscale/config_flow.py
@@ -8,7 +8,7 @@ from typing import Any
from tailscale import Tailscale, TailscaleAuthenticationError, TailscaleError
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -34,6 +34,8 @@ class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ reauth_entry: ConfigEntry | None = None
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -84,6 +86,9 @@ class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle initiation of re-authentication with Tailscale."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -92,12 +97,11 @@ class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle re-authentication with Tailscale."""
errors = {}
- if user_input is not None:
- reauth_entry = self._get_reauth_entry()
+ if user_input is not None and self.reauth_entry:
try:
await validate_input(
self.hass,
- tailnet=reauth_entry.data[CONF_TAILNET],
+ tailnet=self.reauth_entry.data[CONF_TAILNET],
api_key=user_input[CONF_API_KEY],
)
except TailscaleAuthenticationError:
@@ -105,10 +109,17 @@ class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN):
except TailscaleError:
errors["base"] = "cannot_connect"
else:
- return self.async_update_reload_and_abort(
- reauth_entry,
- data_updates={CONF_API_KEY: user_input[CONF_API_KEY]},
+ self.hass.config_entries.async_update_entry(
+ self.reauth_entry,
+ data={
+ **self.reauth_entry.data,
+ CONF_API_KEY: user_input[CONF_API_KEY],
+ },
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
diff --git a/homeassistant/components/tailwind/config_flow.py b/homeassistant/components/tailwind/config_flow.py
index 48fe2d23727..13682a3e9c4 100644
--- a/homeassistant/components/tailwind/config_flow.py
+++ b/homeassistant/components/tailwind/config_flow.py
@@ -17,7 +17,7 @@ import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.components.dhcp import DhcpServiceInfo
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -41,6 +41,7 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
host: str
+ reauth_entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -147,6 +148,9 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle initiation of re-authentication with a Tailwind device."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -155,10 +159,10 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle re-authentication with a Tailwind device."""
errors = {}
- if user_input is not None:
+ if user_input is not None and self.reauth_entry:
try:
return await self._async_step_create_entry(
- host=self._get_reauth_entry().data[CONF_HOST],
+ host=self.reauth_entry.data[CONF_HOST],
token=user_input[CONF_TOKEN],
)
except TailwindAuthenticationError:
@@ -210,9 +214,9 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN):
except TailwindUnsupportedFirmwareVersionError:
return self.async_abort(reason="unsupported_firmware")
- if self.source == SOURCE_REAUTH:
+ if self.reauth_entry:
return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
+ self.reauth_entry,
data={
CONF_HOST: host,
CONF_TOKEN: token,
diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py
index 509f293665d..e5a84374a09 100644
--- a/homeassistant/components/tankerkoenig/config_flow.py
+++ b/homeassistant/components/tankerkoenig/config_flow.py
@@ -74,7 +74,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -144,8 +144,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
if not user_input:
return self._show_form_reauth()
- reauth_entry = self._get_reauth_entry()
- user_input = {**reauth_entry.data, **user_input}
+ entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+ assert entry
+ user_input = {**entry.data, **user_input}
tankerkoenig = Tankerkoenig(
api_key=user_input[CONF_API_KEY],
@@ -156,7 +157,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
except TankerkoenigInvalidKeyError:
return self._show_form_reauth(user_input, {CONF_API_KEY: "invalid_auth"})
- return self.async_update_reload_and_abort(reauth_entry, data=user_input)
+ self.hass.config_entries.async_update_entry(entry, data=user_input)
+ await self.hass.config_entries.async_reload(entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
def _show_form_user(
self,
@@ -236,8 +239,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(OptionsFlow):
"""Handle an options flow."""
- def __init__(self) -> None:
+ def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
+ self.config_entry = config_entry
self._stations: dict[str, str] = {}
async def async_step_init(
diff --git a/homeassistant/components/tankerkoenig/strings.json b/homeassistant/components/tankerkoenig/strings.json
index 29f4f439dd5..7017c6e5fed 100644
--- a/homeassistant/components/tankerkoenig/strings.json
+++ b/homeassistant/components/tankerkoenig/strings.json
@@ -42,9 +42,6 @@
"show_on_map": "Show stations on map"
}
}
- },
- "error": {
- "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
}
},
"entity": {
diff --git a/homeassistant/components/tautulli/config_flow.py b/homeassistant/components/tautulli/config_flow.py
index 369f9ead2f2..a8378786d18 100644
--- a/homeassistant/components/tautulli/config_flow.py
+++ b/homeassistant/components/tautulli/config_flow.py
@@ -60,11 +60,14 @@ class TautulliConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
errors = {}
- if user_input is not None:
- reauth_entry = self._get_reauth_entry()
- _input = {**reauth_entry.data, CONF_API_KEY: user_input[CONF_API_KEY]}
+ if user_input is not None and (
+ entry := self.hass.config_entries.async_get_entry(self.context["entry_id"])
+ ):
+ _input = {**entry.data, CONF_API_KEY: user_input[CONF_API_KEY]}
if (error := await self.validate_input(_input)) is None:
- return self.async_update_reload_and_abort(reauth_entry, data=_input)
+ self.hass.config_entries.async_update_entry(entry, data=_input)
+ await self.hass.config_entries.async_reload(entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
errors["base"] = error
return self.async_show_form(
step_id="reauth_confirm",
diff --git a/homeassistant/components/tedee/__init__.py b/homeassistant/components/tedee/__init__.py
index 528a5052678..a1b87cf13a4 100644
--- a/homeassistant/components/tedee/__init__.py
+++ b/homeassistant/components/tedee/__init__.py
@@ -7,7 +7,7 @@ from typing import Any
from aiohttp.hdrs import METH_POST
from aiohttp.web import Request, Response
-from aiotedee.exception import TedeeDataUpdateException, TedeeWebhookException
+from pytedee_async.exception import TedeeDataUpdateException, TedeeWebhookException
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.webhook import (
@@ -23,7 +23,7 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.network import get_url
from .const import DOMAIN, NAME
-from .coordinator import TedeeApiCoordinator, TedeeConfigEntry
+from .coordinator import TedeeApiCoordinator
PLATFORMS = [
Platform.BINARY_SENSOR,
@@ -33,11 +33,13 @@ PLATFORMS = [
_LOGGER = logging.getLogger(__name__)
+type TedeeConfigEntry = ConfigEntry[TedeeApiCoordinator]
+
async def async_setup_entry(hass: HomeAssistant, entry: TedeeConfigEntry) -> bool:
"""Integration setup."""
- coordinator = TedeeApiCoordinator(hass, entry)
+ coordinator = TedeeApiCoordinator(hass)
await coordinator.async_config_entry_first_refresh()
diff --git a/homeassistant/components/tedee/binary_sensor.py b/homeassistant/components/tedee/binary_sensor.py
index b586db7c2a7..3a7d1a12f2e 100644
--- a/homeassistant/components/tedee/binary_sensor.py
+++ b/homeassistant/components/tedee/binary_sensor.py
@@ -3,8 +3,8 @@
from collections.abc import Callable
from dataclasses import dataclass
-from aiotedee import TedeeLock
-from aiotedee.lock import TedeeLockState
+from pytedee_async import TedeeLock
+from pytedee_async.lock import TedeeLockState
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -15,7 +15,7 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from .coordinator import TedeeConfigEntry
+from . import TedeeConfigEntry
from .entity import TedeeDescriptionEntity
diff --git a/homeassistant/components/tedee/config_flow.py b/homeassistant/components/tedee/config_flow.py
index 422d818d1b5..6d399901c9a 100644
--- a/homeassistant/components/tedee/config_flow.py
+++ b/homeassistant/components/tedee/config_flow.py
@@ -4,7 +4,7 @@ from collections.abc import Mapping
import logging
from typing import Any
-from aiotedee import (
+from pytedee_async import (
TedeeAuthException,
TedeeClient,
TedeeClientException,
@@ -17,6 +17,7 @@ from homeassistant.components.webhook import async_generate_id as webhook_genera
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
+ ConfigEntry,
ConfigFlow,
ConfigFlowResult,
)
@@ -34,6 +35,9 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
MINOR_VERSION = 2
+ reauth_entry: ConfigEntry
+ reconfigure_entry: ConfigEntry
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -42,7 +46,7 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
if self.source == SOURCE_REAUTH:
- host = self._get_reauth_entry().data[CONF_HOST]
+ host = self.reauth_entry.data[CONF_HOST]
else:
host = user_input[CONF_HOST]
local_access_token = user_input[CONF_LOCAL_ACCESS_TOKEN]
@@ -61,17 +65,19 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.error("Error during local bridge discovery: %s", exc)
errors["base"] = "cannot_connect"
else:
- await self.async_set_unique_id(local_bridge.serial)
if self.source == SOURCE_REAUTH:
- self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=user_input
+ self.reauth_entry,
+ data={**self.reauth_entry.data, **user_input},
+ reason="reauth_successful",
)
if self.source == SOURCE_RECONFIGURE:
- self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
- self._get_reconfigure_entry(), data_updates=user_input
+ self.reconfigure_entry,
+ data={**self.reconfigure_entry.data, **user_input},
+ reason="reconfigure_successful",
)
+ await self.async_set_unique_id(local_bridge.serial)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=NAME,
@@ -97,6 +103,7 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self._get_reauth_entry()
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -110,9 +117,7 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN):
{
vol.Required(
CONF_LOCAL_ACCESS_TOKEN,
- default=self._get_reauth_entry().data[
- CONF_LOCAL_ACCESS_TOKEN
- ],
+ default=self.reauth_entry.data[CONF_LOCAL_ACCESS_TOKEN],
): str,
}
),
@@ -123,18 +128,26 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Perform a reconfiguration."""
+ self.reconfigure_entry = self._get_reconfigure_entry()
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Add reconfigure step to allow to reconfigure a config entry."""
if not user_input:
- reconfigure_entry = self._get_reconfigure_entry()
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=vol.Schema(
{
vol.Required(
- CONF_HOST, default=reconfigure_entry.data[CONF_HOST]
+ CONF_HOST, default=self.reconfigure_entry.data[CONF_HOST]
): str,
vol.Required(
CONF_LOCAL_ACCESS_TOKEN,
- default=reconfigure_entry.data[CONF_LOCAL_ACCESS_TOKEN],
+ default=self.reconfigure_entry.data[
+ CONF_LOCAL_ACCESS_TOKEN
+ ],
): str,
}
),
diff --git a/homeassistant/components/tedee/coordinator.py b/homeassistant/components/tedee/coordinator.py
index 445585a1a2c..1dab31b052b 100644
--- a/homeassistant/components/tedee/coordinator.py
+++ b/homeassistant/components/tedee/coordinator.py
@@ -1,14 +1,12 @@
"""Coordinator for Tedee locks."""
-from __future__ import annotations
-
from collections.abc import Awaitable, Callable
from datetime import timedelta
import logging
import time
from typing import Any
-from aiotedee import (
+from pytedee_async import (
TedeeClient,
TedeeClientException,
TedeeDataUpdateException,
@@ -16,7 +14,7 @@ from aiotedee import (
TedeeLock,
TedeeWebhookException,
)
-from aiotedee.bridge import TedeeBridge
+from pytedee_async.bridge import TedeeBridge
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
@@ -33,21 +31,18 @@ GET_LOCKS_INTERVAL_SECONDS = 3600
_LOGGER = logging.getLogger(__name__)
-type TedeeConfigEntry = ConfigEntry[TedeeApiCoordinator]
-
class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]):
"""Class to handle fetching data from the tedee API centrally."""
- config_entry: TedeeConfigEntry
+ config_entry: ConfigEntry
bridge: TedeeBridge
- def __init__(self, hass: HomeAssistant, entry: TedeeConfigEntry) -> None:
+ def __init__(self, hass: HomeAssistant) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
_LOGGER,
- config_entry=entry,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
diff --git a/homeassistant/components/tedee/entity.py b/homeassistant/components/tedee/entity.py
index 96cc6f2b3f5..59e3354aa1a 100644
--- a/homeassistant/components/tedee/entity.py
+++ b/homeassistant/components/tedee/entity.py
@@ -1,6 +1,6 @@
"""Bases for Tedee entities."""
-from aiotedee.lock import TedeeLock
+from pytedee_async.lock import TedeeLock
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
@@ -32,7 +32,6 @@ class TedeeEntity(CoordinatorEntity[TedeeApiCoordinator]):
name=lock.lock_name,
manufacturer="Tedee",
model=lock.lock_type,
- model_id=lock.lock_type,
via_device=(DOMAIN, coordinator.bridge.serial),
)
diff --git a/homeassistant/components/tedee/lock.py b/homeassistant/components/tedee/lock.py
index 6e89a48f2a0..8f0587de8ae 100644
--- a/homeassistant/components/tedee/lock.py
+++ b/homeassistant/components/tedee/lock.py
@@ -2,15 +2,16 @@
from typing import Any
-from aiotedee import TedeeClientException, TedeeLock, TedeeLockState
+from pytedee_async import TedeeClientException, TedeeLock, TedeeLockState
from homeassistant.components.lock import LockEntity, LockEntityFeature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from . import TedeeConfigEntry
from .const import DOMAIN
-from .coordinator import TedeeApiCoordinator, TedeeConfigEntry
+from .coordinator import TedeeApiCoordinator
from .entity import TedeeEntity
diff --git a/homeassistant/components/tedee/manifest.json b/homeassistant/components/tedee/manifest.json
index bca51f08f93..4f071267a25 100644
--- a/homeassistant/components/tedee/manifest.json
+++ b/homeassistant/components/tedee/manifest.json
@@ -6,7 +6,7 @@
"dependencies": ["http", "webhook"],
"documentation": "https://www.home-assistant.io/integrations/tedee",
"iot_class": "local_push",
- "loggers": ["aiotedee"],
+ "loggers": ["pytedee_async"],
"quality_scale": "platinum",
- "requirements": ["aiotedee==0.2.20"]
+ "requirements": ["pytedee-async==0.2.20"]
}
diff --git a/homeassistant/components/tedee/sensor.py b/homeassistant/components/tedee/sensor.py
index 90f76317fff..c7d14af1f31 100644
--- a/homeassistant/components/tedee/sensor.py
+++ b/homeassistant/components/tedee/sensor.py
@@ -3,7 +3,7 @@
from collections.abc import Callable
from dataclasses import dataclass
-from aiotedee import TedeeLock
+from pytedee_async import TedeeLock
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -15,7 +15,7 @@ from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from .coordinator import TedeeConfigEntry
+from . import TedeeConfigEntry
from .entity import TedeeDescriptionEntity
diff --git a/homeassistant/components/tedee/strings.json b/homeassistant/components/tedee/strings.json
index b6966fa2933..b0b15b76fcd 100644
--- a/homeassistant/components/tedee/strings.json
+++ b/homeassistant/components/tedee/strings.json
@@ -22,7 +22,7 @@
"local_access_token": "[%key:component::tedee::config::step::user::data_description::local_access_token%]"
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"title": "Reconfigure Tedee",
"description": "Update the settings of this integration.",
"data": {
@@ -38,8 +38,7 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
- "unique_id_mismatch": "You selected a different bridge than the one this config entry was configured with, this is not allowed."
+ "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py
index b9a032d7f28..64e2517a40b 100644
--- a/homeassistant/components/telegram_bot/__init__.py
+++ b/homeassistant/components/telegram_bot/__init__.py
@@ -37,6 +37,7 @@ from homeassistant.const import (
HTTP_DIGEST_AUTHENTICATION,
)
from homeassistant.core import Context, HomeAssistant, ServiceCall
+from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_loaded_integration
@@ -174,14 +175,14 @@ BASE_SERVICE_SCHEMA = vol.Schema(
)
SERVICE_SCHEMA_SEND_MESSAGE = BASE_SERVICE_SCHEMA.extend(
- {vol.Required(ATTR_MESSAGE): cv.string, vol.Optional(ATTR_TITLE): cv.string}
+ {vol.Required(ATTR_MESSAGE): cv.template, vol.Optional(ATTR_TITLE): cv.template}
)
SERVICE_SCHEMA_SEND_FILE = BASE_SERVICE_SCHEMA.extend(
{
- vol.Optional(ATTR_URL): cv.string,
- vol.Optional(ATTR_FILE): cv.string,
- vol.Optional(ATTR_CAPTION): cv.string,
+ vol.Optional(ATTR_URL): cv.template,
+ vol.Optional(ATTR_FILE): cv.template,
+ vol.Optional(ATTR_CAPTION): cv.template,
vol.Optional(ATTR_USERNAME): cv.string,
vol.Optional(ATTR_PASSWORD): cv.string,
vol.Optional(ATTR_AUTHENTICATION): cv.string,
@@ -195,8 +196,8 @@ SERVICE_SCHEMA_SEND_STICKER = SERVICE_SCHEMA_SEND_FILE.extend(
SERVICE_SCHEMA_SEND_LOCATION = BASE_SERVICE_SCHEMA.extend(
{
- vol.Required(ATTR_LONGITUDE): cv.string,
- vol.Required(ATTR_LATITUDE): cv.string,
+ vol.Required(ATTR_LONGITUDE): cv.template,
+ vol.Required(ATTR_LATITUDE): cv.template,
}
)
@@ -228,7 +229,7 @@ SERVICE_SCHEMA_EDIT_CAPTION = vol.Schema(
cv.positive_int, vol.All(cv.string, "last")
),
vol.Required(ATTR_CHAT_ID): vol.Coerce(int),
- vol.Required(ATTR_CAPTION): cv.string,
+ vol.Required(ATTR_CAPTION): cv.template,
vol.Optional(ATTR_KEYBOARD_INLINE): cv.ensure_list,
},
extra=vol.ALLOW_EXTRA,
@@ -247,7 +248,7 @@ SERVICE_SCHEMA_EDIT_REPLYMARKUP = vol.Schema(
SERVICE_SCHEMA_ANSWER_CALLBACK_QUERY = vol.Schema(
{
- vol.Required(ATTR_MESSAGE): cv.string,
+ vol.Required(ATTR_MESSAGE): cv.template,
vol.Required(ATTR_CALLBACK_QUERY_ID): vol.Coerce(int),
vol.Optional(ATTR_SHOW_ALERT): cv.boolean,
},
@@ -401,8 +402,38 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_send_telegram_message(service: ServiceCall) -> None:
"""Handle sending Telegram Bot message service calls."""
+ def _render_template_attr(data, attribute):
+ if attribute_templ := data.get(attribute):
+ if any(
+ isinstance(attribute_templ, vtype) for vtype in (float, int, str)
+ ):
+ data[attribute] = attribute_templ
+ else:
+ try:
+ data[attribute] = attribute_templ.async_render(
+ parse_result=False
+ )
+ except TemplateError as exc:
+ _LOGGER.error(
+ "TemplateError in %s: %s -> %s",
+ attribute,
+ attribute_templ.template,
+ exc,
+ )
+ data[attribute] = attribute_templ.template
+
msgtype = service.service
kwargs = dict(service.data)
+ for attribute in (
+ ATTR_MESSAGE,
+ ATTR_TITLE,
+ ATTR_URL,
+ ATTR_FILE,
+ ATTR_CAPTION,
+ ATTR_LONGITUDE,
+ ATTR_LATITUDE,
+ ):
+ _render_template_attr(kwargs, attribute)
_LOGGER.debug("New telegram message %s: %s", msgtype, kwargs)
if msgtype == SERVICE_SEND_MESSAGE:
diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py
index aa1f99f0423..6c8a70b328e 100644
--- a/homeassistant/components/template/alarm_control_panel.py
+++ b/homeassistant/components/template/alarm_control_panel.py
@@ -13,7 +13,6 @@ from homeassistant.components.alarm_control_panel import (
PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA,
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
from homeassistant.config_entries import ConfigEntry
@@ -23,6 +22,15 @@ from homeassistant.const import (
CONF_NAME,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
@@ -43,15 +51,15 @@ from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_con
_LOGGER = logging.getLogger(__name__)
_VALID_STATES = [
- AlarmControlPanelState.ARMED_AWAY,
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
- AlarmControlPanelState.ARMED_HOME,
- AlarmControlPanelState.ARMED_NIGHT,
- AlarmControlPanelState.ARMED_VACATION,
- AlarmControlPanelState.ARMING,
- AlarmControlPanelState.DISARMED,
- AlarmControlPanelState.PENDING,
- AlarmControlPanelState.TRIGGERED,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
STATE_UNAVAILABLE,
]
@@ -225,7 +233,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore
if (trigger_action := config.get(CONF_TRIGGER_ACTION)) is not None:
self._trigger_script = Script(hass, trigger_action, name, DOMAIN)
- self._state: AlarmControlPanelState | None = None
+ self._state: str | None = None
self._attr_device_info = async_device_info_to_link_from_device_id(
hass,
config.get(CONF_DEVICE_ID),
@@ -273,10 +281,10 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore
# then we should not restore state
and self._state is None
):
- self._state = AlarmControlPanelState(last_state.state)
+ self._state = last_state.state
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the device."""
return self._state
@@ -327,39 +335,31 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Arm the panel to Away."""
await self._async_alarm_arm(
- AlarmControlPanelState.ARMED_AWAY,
- script=self._arm_away_script,
- code=code,
+ STATE_ALARM_ARMED_AWAY, script=self._arm_away_script, code=code
)
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Arm the panel to Home."""
await self._async_alarm_arm(
- AlarmControlPanelState.ARMED_HOME,
- script=self._arm_home_script,
- code=code,
+ STATE_ALARM_ARMED_HOME, script=self._arm_home_script, code=code
)
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Arm the panel to Night."""
await self._async_alarm_arm(
- AlarmControlPanelState.ARMED_NIGHT,
- script=self._arm_night_script,
- code=code,
+ STATE_ALARM_ARMED_NIGHT, script=self._arm_night_script, code=code
)
async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
"""Arm the panel to Vacation."""
await self._async_alarm_arm(
- AlarmControlPanelState.ARMED_VACATION,
- script=self._arm_vacation_script,
- code=code,
+ STATE_ALARM_ARMED_VACATION, script=self._arm_vacation_script, code=code
)
async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
"""Arm the panel to Custom Bypass."""
await self._async_alarm_arm(
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
script=self._arm_custom_bypass_script,
code=code,
)
@@ -367,13 +367,11 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Disarm the panel."""
await self._async_alarm_arm(
- AlarmControlPanelState.DISARMED, script=self._disarm_script, code=code
+ STATE_ALARM_DISARMED, script=self._disarm_script, code=code
)
async def async_alarm_trigger(self, code: str | None = None) -> None:
"""Trigger the panel."""
await self._async_alarm_arm(
- AlarmControlPanelState.TRIGGERED,
- script=self._trigger_script,
- code=code,
+ STATE_ALARM_TRIGGERED, script=self._trigger_script, code=code
)
diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py
index 922f1d88ffb..187c7079f59 100644
--- a/homeassistant/components/template/binary_sensor.py
+++ b/homeassistant/components/template/binary_sensor.py
@@ -250,6 +250,7 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity):
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
self._template = config[CONF_STATE]
+ self._state: bool | None = None
self._delay_cancel = None
self._delay_on = None
self._delay_on_raw = config.get(CONF_DELAY_ON)
@@ -267,7 +268,7 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity):
and (last_state := await self.async_get_last_state()) is not None
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
):
- self._attr_is_on = last_state.state == STATE_ON
+ self._state = last_state.state == STATE_ON
await super().async_added_to_hass()
@callback
@@ -307,7 +308,7 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity):
else template.result_as_boolean(result)
)
- if state == self._attr_is_on:
+ if state == self._state:
return
# state without delay
@@ -316,19 +317,24 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity):
or (state and not self._delay_on)
or (not state and not self._delay_off)
):
- self._attr_is_on = state
+ self._state = state
return
@callback
def _set_state(_):
"""Set state of template binary sensor."""
- self._attr_is_on = state
+ self._state = state
self.async_write_ha_state()
delay = (self._delay_on if state else self._delay_off).total_seconds()
# state with delay. Cancelled if template result changes.
self._delay_cancel = async_call_later(self.hass, delay, _set_state)
+ @property
+ def is_on(self) -> bool | None:
+ """Return true if sensor is on."""
+ return self._state
+
class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity):
"""Sensor entity based on trigger data."""
@@ -353,6 +359,7 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity
self._delay_cancel: CALLBACK_TYPE | None = None
self._auto_off_cancel: CALLBACK_TYPE | None = None
self._auto_off_time: datetime | None = None
+ self._state: bool | None = None
async def async_added_to_hass(self) -> None:
"""Restore last state."""
@@ -364,9 +371,9 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
# The trigger might have fired already while we waited for stored data,
# then we should not restore state
- and self._attr_is_on is None
+ and self._state is None
):
- self._attr_is_on = last_state.state == STATE_ON
+ self._state = last_state.state == STATE_ON
self.restore_attributes(last_state)
if CONF_AUTO_OFF not in self._config:
@@ -376,11 +383,16 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity
auto_off_time := extra_data.auto_off_time
) is not None and auto_off_time <= dt_util.utcnow():
# It's already past the saved auto off time
- self._attr_is_on = False
+ self._state = False
- if self._attr_is_on and auto_off_time is not None:
+ if self._state and auto_off_time is not None:
self._set_auto_off(auto_off_time)
+ @property
+ def is_on(self) -> bool | None:
+ """Return state of the sensor."""
+ return self._state
+
@callback
def _handle_coordinator_update(self) -> None:
"""Handle update of the data."""
@@ -406,7 +418,7 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity
delay = self._rendered.get(key) or self._config.get(key)
# state without delay. None means rendering failed.
- if self._attr_is_on == state or state is None or delay is None:
+ if self._state == state or state is None or delay is None:
self._set_state(state)
return
@@ -427,7 +439,7 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity
@callback
def _set_state(self, state, _=None):
"""Set up auto off."""
- self._attr_is_on = state
+ self._state = state
self.async_set_context(self.coordinator.data["context"])
self.async_write_ha_state()
@@ -457,7 +469,7 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity
@callback
def _auto_off(_):
"""Reset state of template binary sensor."""
- self._attr_is_on = False
+ self._state = False
self.async_write_ha_state()
self._auto_off_time = auto_off_time
diff --git a/homeassistant/components/template/coordinator.py b/homeassistant/components/template/coordinator.py
index 4d8fe78f2b5..b9bbd3625af 100644
--- a/homeassistant/components/template/coordinator.py
+++ b/homeassistant/components/template/coordinator.py
@@ -24,9 +24,7 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
"""Instantiate trigger data."""
- super().__init__(
- hass, _LOGGER, config_entry=None, name="Trigger Update Coordinator"
- )
+ super().__init__(hass, _LOGGER, name="Trigger Update Coordinator")
self.config = config
self._cond_func: Callable[[Mapping[str, Any] | None], bool] | None = None
self._unsub_start: Callable[[], None] | None = None
diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json
index f1225f74f06..57188aebaa3 100644
--- a/homeassistant/components/template/manifest.json
+++ b/homeassistant/components/template/manifest.json
@@ -2,7 +2,7 @@
"domain": "template",
"name": "Template",
"after_dependencies": ["group"],
- "codeowners": ["@PhracturedBlue", "@home-assistant/core"],
+ "codeowners": ["@PhracturedBlue", "@tetienne", "@home-assistant/core"],
"config_flow": true,
"dependencies": ["blueprint"],
"documentation": "https://www.home-assistant.io/integrations/template",
diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json
index 66864a027ba..0b20ab2f3a3 100644
--- a/homeassistant/components/template/strings.json
+++ b/homeassistant/components/template/strings.json
@@ -106,7 +106,7 @@
"alarm_control_panel": "Template an alarm control panel",
"binary_sensor": "Template a binary sensor",
"button": "Template a button",
- "image": "Template an image",
+ "image": "Template a image",
"number": "Template a number",
"select": "Template a select",
"sensor": "Template a sensor",
diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py
index f5b84b1ad7a..3e70e1c3546 100644
--- a/homeassistant/components/template/template_entity.py
+++ b/homeassistant/components/template/template_entity.py
@@ -535,15 +535,13 @@ class TemplateEntity(Entity): # pylint: disable=hass-enforce-class-module
)
if self._entity_picture_template is not None:
self.add_template_attribute(
- "_attr_entity_picture", self._entity_picture_template, cv.string
+ "_attr_entity_picture", self._entity_picture_template
)
if (
self._friendly_name_template is not None
and not self._friendly_name_template.is_static
):
- self.add_template_attribute(
- "_attr_name", self._friendly_name_template, cv.string
- )
+ self.add_template_attribute("_attr_name", self._friendly_name_template)
@callback
def async_start_preview(
diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py
index 5130f332d5b..df84ce057c3 100644
--- a/homeassistant/components/template/trigger_entity.py
+++ b/homeassistant/components/template/trigger_entity.py
@@ -3,7 +3,6 @@
from __future__ import annotations
from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.template import TemplateStateFromEntityId
from homeassistant.helpers.trigger_template_entity import TriggerBaseEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -42,11 +41,11 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
def _process_data(self) -> None:
"""Process new data."""
+ this = None
+ if state := self.hass.states.get(self.entity_id):
+ this = state.as_dict()
run_variables = self.coordinator.data["run_variables"]
- variables = {
- "this": TemplateStateFromEntityId(self.hass, self.entity_id),
- **(run_variables or {}),
- }
+ variables = {"this": this, **(run_variables or {})}
self._render_templates(variables)
diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json
index 86fd83ad088..4f2b6f19285 100644
--- a/homeassistant/components/tensorflow/manifest.json
+++ b/homeassistant/components/tensorflow/manifest.json
@@ -9,7 +9,7 @@
"tensorflow==2.5.0",
"tf-models-official==2.5.0",
"pycocotools==2.0.6",
- "numpy==2.1.3",
- "Pillow==11.0.0"
+ "numpy==1.26.4",
+ "Pillow==10.4.0"
]
}
diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py
index e7030b568b3..4cd8c5c7142 100644
--- a/homeassistant/components/tesla_fleet/__init__.py
+++ b/homeassistant/components/tesla_fleet/__init__.py
@@ -5,12 +5,7 @@ from typing import Final
from aiohttp.client_exceptions import ClientResponseError
import jwt
-from tesla_fleet_api import (
- EnergySpecific,
- TeslaFleetApi,
- VehicleSigned,
- VehicleSpecific,
-)
+from tesla_fleet_api import EnergySpecific, TeslaFleetApi, VehicleSpecific
from tesla_fleet_api.const import Scope
from tesla_fleet_api.exceptions import (
InvalidRegion,
@@ -131,13 +126,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
# Remove the protobuff 'cached_data' that we do not use to save memory
product.pop("cached_data", None)
vin = product["vin"]
- signing = product["command_signing"] == "required"
- if signing:
- if not tesla.private_key:
- await tesla.get_private_key(hass.config.path("tesla_fleet.key"))
- api = VehicleSigned(tesla.vehicle, vin)
- else:
- api = VehicleSpecific(tesla.vehicle, vin)
+ api = VehicleSpecific(tesla.vehicle, vin)
coordinator = TeslaFleetVehicleDataCoordinator(hass, api, product)
await coordinator.async_config_entry_first_refresh()
@@ -156,7 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
coordinator=coordinator,
vin=vin,
device=device,
- signing=signing,
+ signing=product["command_signing"] == "required",
)
)
elif "energy_site_id" in product and hasattr(tesla, "energy"):
diff --git a/homeassistant/components/tesla_fleet/button.py b/homeassistant/components/tesla_fleet/button.py
index aea0f91a97c..87cd95576d2 100644
--- a/homeassistant/components/tesla_fleet/button.py
+++ b/homeassistant/components/tesla_fleet/button.py
@@ -70,6 +70,8 @@ async def async_setup_entry(
for vehicle in entry.runtime_data.vehicles
for description in DESCRIPTIONS
if Scope.VEHICLE_CMDS in entry.runtime_data.scopes
+ and (not vehicle.signing or description.key == "wake")
+ # Wake doesn't need signing
)
diff --git a/homeassistant/components/tesla_fleet/climate.py b/homeassistant/components/tesla_fleet/climate.py
index 9a1533a688f..6199ee112b5 100644
--- a/homeassistant/components/tesla_fleet/climate.py
+++ b/homeassistant/components/tesla_fleet/climate.py
@@ -84,7 +84,7 @@ class TeslaFleetClimateEntity(TeslaFleetVehicleEntity, ClimateEntity):
) -> None:
"""Initialize the climate."""
- self.read_only = Scope.VEHICLE_CMDS not in scopes
+ self.read_only = Scope.VEHICLE_CMDS not in scopes or data.signing
if self.read_only:
self._attr_supported_features = ClimateEntityFeature(0)
@@ -231,7 +231,7 @@ class TeslaFleetCabinOverheatProtectionEntity(TeslaFleetVehicleEntity, ClimateEn
"""Initialize the cabin overheat climate entity."""
# Scopes
- self.read_only = Scope.VEHICLE_CMDS not in scopes
+ self.read_only = Scope.VEHICLE_CMDS not in scopes or data.signing
# Supported Features
if self.read_only:
diff --git a/homeassistant/components/tesla_fleet/cover.py b/homeassistant/components/tesla_fleet/cover.py
index f270734424f..4e49e24b689 100644
--- a/homeassistant/components/tesla_fleet/cover.py
+++ b/homeassistant/components/tesla_fleet/cover.py
@@ -57,7 +57,7 @@ class TeslaFleetWindowEntity(TeslaFleetVehicleEntity, CoverEntity):
self._attr_supported_features = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
)
- if not self.scoped:
+ if not self.scoped or self.vehicle.signing:
self._attr_supported_features = CoverEntityFeature(0)
def _async_update_attrs(self) -> None:
@@ -111,7 +111,7 @@ class TeslaFleetChargePortEntity(TeslaFleetVehicleEntity, CoverEntity):
self._attr_supported_features = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
)
- if not self.scoped:
+ if not self.scoped or self.vehicle.signing:
self._attr_supported_features = CoverEntityFeature(0)
def _async_update_attrs(self) -> None:
@@ -144,7 +144,7 @@ class TeslaFleetFrontTrunkEntity(TeslaFleetVehicleEntity, CoverEntity):
self.scoped = Scope.VEHICLE_CMDS in scopes
self._attr_supported_features = CoverEntityFeature.OPEN
- if not self.scoped:
+ if not self.scoped or self.vehicle.signing:
self._attr_supported_features = CoverEntityFeature(0)
def _async_update_attrs(self) -> None:
@@ -172,12 +172,18 @@ class TeslaFleetRearTrunkEntity(TeslaFleetVehicleEntity, CoverEntity):
self._attr_supported_features = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
)
- if not self.scoped:
+ if not self.scoped or self.vehicle.signing:
self._attr_supported_features = CoverEntityFeature(0)
def _async_update_attrs(self) -> None:
"""Update the entity attributes."""
- self._attr_is_closed = self._value == CLOSED
+ value = self._value
+ if value == CLOSED:
+ self._attr_is_closed = True
+ elif value == OPEN:
+ self._attr_is_closed = False
+ else:
+ self._attr_is_closed = None
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open rear trunk."""
@@ -210,7 +216,7 @@ class TeslaFleetSunroofEntity(TeslaFleetVehicleEntity, CoverEntity):
super().__init__(vehicle, "vehicle_state_sun_roof_state")
self.scoped = Scope.VEHICLE_CMDS in scopes
- if not self.scoped:
+ if not self.scoped or self.vehicle.signing:
self._attr_supported_features = CoverEntityFeature(0)
def _async_update_attrs(self) -> None:
diff --git a/homeassistant/components/tesla_fleet/device_tracker.py b/homeassistant/components/tesla_fleet/device_tracker.py
index d6dcef895a6..62c084c9fe5 100644
--- a/homeassistant/components/tesla_fleet/device_tracker.py
+++ b/homeassistant/components/tesla_fleet/device_tracker.py
@@ -4,7 +4,6 @@ from __future__ import annotations
from homeassistant.components.device_tracker.config_entry import TrackerEntity
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import STATE_HOME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
@@ -85,7 +84,4 @@ class TeslaFleetDeviceTrackerRouteEntity(TeslaFleetDeviceTrackerEntity):
@property
def location_name(self) -> str | None:
"""Return a location name for the current location of the device."""
- location = self.get("drive_state_active_route_destination")
- if location == "Home":
- return STATE_HOME
- return location
+ return self.get("drive_state_active_route_destination")
diff --git a/homeassistant/components/tesla_fleet/entity.py b/homeassistant/components/tesla_fleet/entity.py
index 0ee41b5e322..60230cd881d 100644
--- a/homeassistant/components/tesla_fleet/entity.py
+++ b/homeassistant/components/tesla_fleet/entity.py
@@ -123,6 +123,14 @@ class TeslaFleetVehicleEntity(TeslaFleetEntity):
"""Wake up the vehicle if its asleep."""
await wake_up_vehicle(self.vehicle)
+ def raise_for_read_only(self, scope: Scope) -> None:
+ """Raise an error if no command signing or a scope is not available."""
+ if self.vehicle.signing:
+ raise ServiceValidationError(
+ translation_domain=DOMAIN, translation_key="command_signing"
+ )
+ super().raise_for_read_only(scope)
+
class TeslaFleetEnergyLiveEntity(TeslaFleetEntity):
"""Parent class for TeslaFleet Energy Site Live entities."""
diff --git a/homeassistant/components/tesla_fleet/manifest.json b/homeassistant/components/tesla_fleet/manifest.json
index 8d6e5f11068..f83f4f93e3c 100644
--- a/homeassistant/components/tesla_fleet/manifest.json
+++ b/homeassistant/components/tesla_fleet/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"quality_scale": "gold",
- "requirements": ["tesla-fleet-api==0.8.4"]
+ "requirements": ["tesla-fleet-api==0.7.8"]
}
diff --git a/homeassistant/components/tesla_fleet/media_player.py b/homeassistant/components/tesla_fleet/media_player.py
index 455c990077d..0a1d18c3407 100644
--- a/homeassistant/components/tesla_fleet/media_player.py
+++ b/homeassistant/components/tesla_fleet/media_player.py
@@ -64,7 +64,7 @@ class TeslaFleetMediaEntity(TeslaFleetVehicleEntity, MediaPlayerEntity):
"""Initialize the media player entity."""
super().__init__(data, "media")
self.scoped = scoped
- if not scoped:
+ if not scoped and data.signing:
self._attr_supported_features = MediaPlayerEntityFeature(0)
def _async_update_attrs(self) -> None:
diff --git a/homeassistant/components/tesla_fleet/oauth.py b/homeassistant/components/tesla_fleet/oauth.py
index 8b43460436b..00976abf56f 100644
--- a/homeassistant/components/tesla_fleet/oauth.py
+++ b/homeassistant/components/tesla_fleet/oauth.py
@@ -49,7 +49,6 @@ class TeslaSystemImplementation(config_entry_oauth2_flow.LocalOAuth2Implementati
def extra_authorize_data(self) -> dict[str, Any]:
"""Extra data that needs to be appended to the authorize url."""
return {
- "prompt": "login",
"scope": " ".join(SCOPES),
"code_challenge": self.code_challenge, # PKCE
}
@@ -84,4 +83,4 @@ class TeslaUserImplementation(AuthImplementation):
@property
def extra_authorize_data(self) -> dict[str, Any]:
"""Extra data that needs to be appended to the authorize url."""
- return {"prompt": "login", "scope": " ".join(SCOPES)}
+ return {"scope": " ".join(SCOPES)}
diff --git a/homeassistant/components/tesla_fleet/sensor.py b/homeassistant/components/tesla_fleet/sensor.py
index b4e7b51faba..a4f86468f0a 100644
--- a/homeassistant/components/tesla_fleet/sensor.py
+++ b/homeassistant/components/tesla_fleet/sensor.py
@@ -486,7 +486,7 @@ class TeslaFleetVehicleSensorEntity(TeslaFleetVehicleEntity, RestoreSensor):
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
- if self.coordinator.data.get("state") != TeslaFleetState.ONLINE:
+ if self.coordinator.data.get("state") == TeslaFleetState.OFFLINE:
if (sensor_data := await self.async_get_last_sensor_data()) is not None:
self._attr_native_value = sensor_data.native_value
@@ -524,7 +524,7 @@ class TeslaFleetVehicleTimeSensorEntity(TeslaFleetVehicleEntity, SensorEntity):
self._attr_native_value = self._get_timestamp(self._value)
-class TeslaFleetEnergyLiveSensorEntity(TeslaFleetEnergyLiveEntity, SensorEntity):
+class TeslaFleetEnergyLiveSensorEntity(TeslaFleetEnergyLiveEntity, RestoreSensor):
"""Base class for Tesla Fleet energy site metric sensors."""
entity_description: SensorEntityDescription
@@ -538,13 +538,20 @@ class TeslaFleetEnergyLiveSensorEntity(TeslaFleetEnergyLiveEntity, SensorEntity)
self.entity_description = description
super().__init__(data, description.key)
+ async def async_added_to_hass(self) -> None:
+ """Handle entity which will be added."""
+ await super().async_added_to_hass()
+ if not self.coordinator.updated_once:
+ if (sensor_data := await self.async_get_last_sensor_data()) is not None:
+ self._attr_native_value = sensor_data.native_value
+
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_available = not self.is_none
self._attr_native_value = self._value
-class TeslaFleetWallConnectorSensorEntity(TeslaFleetWallConnectorEntity, SensorEntity):
+class TeslaFleetWallConnectorSensorEntity(TeslaFleetWallConnectorEntity, RestoreSensor):
"""Base class for Tesla Fleet energy site metric sensors."""
entity_description: SensorEntityDescription
@@ -563,13 +570,20 @@ class TeslaFleetWallConnectorSensorEntity(TeslaFleetWallConnectorEntity, SensorE
description.key,
)
+ async def async_added_to_hass(self) -> None:
+ """Handle entity which will be added."""
+ await super().async_added_to_hass()
+ if not self.coordinator.updated_once:
+ if (sensor_data := await self.async_get_last_sensor_data()) is not None:
+ self._attr_native_value = sensor_data.native_value
+
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_available = not self.is_none
self._attr_native_value = self._value
-class TeslaFleetEnergyInfoSensorEntity(TeslaFleetEnergyInfoEntity, SensorEntity):
+class TeslaFleetEnergyInfoSensorEntity(TeslaFleetEnergyInfoEntity, RestoreSensor):
"""Base class for Tesla Fleet energy site metric sensors."""
entity_description: SensorEntityDescription
@@ -583,6 +597,13 @@ class TeslaFleetEnergyInfoSensorEntity(TeslaFleetEnergyInfoEntity, SensorEntity)
self.entity_description = description
super().__init__(data, description.key)
+ async def async_added_to_hass(self) -> None:
+ """Handle entity which will be added."""
+ await super().async_added_to_hass()
+ if not self.coordinator.updated_once:
+ if (sensor_data := await self.async_get_last_sensor_data()) is not None:
+ self._attr_native_value = sensor_data.native_value
+
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_available = not self.is_none
diff --git a/homeassistant/components/tesla_fleet/strings.json b/homeassistant/components/tesla_fleet/strings.json
index fe5cd06c1ef..942824c5043 100644
--- a/homeassistant/components/tesla_fleet/strings.json
+++ b/homeassistant/components/tesla_fleet/strings.json
@@ -504,6 +504,9 @@
"command_no_reason": {
"message": "Command was unsuccessful but did not return a reason why."
},
+ "command_signing": {
+ "message": "Vehicle requires command signing. Please see documentation for more details."
+ },
"invalid_cop_temp": {
"message": "Cabin overheat protection does not support that temperature."
},
diff --git a/homeassistant/components/tesla_wall_connector/__init__.py b/homeassistant/components/tesla_wall_connector/__init__.py
index 01c657fbcaa..f4d04ca8cc6 100644
--- a/homeassistant/components/tesla_wall_connector/__init__.py
+++ b/homeassistant/components/tesla_wall_connector/__init__.py
@@ -71,7 +71,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator: DataUpdateCoordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name="tesla-wallconnector",
update_interval=get_poll_interval(entry),
update_method=async_update_data,
diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py
index aa1d2b42660..ab2e4c04734 100644
--- a/homeassistant/components/teslemetry/__init__.py
+++ b/homeassistant/components/teslemetry/__init__.py
@@ -1,7 +1,6 @@
"""Teslemetry integration."""
import asyncio
-from collections.abc import Callable
from typing import Final
from tesla_fleet_api import EnergySpecific, Teslemetry, VehicleSpecific
@@ -11,7 +10,6 @@ from tesla_fleet_api.exceptions import (
SubscriptionRequired,
TeslaFleetError,
)
-from teslemetry_stream import TeslemetryStream
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
@@ -30,7 +28,6 @@ from .coordinator import (
TeslemetryEnergySiteLiveCoordinator,
TeslemetryVehicleDataCoordinator,
)
-from .helpers import flatten
from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData
from .services import async_register_services
@@ -72,10 +69,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
access_token=access_token,
)
try:
- calls = await asyncio.gather(
- teslemetry.metadata(),
- teslemetry.products(),
- )
+ scopes = (await teslemetry.metadata())["scopes"]
+ products = (await teslemetry.products())["response"]
except InvalidToken as e:
raise ConfigEntryAuthFailed from e
except SubscriptionRequired as e:
@@ -83,24 +78,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
except TeslaFleetError as e:
raise ConfigEntryNotReady from e
- scopes = calls[0]["scopes"]
- region = calls[0]["region"]
- products = calls[1]["response"]
-
device_registry = dr.async_get(hass)
# Create array of classes
vehicles: list[TeslemetryVehicleData] = []
energysites: list[TeslemetryEnergyData] = []
-
- # Create the stream
- stream = TeslemetryStream(
- session,
- access_token,
- server=f"{region.lower()}.teslemetry.com",
- parse_timestamp=True,
- )
-
for product in products:
if "vin" in product and Scope.VEHICLE_DEVICE_DATA in scopes:
# Remove the protobuff 'cached_data' that we do not use to save memory
@@ -117,29 +99,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
serial_number=vin,
)
- remove_listener = stream.async_add_listener(
- create_handle_vehicle_stream(vin, coordinator),
- {"vin": vin},
- )
-
vehicles.append(
TeslemetryVehicleData(
api=api,
coordinator=coordinator,
- stream=stream,
vin=vin,
device=device,
- remove_listener=remove_listener,
)
)
elif "energy_site_id" in product and Scope.ENERGY_DEVICE_DATA in scopes:
site_id = product["energy_site_id"]
- powerwall = (
- product["components"]["battery"] or product["components"]["solar"]
- )
- wall_connector = "wall_connectors" in product["components"]
- if not powerwall and not wall_connector:
+ if not (
+ product["components"]["battery"]
+ or product["components"]["solar"]
+ or "wall_connectors" in product["components"]
+ ):
LOGGER.debug(
"Skipping Energy Site %s as it has no components",
site_id,
@@ -162,11 +137,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
info_coordinator=TeslemetryEnergySiteInfoCoordinator(
hass, api, product
),
- history_coordinator=(
- TeslemetryEnergyHistoryCoordinator(hass, api)
- if powerwall
- else None
- ),
+ history_coordinator=TeslemetryEnergyHistoryCoordinator(hass, api),
id=site_id,
device=device,
)
@@ -189,7 +160,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
*(
energysite.history_coordinator.async_config_entry_first_refresh()
for energysite in energysites
- if energysite.history_coordinator
),
)
@@ -244,20 +214,3 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
config_entry, unique_id=metadata["uid"], version=1, minor_version=2
)
return True
-
-
-def create_handle_vehicle_stream(vin: str, coordinator) -> Callable[[dict], None]:
- """Create a handle vehicle stream function."""
-
- def handle_vehicle_stream(data: dict) -> None:
- """Handle vehicle data from the stream."""
- if "vehicle_data" in data:
- LOGGER.debug("Streaming received vehicle data from %s", vin)
- coordinator.updated_once = True
- coordinator.async_set_updated_data(flatten(data["vehicle_data"]))
- elif "state" in data:
- LOGGER.debug("Streaming received state from %s", vin)
- coordinator.data["state"] = data["state"]
- coordinator.async_set_updated_data(coordinator.data)
-
- return handle_vehicle_stream
diff --git a/homeassistant/components/teslemetry/config_flow.py b/homeassistant/components/teslemetry/config_flow.py
index d8cf2bd7945..73921986f44 100644
--- a/homeassistant/components/teslemetry/config_flow.py
+++ b/homeassistant/components/teslemetry/config_flow.py
@@ -14,7 +14,7 @@ from tesla_fleet_api.exceptions import (
)
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -22,7 +22,6 @@ from .const import DOMAIN, LOGGER
TESLEMETRY_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
DESCRIPTION_PLACEHOLDERS = {
- "name": "Teslemetry",
"short_url": "teslemetry.com/console",
"url": "[teslemetry.com/console](https://teslemetry.com/console)",
}
@@ -33,6 +32,7 @@ class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
MINOR_VERSION = 2
+ _entry: ConfigEntry | None = None
async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]:
"""Reusable Auth Helper."""
@@ -78,6 +78,7 @@ class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth on failure."""
+ self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -85,11 +86,12 @@ class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle users reauth credentials."""
+ assert self._entry
errors: dict[str, str] = {}
if user_input and not (errors := await self.async_auth(user_input)):
return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
+ self._entry,
data=user_input,
)
diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py
index f37d0613de9..4612408e14d 100644
--- a/homeassistant/components/teslemetry/coordinator.py
+++ b/homeassistant/components/teslemetry/coordinator.py
@@ -18,7 +18,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ENERGY_HISTORY_FIELDS, LOGGER, TeslemetryState
-from .helpers import flatten
VEHICLE_INTERVAL = timedelta(seconds=30)
VEHICLE_WAIT = timedelta(minutes=15)
@@ -36,6 +35,19 @@ ENDPOINTS = [
]
+def flatten(data: dict[str, Any], parent: str | None = None) -> dict[str, Any]:
+ """Flatten the data structure."""
+ result = {}
+ for key, value in data.items():
+ if parent:
+ key = f"{parent}_{key}"
+ if isinstance(value, dict):
+ result.update(flatten(value, key))
+ else:
+ result[key] = value
+ return result
+
+
class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching data from the Teslemetry API."""
diff --git a/homeassistant/components/teslemetry/cover.py b/homeassistant/components/teslemetry/cover.py
index 8775da931d5..190f729d99f 100644
--- a/homeassistant/components/teslemetry/cover.py
+++ b/homeassistant/components/teslemetry/cover.py
@@ -182,7 +182,13 @@ class TeslemetryRearTrunkEntity(TeslemetryVehicleEntity, CoverEntity):
def _async_update_attrs(self) -> None:
"""Update the entity attributes."""
- self._attr_is_closed = self._value == CLOSED
+ value = self._value
+ if value == CLOSED:
+ self._attr_is_closed = True
+ elif value == OPEN:
+ self._attr_is_closed = False
+ else:
+ self._attr_is_closed = None
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open rear trunk."""
diff --git a/homeassistant/components/teslemetry/device_tracker.py b/homeassistant/components/teslemetry/device_tracker.py
index 2b0ffd88cc6..6577bcf88d6 100644
--- a/homeassistant/components/teslemetry/device_tracker.py
+++ b/homeassistant/components/teslemetry/device_tracker.py
@@ -3,7 +3,6 @@
from __future__ import annotations
from homeassistant.components.device_tracker.config_entry import TrackerEntity
-from homeassistant.const import STATE_HOME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -81,7 +80,4 @@ class TeslemetryDeviceTrackerRouteEntity(TeslemetryDeviceTrackerEntity):
@property
def location_name(self) -> str | None:
"""Return a location name for the current location of the device."""
- location = self.get("drive_state_active_route_destination")
- if location == "Home":
- return STATE_HOME
- return location
+ return self.get("drive_state_active_route_destination")
diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py
index d14f3a42734..ca40d4d00ce 100644
--- a/homeassistant/components/teslemetry/entity.py
+++ b/homeassistant/components/teslemetry/entity.py
@@ -175,8 +175,6 @@ class TeslemetryEnergyHistoryEntity(TeslemetryEntity):
) -> None:
"""Initialize common aspects of a Teslemetry Energy Site Info entity."""
- assert data.history_coordinator
-
self.api = data.api
self._attr_unique_id = f"{data.id}-{key}"
self._attr_device_info = data.device
diff --git a/homeassistant/components/teslemetry/helpers.py b/homeassistant/components/teslemetry/helpers.py
index 30601feccbc..4e086008333 100644
--- a/homeassistant/components/teslemetry/helpers.py
+++ b/homeassistant/components/teslemetry/helpers.py
@@ -10,19 +10,6 @@ from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN, LOGGER, TeslemetryState
-def flatten(data: dict[str, Any], parent: str | None = None) -> dict[str, Any]:
- """Flatten the data structure."""
- result = {}
- for key, value in data.items():
- if parent:
- key = f"{parent}_{key}"
- if isinstance(value, dict):
- result.update(flatten(value, key))
- else:
- result[key] = value
- return result
-
-
async def wake_up_vehicle(vehicle) -> None:
"""Wake up a vehicle."""
async with vehicle.wakelock:
diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json
index 6b667094d62..715c6cd2159 100644
--- a/homeassistant/components/teslemetry/manifest.json
+++ b/homeassistant/components/teslemetry/manifest.json
@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"quality_scale": "platinum",
- "requirements": ["tesla-fleet-api==0.8.4", "teslemetry-stream==0.4.2"]
+ "requirements": ["tesla-fleet-api==0.7.8"]
}
diff --git a/homeassistant/components/teslemetry/models.py b/homeassistant/components/teslemetry/models.py
index d3969b30a7c..a6d549b8937 100644
--- a/homeassistant/components/teslemetry/models.py
+++ b/homeassistant/components/teslemetry/models.py
@@ -3,12 +3,10 @@
from __future__ import annotations
import asyncio
-from collections.abc import Callable
from dataclasses import dataclass
from tesla_fleet_api import EnergySpecific, VehicleSpecific
from tesla_fleet_api.const import Scope
-from teslemetry_stream import TeslemetryStream
from homeassistant.helpers.device_registry import DeviceInfo
@@ -35,11 +33,9 @@ class TeslemetryVehicleData:
api: VehicleSpecific
coordinator: TeslemetryVehicleDataCoordinator
- stream: TeslemetryStream
vin: str
wakelock = asyncio.Lock()
device: DeviceInfo
- remove_listener: Callable
@dataclass
@@ -49,6 +45,6 @@ class TeslemetryEnergyData:
api: EnergySpecific
live_coordinator: TeslemetryEnergySiteLiveCoordinator
info_coordinator: TeslemetryEnergySiteInfoCoordinator
- history_coordinator: TeslemetryEnergyHistoryCoordinator | None
+ history_coordinator: TeslemetryEnergyHistoryCoordinator
id: int
device: DeviceInfo
diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py
index 95876cc2cf9..ba7d930fcd0 100644
--- a/homeassistant/components/teslemetry/sensor.py
+++ b/homeassistant/components/teslemetry/sensor.py
@@ -482,7 +482,8 @@ async def async_setup_entry(
TeslemetryEnergyHistorySensorEntity(energysite, description)
for energysite in entry.runtime_data.energysites
for description in ENERGY_HISTORY_DESCRIPTIONS
- if energysite.history_coordinator
+ if energysite.info_coordinator.data.get("components_battery")
+ or energysite.info_coordinator.data.get("components_solar")
),
)
)
diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json
index 4f4bc2ae60c..253c19632ea 100644
--- a/homeassistant/components/teslemetry/strings.json
+++ b/homeassistant/components/teslemetry/strings.json
@@ -1,9 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Account is already configured",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
- "reauth_account_mismatch": "The reauthentication account does not match the original account"
+ "already_configured": "Account is already configured"
},
"error": {
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
@@ -17,13 +15,6 @@
"access_token": "[%key:common::config_flow::data::access_token%]"
},
"description": "Enter an access token from {url}."
- },
- "reauth_confirm": {
- "title": "[%key:common::config_flow::title::reauth%]",
- "description": "The {name} integration needs to re-authenticate your account, please enter an access token from {url}",
- "data": {
- "access_token": "[%key:common::config_flow::data::access_token%]"
- }
}
}
},
diff --git a/homeassistant/components/teslemetry/update.py b/homeassistant/components/teslemetry/update.py
index 670cd0e0eda..1884689ae64 100644
--- a/homeassistant/components/teslemetry/update.py
+++ b/homeassistant/components/teslemetry/update.py
@@ -92,12 +92,12 @@ class TeslemetryUpdateEntity(TeslemetryVehicleEntity, UpdateEntity):
SCHEDULED,
INSTALLING,
):
- self._attr_in_progress = True
- if install_perc := self.get("vehicle_state_software_update_install_perc"):
- self._attr_update_percentage = cast(int, install_perc)
+ self._attr_in_progress = (
+ cast(int, self.get("vehicle_state_software_update_install_perc"))
+ or True
+ )
else:
self._attr_in_progress = False
- self._attr_update_percentage = None
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
@@ -107,5 +107,4 @@ class TeslemetryUpdateEntity(TeslemetryVehicleEntity, UpdateEntity):
await self.wake_up_if_asleep()
await handle_vehicle_command(self.api.schedule_software_update(offset_sec=60))
self._attr_in_progress = True
- self._attr_update_percentage = None
self.async_write_ha_state()
diff --git a/homeassistant/components/tessie/config_flow.py b/homeassistant/components/tessie/config_flow.py
index 14c6b93fdfd..bee518ce95f 100644
--- a/homeassistant/components/tessie/config_flow.py
+++ b/homeassistant/components/tessie/config_flow.py
@@ -14,12 +14,12 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from . import TessieConfigEntry
from .const import DOMAIN
TESSIE_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
DESCRIPTION_PLACEHOLDERS = {
- "name": "Tessie",
- "url": "[my.tessie.com/settings/api](https://my.tessie.com/settings/api)",
+ "url": "[my.tessie.com/settings/api](https://my.tessie.com/settings/api)"
}
@@ -28,6 +28,10 @@ class TessieConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ def __init__(self) -> None:
+ """Initialize."""
+ self._reauth_entry: TessieConfigEntry | None = None
+
async def async_step_user(
self, user_input: Mapping[str, Any] | None = None
) -> ConfigFlowResult:
@@ -65,6 +69,9 @@ class TessieConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-auth."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -72,7 +79,7 @@ class TessieConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Get update API Key from the user."""
errors: dict[str, str] = {}
-
+ assert self._reauth_entry
if user_input:
try:
await get_state_of_all_vehicles(
@@ -88,7 +95,7 @@ class TessieConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
else:
return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=user_input
+ self._reauth_entry, data=user_input
)
return self.async_show_form(
diff --git a/homeassistant/components/tessie/lock.py b/homeassistant/components/tessie/lock.py
index 76d58a9070c..4f6ce3800e3 100644
--- a/homeassistant/components/tessie/lock.py
+++ b/homeassistant/components/tessie/lock.py
@@ -4,11 +4,21 @@ from __future__ import annotations
from typing import Any
-from tessie_api import lock, open_unlock_charge_port, unlock
+from tessie_api import (
+ disable_speed_limit,
+ enable_speed_limit,
+ lock,
+ open_unlock_charge_port,
+ unlock,
+)
-from homeassistant.components.lock import LockEntity
+from homeassistant.components.automation import automations_with_entity
+from homeassistant.components.lock import ATTR_CODE, LockEntity
+from homeassistant.components.script import scripts_with_entity
+from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
+from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TessieConfigEntry
@@ -27,11 +37,46 @@ async def async_setup_entry(
"""Set up the Tessie sensor platform from a config entry."""
data = entry.runtime_data
- async_add_entities(
+ entities: list[TessieEntity] = [
klass(vehicle)
for klass in (TessieLockEntity, TessieCableLockEntity)
for vehicle in data.vehicles
- )
+ ]
+
+ ent_reg = er.async_get(hass)
+
+ for vehicle in data.vehicles:
+ entity_id = ent_reg.async_get_entity_id(
+ Platform.LOCK,
+ DOMAIN,
+ f"{vehicle.vin}-vehicle_state_speed_limit_mode_active",
+ )
+ if entity_id:
+ entity_entry = ent_reg.async_get(entity_id)
+ assert entity_entry
+ if entity_entry.disabled:
+ ent_reg.async_remove(entity_id)
+ else:
+ entities.append(TessieSpeedLimitEntity(vehicle))
+
+ entity_automations = automations_with_entity(hass, entity_id)
+ entity_scripts = scripts_with_entity(hass, entity_id)
+ for item in entity_automations + entity_scripts:
+ ir.async_create_issue(
+ hass,
+ DOMAIN,
+ f"deprecated_speed_limit_{entity_id}_{item}",
+ breaks_in_ha_version="2024.11.0",
+ is_fixable=True,
+ is_persistent=False,
+ severity=ir.IssueSeverity.WARNING,
+ translation_key="deprecated_speed_limit_entity",
+ translation_placeholders={
+ "entity": entity_id,
+ "info": item,
+ },
+ )
+ async_add_entities(entities)
class TessieLockEntity(TessieEntity, LockEntity):
@@ -60,6 +105,58 @@ class TessieLockEntity(TessieEntity, LockEntity):
self.set((self.key, False))
+class TessieSpeedLimitEntity(TessieEntity, LockEntity):
+ """Speed Limit with PIN entity for Tessie."""
+
+ _attr_code_format = r"^\d\d\d\d$"
+
+ def __init__(
+ self,
+ vehicle: TessieVehicleData,
+ ) -> None:
+ """Initialize the sensor."""
+ super().__init__(vehicle, "vehicle_state_speed_limit_mode_active")
+
+ @property
+ def is_locked(self) -> bool | None:
+ """Return the state of the Lock."""
+ return self._value
+
+ async def async_lock(self, **kwargs: Any) -> None:
+ """Enable speed limit with pin."""
+ ir.async_create_issue(
+ self.coordinator.hass,
+ DOMAIN,
+ "deprecated_speed_limit_locked",
+ breaks_in_ha_version="2024.11.0",
+ is_fixable=True,
+ is_persistent=False,
+ severity=ir.IssueSeverity.WARNING,
+ translation_key="deprecated_speed_limit_locked",
+ )
+ code: str | None = kwargs.get(ATTR_CODE)
+ if code:
+ await self.run(enable_speed_limit, pin=code)
+ self.set((self.key, True))
+
+ async def async_unlock(self, **kwargs: Any) -> None:
+ """Disable speed limit with pin."""
+ ir.async_create_issue(
+ self.coordinator.hass,
+ DOMAIN,
+ "deprecated_speed_limit_unlocked",
+ breaks_in_ha_version="2024.11.0",
+ is_fixable=True,
+ is_persistent=False,
+ severity=ir.IssueSeverity.WARNING,
+ translation_key="deprecated_speed_limit_unlocked",
+ )
+ code: str | None = kwargs.get(ATTR_CODE)
+ if code:
+ await self.run(disable_speed_limit, pin=code)
+ self.set((self.key, False))
+
+
class TessieCableLockEntity(TessieEntity, LockEntity):
"""Cable Lock entity for Tessie."""
diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json
index 92aa289ca47..d9f2cea9618 100644
--- a/homeassistant/components/tessie/manifest.json
+++ b/homeassistant/components/tessie/manifest.json
@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["tessie", "tesla-fleet-api"],
"quality_scale": "platinum",
- "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.8.4"]
+ "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.7.8"]
}
diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json
index 5b677594b42..52c03c8700b 100644
--- a/homeassistant/components/tessie/strings.json
+++ b/homeassistant/components/tessie/strings.json
@@ -1,8 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"error": {
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
@@ -63,6 +62,9 @@
},
"charge_state_charge_port_latch": {
"name": "Charge cable lock"
+ },
+ "vehicle_state_speed_limit_mode_active": {
+ "name": "Speed limit"
}
},
"media_player": {
@@ -529,5 +531,40 @@
"command_failed": {
"message": "Command failed, {message}"
}
+ },
+ "issues": {
+ "deprecated_speed_limit_entity": {
+ "title": "Detected Tessie speed limit lock entity usage",
+ "fix_flow": {
+ "step": {
+ "confirm": {
+ "title": "[%key:component::tessie::issues::deprecated_speed_limit_entity::title%]",
+ "description": "The Tessie integration's speed limit lock entity has been deprecated and will be remove in 2024.11.0.\nHome Assistant detected that entity `{entity}` is being used in `{info}`\n\nYou should remove the speed limit lock entity from `{info}` then select **Submit** to fix this issue."
+ }
+ }
+ }
+ },
+ "deprecated_speed_limit_locked": {
+ "title": "Detected Tessie speed limit lock entity locked",
+ "fix_flow": {
+ "step": {
+ "confirm": {
+ "title": "[%key:component::tessie::issues::deprecated_speed_limit_locked::title%]",
+ "description": "The Tessie integration's speed limit lock entity has been deprecated and will be remove in 2024.11.0.\n\nPlease remove this entity from any automation or script, disable the entity then select **Submit** to fix this issue."
+ }
+ }
+ }
+ },
+ "deprecated_speed_limit_unlocked": {
+ "title": "Detected Tessie speed limit lock entity unlocked",
+ "fix_flow": {
+ "step": {
+ "confirm": {
+ "title": "[%key:component::tessie::issues::deprecated_speed_limit_unlocked::title%]",
+ "description": "The Tessie integration's speed limit lock entity has been deprecated and will be remove in 2024.11.0.\n\nPlease remove this entity from any automation or script, disable the entity then select **Submit** to fix this issue."
+ }
+ }
+ }
+ }
}
}
diff --git a/homeassistant/components/tessie/update.py b/homeassistant/components/tessie/update.py
index f6198fa6c03..959a713047f 100644
--- a/homeassistant/components/tessie/update.py
+++ b/homeassistant/components/tessie/update.py
@@ -71,22 +71,14 @@ class TessieUpdateEntity(TessieEntity, UpdateEntity):
return self.installed_version
@property
- def in_progress(self) -> bool:
- """Update installation progress."""
- return (
- self.get("vehicle_state_software_update_status")
- == TessieUpdateStatus.INSTALLING
- )
-
- @property
- def update_percentage(self) -> int | None:
+ def in_progress(self) -> bool | int | None:
"""Update installation progress."""
if (
self.get("vehicle_state_software_update_status")
== TessieUpdateStatus.INSTALLING
):
return self.get("vehicle_state_software_update_install_perc")
- return None
+ return False
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
diff --git a/homeassistant/components/thethingsnetwork/__init__.py b/homeassistant/components/thethingsnetwork/__init__.py
index d3c6c8356cb..253ce7a052e 100644
--- a/homeassistant/components/thethingsnetwork/__init__.py
+++ b/homeassistant/components/thethingsnetwork/__init__.py
@@ -2,15 +2,55 @@
import logging
+import voluptuous as vol
+
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_HOST
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import issue_registry as ir
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.typing import ConfigType
-from .const import DOMAIN, PLATFORMS, TTN_API_HOST
+from .const import CONF_APP_ID, DOMAIN, PLATFORMS, TTN_API_HOST
from .coordinator import TTNCoordinator
_LOGGER = logging.getLogger(__name__)
+CONFIG_SCHEMA = vol.Schema(
+ {
+ # Configuration via yaml not longer supported - keeping to warn about migration
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_APP_ID): cv.string,
+ vol.Required("access_key"): cv.string,
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+ """Initialize of The Things Network component."""
+
+ if DOMAIN in config:
+ ir.async_create_issue(
+ hass,
+ DOMAIN,
+ "manual_migration",
+ breaks_in_ha_version="2024.12.0",
+ is_fixable=False,
+ severity=ir.IssueSeverity.ERROR,
+ translation_key="manual_migration",
+ translation_placeholders={
+ "domain": DOMAIN,
+ "v2_v3_migration_url": "https://www.thethingsnetwork.org/forum/c/v2-to-v3-upgrade/102",
+ "v2_deprecation_url": "https://www.thethingsnetwork.org/forum/t/the-things-network-v2-is-permanently-shutting-down-completed/50710",
+ },
+ )
+
+ return True
+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Establish connection with The Things Network."""
diff --git a/homeassistant/components/thethingsnetwork/config_flow.py b/homeassistant/components/thethingsnetwork/config_flow.py
index 412c5da4ef9..7480e4cb1d9 100644
--- a/homeassistant/components/thethingsnetwork/config_flow.py
+++ b/homeassistant/components/thethingsnetwork/config_flow.py
@@ -7,7 +7,7 @@ from typing import Any
from ttn_client import TTNAuthError, TTNClient
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_HOST
from homeassistant.helpers.selector import (
TextSelector,
@@ -25,6 +25,8 @@ class TTNFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ _reauth_entry: ConfigEntry | None = None
+
async def async_step_user(
self, user_input: Mapping[str, Any] | None = None
) -> ConfigFlowResult:
@@ -49,9 +51,11 @@ class TTNFlowHandler(ConfigFlow, domain=DOMAIN):
if not errors:
# Create entry
- if self.source == SOURCE_REAUTH:
+ if self._reauth_entry:
return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=user_input
+ self._reauth_entry,
+ data=user_input,
+ reason="reauth_successful",
)
await self.async_set_unique_id(user_input[CONF_APP_ID])
self._abort_if_unique_id_configured()
@@ -63,8 +67,8 @@ class TTNFlowHandler(ConfigFlow, domain=DOMAIN):
# Show form for user to provide settings
if not user_input:
- if self.source == SOURCE_REAUTH:
- user_input = self._get_reauth_entry().data
+ if self._reauth_entry:
+ user_input = self._reauth_entry.data
else:
user_input = {CONF_HOST: TTN_API_HOST}
@@ -88,6 +92,11 @@ class TTNFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle a flow initialized by a reauth event."""
+
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
+
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py
index 25dd2f1e1eb..82dd169a52d 100644
--- a/homeassistant/components/thethingsnetwork/sensor.py
+++ b/homeassistant/components/thethingsnetwork/sensor.py
@@ -4,11 +4,10 @@ import logging
from ttn_client import TTNSensorValue
-from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.sensor import SensorEntity, StateType
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import StateType
from .const import CONF_APP_ID, DOMAIN
from .entity import TTNEntity
diff --git a/homeassistant/components/thethingsnetwork/strings.json b/homeassistant/components/thethingsnetwork/strings.json
index f5a4fcef8fd..98572cb318c 100644
--- a/homeassistant/components/thethingsnetwork/strings.json
+++ b/homeassistant/components/thethingsnetwork/strings.json
@@ -22,5 +22,11 @@
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
+ },
+ "issues": {
+ "manual_migration": {
+ "description": "Configuring {domain} using YAML was removed as part of migrating to [The Things Network v3]({v2_v3_migration_url}). [The Things Network v2 has shutted down]({v2_deprecation_url}).\n\nPlease remove the {domain} entry from the configuration.yaml and add re-add the integration using the config_flow",
+ "title": "The {domain} YAML configuration is not supported"
+ }
}
}
diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py
index 3d52d2225be..9440e251586 100644
--- a/homeassistant/components/threshold/binary_sensor.py
+++ b/homeassistant/components/threshold/binary_sensor.py
@@ -61,29 +61,15 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME: Final = "Threshold"
-
-def no_missing_threshold(value: dict) -> dict:
- """Validate data point list is greater than polynomial degrees."""
- if value.get(CONF_LOWER) is None and value.get(CONF_UPPER) is None:
- raise vol.Invalid("Lower or Upper thresholds are not provided")
-
- return value
-
-
-PLATFORM_SCHEMA = vol.All(
- BINARY_SENSOR_PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_ENTITY_ID): cv.entity_id,
- vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
- vol.Optional(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce(
- float
- ),
- vol.Optional(CONF_LOWER): vol.Coerce(float),
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_UPPER): vol.Coerce(float),
- }
- ),
- no_missing_threshold,
+PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce(float),
+ vol.Optional(CONF_LOWER): vol.Coerce(float),
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_UPPER): vol.Coerce(float),
+ }
)
@@ -140,6 +126,9 @@ async def async_setup_platform(
hysteresis: float = config[CONF_HYSTERESIS]
device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS)
+ if lower is None and upper is None:
+ raise ValueError("Lower or Upper thresholds not provided")
+
async_add_entities(
[
ThresholdSensor(
@@ -162,9 +151,6 @@ class ThresholdSensor(BinarySensorEntity):
"""Representation of a Threshold sensor."""
_attr_should_poll = False
- _unrecorded_attributes = frozenset(
- {ATTR_ENTITY_ID, ATTR_HYSTERESIS, ATTR_LOWER, ATTR_TYPE, ATTR_UPPER}
- )
def __init__(
self,
@@ -191,6 +177,7 @@ class ThresholdSensor(BinarySensorEntity):
self._hysteresis: float = hysteresis
self._attr_device_class = device_class
self._state_position = POSITION_UNKNOWN
+ self._state: bool | None = None
self.sensor_value: float | None = None
async def async_added_to_hass(self) -> None:
@@ -242,6 +229,11 @@ class ThresholdSensor(BinarySensorEntity):
)
_update_sensor_state()
+ @property
+ def is_on(self) -> bool | None:
+ """Return true if sensor is on."""
+ return self._state
+
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the sensor."""
@@ -269,53 +261,53 @@ class ThresholdSensor(BinarySensorEntity):
if self.sensor_value is None:
self._state_position = POSITION_UNKNOWN
- self._attr_is_on = None
+ self._state = None
return
if self.threshold_type == TYPE_LOWER:
- if self._attr_is_on is None:
- self._attr_is_on = False
+ if self._state is None:
+ self._state = False
self._state_position = POSITION_ABOVE
if below(self.sensor_value, self._threshold_lower):
self._state_position = POSITION_BELOW
- self._attr_is_on = True
+ self._state = True
elif above(self.sensor_value, self._threshold_lower):
self._state_position = POSITION_ABOVE
- self._attr_is_on = False
+ self._state = False
return
if self.threshold_type == TYPE_UPPER:
assert self._threshold_upper is not None
- if self._attr_is_on is None:
- self._attr_is_on = False
+ if self._state is None:
+ self._state = False
self._state_position = POSITION_BELOW
if above(self.sensor_value, self._threshold_upper):
self._state_position = POSITION_ABOVE
- self._attr_is_on = True
+ self._state = True
elif below(self.sensor_value, self._threshold_upper):
self._state_position = POSITION_BELOW
- self._attr_is_on = False
+ self._state = False
return
if self.threshold_type == TYPE_RANGE:
- if self._attr_is_on is None:
- self._attr_is_on = True
+ if self._state is None:
+ self._state = True
self._state_position = POSITION_IN_RANGE
if below(self.sensor_value, self._threshold_lower):
self._state_position = POSITION_BELOW
- self._attr_is_on = False
+ self._state = False
if above(self.sensor_value, self._threshold_upper):
self._state_position = POSITION_ABOVE
- self._attr_is_on = False
+ self._state = False
elif above(self.sensor_value, self._threshold_lower) and below(
self.sensor_value, self._threshold_upper
):
self._state_position = POSITION_IN_RANGE
- self._attr_is_on = True
+ self._state = True
return
@callback
diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py
index 9b5c7ee1168..ce05b8070f6 100644
--- a/homeassistant/components/tibber/__init__.py
+++ b/homeassistant/components/tibber/__init__.py
@@ -6,9 +6,15 @@ import aiohttp
import tibber
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_ACCESS_TOKEN, EVENT_HOMEASSISTANT_STOP, Platform
+from homeassistant.const import (
+ CONF_ACCESS_TOKEN,
+ CONF_NAME,
+ EVENT_HOMEASSISTANT_STOP,
+ Platform,
+)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
@@ -67,6 +73,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+ # Use discovery to load platform legacy notify platform
+ # The use of the legacy notify service was deprecated with HA Core 2024.6
+ # Support will be removed with HA Core 2024.12
+ hass.async_create_task(
+ discovery.async_load_platform(
+ hass,
+ Platform.NOTIFY,
+ DOMAIN,
+ {CONF_NAME: DOMAIN},
+ hass.data[DATA_HASS_CONFIG],
+ )
+ )
+
return True
diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json
index bc9304ab59d..eb59d2456fb 100644
--- a/homeassistant/components/tibber/manifest.json
+++ b/homeassistant/components/tibber/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["tibber"],
"quality_scale": "silver",
- "requirements": ["pyTibber==0.30.8"]
+ "requirements": ["pyTibber==0.30.2"]
}
diff --git a/homeassistant/components/tibber/notify.py b/homeassistant/components/tibber/notify.py
index fdeeeba68ef..1c9f86ed502 100644
--- a/homeassistant/components/tibber/notify.py
+++ b/homeassistant/components/tibber/notify.py
@@ -2,21 +2,38 @@
from __future__ import annotations
+from collections.abc import Callable
+from typing import Any
+
from tibber import Tibber
from homeassistant.components.notify import (
+ ATTR_TITLE,
ATTR_TITLE_DEFAULT,
+ BaseNotificationService,
NotifyEntity,
NotifyEntityFeature,
+ migrate_notify_issue,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN as TIBBER_DOMAIN
+async def async_get_service(
+ hass: HomeAssistant,
+ config: ConfigType,
+ discovery_info: DiscoveryInfoType | None = None,
+) -> TibberNotificationService:
+ """Get the Tibber notification service."""
+ tibber_connection: Tibber = hass.data[TIBBER_DOMAIN]
+ return TibberNotificationService(tibber_connection.send_notification)
+
+
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
@@ -24,6 +41,31 @@ async def async_setup_entry(
async_add_entities([TibberNotificationEntity(entry.entry_id)])
+class TibberNotificationService(BaseNotificationService):
+ """Implement the notification service for Tibber."""
+
+ def __init__(self, notify: Callable) -> None:
+ """Initialize the service."""
+ self._notify = notify
+
+ async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
+ """Send a message to Tibber devices."""
+ migrate_notify_issue(
+ self.hass,
+ TIBBER_DOMAIN,
+ "Tibber",
+ "2024.12.0",
+ service_name=self._service_name,
+ )
+ title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
+ try:
+ await self._notify(title=title, message=message)
+ except TimeoutError as exc:
+ raise HomeAssistantError(
+ translation_domain=TIBBER_DOMAIN, translation_key="send_message_timeout"
+ ) from exc
+
+
class TibberNotificationEntity(NotifyEntity):
"""Implement the notification entity service for Tibber."""
diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py
index 125dc8eae6f..adac836aca6 100644
--- a/homeassistant/components/tibber/sensor.py
+++ b/homeassistant/components/tibber/sensor.py
@@ -50,7 +50,7 @@ ICON = "mdi:currency-usd"
SCAN_INTERVAL = timedelta(minutes=1)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
PARALLEL_UPDATES = 0
-TWENTY_MINUTES = 20 * 60
+
RT_SENSORS_UNIQUE_ID_MIGRATION = {
"accumulated_consumption_last_hour": "accumulated consumption current hour",
@@ -369,7 +369,7 @@ class TibberSensorElPrice(TibberSensor):
"""Initialize the sensor."""
super().__init__(tibber_home=tibber_home)
self._last_updated: datetime.datetime | None = None
- self._spread_load_constant = randrange(TWENTY_MINUTES)
+ self._spread_load_constant = randrange(5000)
self._attr_available = False
self._attr_extra_state_attributes = {
@@ -397,7 +397,7 @@ class TibberSensorElPrice(TibberSensor):
if (
not self._tibber_home.last_data_timestamp
or (self._tibber_home.last_data_timestamp - now).total_seconds()
- < 11 * 3600 + self._spread_load_constant
+ < 5 * 3600 + self._spread_load_constant
or not self.available
):
_LOGGER.debug("Asking for new data")
diff --git a/homeassistant/components/tibber/services.py b/homeassistant/components/tibber/services.py
index 72943a0215a..35facbcd545 100644
--- a/homeassistant/components/tibber/services.py
+++ b/homeassistant/components/tibber/services.py
@@ -47,19 +47,21 @@ async def __get_prices(call: ServiceCall, *, hass: HomeAssistant) -> ServiceResp
for tibber_home in tibber_connection.get_homes(only_active=True):
home_nickname = tibber_home.name
+ price_info = tibber_home.info["viewer"]["home"]["currentSubscription"][
+ "priceInfo"
+ ]
price_data = [
{
- "start_time": starts_at,
- "price": price,
- "level": tibber_home.price_level.get(starts_at),
+ "start_time": dt.datetime.fromisoformat(price["startsAt"]),
+ "price": price["total"],
+ "level": price["level"],
}
- for starts_at, price in tibber_home.price_total.items()
+ for key in ("today", "tomorrow")
+ for price in price_info[key]
]
selected_data = [
- price
- for price in price_data
- if start <= dt.datetime.fromisoformat(price["start_time"]) < end
+ price for price in price_data if start <= price["start_time"] < end
]
tibber_prices[home_nickname] = selected_data
diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py
index 594c4e7bdcb..7fd5afcea7d 100644
--- a/homeassistant/components/tile/__init__.py
+++ b/homeassistant/components/tile/__init__.py
@@ -101,7 +101,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = coordinators[tile_uuid] = DataUpdateCoordinator(
hass,
LOGGER,
- config_entry=entry,
name=tile.name,
update_interval=DEFAULT_UPDATE_INTERVAL,
update_method=partial(async_update_tile, tile),
diff --git a/homeassistant/components/timer/strings.json b/homeassistant/components/timer/strings.json
index 064ec81df1d..1ebf0c6f50a 100644
--- a/homeassistant/components/timer/strings.json
+++ b/homeassistant/components/timer/strings.json
@@ -1,5 +1,4 @@
{
- "title": "Timer",
"entity_component": {
"_": {
"name": "Timer",
diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py
index 3ac90b5578c..907df849ea1 100644
--- a/homeassistant/components/tod/binary_sensor.py
+++ b/homeassistant/components/tod/binary_sensor.py
@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable
from datetime import datetime, time, timedelta
import logging
-from typing import Any, Literal, TypeGuard
+from typing import TYPE_CHECKING, Any, Literal, TypeGuard
import voluptuous as vol
@@ -109,9 +109,6 @@ class TodSensor(BinarySensorEntity):
"""Time of the Day Sensor."""
_attr_should_poll = False
- _time_before: datetime
- _time_after: datetime
- _next_update: datetime
def __init__(
self,
@@ -125,6 +122,9 @@ class TodSensor(BinarySensorEntity):
"""Init the ToD Sensor..."""
self._attr_unique_id = unique_id
self._attr_name = name
+ self._time_before: datetime | None = None
+ self._time_after: datetime | None = None
+ self._next_update: datetime | None = None
self._after_offset = after_offset
self._before_offset = before_offset
self._before = before
@@ -134,6 +134,9 @@ class TodSensor(BinarySensorEntity):
@property
def is_on(self) -> bool:
"""Return True is sensor is on."""
+ if TYPE_CHECKING:
+ assert self._time_after is not None
+ assert self._time_before is not None
if self._time_after < self._time_before:
return self._time_after <= dt_util.utcnow() < self._time_before
return False
@@ -141,6 +144,10 @@ class TodSensor(BinarySensorEntity):
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the sensor."""
+ if TYPE_CHECKING:
+ assert self._time_after is not None
+ assert self._time_before is not None
+ assert self._next_update is not None
if time_zone := dt_util.get_default_time_zone():
return {
ATTR_AFTER: self._time_after.astimezone(time_zone).isoformat(),
@@ -237,6 +244,9 @@ class TodSensor(BinarySensorEntity):
def _turn_to_next_day(self) -> None:
"""Turn to to the next day."""
+ if TYPE_CHECKING:
+ assert self._time_after is not None
+ assert self._time_before is not None
if _is_sun_event(self._after):
self._time_after = get_astral_event_next(
self.hass, self._after, self._time_after - self._after_offset
@@ -272,12 +282,17 @@ class TodSensor(BinarySensorEntity):
self.async_on_remove(_clean_up_listener)
+ if TYPE_CHECKING:
+ assert self._next_update is not None
self._unsub_update = event.async_track_point_in_utc_time(
self.hass, self._point_in_time_listener, self._next_update
)
def _calculate_next_update(self) -> None:
"""Datetime when the next update to the state."""
+ if TYPE_CHECKING:
+ assert self._time_after is not None
+ assert self._time_before is not None
now = dt_util.utcnow()
if now < self._time_after:
self._next_update = self._time_after
@@ -294,6 +309,9 @@ class TodSensor(BinarySensorEntity):
self._calculate_next_update()
self.async_write_ha_state()
+ if TYPE_CHECKING:
+ assert self._next_update is not None
+
self._unsub_update = event.async_track_point_in_utc_time(
self.hass, self._point_in_time_listener, self._next_update
)
diff --git a/homeassistant/components/todo/intent.py b/homeassistant/components/todo/intent.py
index c678408a576..6233ea6029e 100644
--- a/homeassistant/components/todo/intent.py
+++ b/homeassistant/components/todo/intent.py
@@ -34,7 +34,7 @@ class ListAddItemIntent(intent.IntentHandler):
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
- item = slots["item"]["value"].strip()
+ item = slots["item"]["value"]
list_name = slots["name"]["value"]
target_list: TodoListEntity | None = None
@@ -62,13 +62,4 @@ class ListAddItemIntent(intent.IntentHandler):
response = intent_obj.create_response()
response.response_type = intent.IntentResponseType.ACTION_DONE
- response.async_set_results(
- [
- intent.IntentResponseTarget(
- type=intent.IntentResponseTargetType.ENTITY,
- name=list_name,
- id=match_result.states[0].entity_id,
- )
- ]
- )
return response
diff --git a/homeassistant/components/todoist/__init__.py b/homeassistant/components/todoist/__init__.py
index 2e30856d0df..60c40b1c03c 100644
--- a/homeassistant/components/todoist/__init__.py
+++ b/homeassistant/components/todoist/__init__.py
@@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
token = entry.data[CONF_TOKEN]
api = TodoistAPIAsync(token)
- coordinator = TodoistCoordinator(hass, _LOGGER, entry, SCAN_INTERVAL, api, token)
+ coordinator = TodoistCoordinator(hass, _LOGGER, SCAN_INTERVAL, api, token)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py
index 62f9fafc02a..31470633cc6 100644
--- a/homeassistant/components/todoist/calendar.py
+++ b/homeassistant/components/todoist/calendar.py
@@ -142,7 +142,7 @@ async def async_setup_platform(
project_id_lookup = {}
api = TodoistAPIAsync(token)
- coordinator = TodoistCoordinator(hass, _LOGGER, None, SCAN_INTERVAL, api, token)
+ coordinator = TodoistCoordinator(hass, _LOGGER, SCAN_INTERVAL, api, token)
await coordinator.async_refresh()
async def _shutdown_coordinator(_: Event) -> None:
diff --git a/homeassistant/components/todoist/coordinator.py b/homeassistant/components/todoist/coordinator.py
index 2f35741c5ab..b55680907ac 100644
--- a/homeassistant/components/todoist/coordinator.py
+++ b/homeassistant/components/todoist/coordinator.py
@@ -6,7 +6,6 @@ import logging
from todoist_api_python.api_async import TodoistAPIAsync
from todoist_api_python.models import Label, Project, Section, Task
-from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -18,19 +17,12 @@ class TodoistCoordinator(DataUpdateCoordinator[list[Task]]):
self,
hass: HomeAssistant,
logger: logging.Logger,
- entry: ConfigEntry | None,
update_interval: timedelta,
api: TodoistAPIAsync,
token: str,
) -> None:
"""Initialize the Todoist coordinator."""
- super().__init__(
- hass,
- logger,
- config_entry=entry,
- name="Todoist",
- update_interval=update_interval,
- )
+ super().__init__(hass, logger, name="Todoist", update_interval=update_interval)
self.api = api
self._projects: list[Project] | None = None
self._labels: list[Label] | None = None
diff --git a/homeassistant/components/todoist/strings.json b/homeassistant/components/todoist/strings.json
index 721b491bbf5..5b083ac58bf 100644
--- a/homeassistant/components/todoist/strings.json
+++ b/homeassistant/components/todoist/strings.json
@@ -78,7 +78,7 @@
"description": "When should user be reminded of this task, in natural language."
},
"reminder_date_lang": {
- "name": "Reminder date language",
+ "name": "Reminder data language",
"description": "The language of reminder_date_string."
},
"reminder_date": {
diff --git a/homeassistant/components/tomorrowio/config_flow.py b/homeassistant/components/tomorrowio/config_flow.py
index cce41b17498..90bb488a7c2 100644
--- a/homeassistant/components/tomorrowio/config_flow.py
+++ b/homeassistant/components/tomorrowio/config_flow.py
@@ -91,6 +91,10 @@ def _get_unique_id(hass: HomeAssistant, input_dict: dict[str, Any]):
class TomorrowioOptionsConfigFlow(OptionsFlow):
"""Handle Tomorrow.io options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize Tomorrow.io options flow."""
+ self._config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -101,7 +105,7 @@ class TomorrowioOptionsConfigFlow(OptionsFlow):
options_schema = {
vol.Required(
CONF_TIMESTEP,
- default=self.config_entry.options[CONF_TIMESTEP],
+ default=self._config_entry.options[CONF_TIMESTEP],
): vol.In([1, 5, 15, 30, 60]),
}
@@ -121,7 +125,7 @@ class TomorrowioConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> TomorrowioOptionsConfigFlow:
"""Get the options flow for this handler."""
- return TomorrowioOptionsConfigFlow()
+ return TomorrowioOptionsConfigFlow(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py
index bc33129a741..fb13c630e3e 100644
--- a/homeassistant/components/totalconnect/alarm_control_panel.py
+++ b/homeassistant/components/totalconnect/alarm_control_panel.py
@@ -9,10 +9,19 @@ from total_connect_client.location import TotalConnectLocation
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_DISARMING,
+ STATE_ALARM_TRIGGERED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_platform
@@ -94,7 +103,7 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity):
self._attr_code_format = CodeFormat.NUMBER
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the device."""
# State attributes can be removed in 2025.3
attr = {
@@ -112,29 +121,29 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity):
else:
attr["location_name"] = f"{self.device.name} partition {self._partition_id}"
- state: AlarmControlPanelState | None = None
+ state: str | None = None
if self._partition.arming_state.is_disarmed():
- state = AlarmControlPanelState.DISARMED
+ state = STATE_ALARM_DISARMED
elif self._partition.arming_state.is_armed_night():
- state = AlarmControlPanelState.ARMED_NIGHT
+ state = STATE_ALARM_ARMED_NIGHT
elif self._partition.arming_state.is_armed_home():
- state = AlarmControlPanelState.ARMED_HOME
+ state = STATE_ALARM_ARMED_HOME
elif self._partition.arming_state.is_armed_away():
- state = AlarmControlPanelState.ARMED_AWAY
+ state = STATE_ALARM_ARMED_AWAY
elif self._partition.arming_state.is_armed_custom_bypass():
- state = AlarmControlPanelState.ARMED_CUSTOM_BYPASS
+ state = STATE_ALARM_ARMED_CUSTOM_BYPASS
elif self._partition.arming_state.is_arming():
- state = AlarmControlPanelState.ARMING
+ state = STATE_ALARM_ARMING
elif self._partition.arming_state.is_disarming():
- state = AlarmControlPanelState.DISARMING
+ state = STATE_ALARM_DISARMING
elif self._partition.arming_state.is_triggered_police():
- state = AlarmControlPanelState.TRIGGERED
+ state = STATE_ALARM_TRIGGERED
attr["triggered_source"] = "Police/Medical"
elif self._partition.arming_state.is_triggered_fire():
- state = AlarmControlPanelState.TRIGGERED
+ state = STATE_ALARM_TRIGGERED
attr["triggered_source"] = "Fire/Smoke"
elif self._partition.arming_state.is_triggered_gas():
- state = AlarmControlPanelState.TRIGGERED
+ state = STATE_ALARM_TRIGGERED
attr["triggered_source"] = "Carbon Monoxide"
self._attr_extra_state_attributes = attr
diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py
index 3f5d05fda13..c64dd5c6120 100644
--- a/homeassistant/components/totalconnect/config_flow.py
+++ b/homeassistant/components/totalconnect/config_flow.py
@@ -193,12 +193,16 @@ class TotalConnectConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> TotalConnectOptionsFlowHandler:
"""Get options flow."""
- return TotalConnectOptionsFlowHandler()
+ return TotalConnectOptionsFlowHandler(config_entry)
class TotalConnectOptionsFlowHandler(OptionsFlow):
"""TotalConnect options flow handler."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, bool] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py
index ee1d90e70b4..ceeb1120ed8 100644
--- a/homeassistant/components/tplink/__init__.py
+++ b/homeassistant/components/tplink/__init__.py
@@ -31,7 +31,6 @@ from homeassistant.const import (
CONF_MAC,
CONF_MODEL,
CONF_PASSWORD,
- CONF_PORT,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback
@@ -142,7 +141,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH)
entry_use_http = entry.data.get(CONF_USES_HTTP, False)
entry_aes_keys = entry.data.get(CONF_AES_KEYS)
- port_override = entry.data.get(CONF_PORT)
conn_params: Device.ConnectionParameters | None = None
if conn_params_dict := entry.data.get(CONF_CONNECTION_PARAMETERS):
@@ -159,7 +157,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
timeout=CONNECT_TIMEOUT,
http_client=client,
aes_keys=entry_aes_keys,
- port_override=port_override,
)
if conn_params:
config.connection_type = conn_params
diff --git a/homeassistant/components/tplink/binary_sensor.py b/homeassistant/components/tplink/binary_sensor.py
index 34375bccf4f..0e426161a0c 100644
--- a/homeassistant/components/tplink/binary_sensor.py
+++ b/homeassistant/components/tplink/binary_sensor.py
@@ -58,10 +58,6 @@ BINARY_SENSOR_DESCRIPTIONS: Final = (
key="water_alert",
device_class=BinarySensorDeviceClass.MOISTURE,
),
- TPLinkBinarySensorEntityDescription(
- key="motion_detected",
- device_class=BinarySensorDeviceClass.MOTION,
- ),
)
BINARYSENSOR_DESCRIPTIONS_MAP = {desc.key: desc for desc in BINARY_SENSOR_DESCRIPTIONS}
diff --git a/homeassistant/components/tplink/button.py b/homeassistant/components/tplink/button.py
index 131325e489d..fd2d7fb664f 100644
--- a/homeassistant/components/tplink/button.py
+++ b/homeassistant/components/tplink/button.py
@@ -9,7 +9,6 @@ from kasa import Feature
from homeassistant.components.button import (
DOMAIN as BUTTON_DOMAIN,
- ButtonDeviceClass,
ButtonEntity,
ButtonEntityDescription,
)
@@ -46,10 +45,6 @@ BUTTON_DESCRIPTIONS: Final = [
breaks_in_ha_version="2025.4.0",
),
),
- TPLinkButtonEntityDescription(
- key="reboot",
- device_class=ButtonDeviceClass.RESTART,
- ),
]
BUTTON_DESCRIPTIONS_MAP = {desc.key: desc for desc in BUTTON_DESCRIPTIONS}
diff --git a/homeassistant/components/tplink/climate.py b/homeassistant/components/tplink/climate.py
index f86992ea0cf..3bd6aba5c26 100644
--- a/homeassistant/components/tplink/climate.py
+++ b/homeassistant/components/tplink/climate.py
@@ -15,7 +15,7 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
-from homeassistant.const import PRECISION_TENTHS
+from homeassistant.const import PRECISION_WHOLE
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -64,7 +64,7 @@ class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity):
| ClimateEntityFeature.TURN_ON
)
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
- _attr_precision = PRECISION_TENTHS
+ _attr_precision = PRECISION_WHOLE
# This disables the warning for async_turn_{on,off}, can be removed later.
_enable_turn_on_off_backwards_compatibility = False
diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py
index 63f1b4e125b..e94cf9558f0 100644
--- a/homeassistant/components/tplink/config_flow.py
+++ b/homeassistant/components/tplink/config_flow.py
@@ -32,7 +32,6 @@ from homeassistant.const import (
CONF_MAC,
CONF_MODEL,
CONF_PASSWORD,
- CONF_PORT,
CONF_USERNAME,
)
from homeassistant.core import callback
@@ -70,7 +69,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
MINOR_VERSION = CONF_CONFIG_ENTRY_MINOR_VERSION
host: str | None = None
- port: int | None = None
+ reauth_entry: ConfigEntry | None = None
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -164,16 +163,12 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="already_in_progress")
credentials = await get_credentials(self.hass)
try:
- # If integration discovery there will be a device or None for dhcp
if device:
self._discovered_device = device
await self._async_try_connect(device, credentials)
else:
await self._async_try_discover_and_update(
- host,
- credentials,
- raise_on_progress=True,
- raise_on_timeout=True,
+ host, credentials, raise_on_progress=True
)
except AuthenticationError:
return await self.async_step_discovery_auth_confirm()
@@ -262,26 +257,6 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="discovery_confirm", description_placeholders=placeholders
)
- @staticmethod
- def _async_get_host_port(host_str: str) -> tuple[str, int | None]:
- """Parse the host string for host and port."""
- if "[" in host_str:
- _, _, bracketed = host_str.partition("[")
- host, _, port_str = bracketed.partition("]")
- _, _, port_str = port_str.partition(":")
- else:
- host, _, port_str = host_str.partition(":")
-
- if not port_str:
- return host, None
-
- try:
- port = int(port_str)
- except ValueError:
- return host, None
-
- return host, port
-
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -292,29 +267,12 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
if not (host := user_input[CONF_HOST]):
return await self.async_step_pick_device()
-
- host, port = self._async_get_host_port(host)
-
- match_dict = {CONF_HOST: host}
- if port:
- self.port = port
- match_dict[CONF_PORT] = port
- self._async_abort_entries_match(match_dict)
-
+ self._async_abort_entries_match({CONF_HOST: host})
self.host = host
credentials = await get_credentials(self.hass)
try:
device = await self._async_try_discover_and_update(
- host,
- credentials,
- raise_on_progress=False,
- raise_on_timeout=False,
- port=port,
- ) or await self._async_try_connect_all(
- host,
- credentials=credentials,
- raise_on_progress=False,
- port=port,
+ host, credentials, raise_on_progress=False
)
except AuthenticationError:
return await self.async_step_user_auth_confirm()
@@ -322,8 +280,6 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
placeholders["error"] = str(ex)
else:
- if not device:
- return await self.async_step_user_auth_confirm()
return self._async_create_entry_from_device(device)
return self.async_show_form(
@@ -343,23 +299,15 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
assert self.host is not None
placeholders: dict[str, str] = {CONF_HOST: self.host}
+ assert self._discovered_device is not None
if user_input:
username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
credentials = Credentials(username, password)
- device: Device | None
try:
- if self._discovered_device:
- device = await self._async_try_connect(
- self._discovered_device, credentials
- )
- else:
- device = await self._async_try_connect_all(
- self.host,
- credentials=credentials,
- raise_on_progress=False,
- port=self.port,
- )
+ device = await self._async_try_connect(
+ self._discovered_device, credentials
+ )
except AuthenticationError as ex:
errors[CONF_PASSWORD] = "invalid_auth"
placeholders["error"] = str(ex)
@@ -367,15 +315,11 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
placeholders["error"] = str(ex)
else:
- if not device:
- errors["base"] = "cannot_connect"
- placeholders["error"] = "try_connect_all failed"
- else:
- await set_credentials(self.hass, username, password)
- self.hass.async_create_task(
- self._async_reload_requires_auth_entries(), eager_start=False
- )
- return self._async_create_entry_from_device(device)
+ await set_credentials(self.hass, username, password)
+ self.hass.async_create_task(
+ self._async_reload_requires_auth_entries(), eager_start=False
+ )
+ return self._async_create_entry_from_device(device)
return self.async_show_form(
step_id="user_auth_confirm",
@@ -428,8 +372,8 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
"""Reload any in progress config flow that now have credentials."""
_config_entries = self.hass.config_entries
- if self.source == SOURCE_REAUTH:
- await _config_entries.async_reload(self._get_reauth_entry().entry_id)
+ if reauth_entry := self.reauth_entry:
+ await _config_entries.async_reload(reauth_entry.entry_id)
for flow in _config_entries.flow.async_progress_by_handler(
DOMAIN, include_uninitialized=True
@@ -460,84 +404,47 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
data[CONF_AES_KEYS] = device.config.aes_keys
if device.credentials_hash:
data[CONF_CREDENTIALS_HASH] = device.credentials_hash
- if port := device.config.port_override:
- data[CONF_PORT] = port
return self.async_create_entry(
title=f"{device.alias} {device.model}",
data=data,
)
- async def _async_try_connect_all(
- self,
- host: str,
- credentials: Credentials | None,
- raise_on_progress: bool,
- *,
- port: int | None = None,
- ) -> Device | None:
- """Try to connect to the device speculatively.
-
- The connection parameters aren't known but discovery has failed so try
- to connect with tcp.
- """
- if credentials:
- device = await Discover.try_connect_all(
- host,
- credentials=credentials,
- http_client=create_async_tplink_clientsession(self.hass),
- port=port,
- )
- else:
- # This will just try the legacy protocol that doesn't require auth
- # and doesn't use http
- try:
- device = await Device.connect(
- config=DeviceConfig(host, port_override=port)
- )
- except Exception: # noqa: BLE001
- return None
- if device:
- await self.async_set_unique_id(
- dr.format_mac(device.mac),
- raise_on_progress=raise_on_progress,
- )
- return device
-
async def _async_try_discover_and_update(
self,
host: str,
credentials: Credentials | None,
raise_on_progress: bool,
- raise_on_timeout: bool,
- *,
- port: int | None = None,
- ) -> Device | None:
+ ) -> Device:
"""Try to discover the device and call update.
- Will try to connect directly if discovery fails.
+ Will try to connect to legacy devices if discovery fails.
"""
- self._discovered_device = None
try:
self._discovered_device = await Discover.discover_single(
- host,
- credentials=credentials,
- port=port,
+ host, credentials=credentials
)
except TimeoutError as ex:
- if raise_on_timeout:
+ # Try connect() to legacy devices if discovery fails. This is a
+ # fallback mechanism for legacy that can handle connections without
+ # discovery info but if it fails raise the original error which is
+ # applicable for newer devices.
+ try:
+ self._discovered_device = await Device.connect(
+ config=DeviceConfig(host)
+ )
+ except Exception: # noqa: BLE001
+ # Raise the original error instead of the fallback error
raise ex from ex
- return None
- if TYPE_CHECKING:
- assert self._discovered_device
+ else:
+ if self._discovered_device.config.uses_http:
+ self._discovered_device.config.http_client = (
+ create_async_tplink_clientsession(self.hass)
+ )
+ await self._discovered_device.update()
await self.async_set_unique_id(
dr.format_mac(self._discovered_device.mac),
raise_on_progress=raise_on_progress,
)
- if self._discovered_device.config.uses_http:
- self._discovered_device.config.http_client = (
- create_async_tplink_clientsession(self.hass)
- )
- await self._discovered_device.update()
return self._discovered_device
async def _async_try_connect(
@@ -566,6 +473,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Start the reauthentication flow if the device needs updated credentials."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -574,10 +484,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
placeholders: dict[str, str] = {}
- reauth_entry = self._get_reauth_entry()
+ reauth_entry = self.reauth_entry
+ assert reauth_entry is not None
entry_data = reauth_entry.data
host = entry_data[CONF_HOST]
- port = entry_data.get(CONF_PORT)
if user_input:
username = user_input[CONF_USERNAME]
@@ -587,14 +497,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
device = await self._async_try_discover_and_update(
host,
credentials=credentials,
- raise_on_progress=False,
- raise_on_timeout=False,
- port=port,
- ) or await self._async_try_connect_all(
- host,
- credentials=credentials,
- raise_on_progress=False,
- port=port,
+ raise_on_progress=True,
)
except AuthenticationError as ex:
errors[CONF_PASSWORD] = "invalid_auth"
@@ -603,23 +506,15 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
placeholders["error"] = str(ex)
else:
- if not device:
- errors["base"] = "cannot_connect"
- placeholders["error"] = "try_connect_all failed"
- else:
- await self.async_set_unique_id(
- dr.format_mac(device.mac),
- raise_on_progress=False,
+ await set_credentials(self.hass, username, password)
+ if updates := self._get_config_updates(reauth_entry, host, device):
+ self.hass.config_entries.async_update_entry(
+ reauth_entry, data=updates
)
- await set_credentials(self.hass, username, password)
- if updates := self._get_config_updates(reauth_entry, host, device):
- self.hass.config_entries.async_update_entry(
- reauth_entry, data=updates
- )
- self.hass.async_create_task(
- self._async_reload_requires_auth_entries(), eager_start=False
- )
- return self.async_abort(reason="reauth_successful")
+ self.hass.async_create_task(
+ self._async_reload_requires_auth_entries(), eager_start=False
+ )
+ return self.async_abort(reason="reauth_successful")
# Old config entries will not have these values.
alias = entry_data.get(CONF_ALIAS) or "unknown"
diff --git a/homeassistant/components/tplink/icons.json b/homeassistant/components/tplink/icons.json
index 0abd68543c5..96ea8f41bb7 100644
--- a/homeassistant/components/tplink/icons.json
+++ b/homeassistant/components/tplink/icons.json
@@ -68,15 +68,6 @@
"state": {
"on": "mdi:sleep"
}
- },
- "child_lock": {
- "default": "mdi:account-lock"
- },
- "pir_enabled": {
- "default": "mdi:motion-sensor-off",
- "state": {
- "on": "mdi:motion-sensor"
- }
}
},
"sensor": {
@@ -97,9 +88,6 @@
},
"alarm_source": {
"default": "mdi:bell"
- },
- "water_alert_timestamp": {
- "default": "mdi:clock-alert-outline"
}
},
"number": {
diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json
index cb8a55b3db2..ab1eac7d0c0 100644
--- a/homeassistant/components/tplink/manifest.json
+++ b/homeassistant/components/tplink/manifest.json
@@ -301,5 +301,5 @@
"iot_class": "local_polling",
"loggers": ["kasa"],
"quality_scale": "platinum",
- "requirements": ["python-kasa[speedups]==0.7.7"]
+ "requirements": ["python-kasa[speedups]==0.7.5"]
}
diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py
index 809d9002768..276334dc8a1 100644
--- a/homeassistant/components/tplink/sensor.py
+++ b/homeassistant/components/tplink/sensor.py
@@ -97,10 +97,6 @@ SENSOR_DESCRIPTIONS: tuple[TPLinkSensorEntityDescription, ...] = (
key="device_time",
device_class=SensorDeviceClass.TIMESTAMP,
),
- TPLinkSensorEntityDescription(
- key="water_alert_timestamp",
- device_class=SensorDeviceClass.TIMESTAMP,
- ),
TPLinkSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
@@ -116,7 +112,6 @@ SENSOR_DESCRIPTIONS: tuple[TPLinkSensorEntityDescription, ...] = (
TPLinkSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
- state_class=SensorStateClass.MEASUREMENT,
),
)
diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json
index 8e5118c2720..fd63a1031d3 100644
--- a/homeassistant/components/tplink/strings.json
+++ b/homeassistant/components/tplink/strings.json
@@ -35,6 +35,10 @@
"password": "[%key:common::config_flow::data::password%]"
}
},
+ "reauth": {
+ "title": "[%key:common::config_flow::title::reauth%]",
+ "description": "[%key:component::tplink::config::step::user_auth_confirm::description%]"
+ },
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "[%key:component::tplink::config::step::user_auth_confirm::description%]",
@@ -51,8 +55,7 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"entity": {
@@ -159,9 +162,6 @@
"device_time": {
"name": "Device time"
},
- "water_alert_timestamp": {
- "name": "Last water leak alert"
- },
"auto_off_at": {
"name": "Auto off at"
},
@@ -190,12 +190,6 @@
},
"fan_sleep_mode": {
"name": "Fan sleep mode"
- },
- "child_lock": {
- "name": "Child lock"
- },
- "pir_enabled": {
- "name": "Motion sensor"
}
},
"number": {
diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py
index c9285d86ba6..6d3e21d88c5 100644
--- a/homeassistant/components/tplink/switch.py
+++ b/homeassistant/components/tplink/switch.py
@@ -48,12 +48,6 @@ SWITCH_DESCRIPTIONS: tuple[TPLinkSwitchEntityDescription, ...] = (
TPLinkSwitchEntityDescription(
key="fan_sleep_mode",
),
- TPLinkSwitchEntityDescription(
- key="child_lock",
- ),
- TPLinkSwitchEntityDescription(
- key="pir_enabled",
- ),
)
SWITCH_DESCRIPTIONS_MAP = {desc.key: desc for desc in SWITCH_DESCRIPTIONS}
diff --git a/homeassistant/components/tplink_omada/__init__.py b/homeassistant/components/tplink_omada/__init__.py
index 573df44122c..7890d5936fb 100644
--- a/homeassistant/components/tplink_omada/__init__.py
+++ b/homeassistant/components/tplink_omada/__init__.py
@@ -24,7 +24,6 @@ from .controller import OmadaSiteController
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.DEVICE_TRACKER,
- Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
]
diff --git a/homeassistant/components/tplink_omada/binary_sensor.py b/homeassistant/components/tplink_omada/binary_sensor.py
index 73d5f54b8b3..da0c1dd9fc9 100644
--- a/homeassistant/components/tplink_omada/binary_sensor.py
+++ b/homeassistant/components/tplink_omada/binary_sensor.py
@@ -99,6 +99,7 @@ class OmadaGatewayPortBinarySensor(
"""Binary status of a property on an internet gateway."""
entity_description: GatewayPortBinarySensorEntityDescription
+ _attr_has_entity_name = True
def __init__(
self,
diff --git a/homeassistant/components/tplink_omada/config_flow.py b/homeassistant/components/tplink_omada/config_flow.py
index eeeddb62495..5ea56a9ad9f 100644
--- a/homeassistant/components/tplink_omada/config_flow.py
+++ b/homeassistant/components/tplink_omada/config_flow.py
@@ -179,9 +179,15 @@ class TpLinkOmadaConfigFlow(ConfigFlow, domain=DOMAIN):
if info is not None:
# Auth successful - update the config entry with the new credentials
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=self._omada_opts
+ entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
)
+ assert entry is not None
+ self.hass.config_entries.async_update_entry(
+ entry, data=self._omada_opts
+ )
+ await self.hass.config_entries.async_reload(entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
diff --git a/homeassistant/components/tplink_omada/const.py b/homeassistant/components/tplink_omada/const.py
index bc55c76c931..f63d82c6bb4 100644
--- a/homeassistant/components/tplink_omada/const.py
+++ b/homeassistant/components/tplink_omada/const.py
@@ -1,17 +1,3 @@
"""Constants for the TP-Link Omada integration."""
-from enum import StrEnum
-
DOMAIN = "tplink_omada"
-
-
-class OmadaDeviceStatus(StrEnum):
- """Possible composite status values for Omada devices."""
-
- DISCONNECTED = "disconnected"
- CONNECTED = "connected"
- PENDING = "pending"
- HEARTBEAT_MISSED = "heartbeat_missed"
- ISOLATED = "isolated"
- ADOPT_FAILED = "adopt_failed"
- MANAGED_EXTERNALLY = "managed_externally"
diff --git a/homeassistant/components/tplink_omada/coordinator.py b/homeassistant/components/tplink_omada/coordinator.py
index a80bedeb65e..e4f15e6567c 100644
--- a/homeassistant/components/tplink_omada/coordinator.py
+++ b/homeassistant/components/tplink_omada/coordinator.py
@@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__)
POLL_SWITCH_PORT = 300
POLL_GATEWAY = 300
POLL_CLIENTS = 300
-POLL_DEVICES = 300
+POLL_DEVICES = 900
class OmadaCoordinator[_T](DataUpdateCoordinator[dict[str, _T]]):
diff --git a/homeassistant/components/tplink_omada/entity.py b/homeassistant/components/tplink_omada/entity.py
index 54021a2ef86..213764aaa12 100644
--- a/homeassistant/components/tplink_omada/entity.py
+++ b/homeassistant/components/tplink_omada/entity.py
@@ -14,8 +14,6 @@ from .coordinator import OmadaCoordinator
class OmadaDeviceEntity[_T: OmadaCoordinator[Any]](CoordinatorEntity[_T]):
"""Common base class for all entities associated with Omada SDN Devices."""
- _attr_has_entity_name = True
-
def __init__(self, coordinator: _T, device: OmadaDevice) -> None:
"""Initialize the device."""
super().__init__(coordinator)
diff --git a/homeassistant/components/tplink_omada/icons.json b/homeassistant/components/tplink_omada/icons.json
index c681b5e1f81..d0c407a9326 100644
--- a/homeassistant/components/tplink_omada/icons.json
+++ b/homeassistant/components/tplink_omada/icons.json
@@ -18,14 +18,6 @@
"off": "mdi:cloud-cancel"
}
}
- },
- "sensor": {
- "cpu_usage": {
- "default": "mdi:cpu-32-bit"
- },
- "mem_usage": {
- "default": "mdi:memory"
- }
}
}
}
diff --git a/homeassistant/components/tplink_omada/manifest.json b/homeassistant/components/tplink_omada/manifest.json
index af20b54675b..6bde656dc30 100644
--- a/homeassistant/components/tplink_omada/manifest.json
+++ b/homeassistant/components/tplink_omada/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tplink_omada",
"integration_type": "hub",
"iot_class": "local_polling",
- "requirements": ["tplink-omada-client==1.4.3"]
+ "requirements": ["tplink-omada-client==1.4.2"]
}
diff --git a/homeassistant/components/tplink_omada/sensor.py b/homeassistant/components/tplink_omada/sensor.py
deleted file mode 100644
index 272334d1b52..00000000000
--- a/homeassistant/components/tplink_omada/sensor.py
+++ /dev/null
@@ -1,132 +0,0 @@
-"""Support for TPLink Omada binary sensors."""
-
-from __future__ import annotations
-
-from collections.abc import Callable
-from dataclasses import dataclass
-
-from tplink_omada_client.definitions import DeviceStatus, DeviceStatusCategory
-from tplink_omada_client.devices import OmadaListDevice
-
-from homeassistant.components.sensor import (
- SensorDeviceClass,
- SensorEntity,
- SensorEntityDescription,
- SensorStateClass,
-)
-from homeassistant.const import PERCENTAGE, EntityCategory
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import StateType
-
-from . import OmadaConfigEntry
-from .const import OmadaDeviceStatus
-from .coordinator import OmadaDevicesCoordinator
-from .entity import OmadaDeviceEntity
-
-# Useful low level status categories, mapped to a more descriptive status.
-DEVICE_STATUS_MAP = {
- DeviceStatus.PROVISIONING: OmadaDeviceStatus.PENDING,
- DeviceStatus.CONFIGURING: OmadaDeviceStatus.PENDING,
- DeviceStatus.UPGRADING: OmadaDeviceStatus.PENDING,
- DeviceStatus.REBOOTING: OmadaDeviceStatus.PENDING,
- DeviceStatus.ADOPT_FAILED: OmadaDeviceStatus.ADOPT_FAILED,
- DeviceStatus.ADOPT_FAILED_WIRELESS: OmadaDeviceStatus.ADOPT_FAILED,
- DeviceStatus.MANAGED_EXTERNALLY: OmadaDeviceStatus.MANAGED_EXTERNALLY,
- DeviceStatus.MANAGED_EXTERNALLY_WIRELESS: OmadaDeviceStatus.MANAGED_EXTERNALLY,
-}
-
-# High level status categories, suitable for most device statuses.
-DEVICE_STATUS_CATEGORY_MAP = {
- DeviceStatusCategory.DISCONNECTED: OmadaDeviceStatus.DISCONNECTED,
- DeviceStatusCategory.CONNECTED: OmadaDeviceStatus.CONNECTED,
- DeviceStatusCategory.PENDING: OmadaDeviceStatus.PENDING,
- DeviceStatusCategory.HEARTBEAT_MISSED: OmadaDeviceStatus.HEARTBEAT_MISSED,
- DeviceStatusCategory.ISOLATED: OmadaDeviceStatus.ISOLATED,
-}
-
-
-def _map_device_status(device: OmadaListDevice) -> str | None:
- """Map the API device status to the best available descriptive device status."""
- display_status = DEVICE_STATUS_MAP.get(
- device.status
- ) or DEVICE_STATUS_CATEGORY_MAP.get(device.status_category)
- return display_status.value if display_status else None
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: OmadaConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up sensors."""
- controller = config_entry.runtime_data
-
- devices_coordinator = controller.devices_coordinator
-
- async_add_entities(
- OmadaDeviceSensor(devices_coordinator, device, desc)
- for device in devices_coordinator.data.values()
- for desc in OMADA_DEVICE_SENSORS
- if desc.exists_func(device)
- )
-
-
-@dataclass(frozen=True, kw_only=True)
-class OmadaDeviceSensorEntityDescription(SensorEntityDescription):
- """Entity description for a status derived from an Omada device in the device list."""
-
- exists_func: Callable[[OmadaListDevice], bool] = lambda _: True
- update_func: Callable[[OmadaListDevice], StateType]
-
-
-OMADA_DEVICE_SENSORS: list[OmadaDeviceSensorEntityDescription] = [
- OmadaDeviceSensorEntityDescription(
- key="device_status",
- translation_key="device_status",
- device_class=SensorDeviceClass.ENUM,
- entity_category=EntityCategory.DIAGNOSTIC,
- update_func=_map_device_status,
- options=[v.value for v in OmadaDeviceStatus],
- ),
- OmadaDeviceSensorEntityDescription(
- key="cpu_usage",
- translation_key="cpu_usage",
- entity_category=EntityCategory.DIAGNOSTIC,
- state_class=SensorStateClass.MEASUREMENT,
- native_unit_of_measurement=PERCENTAGE,
- update_func=lambda device: device.cpu_usage,
- ),
- OmadaDeviceSensorEntityDescription(
- key="mem_usage",
- translation_key="mem_usage",
- entity_category=EntityCategory.DIAGNOSTIC,
- state_class=SensorStateClass.MEASUREMENT,
- native_unit_of_measurement=PERCENTAGE,
- update_func=lambda device: device.mem_usage,
- ),
-]
-
-
-class OmadaDeviceSensor(OmadaDeviceEntity[OmadaDevicesCoordinator], SensorEntity):
- """Sensor for property of a generic Omada device."""
-
- entity_description: OmadaDeviceSensorEntityDescription
-
- def __init__(
- self,
- coordinator: OmadaDevicesCoordinator,
- device: OmadaListDevice,
- entity_description: OmadaDeviceSensorEntityDescription,
- ) -> None:
- """Initialize the device sensor."""
- super().__init__(coordinator, device)
- self.entity_description = entity_description
- self._attr_unique_id = f"{device.mac}_{entity_description.key}"
-
- @property
- def native_value(self) -> StateType:
- """Return the state of the sensor."""
- return self.entity_description.update_func(
- self.coordinator.data[self.device.mac]
- )
diff --git a/homeassistant/components/tplink_omada/strings.json b/homeassistant/components/tplink_omada/strings.json
index 7fcede3fb12..49873b7d088 100644
--- a/homeassistant/components/tplink_omada/strings.json
+++ b/homeassistant/components/tplink_omada/strings.json
@@ -65,27 +65,6 @@
"poe_delivery": {
"name": "Port {port_name} PoE Delivery"
}
- },
- "sensor": {
- "device_status": {
- "name": "Device status",
- "state": {
- "error": "Error",
- "disconnected": "[%key:common::state::disconnected%]",
- "connected": "[%key:common::state::connected%]",
- "pending": "Pending",
- "heartbeat_missed": "Heartbeat missed",
- "isolated": "Isolated",
- "adopt_failed": "Adopt failed",
- "managed_externally": "Managed externally"
- }
- },
- "cpu_usage": {
- "name": "CPU usage"
- },
- "mem_usage": {
- "name": "Memory usage"
- }
}
}
}
diff --git a/homeassistant/components/tplink_omada/switch.py b/homeassistant/components/tplink_omada/switch.py
index f99d8aaedde..26bedc5a88e 100644
--- a/homeassistant/components/tplink_omada/switch.py
+++ b/homeassistant/components/tplink_omada/switch.py
@@ -229,6 +229,7 @@ class OmadaDevicePortSwitchEntity(
):
"""Generic toggle switch entity for a Netork Port of an Omada Device."""
+ _attr_has_entity_name = True
entity_description: OmadaDevicePortSwitchEntityDescription[
TCoordinator, TDevice, TPort
]
diff --git a/homeassistant/components/tplink_omada/update.py b/homeassistant/components/tplink_omada/update.py
index 54b586794be..d1e0a08b803 100644
--- a/homeassistant/components/tplink_omada/update.py
+++ b/homeassistant/components/tplink_omada/update.py
@@ -119,6 +119,7 @@ class OmadaDeviceUpdate(
| UpdateEntityFeature.PROGRESS
| UpdateEntityFeature.RELEASE_NOTES
)
+ _attr_has_entity_name = True
_attr_device_class = UpdateDeviceClass.FIRMWARE
def __init__(
diff --git a/homeassistant/components/trafikverket_camera/config_flow.py b/homeassistant/components/trafikverket_camera/config_flow.py
index 18e210beb16..6c36d925f88 100644
--- a/homeassistant/components/trafikverket_camera/config_flow.py
+++ b/homeassistant/components/trafikverket_camera/config_flow.py
@@ -94,6 +94,12 @@ class TVCameraConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle re-configuration with Trafikverket."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Confirm re-configuration with Trafikverket."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
@@ -128,7 +134,7 @@ class TVCameraConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=schema,
errors=errors,
)
diff --git a/homeassistant/components/trafikverket_camera/strings.json b/homeassistant/components/trafikverket_camera/strings.json
index b6e2209fc57..142dcba5e85 100644
--- a/homeassistant/components/trafikverket_camera/strings.json
+++ b/homeassistant/components/trafikverket_camera/strings.json
@@ -26,20 +26,6 @@
"data": {
"id": "Choose camera"
}
- },
- "reauth_confirm": {
- "data": {
- "api_key": "[%key:common::config_flow::data::api_key%]"
- }
- },
- "reconfigure": {
- "data": {
- "api_key": "[%key:common::config_flow::data::api_key%]",
- "location": "[%key:common::config_flow::data::location%]"
- },
- "data_description": {
- "location": "[%key:component::trafikverket_camera::config::step::user::data_description::location%]"
- }
}
}
},
diff --git a/homeassistant/components/trafikverket_ferry/config_flow.py b/homeassistant/components/trafikverket_ferry/config_flow.py
index 002dc421273..1f82a535f16 100644
--- a/homeassistant/components/trafikverket_ferry/config_flow.py
+++ b/homeassistant/components/trafikverket_ferry/config_flow.py
@@ -9,7 +9,7 @@ from pytrafikverket import TrafikverketFerry
from pytrafikverket.exceptions import InvalidAuthentication, NoFerryFound
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS
from homeassistant.helpers import selector
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -49,6 +49,8 @@ class TVFerryConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ entry: ConfigEntry | None
+
async def validate_input(
self, api_key: str, ferry_from: str, ferry_to: str
) -> None:
@@ -61,6 +63,8 @@ class TVFerryConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication with Trafikverket."""
+
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -72,10 +76,10 @@ class TVFerryConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input:
api_key = user_input[CONF_API_KEY]
- reauth_entry = self._get_reauth_entry()
+ assert self.entry is not None
try:
await self.validate_input(
- api_key, reauth_entry.data[CONF_FROM], reauth_entry.data[CONF_TO]
+ api_key, self.entry.data[CONF_FROM], self.entry.data[CONF_TO]
)
except InvalidAuthentication:
errors["base"] = "invalid_auth"
@@ -84,10 +88,15 @@ class TVFerryConfigFlow(ConfigFlow, domain=DOMAIN):
except Exception: # noqa: BLE001
errors["base"] = "cannot_connect"
else:
- return self.async_update_reload_and_abort(
- reauth_entry,
- data_updates={CONF_API_KEY: api_key},
+ self.hass.config_entries.async_update_entry(
+ self.entry,
+ data={
+ **self.entry.data,
+ CONF_API_KEY: api_key,
+ },
)
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
diff --git a/homeassistant/components/trafikverket_train/config_flow.py b/homeassistant/components/trafikverket_train/config_flow.py
index f498a7b0d0e..d03eeca8f65 100644
--- a/homeassistant/components/trafikverket_train/config_flow.py
+++ b/homeassistant/components/trafikverket_train/config_flow.py
@@ -21,7 +21,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS
from homeassistant.core import HomeAssistant, callback
@@ -126,18 +126,22 @@ class TVTrainConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
+ entry: ConfigEntry | None
+
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> TVTrainOptionsFlowHandler:
"""Get the options flow for this handler."""
- return TVTrainOptionsFlowHandler()
+ return TVTrainOptionsFlowHandler(config_entry)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication with Trafikverket."""
+
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -149,21 +153,26 @@ class TVTrainConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input:
api_key = user_input[CONF_API_KEY]
- reauth_entry = self._get_reauth_entry()
+ assert self.entry is not None
errors = await validate_input(
self.hass,
api_key,
- reauth_entry.data[CONF_FROM],
- reauth_entry.data[CONF_TO],
- reauth_entry.data.get(CONF_TIME),
- reauth_entry.data[CONF_WEEKDAY],
- reauth_entry.options.get(CONF_FILTER_PRODUCT),
+ self.entry.data[CONF_FROM],
+ self.entry.data[CONF_TO],
+ self.entry.data.get(CONF_TIME),
+ self.entry.data[CONF_WEEKDAY],
+ self.entry.options.get(CONF_FILTER_PRODUCT),
)
if not errors:
- return self.async_update_reload_and_abort(
- reauth_entry,
- data_updates={CONF_API_KEY: api_key},
+ self.hass.config_entries.async_update_entry(
+ self.entry,
+ data={
+ **self.entry.data,
+ CONF_API_KEY: api_key,
+ },
)
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
@@ -229,7 +238,7 @@ class TVTrainConfigFlow(ConfigFlow, domain=DOMAIN):
)
-class TVTrainOptionsFlowHandler(OptionsFlow):
+class TVTrainOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle Trafikverket Train options."""
async def async_step_init(
@@ -247,7 +256,7 @@ class TVTrainOptionsFlowHandler(OptionsFlow):
step_id="init",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(OPTION_SCHEMA),
- user_input or self.config_entry.options,
+ user_input or self.options,
),
errors=errors,
)
diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py
index 30e9f5a146b..2a4fd5aae0b 100644
--- a/homeassistant/components/transmission/config_flow.py
+++ b/homeassistant/components/transmission/config_flow.py
@@ -15,7 +15,6 @@ from homeassistant.config_entries import (
)
from homeassistant.const import (
CONF_HOST,
- CONF_NAME,
CONF_PASSWORD,
CONF_PATH,
CONF_PORT,
@@ -56,6 +55,7 @@ class TransmissionFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
MINOR_VERSION = 2
+ _reauth_entry: ConfigEntry | None
@staticmethod
@callback
@@ -63,7 +63,7 @@ class TransmissionFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> TransmissionOptionsFlowHandler:
"""Get the options flow for this handler."""
- return TransmissionOptionsFlowHandler()
+ return TransmissionOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -100,6 +100,9 @@ class TransmissionFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -107,9 +110,9 @@ class TransmissionFlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
errors = {}
- reauth_entry = self._get_reauth_entry()
+ assert self._reauth_entry
if user_input is not None:
- user_input = {**reauth_entry.data, **user_input}
+ user_input = {**self._reauth_entry.data, **user_input}
try:
await get_api(self.hass, user_input)
@@ -118,12 +121,15 @@ class TransmissionFlowHandler(ConfigFlow, domain=DOMAIN):
except (CannotConnect, UnknownError):
errors["base"] = "cannot_connect"
else:
- return self.async_update_reload_and_abort(reauth_entry, data=user_input)
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry, data=user_input
+ )
+ await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
description_placeholders={
- CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
- CONF_NAME: reauth_entry.title,
+ CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME]
},
step_id="reauth_confirm",
data_schema=vol.Schema(
@@ -138,6 +144,10 @@ class TransmissionFlowHandler(ConfigFlow, domain=DOMAIN):
class TransmissionOptionsFlowHandler(OptionsFlow):
"""Handle Transmission client options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize Transmission options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py
index 681680f180f..693c080e86e 100644
--- a/homeassistant/components/trend/binary_sensor.py
+++ b/homeassistant/components/trend/binary_sensor.py
@@ -199,6 +199,11 @@ class SensorTrend(BinarySensorEntity, RestoreEntity):
if sensor_entity_id:
self.entity_id = sensor_entity_id
+ @property
+ def is_on(self) -> bool | None:
+ """Return true if sensor is on."""
+ return self._state
+
@property
def extra_state_attributes(self) -> Mapping[str, Any]:
"""Return the state attributes of the sensor."""
@@ -242,9 +247,9 @@ class SensorTrend(BinarySensorEntity, RestoreEntity):
if not (state := await self.async_get_last_state()):
return
- if state.state in {STATE_UNKNOWN, STATE_UNAVAILABLE}:
+ if state.state == STATE_UNKNOWN:
return
- self._attr_is_on = state.state == STATE_ON
+ self._state = state.state == STATE_ON
async def async_update(self) -> None:
"""Get the latest data and update the states."""
@@ -261,13 +266,13 @@ class SensorTrend(BinarySensorEntity, RestoreEntity):
await self.hass.async_add_executor_job(self._calculate_gradient)
# Update state
- self._attr_is_on = (
+ self._state = (
abs(self._gradient) > abs(self._min_gradient)
and math.copysign(self._gradient, self._min_gradient) == self._gradient
)
if self._invert:
- self._attr_is_on = not self._attr_is_on
+ self._state = not self._state
def _calculate_gradient(self) -> None:
"""Compute the linear trend gradient of the current samples.
diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json
index d7981105fd2..56b4b811171 100644
--- a/homeassistant/components/trend/manifest.json
+++ b/homeassistant/components/trend/manifest.json
@@ -7,5 +7,5 @@
"integration_type": "helper",
"iot_class": "calculated",
"quality_scale": "internal",
- "requirements": ["numpy==2.1.3"]
+ "requirements": ["numpy==1.26.4"]
}
diff --git a/homeassistant/components/trend/strings.json b/homeassistant/components/trend/strings.json
index fb70a6e7032..2fe0b35ee3c 100644
--- a/homeassistant/components/trend/strings.json
+++ b/homeassistant/components/trend/strings.json
@@ -1,5 +1,4 @@
{
- "title": "Trend",
"services": {
"reload": {
"name": "[%key:common::action::reload%]",
diff --git a/homeassistant/components/triggercmd/config_flow.py b/homeassistant/components/triggercmd/config_flow.py
index fc02dd0b2fc..f39d3abc9d4 100644
--- a/homeassistant/components/triggercmd/config_flow.py
+++ b/homeassistant/components/triggercmd/config_flow.py
@@ -56,7 +56,7 @@ class TriggerCMDConfigFlow(ConfigFlow, domain=DOMAIN):
except InvalidToken:
errors[CONF_TOKEN] = "invalid_token"
except TRIGGERcmdConnectionError:
- errors["base"] = "cannot_connect"
+ errors["base"] = "connection_error"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
diff --git a/homeassistant/components/triggercmd/manifest.json b/homeassistant/components/triggercmd/manifest.json
index a0ee4eaf63e..b71a5b83a81 100644
--- a/homeassistant/components/triggercmd/manifest.json
+++ b/homeassistant/components/triggercmd/manifest.json
@@ -3,7 +3,7 @@
"name": "TRIGGERcmd",
"codeowners": ["@rvmey"],
"config_flow": true,
- "documentation": "https://www.home-assistant.io/integrations/triggercmd",
+ "documentation": "https://docs.triggercmd.com",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["triggercmd==0.0.27"]
diff --git a/homeassistant/components/triggercmd/strings.json b/homeassistant/components/triggercmd/strings.json
index 6725b92f59f..cbbbbc312be 100644
--- a/homeassistant/components/triggercmd/strings.json
+++ b/homeassistant/components/triggercmd/strings.json
@@ -13,7 +13,6 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
- "invalid_token": "Invalid token",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py
index c8a639cd239..47143f3595c 100644
--- a/homeassistant/components/tuya/__init__.py
+++ b/homeassistant/components/tuya/__init__.py
@@ -146,21 +146,14 @@ class DeviceListener(SharingDeviceListener):
self.hass = hass
self.manager = manager
- def update_device(
- self, device: CustomerDevice, updated_status_properties: list[str] | None
- ) -> None:
+ def update_device(self, device: CustomerDevice) -> None:
"""Update device status."""
LOGGER.debug(
- "Received update for device %s: %s (updated properties: %s)",
+ "Received update for device %s: %s",
device.id,
self.manager.device_map[device.id].status,
- updated_status_properties,
- )
- dispatcher_send(
- self.hass,
- f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}",
- updated_status_properties,
)
+ dispatcher_send(self.hass, f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}")
def add_device(self, device: CustomerDevice) -> None:
"""Add device added listener."""
diff --git a/homeassistant/components/tuya/alarm_control_panel.py b/homeassistant/components/tuya/alarm_control_panel.py
index 56bccc73581..fbea8d352a0 100644
--- a/homeassistant/components/tuya/alarm_control_panel.py
+++ b/homeassistant/components/tuya/alarm_control_panel.py
@@ -10,7 +10,12 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityDescription,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
+)
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -30,11 +35,11 @@ class Mode(StrEnum):
SOS = "sos"
-STATE_MAPPING: dict[str, AlarmControlPanelState] = {
- Mode.DISARMED: AlarmControlPanelState.DISARMED,
- Mode.ARM: AlarmControlPanelState.ARMED_AWAY,
- Mode.HOME: AlarmControlPanelState.ARMED_HOME,
- Mode.SOS: AlarmControlPanelState.TRIGGERED,
+STATE_MAPPING: dict[str, str] = {
+ Mode.DISARMED: STATE_ALARM_DISARMED,
+ Mode.ARM: STATE_ALARM_ARMED_AWAY,
+ Mode.HOME: STATE_ALARM_ARMED_HOME,
+ Mode.SOS: STATE_ALARM_TRIGGERED,
}
@@ -110,7 +115,7 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity):
self._attr_supported_features |= AlarmControlPanelEntityFeature.TRIGGER
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the device."""
if not (status := self.device.status.get(self.entity_description.key)):
return None
diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py
index 12661a26fd1..a8c9157caa7 100644
--- a/homeassistant/components/tuya/binary_sensor.py
+++ b/homeassistant/components/tuya/binary_sensor.py
@@ -151,7 +151,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
TuyaBinarySensorEntityDescription(
key=DPCode.PRESENCE_STATE,
device_class=BinarySensorDeviceClass.OCCUPANCY,
- on_value={"presence", "small_move", "large_move", "peaceful"},
+ on_value="presence",
),
),
# Formaldehyde Detector
diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py
index 30d04eb61e2..104c3b7c9fa 100644
--- a/homeassistant/components/tuya/config_flow.py
+++ b/homeassistant/components/tuya/config_flow.py
@@ -8,7 +8,7 @@ from typing import Any
from tuya_sharing import LoginControl
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.helpers import selector
from .const import (
@@ -32,6 +32,7 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN):
__user_code: str
__qr_code: str
+ __reauth_entry: ConfigEntry | None = None
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -134,9 +135,9 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_ENDPOINT: info[CONF_ENDPOINT],
}
- if self.source == SOURCE_REAUTH:
+ if self.__reauth_entry:
return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
+ self.__reauth_entry,
data=entry_data,
)
@@ -149,8 +150,14 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle initiation of re-authentication with Tuya."""
- if CONF_USER_CODE in entry_data:
- success, _ = await self.__async_get_qr_code(entry_data[CONF_USER_CODE])
+ self.__reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
+
+ if self.__reauth_entry and CONF_USER_CODE in self.__reauth_entry.data:
+ success, _ = await self.__async_get_qr_code(
+ self.__reauth_entry.data[CONF_USER_CODE]
+ )
if success:
return await self.async_step_scan()
diff --git a/homeassistant/components/tuya/entity.py b/homeassistant/components/tuya/entity.py
index cc258560067..4d3710f7570 100644
--- a/homeassistant/components/tuya/entity.py
+++ b/homeassistant/components/tuya/entity.py
@@ -283,15 +283,10 @@ class TuyaEntity(Entity):
async_dispatcher_connect(
self.hass,
f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{self.device.id}",
- self._handle_state_update,
+ self.async_write_ha_state,
)
)
- async def _handle_state_update(
- self, updated_status_properties: list[str] | None
- ) -> None:
- self.async_write_ha_state()
-
def _send_command(self, commands: list[dict[str, Any]]) -> None:
"""Send command to the device."""
LOGGER.debug("Sending commands for device %s: %s", self.device.id, commands)
diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json
index b53e6fa27d8..305a74160de 100644
--- a/homeassistant/components/tuya/manifest.json
+++ b/homeassistant/components/tuya/manifest.json
@@ -43,5 +43,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["tuya_iot"],
- "requirements": ["tuya-device-sharing-sdk==0.2.1"]
+ "requirements": ["tuya-device-sharing-sdk==0.1.9"]
}
diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py
index b9677037b7e..fd8efcac95d 100644
--- a/homeassistant/components/tuya/sensor.py
+++ b/homeassistant/components/tuya/sensor.py
@@ -203,17 +203,6 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
),
- TuyaSensorEntityDescription(
- key=DPCode.CH2O_VALUE,
- translation_key="formaldehyde",
- state_class=SensorStateClass.MEASUREMENT,
- ),
- TuyaSensorEntityDescription(
- key=DPCode.VOC_VALUE,
- translation_key="voc",
- device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
- state_class=SensorStateClass.MEASUREMENT,
- ),
*BATTERY_SENSORS,
),
# Two-way temperature and humidity switch
diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py
index b6728b96536..f447ef6257d 100644
--- a/homeassistant/components/twentemilieu/__init__.py
+++ b/homeassistant/components/twentemilieu/__init__.py
@@ -42,7 +42,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator: TwenteMilieuDataUpdateCoordinator = DataUpdateCoordinator(
hass,
LOGGER,
- config_entry=entry,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
update_method=twentemilieu.update,
diff --git a/homeassistant/components/twitch/__init__.py b/homeassistant/components/twitch/__init__.py
index 6979a016447..40a744684b9 100644
--- a/homeassistant/components/twitch/__init__.py
+++ b/homeassistant/components/twitch/__init__.py
@@ -17,8 +17,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
async_get_config_entry_implementation,
)
-from .const import DOMAIN, OAUTH_SCOPES, PLATFORMS
-from .coordinator import TwitchCoordinator
+from .const import CLIENT, DOMAIN, OAUTH_SCOPES, PLATFORMS, SESSION
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -47,11 +46,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
client.auto_refresh_auth = False
await client.set_user_authentication(access_token, scope=OAUTH_SCOPES)
- coordinator = TwitchCoordinator(hass, client, session)
-
- await coordinator.async_config_entry_first_refresh()
-
- hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
+ hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
+ CLIENT: client,
+ SESSION: session,
+ }
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
diff --git a/homeassistant/components/twitch/config_flow.py b/homeassistant/components/twitch/config_flow.py
index ed196897c11..7f006f194f5 100644
--- a/homeassistant/components/twitch/config_flow.py
+++ b/homeassistant/components/twitch/config_flow.py
@@ -9,7 +9,7 @@ from typing import Any, cast
from twitchAPI.helper import first
from twitchAPI.twitch import Twitch
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.config_entry_oauth2_flow import LocalOAuth2Implementation
@@ -23,6 +23,7 @@ class OAuth2FlowHandler(
"""Config flow to handle Twitch OAuth2 authentication."""
DOMAIN = DOMAIN
+ reauth_entry: ConfigEntry | None = None
def __init__(self) -> None:
"""Initialize flow."""
@@ -62,8 +63,8 @@ class OAuth2FlowHandler(
user_id = user.id
- await self.async_set_unique_id(user_id)
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
+ await self.async_set_unique_id(user_id)
self._abort_if_unique_id_configured()
channels = [
@@ -75,36 +76,38 @@ class OAuth2FlowHandler(
title=user.display_name, data=data, options={CONF_CHANNELS: channels}
)
- reauth_entry = self._get_reauth_entry()
- self._abort_if_unique_id_mismatch(
+ if self.reauth_entry.unique_id == user_id:
+ new_channels = self.reauth_entry.options[CONF_CHANNELS]
+ # Since we could not get all channels at import, we do it at the reauth
+ # immediately after.
+ if "imported" in self.reauth_entry.data:
+ channels = [
+ channel.broadcaster_login
+ async for channel in await client.get_followed_channels(user_id)
+ ]
+ options = list(set(channels) - set(new_channels))
+ new_channels = [*new_channels, *options]
+
+ self.hass.config_entries.async_update_entry(
+ self.reauth_entry,
+ data=data,
+ options={CONF_CHANNELS: new_channels},
+ )
+ await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+
+ return self.async_abort(
reason="wrong_account",
- description_placeholders={
- "title": reauth_entry.title,
- "username": str(reauth_entry.unique_id),
- },
- )
-
- new_channels = reauth_entry.options[CONF_CHANNELS]
- # Since we could not get all channels at import, we do it at the reauth
- # immediately after.
- if "imported" in reauth_entry.data:
- channels = [
- channel.broadcaster_login
- async for channel in await client.get_followed_channels(user_id)
- ]
- options = list(set(channels) - set(new_channels))
- new_channels = [*new_channels, *options]
-
- return self.async_update_reload_and_abort(
- reauth_entry,
- data=data,
- options={CONF_CHANNELS: new_channels},
+ description_placeholders={"title": self.reauth_entry.title},
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
diff --git a/homeassistant/components/twitch/const.py b/homeassistant/components/twitch/const.py
index fc7c2f73487..b46bf8113b4 100644
--- a/homeassistant/components/twitch/const.py
+++ b/homeassistant/components/twitch/const.py
@@ -17,5 +17,7 @@ CONF_REFRESH_TOKEN = "refresh_token"
DOMAIN = "twitch"
CONF_CHANNELS = "channels"
+CLIENT = "client"
+SESSION = "session"
OAUTH_SCOPES = [AuthScope.USER_READ_SUBSCRIPTIONS, AuthScope.USER_READ_FOLLOWS]
diff --git a/homeassistant/components/twitch/coordinator.py b/homeassistant/components/twitch/coordinator.py
deleted file mode 100644
index c34eeaa5325..00000000000
--- a/homeassistant/components/twitch/coordinator.py
+++ /dev/null
@@ -1,127 +0,0 @@
-"""Define a class to manage fetching Twitch data."""
-
-from dataclasses import dataclass
-from datetime import datetime, timedelta
-
-from twitchAPI.helper import first
-from twitchAPI.object.api import FollowedChannel, Stream, TwitchUser, UserSubscription
-from twitchAPI.twitch import Twitch
-from twitchAPI.type import TwitchAPIException, TwitchResourceNotFound
-
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-
-from .const import CONF_CHANNELS, DOMAIN, LOGGER, OAUTH_SCOPES
-
-
-def chunk_list(lst: list, chunk_size: int) -> list[list]:
- """Split a list into chunks of chunk_size."""
- return [lst[i : i + chunk_size] for i in range(0, len(lst), chunk_size)]
-
-
-@dataclass
-class TwitchUpdate:
- """Class for holding Twitch data."""
-
- name: str
- followers: int
- is_streaming: bool
- game: str | None
- title: str | None
- started_at: datetime | None
- stream_picture: str | None
- picture: str
- subscribed: bool | None
- subscription_gifted: bool | None
- subscription_tier: int | None
- follows: bool
- following_since: datetime | None
- viewers: int | None
-
-
-class TwitchCoordinator(DataUpdateCoordinator[dict[str, TwitchUpdate]]):
- """Class to manage fetching Twitch data."""
-
- config_entry: ConfigEntry
- users: list[TwitchUser]
- current_user: TwitchUser
-
- def __init__(
- self, hass: HomeAssistant, twitch: Twitch, session: OAuth2Session
- ) -> None:
- """Initialize the coordinator."""
- self.twitch = twitch
- super().__init__(
- hass,
- LOGGER,
- name=DOMAIN,
- update_interval=timedelta(minutes=5),
- )
- self.session = session
-
- async def _async_setup(self) -> None:
- channels = self.config_entry.options[CONF_CHANNELS]
- self.users = []
- # Split channels into chunks of 100 to avoid hitting the rate limit
- for chunk in chunk_list(channels, 100):
- self.users.extend(
- [channel async for channel in self.twitch.get_users(logins=chunk)]
- )
- if not (user := await first(self.twitch.get_users())):
- raise UpdateFailed("Logged in user not found")
- self.current_user = user
-
- async def _async_update_data(self) -> dict[str, TwitchUpdate]:
- await self.session.async_ensure_token_valid()
- await self.twitch.set_user_authentication(
- self.session.token["access_token"],
- OAUTH_SCOPES,
- self.session.token["refresh_token"],
- False,
- )
- data: dict[str, TwitchUpdate] = {}
- streams: dict[str, Stream] = {
- s.user_id: s
- async for s in self.twitch.get_followed_streams(
- user_id=self.current_user.id, first=100
- )
- }
- follows: dict[str, FollowedChannel] = {
- f.broadcaster_id: f
- async for f in await self.twitch.get_followed_channels(
- user_id=self.current_user.id, first=100
- )
- }
- for channel in self.users:
- followers = await self.twitch.get_channel_followers(channel.id)
- stream = streams.get(channel.id)
- follow = follows.get(channel.id)
- sub: UserSubscription | None = None
- try:
- sub = await self.twitch.check_user_subscription(
- user_id=self.current_user.id, broadcaster_id=channel.id
- )
- except TwitchResourceNotFound:
- LOGGER.debug("User is not subscribed to %s", channel.display_name)
- except TwitchAPIException as exc:
- LOGGER.error("Error response on check_user_subscription: %s", exc)
-
- data[channel.id] = TwitchUpdate(
- channel.display_name,
- followers.total,
- bool(stream),
- stream.game_name if stream else None,
- stream.title if stream else None,
- stream.started_at if stream else None,
- stream.thumbnail_url if stream else None,
- channel.profile_image_url,
- bool(sub),
- sub.is_gift if sub else None,
- {"1000": 1, "2000": 2, "3000": 3}.get(sub.tier) if sub else None,
- bool(follow),
- follow.followed_at if follow else None,
- stream.viewer_count if stream else None,
- )
- return data
diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py
index bd5fc509989..a6e2f4e04af 100644
--- a/homeassistant/components/twitch/sensor.py
+++ b/homeassistant/components/twitch/sensor.py
@@ -2,28 +2,32 @@
from __future__ import annotations
-from typing import Any
+from twitchAPI.helper import first
+from twitchAPI.twitch import (
+ AuthType,
+ Twitch,
+ TwitchAPIException,
+ TwitchResourceNotFound,
+ TwitchUser,
+)
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
+from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import StateType
-from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from . import TwitchCoordinator
-from .const import DOMAIN
-from .coordinator import TwitchUpdate
+from .const import CLIENT, CONF_CHANNELS, DOMAIN, LOGGER, OAUTH_SCOPES, SESSION
ATTR_GAME = "game"
ATTR_TITLE = "title"
ATTR_SUBSCRIPTION = "subscribed"
+ATTR_SUBSCRIPTION_SINCE = "subscribed_since"
ATTR_SUBSCRIPTION_GIFTED = "subscription_is_gifted"
-ATTR_SUBSCRIPTION_TIER = "subscription_tier"
ATTR_FOLLOW = "following"
ATTR_FOLLOW_SINCE = "following_since"
ATTR_FOLLOWING = "followers"
-ATTR_VIEWERS = "viewers"
+ATTR_VIEWS = "views"
ATTR_STARTED_AT = "started_at"
STATE_OFFLINE = "offline"
@@ -32,71 +36,109 @@ STATE_STREAMING = "streaming"
PARALLEL_UPDATES = 1
+def chunk_list(lst: list, chunk_size: int) -> list[list]:
+ """Split a list into chunks of chunk_size."""
+ return [lst[i : i + chunk_size] for i in range(0, len(lst), chunk_size)]
+
+
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize entries."""
- coordinator = hass.data[DOMAIN][entry.entry_id]
+ client = hass.data[DOMAIN][entry.entry_id][CLIENT]
+ session = hass.data[DOMAIN][entry.entry_id][SESSION]
- async_add_entities(
- TwitchSensor(coordinator, channel_id) for channel_id in coordinator.data
- )
+ channels = entry.options[CONF_CHANNELS]
+
+ entities: list[TwitchSensor] = []
+
+ # Split channels into chunks of 100 to avoid hitting the rate limit
+ for chunk in chunk_list(channels, 100):
+ entities.extend(
+ [
+ TwitchSensor(channel, session, client)
+ async for channel in client.get_users(logins=chunk)
+ ]
+ )
+
+ async_add_entities(entities, True)
-class TwitchSensor(CoordinatorEntity[TwitchCoordinator], SensorEntity):
+class TwitchSensor(SensorEntity):
"""Representation of a Twitch channel."""
_attr_translation_key = "channel"
- def __init__(self, coordinator: TwitchCoordinator, channel_id: str) -> None:
+ def __init__(
+ self, channel: TwitchUser, session: OAuth2Session, client: Twitch
+ ) -> None:
"""Initialize the sensor."""
- super().__init__(coordinator)
- self.channel_id = channel_id
- self._attr_unique_id = channel_id
- self._attr_name = self.channel.name
+ self._session = session
+ self._client = client
+ self._channel = channel
+ self._enable_user_auth = client.has_required_auth(AuthType.USER, OAUTH_SCOPES)
+ self._attr_name = channel.display_name
+ self._attr_unique_id = channel.id
- @property
- def available(self) -> bool:
- """Return if entity is available."""
- return super().available and self.channel_id in self.coordinator.data
+ async def async_update(self) -> None:
+ """Update device state."""
+ await self._session.async_ensure_token_valid()
+ await self._client.set_user_authentication(
+ self._session.token["access_token"],
+ OAUTH_SCOPES,
+ self._session.token["refresh_token"],
+ False,
+ )
+ followers = await self._client.get_channel_followers(self._channel.id)
- @property
- def channel(self) -> TwitchUpdate:
- """Return the channel data."""
- return self.coordinator.data[self.channel_id]
-
- @property
- def native_value(self) -> StateType:
- """Return the state of the sensor."""
- return STATE_STREAMING if self.channel.is_streaming else STATE_OFFLINE
-
- @property
- def extra_state_attributes(self) -> dict[str, Any]:
- """Return the state attributes."""
- channel = self.channel
- resp = {
- ATTR_FOLLOWING: channel.followers,
- ATTR_GAME: channel.game,
- ATTR_TITLE: channel.title,
- ATTR_STARTED_AT: channel.started_at,
- ATTR_VIEWERS: channel.viewers,
+ self._attr_extra_state_attributes = {
+ ATTR_FOLLOWING: followers.total,
+ ATTR_VIEWS: self._channel.view_count,
}
- resp[ATTR_SUBSCRIPTION] = False
- if channel.subscribed is not None:
- resp[ATTR_SUBSCRIPTION] = channel.subscribed
- resp[ATTR_SUBSCRIPTION_GIFTED] = channel.subscription_gifted
- resp[ATTR_SUBSCRIPTION_TIER] = channel.subscription_tier
- resp[ATTR_FOLLOW] = channel.follows
- if channel.follows:
- resp[ATTR_FOLLOW_SINCE] = channel.following_since
- return resp
+ if self._enable_user_auth:
+ await self._async_add_user_attributes()
+ if stream := (
+ await first(self._client.get_streams(user_id=[self._channel.id], first=1))
+ ):
+ self._attr_native_value = STATE_STREAMING
+ self._attr_extra_state_attributes[ATTR_GAME] = stream.game_name
+ self._attr_extra_state_attributes[ATTR_TITLE] = stream.title
+ self._attr_extra_state_attributes[ATTR_STARTED_AT] = stream.started_at
+ self._attr_entity_picture = stream.thumbnail_url
+ if self._attr_entity_picture is not None:
+ self._attr_entity_picture = self._attr_entity_picture.format(
+ height=24,
+ width=24,
+ )
+ else:
+ self._attr_native_value = STATE_OFFLINE
+ self._attr_extra_state_attributes[ATTR_GAME] = None
+ self._attr_extra_state_attributes[ATTR_TITLE] = None
+ self._attr_extra_state_attributes[ATTR_STARTED_AT] = None
+ self._attr_entity_picture = self._channel.profile_image_url
- @property
- def entity_picture(self) -> str | None:
- """Return the picture of the sensor."""
- if self.channel.is_streaming:
- assert self.channel.stream_picture is not None
- return self.channel.stream_picture
- return self.channel.picture
+ async def _async_add_user_attributes(self) -> None:
+ if not (user := await first(self._client.get_users())):
+ return
+ self._attr_extra_state_attributes[ATTR_SUBSCRIPTION] = False
+ try:
+ sub = await self._client.check_user_subscription(
+ user_id=user.id, broadcaster_id=self._channel.id
+ )
+ self._attr_extra_state_attributes[ATTR_SUBSCRIPTION] = True
+ self._attr_extra_state_attributes[ATTR_SUBSCRIPTION_GIFTED] = sub.is_gift
+ except TwitchResourceNotFound:
+ LOGGER.debug("User is not subscribed to %s", self._channel.display_name)
+ except TwitchAPIException as exc:
+ LOGGER.error("Error response on check_user_subscription: %s", exc)
+
+ follows = await self._client.get_followed_channels(
+ user.id, broadcaster_id=self._channel.id
+ )
+ self._attr_extra_state_attributes[ATTR_FOLLOW] = follows.total > 0
+ if follows.total:
+ self._attr_extra_state_attributes[ATTR_FOLLOW_SINCE] = follows.data[
+ 0
+ ].followed_at
diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py
index 63c8533aa2e..b5ad1ea2ff0 100644
--- a/homeassistant/components/unifi/config_flow.py
+++ b/homeassistant/components/unifi/config_flow.py
@@ -20,7 +20,7 @@ import voluptuous as vol
from homeassistant.components import ssdp
from homeassistant.config_entries import (
- SOURCE_REAUTH,
+ ConfigEntry,
ConfigEntryState,
ConfigFlow,
ConfigFlowResult,
@@ -78,7 +78,7 @@ class UnifiFlowHandler(ConfigFlow, domain=UNIFI_DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
- config_entry: UnifiConfigEntry,
+ config_entry: ConfigEntry,
) -> UnifiOptionsFlowHandler:
"""Get the options flow for this handler."""
return UnifiOptionsFlowHandler(config_entry)
@@ -86,6 +86,7 @@ class UnifiFlowHandler(ConfigFlow, domain=UNIFI_DOMAIN):
def __init__(self) -> None:
"""Initialize the UniFi Network flow."""
self.config: dict[str, Any] = {}
+ self.reauth_config_entry: ConfigEntry | None = None
self.reauth_schema: dict[vol.Marker, Any] = {}
async def async_step_user(
@@ -117,14 +118,13 @@ class UnifiFlowHandler(ConfigFlow, domain=UNIFI_DOMAIN):
else:
if (
- self.source == SOURCE_REAUTH
- and (
- (reauth_unique_id := self._get_reauth_entry().unique_id)
- is not None
- )
- and reauth_unique_id in self.sites
+ self.reauth_config_entry
+ and self.reauth_config_entry.unique_id is not None
+ and self.reauth_config_entry.unique_id in self.sites
):
- return await self.async_step_site({CONF_SITE_ID: reauth_unique_id})
+ return await self.async_step_site(
+ {CONF_SITE_ID: self.reauth_config_entry.unique_id}
+ )
return await self.async_step_site()
@@ -160,8 +160,8 @@ class UnifiFlowHandler(ConfigFlow, domain=UNIFI_DOMAIN):
config_entry = await self.async_set_unique_id(unique_id)
abort_reason = "configuration_updated"
- if self.source == SOURCE_REAUTH:
- config_entry = self._get_reauth_entry()
+ if self.reauth_config_entry:
+ config_entry = self.reauth_config_entry
abort_reason = "reauth_successful"
if config_entry:
@@ -192,20 +192,24 @@ class UnifiFlowHandler(ConfigFlow, domain=UNIFI_DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Trigger a reauthentication flow."""
- reauth_entry = self._get_reauth_entry()
+ config_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
+ assert config_entry
+ self.reauth_config_entry = config_entry
self.context["title_placeholders"] = {
- CONF_HOST: reauth_entry.data[CONF_HOST],
- CONF_SITE_ID: reauth_entry.title,
+ CONF_HOST: config_entry.data[CONF_HOST],
+ CONF_SITE_ID: config_entry.title,
}
self.reauth_schema = {
- vol.Required(CONF_HOST, default=reauth_entry.data[CONF_HOST]): str,
- vol.Required(CONF_USERNAME, default=reauth_entry.data[CONF_USERNAME]): str,
+ vol.Required(CONF_HOST, default=config_entry.data[CONF_HOST]): str,
+ vol.Required(CONF_USERNAME, default=config_entry.data[CONF_USERNAME]): str,
vol.Required(CONF_PASSWORD): str,
- vol.Required(CONF_PORT, default=reauth_entry.data[CONF_PORT]): int,
+ vol.Required(CONF_PORT, default=config_entry.data[CONF_PORT]): int,
vol.Required(
- CONF_VERIFY_SSL, default=reauth_entry.data[CONF_VERIFY_SSL]
+ CONF_VERIFY_SSL, default=config_entry.data[CONF_VERIFY_SSL]
): bool,
}
@@ -249,6 +253,7 @@ class UnifiOptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: UnifiConfigEntry) -> None:
"""Initialize UniFi Network options flow."""
+ self.config_entry = config_entry
self.options = dict(config_entry.options)
async def async_step_init(
diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json
index 1c7317c4267..ba426c2f08a 100644
--- a/homeassistant/components/unifi/strings.json
+++ b/homeassistant/components/unifi/strings.json
@@ -2,11 +2,6 @@
"config": {
"flow_title": "{site} ({host})",
"step": {
- "site": {
- "data": {
- "site": "Site ID"
- }
- },
"user": {
"title": "Set up UniFi Network",
"data": {
diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py
index a40939be917..62c35d00171 100644
--- a/homeassistant/components/unifiprotect/camera.py
+++ b/homeassistant/components/unifiprotect/camera.py
@@ -156,8 +156,7 @@ async def async_setup_entry(
async_add_entities(_async_camera_entities(hass, entry, data))
-_DISABLE_FEATURE = CameraEntityFeature(0)
-_ENABLE_FEATURE = CameraEntityFeature.STREAM
+_EMPTY_CAMERA_FEATURES = CameraEntityFeature(0)
class ProtectCamera(ProtectDeviceEntity, Camera):
@@ -196,22 +195,24 @@ class ProtectCamera(ProtectDeviceEntity, Camera):
self._attr_name = f"{camera_name} (insecure)"
# only the default (first) channel is enabled by default
self._attr_entity_registry_enabled_default = is_default and secure
- # Set the stream source before finishing the init
- # because async_added_to_hass is too late and camera
- # integration uses async_internal_added_to_hass to access
- # the stream source which is called before async_added_to_hass
- self._async_set_stream_source()
@callback
def _async_set_stream_source(self) -> None:
+ disable_stream = self._disable_stream
channel = self.channel
- enable_stream = not self._disable_stream and channel.is_rtsp_enabled
- # SRTP disabled because go2rtc does not support it
- # https://github.com/AlexxIT/go2rtc/#source-rtsp
- rtsp_url = channel.rtsps_no_srtp_url if self._secure else channel.rtsp_url
- source = rtsp_url if enable_stream else None
- self._attr_supported_features = _ENABLE_FEATURE if source else _DISABLE_FEATURE
- self._stream_source = source
+
+ if not channel.is_rtsp_enabled:
+ disable_stream = False
+
+ rtsp_url = channel.rtsps_url if self._secure else channel.rtsp_url
+
+ # _async_set_stream_source called by __init__
+ # pylint: disable-next=attribute-defined-outside-init
+ self._stream_source = None if disable_stream else rtsp_url
+ if self._stream_source:
+ self._attr_supported_features = CameraEntityFeature.STREAM
+ else:
+ self._attr_supported_features = _EMPTY_CAMERA_FEATURES
@callback
def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None:
diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py
index 31950f8f7e4..284b7003485 100644
--- a/homeassistant/components/unifiprotect/config_flow.py
+++ b/homeassistant/components/unifiprotect/config_flow.py
@@ -104,6 +104,7 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Init the config flow."""
super().__init__()
+ self.entry: ConfigEntry | None = None
self._discovered_device: dict[str, str] = {}
async def async_step_dhcp(
@@ -225,7 +226,7 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Get the options flow for this handler."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
@callback
def _async_create_entry(self, title: str, data: dict[str, Any]) -> ConfigFlowResult:
@@ -294,6 +295,8 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -301,21 +304,21 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm reauth."""
errors: dict[str, str] = {}
+ assert self.entry is not None
# prepopulate fields
- reauth_entry = self._get_reauth_entry()
- form_data = {**reauth_entry.data}
+ form_data = {**self.entry.data}
if user_input is not None:
form_data.update(user_input)
# validate login data
_, errors = await self._async_get_nvr_data(form_data)
if not errors:
- return self.async_update_reload_and_abort(reauth_entry, data=form_data)
+ return self.async_update_reload_and_abort(self.entry, data=form_data)
self.context["title_placeholders"] = {
- "name": reauth_entry.title,
- "ip_address": reauth_entry.data[CONF_HOST],
+ "name": self.entry.title,
+ "ip_address": self.entry.data[CONF_HOST],
}
return self.async_show_form(
step_id="reauth_confirm",
@@ -376,6 +379,10 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(OptionsFlow):
"""Handle options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json
index 85867b5c87c..ae7b2d94f21 100644
--- a/homeassistant/components/unifiprotect/manifest.json
+++ b/homeassistant/components/unifiprotect/manifest.json
@@ -40,7 +40,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"],
- "requirements": ["uiprotect==6.4.0", "unifi-discovery==1.2.0"],
+ "requirements": ["uiprotect==6.3.1", "unifi-discovery==1.2.0"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",
diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json
index 9238c825390..aaef111a351 100644
--- a/homeassistant/components/unifiprotect/strings.json
+++ b/homeassistant/components/unifiprotect/strings.json
@@ -42,8 +42,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "discovery_started": "Discovery started",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ "discovery_started": "Discovery started"
}
},
"options": {
diff --git a/homeassistant/components/upcloud/config_flow.py b/homeassistant/components/upcloud/config_flow.py
index bb988726ba5..20860df5553 100644
--- a/homeassistant/components/upcloud/config_flow.py
+++ b/homeassistant/components/upcloud/config_flow.py
@@ -95,12 +95,16 @@ class UpCloudConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> UpCloudOptionsFlow:
"""Get options flow."""
- return UpCloudOptionsFlow()
+ return UpCloudOptionsFlow(config_entry)
class UpCloudOptionsFlow(OptionsFlow):
"""UpCloud options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json
index 38581d31709..cd829f6dd9d 100644
--- a/homeassistant/components/upcloud/manifest.json
+++ b/homeassistant/components/upcloud/manifest.json
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/upcloud",
"iot_class": "cloud_polling",
- "requirements": ["upcloud-api==2.6.0"]
+ "requirements": ["upcloud-api==2.5.1"]
}
diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py
index 6f0b56b14e8..82f2792afa3 100644
--- a/homeassistant/components/update/__init__.py
+++ b/homeassistant/components/update/__init__.py
@@ -27,7 +27,6 @@ from homeassistant.util.hass_dict import HassKey
from .const import (
ATTR_AUTO_UPDATE,
ATTR_BACKUP,
- ATTR_DISPLAY_PRECISION,
ATTR_IN_PROGRESS,
ATTR_INSTALLED_VERSION,
ATTR_LATEST_VERSION,
@@ -35,7 +34,6 @@ from .const import (
ATTR_RELEASE_URL,
ATTR_SKIPPED_VERSION,
ATTR_TITLE,
- ATTR_UPDATE_PERCENTAGE,
ATTR_VERSION,
DOMAIN,
SERVICE_INSTALL,
@@ -179,7 +177,6 @@ class UpdateEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes update entities."""
device_class: UpdateDeviceClass | None = None
- display_precision: int = 0
entity_category: EntityCategory | None = EntityCategory.CONFIG
@@ -193,14 +190,12 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
"auto_update",
"installed_version",
"device_class",
- "display_precision",
"in_progress",
"latest_version",
"release_summary",
"release_url",
"supported_features",
"title",
- "update_percentage",
}
@@ -212,20 +207,13 @@ class UpdateEntity(
"""Representation of an update entity."""
_entity_component_unrecorded_attributes = frozenset(
- {
- ATTR_DISPLAY_PRECISION,
- ATTR_ENTITY_PICTURE,
- ATTR_IN_PROGRESS,
- ATTR_RELEASE_SUMMARY,
- ATTR_UPDATE_PERCENTAGE,
- }
+ {ATTR_ENTITY_PICTURE, ATTR_IN_PROGRESS, ATTR_RELEASE_SUMMARY}
)
entity_description: UpdateEntityDescription
_attr_auto_update: bool = False
_attr_installed_version: str | None = None
_attr_device_class: UpdateDeviceClass | None
- _attr_display_precision: int
_attr_in_progress: bool | int = False
_attr_latest_version: str | None = None
_attr_release_summary: str | None = None
@@ -233,7 +221,6 @@ class UpdateEntity(
_attr_state: None = None
_attr_supported_features: UpdateEntityFeature = UpdateEntityFeature(0)
_attr_title: str | None = None
- _attr_update_percentage: int | float | None = None
__skipped_version: str | None = None
__in_progress: bool = False
@@ -263,15 +250,6 @@ class UpdateEntity(
return self.entity_description.device_class
return None
- @cached_property
- def display_precision(self) -> int:
- """Return number of decimal digits for display of update progress."""
- if hasattr(self, "_attr_display_precision"):
- return self._attr_display_precision
- if hasattr(self, "entity_description"):
- return self.entity_description.display_precision
- return 0
-
@property
def entity_category(self) -> EntityCategory | None:
"""Return the category of the entity, if any."""
@@ -300,7 +278,8 @@ class UpdateEntity(
Needs UpdateEntityFeature.PROGRESS flag to be set for it to be used.
- Should return a boolean (True if in progress, False if not).
+ Can either return a boolean (True if in progress, False if not)
+ or an integer to indicate the progress in from 0 to 100%.
"""
return self._attr_in_progress
@@ -350,16 +329,6 @@ class UpdateEntity(
return new_features
return features
- @cached_property
- def update_percentage(self) -> int | float | None:
- """Update installation progress.
-
- Needs UpdateEntityFeature.PROGRESS flag to be set for it to be used.
-
- Can either return a number to indicate the progress from 0 to 100% or None.
- """
- return self._attr_update_percentage
-
@final
async def async_skip(self) -> None:
"""Skip the current offered version to update."""
@@ -453,13 +422,8 @@ class UpdateEntity(
# Otherwise, we use the internal progress value.
if UpdateEntityFeature.PROGRESS in self.supported_features_compat:
in_progress = self.in_progress
- update_percentage = self.update_percentage if in_progress else None
- if type(in_progress) is not bool and isinstance(in_progress, int):
- update_percentage = in_progress
- in_progress = True
else:
in_progress = self.__in_progress
- update_percentage = None
installed_version = self.installed_version
latest_version = self.latest_version
@@ -474,7 +438,6 @@ class UpdateEntity(
return {
ATTR_AUTO_UPDATE: self.auto_update,
- ATTR_DISPLAY_PRECISION: self.display_precision,
ATTR_INSTALLED_VERSION: installed_version,
ATTR_IN_PROGRESS: in_progress,
ATTR_LATEST_VERSION: latest_version,
@@ -482,7 +445,6 @@ class UpdateEntity(
ATTR_RELEASE_URL: self.release_url,
ATTR_SKIPPED_VERSION: skipped_version,
ATTR_TITLE: self.title,
- ATTR_UPDATE_PERCENTAGE: update_percentage,
}
@final
diff --git a/homeassistant/components/update/const.py b/homeassistant/components/update/const.py
index 83a74ef6789..0d7da94f656 100644
--- a/homeassistant/components/update/const.py
+++ b/homeassistant/components/update/const.py
@@ -23,7 +23,6 @@ SERVICE_SKIP: Final = "skip"
ATTR_AUTO_UPDATE: Final = "auto_update"
ATTR_BACKUP: Final = "backup"
-ATTR_DISPLAY_PRECISION: Final = "display_precision"
ATTR_INSTALLED_VERSION: Final = "installed_version"
ATTR_IN_PROGRESS: Final = "in_progress"
ATTR_LATEST_VERSION: Final = "latest_version"
@@ -31,5 +30,4 @@ ATTR_RELEASE_SUMMARY: Final = "release_summary"
ATTR_RELEASE_URL: Final = "release_url"
ATTR_SKIPPED_VERSION: Final = "skipped_version"
ATTR_TITLE: Final = "title"
-ATTR_UPDATE_PERCENTAGE: Final = "update_percentage"
ATTR_VERSION: Final = "version"
diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py
index 41e481fa58c..1a40d4b3442 100644
--- a/homeassistant/components/upnp/config_flow.py
+++ b/homeassistant/components/upnp/config_flow.py
@@ -16,6 +16,7 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.core import HomeAssistant, callback
@@ -93,11 +94,9 @@ class UpnpFlowHandler(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> UpnpOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
- return UpnpOptionsFlowHandler()
+ return UpnpOptionsFlowHandler(config_entry)
@property
def _discoveries(self) -> dict[str, SsdpServiceInfo]:
@@ -300,7 +299,7 @@ class UpnpFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=title, data=data, options=options)
-class UpnpOptionsFlowHandler(OptionsFlow):
+class UpnpOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle an options flow."""
async def async_step_init(
@@ -314,7 +313,7 @@ class UpnpOptionsFlowHandler(OptionsFlow):
{
vol.Optional(
CONFIG_ENTRY_FORCE_POLL,
- default=self.config_entry.options.get(
+ default=self.options.get(
CONFIG_ENTRY_FORCE_POLL, DEFAULT_CONFIG_ENTRY_FORCE_POLL
),
): bool,
diff --git a/homeassistant/components/utility_meter/manifest.json b/homeassistant/components/utility_meter/manifest.json
index 31a2d4e9584..25e803e6a2d 100644
--- a/homeassistant/components/utility_meter/manifest.json
+++ b/homeassistant/components/utility_meter/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["croniter"],
"quality_scale": "internal",
- "requirements": ["cronsim==2.6"]
+ "requirements": ["croniter==2.0.2"]
}
diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py
index 5815ce7ec95..d5b1206d046 100644
--- a/homeassistant/components/utility_meter/select.py
+++ b/homeassistant/components/utility_meter/select.py
@@ -6,7 +6,7 @@ import logging
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID
+from homeassistant.const import CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import async_device_info_to_link_from_entity
from homeassistant.helpers.device_registry import DeviceInfo
@@ -36,9 +36,9 @@ async def async_setup_entry(
)
tariff_select = TariffSelect(
- name=name,
- tariffs=tariffs,
- unique_id=unique_id,
+ name,
+ tariffs,
+ unique_id,
device_info=device_info,
)
async_add_entities([tariff_select])
@@ -62,15 +62,13 @@ async def async_setup_platform(
conf_meter_unique_id: str | None = hass.data[DATA_UTILITY][meter].get(
CONF_UNIQUE_ID
)
- conf_meter_name = hass.data[DATA_UTILITY][meter].get(CONF_NAME, meter)
async_add_entities(
[
TariffSelect(
- name=conf_meter_name,
- tariffs=discovery_info[CONF_TARIFFS],
- yaml_slug=meter,
- unique_id=conf_meter_unique_id,
+ meter,
+ discovery_info[CONF_TARIFFS],
+ conf_meter_unique_id,
)
]
)
@@ -84,16 +82,12 @@ class TariffSelect(SelectEntity, RestoreEntity):
def __init__(
self,
name,
- tariffs: list[str],
- *,
- yaml_slug: str | None = None,
- unique_id: str | None = None,
+ tariffs,
+ unique_id,
device_info: DeviceInfo | None = None,
) -> None:
"""Initialize a tariff selector."""
self._attr_name = name
- if yaml_slug: # Backwards compatibility with YAML configuration entries
- self.entity_id = f"select.{yaml_slug}"
self._attr_unique_id = unique_id
self._attr_device_info = device_info
self._current_tariff: str | None = None
diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py
index 19ef3c1f3a8..6b8c07c7ef7 100644
--- a/homeassistant/components/utility_meter/sensor.py
+++ b/homeassistant/components/utility_meter/sensor.py
@@ -9,7 +9,7 @@ from decimal import Decimal, DecimalException, InvalidOperation
import logging
from typing import Any, Self
-from cronsim import CronSim
+from croniter import croniter
import voluptuous as vol
from homeassistant.components.sensor import (
@@ -379,13 +379,14 @@ class UtilityMeterSensor(RestoreSensor):
self.entity_id = suggested_entity_id
self._parent_meter = parent_meter
self._sensor_source_id = source_entity
+ self._state = None
self._last_period = Decimal(0)
self._last_reset = dt_util.utcnow()
self._last_valid_state = None
self._collecting = None
- self._attr_name = name
+ self._name = name
self._input_device_class = None
- self._attr_native_unit_of_measurement = None
+ self._unit_of_measurement = None
self._period = meter_type
if meter_type is not None:
# For backwards compatibility reasons we convert the period and offset into a cron pattern
@@ -404,22 +405,12 @@ class UtilityMeterSensor(RestoreSensor):
self._tariff = tariff
self._tariff_entity = tariff_entity
self._next_reset = None
- self.scheduler = (
- CronSim(
- self._cron_pattern,
- dt_util.now(
- dt_util.get_default_time_zone()
- ), # we need timezone for DST purposes (see issue #102984)
- )
- if self._cron_pattern
- else None
- )
def start(self, attributes: Mapping[str, Any]) -> None:
"""Initialize unit and state upon source initial update."""
self._input_device_class = attributes.get(ATTR_DEVICE_CLASS)
- self._attr_native_unit_of_measurement = attributes.get(ATTR_UNIT_OF_MEASUREMENT)
- self._attr_native_value = 0
+ self._unit_of_measurement = attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ self._state = 0
self.async_write_ha_state()
@staticmethod
@@ -494,13 +485,13 @@ class UtilityMeterSensor(RestoreSensor):
)
return
- if self.native_value is None:
+ if self._state is None:
# First state update initializes the utility_meter sensors
for sensor in self.hass.data[DATA_UTILITY][self._parent_meter][
DATA_TARIFF_SENSORS
]:
sensor.start(new_state_attributes)
- if self.native_unit_of_measurement is None:
+ if self._unit_of_measurement is None:
_LOGGER.warning(
"Source sensor %s has no unit of measurement. Please %s",
self._sensor_source_id,
@@ -511,12 +502,10 @@ class UtilityMeterSensor(RestoreSensor):
adjustment := self.calculate_adjustment(old_state, new_state)
) is not None and (self._sensor_net_consumption or adjustment >= 0):
# If net_consumption is off, the adjustment must be non-negative
- self._attr_native_value += adjustment # type: ignore[operator] # self._attr_native_value will be set to by the start function if it is None, therefore it always has a valid Decimal value at this line
+ self._state += adjustment # type: ignore[operator] # self._state will be set to by the start function if it is None, therefore it always has a valid Decimal value at this line
self._input_device_class = new_state_attributes.get(ATTR_DEVICE_CLASS)
- self._attr_native_unit_of_measurement = new_state_attributes.get(
- ATTR_UNIT_OF_MEASUREMENT
- )
+ self._unit_of_measurement = new_state_attributes.get(ATTR_UNIT_OF_MEASUREMENT)
self._last_valid_state = new_state_val
self.async_write_ha_state()
@@ -545,7 +534,7 @@ class UtilityMeterSensor(RestoreSensor):
_LOGGER.debug(
"%s - %s - source <%s>",
- self.name,
+ self._name,
COLLECTING if self._collecting is not None else PAUSED,
self._sensor_source_id,
)
@@ -554,10 +543,11 @@ class UtilityMeterSensor(RestoreSensor):
async def _program_reset(self):
"""Program the reset of the utility meter."""
- if self.scheduler:
- self._next_reset = next(self.scheduler)
-
- _LOGGER.debug("Next reset of %s is %s", self.entity_id, self._next_reset)
+ if self._cron_pattern is not None:
+ tz = dt_util.get_default_time_zone()
+ self._next_reset = croniter(self._cron_pattern, dt_util.now(tz)).get_next(
+ datetime
+ ) # we need timezone for DST purposes (see issue #102984)
self.async_on_remove(
async_track_point_in_time(
self.hass,
@@ -585,16 +575,14 @@ class UtilityMeterSensor(RestoreSensor):
return
_LOGGER.debug("Reset utility meter <%s>", self.entity_id)
self._last_reset = dt_util.utcnow()
- self._last_period = (
- Decimal(self.native_value) if self.native_value else Decimal(0)
- )
- self._attr_native_value = 0
+ self._last_period = Decimal(self._state) if self._state else Decimal(0)
+ self._state = 0
self.async_write_ha_state()
async def async_calibrate(self, value):
"""Calibrate the Utility Meter with a given value."""
- _LOGGER.debug("Calibrate %s = %s type(%s)", self.name, value, type(value))
- self._attr_native_value = Decimal(str(value))
+ _LOGGER.debug("Calibrate %s = %s type(%s)", self._name, value, type(value))
+ self._state = Decimal(str(value))
self.async_write_ha_state()
async def async_added_to_hass(self):
@@ -610,11 +598,10 @@ class UtilityMeterSensor(RestoreSensor):
)
if (last_sensor_data := await self.async_get_last_sensor_data()) is not None:
- self._attr_native_value = last_sensor_data.native_value
+ # new introduced in 2022.04
+ self._state = last_sensor_data.native_value
self._input_device_class = last_sensor_data.input_device_class
- self._attr_native_unit_of_measurement = (
- last_sensor_data.native_unit_of_measurement
- )
+ self._unit_of_measurement = last_sensor_data.native_unit_of_measurement
self._last_period = last_sensor_data.last_period
self._last_reset = last_sensor_data.last_reset
self._last_valid_state = last_sensor_data.last_valid_state
@@ -622,6 +609,39 @@ class UtilityMeterSensor(RestoreSensor):
# Null lambda to allow cancelling the collection on tariff change
self._collecting = lambda: None
+ elif state := await self.async_get_last_state():
+ # legacy to be removed on 2022.10 (we are keeping this to avoid utility_meter counter losses)
+ try:
+ self._state = Decimal(state.state)
+ except InvalidOperation:
+ _LOGGER.error(
+ "Could not restore state <%s>. Resetting utility_meter.%s",
+ state.state,
+ self.name,
+ )
+ else:
+ self._unit_of_measurement = state.attributes.get(
+ ATTR_UNIT_OF_MEASUREMENT
+ )
+ self._last_period = (
+ Decimal(state.attributes[ATTR_LAST_PERIOD])
+ if state.attributes.get(ATTR_LAST_PERIOD)
+ and is_number(state.attributes[ATTR_LAST_PERIOD])
+ else Decimal(0)
+ )
+ self._last_valid_state = (
+ Decimal(state.attributes[ATTR_LAST_VALID_STATE])
+ if state.attributes.get(ATTR_LAST_VALID_STATE)
+ and is_number(state.attributes[ATTR_LAST_VALID_STATE])
+ else None
+ )
+ self._last_reset = dt_util.as_utc(
+ dt_util.parse_datetime(state.attributes.get(ATTR_LAST_RESET))
+ )
+ if state.attributes.get(ATTR_STATUS) == COLLECTING:
+ # Null lambda to allow cancelling the collection on tariff change
+ self._collecting = lambda: None
+
@callback
def async_source_tracking(event):
"""Wait for source to be ready, then start meter."""
@@ -646,7 +666,7 @@ class UtilityMeterSensor(RestoreSensor):
_LOGGER.debug(
"<%s> collecting %s from %s",
self.name,
- self.native_unit_of_measurement,
+ self._unit_of_measurement,
self._sensor_source_id,
)
self._collecting = async_track_state_change_event(
@@ -661,15 +681,22 @@ class UtilityMeterSensor(RestoreSensor):
self._collecting()
self._collecting = None
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def native_value(self):
+ """Return the state of the sensor."""
+ return self._state
+
@property
def device_class(self):
"""Return the device class of the sensor."""
if self._input_device_class is not None:
return self._input_device_class
- if (
- self.native_unit_of_measurement
- in DEVICE_CLASS_UNITS[SensorDeviceClass.ENERGY]
- ):
+ if self._unit_of_measurement in DEVICE_CLASS_UNITS[SensorDeviceClass.ENERGY]:
return SensorDeviceClass.ENERGY
return None
@@ -682,6 +709,11 @@ class UtilityMeterSensor(RestoreSensor):
else SensorStateClass.TOTAL_INCREASING
)
+ @property
+ def native_unit_of_measurement(self):
+ """Return the unit the value is expressed in."""
+ return self._unit_of_measurement
+
@property
def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
diff --git a/homeassistant/components/vallox/config_flow.py b/homeassistant/components/vallox/config_flow.py
index 30d1d153d9e..9a95952ed25 100644
--- a/homeassistant/components/vallox/config_flow.py
+++ b/homeassistant/components/vallox/config_flow.py
@@ -84,12 +84,18 @@ class ValloxConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Handle reconfiguration of the Vallox device host address."""
+ return await self.async_step_reconfigure_confirm()
+
+ async def async_step_reconfigure_confirm(
+ self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the Vallox device host address."""
reconfigure_entry = self._get_reconfigure_entry()
if not user_input:
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=self.add_suggested_values_to_schema(
CONFIG_SCHEMA, {CONF_HOST: reconfigure_entry.data.get(CONF_HOST)}
),
@@ -117,7 +123,7 @@ class ValloxConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
- step_id="reconfigure",
+ step_id="reconfigure_confirm",
data_schema=self.add_suggested_values_to_schema(
CONFIG_SCHEMA, {CONF_HOST: updated_host}
),
diff --git a/homeassistant/components/vallox/strings.json b/homeassistant/components/vallox/strings.json
index 8a30ed4ad01..608a5eb1782 100644
--- a/homeassistant/components/vallox/strings.json
+++ b/homeassistant/components/vallox/strings.json
@@ -9,7 +9,7 @@
"host": "Hostname or IP address of your Vallox device."
}
},
- "reconfigure": {
+ "reconfigure_confirm": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py
index ca8cfb0f2a7..685f8b49500 100644
--- a/homeassistant/components/velbus/__init__.py
+++ b/homeassistant/components/velbus/__init__.py
@@ -122,7 +122,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await (
hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"]
.get_module(call.data[CONF_ADDRESS])
- .set_memo_text(memo_text)
+ .set_memo_text(memo_text.async_render())
)
hass.services.async_register(
@@ -135,7 +135,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
vol.Required(CONF_ADDRESS): vol.All(
vol.Coerce(int), vol.Range(min=0, max=255)
),
- vol.Optional(CONF_MEMO_TEXT, default=""): cv.string,
+ vol.Optional(CONF_MEMO_TEXT, default=""): cv.template,
}
),
)
diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json
index 5443afeef77..c1cf2951bbd 100644
--- a/homeassistant/components/velbus/manifest.json
+++ b/homeassistant/components/velbus/manifest.json
@@ -13,7 +13,7 @@
"velbus-packet",
"velbus-protocol"
],
- "requirements": ["velbus-aio==2024.10.0"],
+ "requirements": ["velbus-aio==2024.7.6"],
"usb": [
{
"vid": "10CF",
diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py
index f2b182cc270..08e7640773b 100644
--- a/homeassistant/components/vera/config_flow.py
+++ b/homeassistant/components/vera/config_flow.py
@@ -76,6 +76,10 @@ def options_data(user_input: dict[str, str]) -> dict[str, list[int]]:
class OptionsFlowHandler(OptionsFlow):
"""Options for the component."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Init object."""
+ self.config_entry = config_entry
+
async def async_step_init(
self,
user_input: dict[str, str] | None = None,
@@ -100,7 +104,7 @@ class VeraFlowHandler(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler:
"""Get the options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py
index 5f34b587163..fc7e7551145 100644
--- a/homeassistant/components/verisure/alarm_control_panel.py
+++ b/homeassistant/components/verisure/alarm_control_panel.py
@@ -7,10 +7,10 @@ import asyncio
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import STATE_ALARM_ARMING, STATE_ALARM_DISARMING
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -86,7 +86,7 @@ class VerisureAlarm(
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
- self._attr_alarm_state = AlarmControlPanelState.DISARMING
+ self._attr_state = STATE_ALARM_DISARMING
self.async_write_ha_state()
await self._async_set_arm_state(
"DISARMED", self.coordinator.verisure.disarm(code)
@@ -94,7 +94,7 @@ class VerisureAlarm(
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
- self._attr_alarm_state = AlarmControlPanelState.ARMING
+ self._attr_state = STATE_ALARM_ARMING
self.async_write_ha_state()
await self._async_set_arm_state(
"ARMED_HOME", self.coordinator.verisure.arm_home(code)
@@ -102,7 +102,7 @@ class VerisureAlarm(
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
- self._attr_alarm_state = AlarmControlPanelState.ARMING
+ self._attr_state = STATE_ALARM_ARMING
self.async_write_ha_state()
await self._async_set_arm_state(
"ARMED_AWAY", self.coordinator.verisure.arm_away(code)
@@ -111,7 +111,7 @@ class VerisureAlarm(
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
- self._attr_alarm_state = ALARM_STATE_TO_HA.get(
+ self._attr_state = ALARM_STATE_TO_HA.get(
self.coordinator.data["alarm"]["statusType"]
)
self._attr_changed_by = self.coordinator.data["alarm"].get("name")
diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py
index 0f1088ccb80..ccf74cd6791 100644
--- a/homeassistant/components/verisure/config_flow.py
+++ b/homeassistant/components/verisure/config_flow.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from collections.abc import Mapping
-from typing import Any
+from typing import Any, cast
from verisure import (
Error as VerisureError,
@@ -38,16 +38,15 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 2
email: str
+ entry: ConfigEntry
password: str
verisure: Verisure
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> VerisureOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> VerisureOptionsFlowHandler:
"""Get the options flow for this handler."""
- return VerisureOptionsFlowHandler()
+ return VerisureOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -180,6 +179,10 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle initiation of re-authentication with Verisure."""
+ self.entry = cast(
+ ConfigEntry,
+ self.hass.config_entries.async_get_entry(self.context["entry_id"]),
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -227,21 +230,25 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
LOGGER.debug("Unexpected response from Verisure, %s", ex)
errors["base"] = "unknown"
else:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
- data_updates={
+ data = self.entry.data.copy()
+ self.hass.config_entries.async_update_entry(
+ self.entry,
+ data={
+ **data,
CONF_EMAIL: user_input[CONF_EMAIL],
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
- vol.Required(
- CONF_EMAIL, default=self._get_reauth_entry().data[CONF_EMAIL]
- ): str,
+ vol.Required(CONF_EMAIL, default=self.entry.data[CONF_EMAIL]): str,
vol.Required(CONF_PASSWORD): str,
}
),
@@ -267,13 +274,18 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
LOGGER.debug("Unexpected response from Verisure, %s", ex)
errors["base"] = "unknown"
else:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
- data_updates={
+ self.hass.config_entries.async_update_entry(
+ self.entry,
+ data={
+ **self.entry.data,
CONF_EMAIL: self.email,
CONF_PASSWORD: self.password,
},
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_mfa",
@@ -292,6 +304,10 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN):
class VerisureOptionsFlowHandler(OptionsFlow):
"""Handle Verisure options."""
+ def __init__(self, entry: ConfigEntry) -> None:
+ """Initialize Verisure options flow."""
+ self.entry = entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -308,7 +324,7 @@ class VerisureOptionsFlowHandler(OptionsFlow):
vol.Optional(
CONF_LOCK_CODE_DIGITS,
description={
- "suggested_value": self.config_entry.options.get(
+ "suggested_value": self.entry.options.get(
CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS
)
},
diff --git a/homeassistant/components/verisure/const.py b/homeassistant/components/verisure/const.py
index 4afb93d957f..5b1aa1a0740 100644
--- a/homeassistant/components/verisure/const.py
+++ b/homeassistant/components/verisure/const.py
@@ -3,7 +3,12 @@
from datetime import timedelta
import logging
-from homeassistant.components.alarm_control_panel import AlarmControlPanelState
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+)
DOMAIN = "verisure"
@@ -38,8 +43,8 @@ DEVICE_TYPE_NAME = {
}
ALARM_STATE_TO_HA = {
- "DISARMED": AlarmControlPanelState.DISARMED,
- "ARMED_HOME": AlarmControlPanelState.ARMED_HOME,
- "ARMED_AWAY": AlarmControlPanelState.ARMED_AWAY,
- "PENDING": AlarmControlPanelState.PENDING,
+ "DISARMED": STATE_ALARM_DISARMED,
+ "ARMED_HOME": STATE_ALARM_ARMED_HOME,
+ "ARMED_AWAY": STATE_ALARM_ARMED_AWAY,
+ "PENDING": STATE_ALARM_PENDING,
}
diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py
index 48215819ce5..50dce95e42a 100644
--- a/homeassistant/components/vesync/const.py
+++ b/homeassistant/components/vesync/const.py
@@ -56,7 +56,6 @@ SKU_TO_BASE_DEVICE = {
"LAP-V201S-WEU": "Vital200S", # Alt ID Model Vital200S
"LAP-V201S-WUS": "Vital200S", # Alt ID Model Vital200S
"LAP-V201-AUSR": "Vital200S", # Alt ID Model Vital200S
- "LAP-V201S-AUSR": "Vital200S", # Alt ID Model Vital200S
"Vital100S": "Vital100S",
"LAP-V102S-WUS": "Vital100S", # Alt ID Model Vital100S
"LAP-V102S-AASR": "Vital100S", # Alt ID Model Vital100S
diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py
index 098a17e90f0..58a262e769f 100644
--- a/homeassistant/components/vesync/fan.py
+++ b/homeassistant/components/vesync/fan.py
@@ -94,7 +94,6 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
| FanEntityFeature.TURN_ON
)
_attr_name = None
- _attr_translation_key = "vesync"
_enable_turn_on_off_backwards_compatibility = False
def __init__(self, fan) -> None:
diff --git a/homeassistant/components/vesync/icons.json b/homeassistant/components/vesync/icons.json
index e4769acc9a5..cfdefb2ed09 100644
--- a/homeassistant/components/vesync/icons.json
+++ b/homeassistant/components/vesync/icons.json
@@ -1,20 +1,4 @@
{
- "entity": {
- "fan": {
- "vesync": {
- "state_attributes": {
- "preset_mode": {
- "state": {
- "auto": "mdi:fan-auto",
- "sleep": "mdi:sleep",
- "pet": "mdi:paw",
- "turbo": "mdi:weather-tornado"
- }
- }
- }
- }
- }
- },
"services": {
"update_devices": {
"service": "mdi:update"
diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json
index b6e4e2fd957..5ff0aa58722 100644
--- a/homeassistant/components/vesync/strings.json
+++ b/homeassistant/components/vesync/strings.json
@@ -42,20 +42,6 @@
"current_voltage": {
"name": "Current voltage"
}
- },
- "fan": {
- "vesync": {
- "state_attributes": {
- "preset_mode": {
- "state": {
- "auto": "Auto",
- "sleep": "Sleep",
- "pet": "Pet",
- "turbo": "Turbo"
- }
- }
- }
- }
}
},
"services": {
diff --git a/homeassistant/components/vicare/config_flow.py b/homeassistant/components/vicare/config_flow.py
index c711cc06074..67ce4f2c186 100644
--- a/homeassistant/components/vicare/config_flow.py
+++ b/homeassistant/components/vicare/config_flow.py
@@ -13,7 +13,7 @@ from PyViCare.PyViCareUtils import (
import voluptuous as vol
from homeassistant.components import dhcp
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import format_mac
@@ -50,6 +50,7 @@ class ViCareConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for ViCare."""
VERSION = 1
+ entry: ConfigEntry | None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -80,6 +81,7 @@ class ViCareConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication with ViCare."""
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -87,11 +89,11 @@ class ViCareConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm re-authentication with ViCare."""
errors: dict[str, str] = {}
+ assert self.entry is not None
- reauth_entry = self._get_reauth_entry()
if user_input:
data = {
- **reauth_entry.data,
+ **self.entry.data,
**user_input,
}
@@ -100,12 +102,17 @@ class ViCareConfigFlow(ConfigFlow, domain=DOMAIN):
except (PyViCareInvalidConfigurationError, PyViCareInvalidCredentialsError):
errors["base"] = "invalid_auth"
else:
- return self.async_update_reload_and_abort(reauth_entry, data=data)
+ self.hass.config_entries.async_update_entry(
+ self.entry,
+ data=data,
+ )
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(
- REAUTH_SCHEMA, reauth_entry.data
+ REAUTH_SCHEMA, self.entry.data
),
errors=errors,
)
diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json
index 8ce996ab81d..869a1ef80d8 100644
--- a/homeassistant/components/vicare/manifest.json
+++ b/homeassistant/components/vicare/manifest.json
@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/vicare",
"iot_class": "cloud_polling",
"loggers": ["PyViCare"],
- "requirements": ["PyViCare==2.35.0"]
+ "requirements": ["PyViCare==2.34.0"]
}
diff --git a/homeassistant/components/vicare/number.py b/homeassistant/components/vicare/number.py
index f9af9636941..529caca6a87 100644
--- a/homeassistant/components/vicare/number.py
+++ b/homeassistant/components/vicare/number.py
@@ -265,72 +265,6 @@ CIRCUIT_ENTITY_DESCRIPTIONS: tuple[ViCareNumberEntityDescription, ...] = (
HeatingProgram.COMFORT_HEATING
),
),
- ViCareNumberEntityDescription(
- key="normal_cooling_temperature",
- translation_key="normal_cooling_temperature",
- entity_category=EntityCategory.CONFIG,
- device_class=NumberDeviceClass.TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- value_getter=lambda api: api.getDesiredTemperatureForProgram(
- HeatingProgram.NORMAL_COOLING
- ),
- value_setter=lambda api, value: api.setProgramTemperature(
- HeatingProgram.NORMAL_COOLING, value
- ),
- min_value_getter=lambda api: api.getProgramMinTemperature(
- HeatingProgram.NORMAL_COOLING
- ),
- max_value_getter=lambda api: api.getProgramMaxTemperature(
- HeatingProgram.NORMAL_COOLING
- ),
- stepping_getter=lambda api: api.getProgramStepping(
- HeatingProgram.NORMAL_COOLING
- ),
- ),
- ViCareNumberEntityDescription(
- key="reduced_cooling_temperature",
- translation_key="reduced_cooling_temperature",
- entity_category=EntityCategory.CONFIG,
- device_class=NumberDeviceClass.TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- value_getter=lambda api: api.getDesiredTemperatureForProgram(
- HeatingProgram.REDUCED_COOLING
- ),
- value_setter=lambda api, value: api.setProgramTemperature(
- HeatingProgram.REDUCED_COOLING, value
- ),
- min_value_getter=lambda api: api.getProgramMinTemperature(
- HeatingProgram.REDUCED_COOLING
- ),
- max_value_getter=lambda api: api.getProgramMaxTemperature(
- HeatingProgram.REDUCED_COOLING
- ),
- stepping_getter=lambda api: api.getProgramStepping(
- HeatingProgram.REDUCED_COOLING
- ),
- ),
- ViCareNumberEntityDescription(
- key="comfort_cooling_temperature",
- translation_key="comfort_cooling_temperature",
- entity_category=EntityCategory.CONFIG,
- device_class=NumberDeviceClass.TEMPERATURE,
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- value_getter=lambda api: api.getDesiredTemperatureForProgram(
- HeatingProgram.COMFORT_COOLING
- ),
- value_setter=lambda api, value: api.setProgramTemperature(
- HeatingProgram.COMFORT_COOLING, value
- ),
- min_value_getter=lambda api: api.getProgramMinTemperature(
- HeatingProgram.COMFORT_COOLING
- ),
- max_value_getter=lambda api: api.getProgramMaxTemperature(
- HeatingProgram.COMFORT_COOLING
- ),
- stepping_getter=lambda api: api.getProgramStepping(
- HeatingProgram.COMFORT_COOLING
- ),
- ),
)
diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py
index 57b7c0bec9a..bedb161edcb 100644
--- a/homeassistant/components/vicare/sensor.py
+++ b/homeassistant/components/vicare/sensor.py
@@ -430,32 +430,6 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
),
- ViCareSensorEntityDescription(
- key="energy_consumption_cooling_today",
- translation_key="energy_consumption_cooling_today",
- native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
- value_getter=lambda api: api.getPowerConsumptionCoolingToday(),
- unit_getter=lambda api: api.getPowerConsumptionCoolingUnit(),
- state_class=SensorStateClass.TOTAL_INCREASING,
- ),
- ViCareSensorEntityDescription(
- key="energy_consumption_cooling_this_month",
- translation_key="energy_consumption_cooling_this_month",
- native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
- value_getter=lambda api: api.getPowerConsumptionCoolingThisMonth(),
- unit_getter=lambda api: api.getPowerConsumptionCoolingUnit(),
- state_class=SensorStateClass.TOTAL_INCREASING,
- entity_registry_enabled_default=False,
- ),
- ViCareSensorEntityDescription(
- key="energy_consumption_cooling_this_year",
- translation_key="energy_consumption_cooling_this_year",
- native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
- value_getter=lambda api: api.getPowerConsumptionCoolingThisYear(),
- unit_getter=lambda api: api.getPowerConsumptionCoolingUnit(),
- state_class=SensorStateClass.TOTAL_INCREASING,
- entity_registry_enabled_default=False,
- ),
ViCareSensorEntityDescription(
key="energy_dhw_summary_consumption_heating_currentday",
translation_key="energy_dhw_summary_consumption_heating_currentday",
diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json
index 77e570da779..15637a75b83 100644
--- a/homeassistant/components/vicare/strings.json
+++ b/homeassistant/components/vicare/strings.json
@@ -97,22 +97,13 @@
"name": "Comfort temperature"
},
"normal_heating_temperature": {
- "name": "Normal heating temperature"
+ "name": "[%key:component::vicare::entity::number::normal_temperature::name%]"
},
"reduced_heating_temperature": {
- "name": "Reduced heating temperature"
+ "name": "[%key:component::vicare::entity::number::reduced_temperature::name%]"
},
"comfort_heating_temperature": {
- "name": "Comfort heating temperature"
- },
- "normal_cooling_temperature": {
- "name": "Normal cooling temperature"
- },
- "reduced_cooling_temperature": {
- "name": "Reduced cooling temperature"
- },
- "comfort_cooling_temperature": {
- "name": "Comfort cooling temperature"
+ "name": "[%key:component::vicare::entity::number::comfort_temperature::name%]"
},
"dhw_temperature": {
"name": "DHW temperature"
@@ -243,49 +234,28 @@
"name": "DHW gas consumption last seven days"
},
"energy_summary_consumption_heating_currentday": {
- "name": "Heating electricity consumption today"
+ "name": "Heating energy consumption today"
},
"energy_summary_consumption_heating_currentmonth": {
- "name": "Heating electricity consumption this month"
+ "name": "Heating energy consumption this month"
},
"energy_summary_consumption_heating_currentyear": {
- "name": "Heating electricity consumption this year"
+ "name": "Heating energy consumption this year"
},
"energy_summary_consumption_heating_lastsevendays": {
- "name": "Heating electricity consumption last seven days"
- },
- "energy_consumption_cooling_today": {
- "name": "Cooling electricity consumption today"
- },
- "energy_consumption_cooling_this_month": {
- "name": "Cooling electricity consumption this month"
- },
- "energy_consumption_cooling_this_year": {
- "name": "Cooling electricity consumption this year"
+ "name": "Heating energy consumption last seven days"
},
"energy_dhw_summary_consumption_heating_currentday": {
- "name": "DHW electricity consumption today"
+ "name": "DHW energy consumption today"
},
"energy_dhw_summary_consumption_heating_currentmonth": {
- "name": "DHW electricity consumption this month"
+ "name": "DHW energy consumption this month"
},
"energy_dhw_summary_consumption_heating_currentyear": {
- "name": "DHW electricity consumption this year"
+ "name": "DHW energy consumption this year"
},
"energy_summary_dhw_consumption_heating_lastsevendays": {
- "name": "DHW electricity consumption last seven days"
- },
- "power_consumption_today": {
- "name": "Electricity consumption today"
- },
- "power_consumption_this_week": {
- "name": "Electricity consumption this week"
- },
- "power_consumption_this_month": {
- "name": "Electricity consumption this month"
- },
- "power_consumption_this_year": {
- "name": "Electricity consumption this year"
+ "name": "DHW energy consumption last seven days"
},
"power_production_current": {
"name": "Power production current"
@@ -320,6 +290,18 @@
"solar_power_production_this_year": {
"name": "Solar energy production this year"
},
+ "power_consumption_today": {
+ "name": "Energy consumption today"
+ },
+ "power_consumption_this_week": {
+ "name": "Power consumption this week"
+ },
+ "power_consumption_this_month": {
+ "name": "Energy consumption this month"
+ },
+ "power_consumption_this_year": {
+ "name": "Energy consumption this year"
+ },
"buffer_top_temperature": {
"name": "Buffer top temperature"
},
diff --git a/homeassistant/components/vicare/types.py b/homeassistant/components/vicare/types.py
index 98d1c0566ce..7e1ec7f8bee 100644
--- a/homeassistant/components/vicare/types.py
+++ b/homeassistant/components/vicare/types.py
@@ -1,7 +1,6 @@
"""Types for the ViCare integration."""
from collections.abc import Callable
-from contextlib import suppress
from dataclasses import dataclass
import enum
from typing import Any
@@ -25,14 +24,11 @@ class HeatingProgram(enum.StrEnum):
COMFORT = "comfort"
COMFORT_HEATING = "comfortHeating"
- COMFORT_COOLING = "comfortCooling"
ECO = "eco"
NORMAL = "normal"
NORMAL_HEATING = "normalHeating"
- NORMAL_COOLING = "normalCooling"
REDUCED = "reduced"
REDUCED_HEATING = "reducedHeating"
- REDUCED_COOLING = "reducedCooling"
STANDBY = "standby"
@staticmethod
@@ -52,12 +48,8 @@ class HeatingProgram(enum.StrEnum):
) -> str | None:
"""Return the mapped ViCare heating program for the Home Assistant preset."""
for program in supported_heating_programs:
- with suppress(ValueError):
- if (
- VICARE_TO_HA_PRESET_HEATING.get(HeatingProgram(program))
- == ha_preset
- ):
- return program
+ if VICARE_TO_HA_PRESET_HEATING.get(HeatingProgram(program)) == ha_preset:
+ return program
return None
diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py
index 49f6a709565..c8f1aaa21cb 100644
--- a/homeassistant/components/vizio/config_flow.py
+++ b/homeassistant/components/vizio/config_flow.py
@@ -108,6 +108,10 @@ def _host_is_same(host1: str, host2: str) -> bool:
class VizioOptionsConfigFlow(OptionsFlow):
"""Handle Vizio options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize vizio options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -180,7 +184,7 @@ class VizioConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> VizioOptionsConfigFlow:
"""Get the options flow for this handler."""
- return VizioOptionsConfigFlow()
+ return VizioOptionsConfigFlow(config_entry)
def __init__(self) -> None:
"""Initialize config flow."""
diff --git a/homeassistant/components/vizio/coordinator.py b/homeassistant/components/vizio/coordinator.py
index a7ca7d7f9ed..1930828b595 100644
--- a/homeassistant/components/vizio/coordinator.py
+++ b/homeassistant/components/vizio/coordinator.py
@@ -34,9 +34,10 @@ class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]
self.fail_threshold = 10
self.store = store
- async def _async_setup(self) -> None:
+ async def async_config_entry_first_refresh(self) -> None:
"""Refresh data for the first time when a config entry is setup."""
self.data = await self.store.async_load() or APPS
+ await super().async_config_entry_first_refresh()
async def _async_update_data(self) -> list[dict[str, Any]]:
"""Update data via library."""
diff --git a/homeassistant/components/vlc_telnet/config_flow.py b/homeassistant/components/vlc_telnet/config_flow.py
index 08564937959..6ccb92e5b8b 100644
--- a/homeassistant/components/vlc_telnet/config_flow.py
+++ b/homeassistant/components/vlc_telnet/config_flow.py
@@ -10,11 +10,11 @@ from aiovlc.client import Client
from aiovlc.exceptions import AuthError, ConnectError
import voluptuous as vol
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.components.hassio import HassioServiceInfo
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from .const import DEFAULT_PORT, DOMAIN
@@ -70,6 +70,7 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for VLC media player Telnet."""
VERSION = 1
+ entry: ConfigEntry | None = None
hassio_discovery: dict[str, Any] | None = None
async def async_step_user(
@@ -107,19 +108,21 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth flow."""
- self.context["title_placeholders"] = {"host": entry_data[CONF_HOST]}
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+ assert self.entry
+ self.context["title_placeholders"] = {"host": self.entry.data[CONF_HOST]}
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauth confirm."""
+ assert self.entry
errors = {}
- reauth_entry = self._get_reauth_entry()
if user_input is not None:
try:
- await validate_input(self.hass, {**reauth_entry.data, **user_input})
+ await validate_input(self.hass, {**self.entry.data, **user_input})
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
@@ -128,14 +131,21 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN):
_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]},
+ self.hass.config_entries.async_update_entry(
+ self.entry,
+ data={
+ **self.entry.data,
+ CONF_PASSWORD: user_input[CONF_PASSWORD],
+ },
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
- description_placeholders={CONF_HOST: reauth_entry.data[CONF_HOST]},
+ description_placeholders={CONF_HOST: self.entry.data[CONF_HOST]},
data_schema=STEP_REAUTH_DATA_SCHEMA,
errors=errors,
)
diff --git a/homeassistant/components/vodafone_station/config_flow.py b/homeassistant/components/vodafone_station/config_flow.py
index 7a80244f8d6..6b6adb6a18d 100644
--- a/homeassistant/components/vodafone_station/config_flow.py
+++ b/homeassistant/components/vodafone_station/config_flow.py
@@ -17,6 +17,7 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
@@ -59,14 +60,13 @@ class VodafoneStationConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Vodafone Station."""
VERSION = 1
+ entry: ConfigEntry | None = None
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> VodafoneStationOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
- return VodafoneStationOptionsFlowHandler()
+ return VodafoneStationOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -106,19 +106,21 @@ class VodafoneStationConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth flow."""
- self.context["title_placeholders"] = {"host": entry_data[CONF_HOST]}
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+ assert self.entry
+ self.context["title_placeholders"] = {"host": self.entry.data[CONF_HOST]}
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauth confirm."""
+ assert self.entry
errors = {}
- reauth_entry = self._get_reauth_entry()
if user_input is not None:
try:
- await validate_input(self.hass, {**reauth_entry.data, **user_input})
+ await validate_input(self.hass, {**self.entry.data, **user_input})
except aiovodafone_exceptions.AlreadyLogged:
errors["base"] = "already_logged"
except aiovodafone_exceptions.CannotConnect:
@@ -129,22 +131,27 @@ class VodafoneStationConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
- return self.async_update_reload_and_abort(
- reauth_entry,
- data_updates={
+ self.hass.config_entries.async_update_entry(
+ self.entry,
+ data={
+ **self.entry.data,
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self.entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
- description_placeholders={CONF_HOST: reauth_entry.data[CONF_HOST]},
+ description_placeholders={CONF_HOST: self.entry.data[CONF_HOST]},
data_schema=STEP_REAUTH_DATA_SCHEMA,
errors=errors,
)
-class VodafoneStationOptionsFlowHandler(OptionsFlow):
+class VodafoneStationOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle a option flow."""
async def async_step_init(
@@ -159,7 +166,7 @@ class VodafoneStationOptionsFlowHandler(OptionsFlow):
{
vol.Optional(
CONF_CONSIDER_HOME,
- default=self.config_entry.options.get(
+ default=self.options.get(
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
),
): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900))
diff --git a/homeassistant/components/vodafone_station/diagnostics.py b/homeassistant/components/vodafone_station/diagnostics.py
deleted file mode 100644
index e306d6caca2..00000000000
--- a/homeassistant/components/vodafone_station/diagnostics.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""Diagnostics support for Vodafone Station."""
-
-from __future__ import annotations
-
-from typing import Any
-
-from homeassistant.components.diagnostics import async_redact_data
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from homeassistant.core import HomeAssistant
-
-from .const import DOMAIN
-from .coordinator import VodafoneStationRouter
-
-TO_REDACT = {CONF_USERNAME, CONF_PASSWORD}
-
-
-async def async_get_config_entry_diagnostics(
- hass: HomeAssistant, entry: ConfigEntry
-) -> dict[str, Any]:
- """Return diagnostics for a config entry."""
-
- coordinator: VodafoneStationRouter = hass.data[DOMAIN][entry.entry_id]
-
- sensors_data = coordinator.data.sensors
- return {
- "entry": async_redact_data(entry.as_dict(), TO_REDACT),
- "device_info": {
- "sys_model_name": sensors_data.get("sys_model_name"),
- "sys_firmware_version": sensors_data["sys_firmware_version"],
- "sys_hardware_version": sensors_data["sys_hardware_version"],
- "sys_cpu_usage": sensors_data["sys_cpu_usage"][:-1],
- "sys_memory_usage": sensors_data["sys_memory_usage"][:-1],
- "sys_reboot_cause": sensors_data["sys_reboot_cause"],
- "last_update success": coordinator.last_update_success,
- "last_exception": coordinator.last_exception,
- "client_devices": [
- {
- "hostname": device_info.device.name,
- "connection_type": device_info.device.connection_type,
- "connected": device_info.device.connected,
- "type": device_info.device.type,
- }
- for _, device_info in coordinator.data.devices.items()
- ],
- },
- }
diff --git a/homeassistant/components/vodafone_station/manifest.json b/homeassistant/components/vodafone_station/manifest.json
index 29cb3c070ab..47137fff26c 100644
--- a/homeassistant/components/vodafone_station/manifest.json
+++ b/homeassistant/components/vodafone_station/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiovodafone"],
"quality_scale": "silver",
- "requirements": ["aiovodafone==0.6.1"]
+ "requirements": ["aiovodafone==0.6.0"]
}
diff --git a/homeassistant/components/vodafone_station/sensor.py b/homeassistant/components/vodafone_station/sensor.py
index 307fcaf0ea8..2a08a9b2ebe 100644
--- a/homeassistant/components/vodafone_station/sensor.py
+++ b/homeassistant/components/vodafone_station/sensor.py
@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
-from typing import Final
+from typing import Any, Final
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -16,49 +16,32 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfDataRate
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import _LOGGER, DOMAIN, LINE_TYPES
from .coordinator import VodafoneStationRouter
NOT_AVAILABLE: list = ["", "N/A", "0.0.0.0"]
-UPTIME_DEVIATION = 60
@dataclass(frozen=True, kw_only=True)
class VodafoneStationEntityDescription(SensorEntityDescription):
"""Vodafone Station entity description."""
- value: Callable[
- [VodafoneStationRouter, str | datetime | float | None, str],
- str | datetime | float | None,
- ] = lambda coordinator, last_value, key: coordinator.data.sensors[key]
+ value: Callable[[Any, Any], Any] = (
+ lambda coordinator, key: coordinator.data.sensors[key]
+ )
is_suitable: Callable[[dict], bool] = lambda val: True
-def _calculate_uptime(
- coordinator: VodafoneStationRouter,
- last_value: str | datetime | float | None,
- key: str,
-) -> datetime:
+def _calculate_uptime(coordinator: VodafoneStationRouter, key: str) -> datetime:
"""Calculate device uptime."""
- delta_uptime = coordinator.api.convert_uptime(coordinator.data.sensors[key])
-
- if (
- not isinstance(last_value, datetime)
- or abs((delta_uptime - last_value).total_seconds()) > UPTIME_DEVIATION
- ):
- return delta_uptime
-
- return last_value
+ return coordinator.api.convert_uptime(coordinator.data.sensors[key])
-def _line_connection(
- coordinator: VodafoneStationRouter,
- last_value: str | datetime | float | None,
- key: str,
-) -> str | None:
+def _line_connection(coordinator: VodafoneStationRouter, key: str) -> str | None:
"""Identify line type."""
value = coordinator.data.sensors
@@ -143,18 +126,14 @@ SENSOR_TYPES: Final = (
translation_key="sys_cpu_usage",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
- value=lambda coordinator, last_value, key: float(
- coordinator.data.sensors[key][:-1]
- ),
+ value=lambda coordinator, key: float(coordinator.data.sensors[key][:-1]),
),
VodafoneStationEntityDescription(
key="sys_memory_usage",
translation_key="sys_memory_usage",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
- value=lambda coordinator, last_value, key: float(
- coordinator.data.sensors[key][:-1]
- ),
+ value=lambda coordinator, key: float(coordinator.data.sensors[key][:-1]),
),
VodafoneStationEntityDescription(
key="sys_reboot_cause",
@@ -199,12 +178,10 @@ class VodafoneStationSensorEntity(
self.entity_description = description
self._attr_device_info = coordinator.device_info
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
- self._old_state: str | datetime | float | None = None
@property
- def native_value(self) -> str | datetime | float | None:
+ def native_value(self) -> StateType:
"""Sensor value."""
- self._old_state = self.entity_description.value(
- self.coordinator, self._old_state, self.entity_description.key
+ return self.entity_description.value(
+ self.coordinator, self.entity_description.key
)
- return self._old_state
diff --git a/homeassistant/components/voip/assist_satellite.py b/homeassistant/components/voip/assist_satellite.py
index 0100435d6dc..5e32585775c 100644
--- a/homeassistant/components/voip/assist_satellite.py
+++ b/homeassistant/components/voip/assist_satellite.py
@@ -21,6 +21,7 @@ from homeassistant.components.assist_satellite import (
AssistSatelliteEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import EntityCategory
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -79,6 +80,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
entity_description = AssistSatelliteEntityDescription(key="assist_satellite")
_attr_translation_key = "assist_satellite"
+ _attr_entity_category = EntityCategory.CONFIG
_attr_name = None
def __init__(
diff --git a/homeassistant/components/voip/config_flow.py b/homeassistant/components/voip/config_flow.py
index 63dcb8f86ee..821c7f29a1e 100644
--- a/homeassistant/components/voip/config_flow.py
+++ b/homeassistant/components/voip/config_flow.py
@@ -47,12 +47,16 @@ class VoIPConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Create the options flow."""
- return VoipOptionsFlowHandler()
+ return VoipOptionsFlowHandler(config_entry)
class VoipOptionsFlowHandler(OptionsFlow):
"""Handle VoIP options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/volvooncall/config_flow.py b/homeassistant/components/volvooncall/config_flow.py
index ccb0a7f62e1..a5e860c9105 100644
--- a/homeassistant/components/volvooncall/config_flow.py
+++ b/homeassistant/components/volvooncall/config_flow.py
@@ -9,7 +9,7 @@ from typing import Any
import voluptuous as vol
from volvooncall import Connection
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_PASSWORD,
CONF_REGION,
@@ -35,6 +35,7 @@ class VolvoOnCallConfigFlow(ConfigFlow, domain=DOMAIN):
"""VolvoOnCall config flow."""
VERSION = 1
+ _reauth_entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -52,7 +53,7 @@ class VolvoOnCallConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_USERNAME])
- if self.source != SOURCE_REAUTH:
+ if not self._reauth_entry:
self._abort_if_unique_id_configured()
try:
@@ -63,18 +64,21 @@ class VolvoOnCallConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unhandled exception in user step")
errors["base"] = "unknown"
if not errors:
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=user_input
+ if self._reauth_entry:
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry, data=self._reauth_entry.data | user_input
)
+ await self.hass.config_entries.async_reload(
+ self._reauth_entry.entry_id
+ )
+ return self.async_abort(reason="reauth_successful")
return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input
)
- elif self.source == SOURCE_REAUTH:
- reauth_entry = self._get_reauth_entry()
+ elif self._reauth_entry:
for key in defaults:
- defaults[key] = reauth_entry.data.get(key)
+ defaults[key] = self._reauth_entry.data.get(key)
user_schema = vol.Schema(
{
@@ -106,6 +110,9 @@ class VolvoOnCallConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_user()
async def is_valid(self, user_input):
diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py
index b2f8ac7fd5d..4ea2cf98be1 100644
--- a/homeassistant/components/wallbox/__init__.py
+++ b/homeassistant/components/wallbox/__init__.py
@@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from .const import CONF_STATION, DOMAIN, UPDATE_INTERVAL
-from .coordinator import InvalidAuth, WallboxCoordinator, async_validate_input
+from .coordinator import InvalidAuth, WallboxCoordinator
PLATFORMS = [Platform.LOCK, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
@@ -22,16 +22,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_PASSWORD],
jwtTokenDrift=UPDATE_INTERVAL,
)
- try:
- await async_validate_input(hass, wallbox)
- except InvalidAuth as ex:
- raise ConfigEntryAuthFailed from ex
-
wallbox_coordinator = WallboxCoordinator(
entry.data[CONF_STATION],
wallbox,
hass,
)
+
+ try:
+ await wallbox_coordinator.async_validate_input()
+
+ except InvalidAuth as ex:
+ raise ConfigEntryAuthFailed from ex
+
await wallbox_coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = wallbox_coordinator
diff --git a/homeassistant/components/wallbox/config_flow.py b/homeassistant/components/wallbox/config_flow.py
index bdc51eef963..44c47149554 100644
--- a/homeassistant/components/wallbox/config_flow.py
+++ b/homeassistant/components/wallbox/config_flow.py
@@ -8,12 +8,12 @@ from typing import Any
import voluptuous as vol
from wallbox import Wallbox
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .const import CONF_STATION, DOMAIN
-from .coordinator import InvalidAuth, async_validate_input
+from .coordinator import InvalidAuth, WallboxCoordinator
COMPONENT_DOMAIN = DOMAIN
@@ -32,8 +32,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
wallbox = Wallbox(data["username"], data["password"])
+ wallbox_coordinator = WallboxCoordinator(data["station"], wallbox, hass)
- await async_validate_input(hass, wallbox)
+ await wallbox_coordinator.async_validate_input()
# Return info that you want to store in the config entry.
return {"title": "Wallbox Portal"}
@@ -42,10 +43,18 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
class WallboxConfigFlow(ConfigFlow, domain=COMPONENT_DOMAIN):
"""Handle a config flow for Wallbox."""
+ def __init__(self) -> None:
+ """Start the Wallbox config flow."""
+ self._reauth_entry: ConfigEntry | None = None
+
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
+
return await self.async_step_user()
async def async_step_user(
@@ -62,13 +71,18 @@ class WallboxConfigFlow(ConfigFlow, domain=COMPONENT_DOMAIN):
try:
await self.async_set_unique_id(user_input["station"])
- if self.source != SOURCE_REAUTH:
+ if not self._reauth_entry:
self._abort_if_unique_id_configured()
info = await validate_input(self.hass, user_input)
return self.async_create_entry(title=info["title"], data=user_input)
- reauth_entry = self._get_reauth_entry()
- if user_input["station"] == reauth_entry.data[CONF_STATION]:
- return self.async_update_reload_and_abort(reauth_entry, data=user_input)
+ if user_input["station"] == self._reauth_entry.data[CONF_STATION]:
+ self.hass.config_entries.async_update_entry(
+ self._reauth_entry, data=user_input, unique_id=user_input["station"]
+ )
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
+ )
+ return self.async_abort(reason="reauth_successful")
errors["base"] = "reauth_invalid"
except ConnectionError:
errors["base"] = "cannot_connect"
diff --git a/homeassistant/components/wallbox/coordinator.py b/homeassistant/components/wallbox/coordinator.py
index 99c565d9c0c..f3679551bc4 100644
--- a/homeassistant/components/wallbox/coordinator.py
+++ b/homeassistant/components/wallbox/coordinator.py
@@ -89,21 +89,6 @@ def _require_authentication[_WallboxCoordinatorT: WallboxCoordinator, **_P](
return require_authentication
-def _validate(wallbox: Wallbox) -> None:
- """Authenticate using Wallbox API."""
- try:
- wallbox.authenticate()
- except requests.exceptions.HTTPError as wallbox_connection_error:
- if wallbox_connection_error.response.status_code == 403:
- raise InvalidAuth from wallbox_connection_error
- raise ConnectionError from wallbox_connection_error
-
-
-async def async_validate_input(hass: HomeAssistant, wallbox: Wallbox) -> None:
- """Get new sensor data for Wallbox component."""
- await hass.async_add_executor_job(_validate, wallbox)
-
-
class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Wallbox Coordinator class."""
@@ -123,6 +108,19 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Authenticate using Wallbox API."""
self._wallbox.authenticate()
+ def _validate(self) -> None:
+ """Authenticate using Wallbox API."""
+ try:
+ self._wallbox.authenticate()
+ except requests.exceptions.HTTPError as wallbox_connection_error:
+ if wallbox_connection_error.response.status_code == 403:
+ raise InvalidAuth from wallbox_connection_error
+ raise ConnectionError from wallbox_connection_error
+
+ async def async_validate_input(self) -> None:
+ """Get new sensor data for Wallbox component."""
+ await self.hass.async_add_executor_job(self._validate)
+
@_require_authentication
def _get_data(self) -> dict[str, Any]:
"""Get new sensor data for Wallbox component."""
diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json
index 07e132a0b5b..741b277d84d 100644
--- a/homeassistant/components/water_heater/strings.json
+++ b/homeassistant/components/water_heater/strings.json
@@ -1,5 +1,4 @@
{
- "title": "Water heater",
"device_automation": {
"action_type": {
"turn_on": "[%key:common::device_automation::action_type::turn_on%]",
@@ -8,7 +7,7 @@
},
"entity_component": {
"_": {
- "name": "[%key:component::water_heater::title%]",
+ "name": "Water heater",
"state": {
"off": "[%key:common::state::off%]",
"eco": "Eco",
diff --git a/homeassistant/components/watttime/__init__.py b/homeassistant/components/watttime/__init__.py
index ed2bdd4ebac..6b32cf723a3 100644
--- a/homeassistant/components/watttime/__init__.py
+++ b/homeassistant/components/watttime/__init__.py
@@ -58,7 +58,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
LOGGER,
- config_entry=entry,
name=entry.title,
update_interval=DEFAULT_UPDATE_INTERVAL,
update_method=async_update_data,
diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py
index ad676e166c5..db68738b302 100644
--- a/homeassistant/components/watttime/config_flow.py
+++ b/homeassistant/components/watttime/config_flow.py
@@ -126,11 +126,9 @@ class WattTimeConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> WattTimeOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Define the config flow to handle options."""
- return WattTimeOptionsFlowHandler()
+ return WattTimeOptionsFlowHandler(config_entry)
async def async_step_coordinates(
self, user_input: dict[str, Any] | None = None
@@ -243,6 +241,10 @@ class WattTimeConfigFlow(ConfigFlow, domain=DOMAIN):
class WattTimeOptionsFlowHandler(OptionsFlow):
"""Handle a WattTime options flow."""
+ def __init__(self, entry: ConfigEntry) -> None:
+ """Initialize."""
+ self.entry = entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -256,7 +258,7 @@ class WattTimeOptionsFlowHandler(OptionsFlow):
{
vol.Required(
CONF_SHOW_ON_MAP,
- default=self.config_entry.options.get(CONF_SHOW_ON_MAP, True),
+ default=self.entry.options.get(CONF_SHOW_ON_MAP, True),
): bool
}
),
diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py
index 6ab6a4b121c..1d75adc6c29 100644
--- a/homeassistant/components/waze_travel_time/config_flow.py
+++ b/homeassistant/components/waze_travel_time/config_flow.py
@@ -113,6 +113,10 @@ def default_options(hass: HomeAssistant) -> dict[str, str | bool | list[str]]:
class WazeOptionsFlow(OptionsFlow):
"""Handle an options flow for Waze Travel Time."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize waze options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(self, user_input=None) -> ConfigFlowResult:
"""Handle the initial step."""
if user_input is not None:
@@ -144,7 +148,7 @@ class WazeConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> WazeOptionsFlow:
"""Get the options flow for this handler."""
- return WazeOptionsFlow()
+ return WazeOptionsFlow(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
diff --git a/homeassistant/components/weatherflow/strings.json b/homeassistant/components/weatherflow/strings.json
index cf23f02d781..8fb3a3cdf31 100644
--- a/homeassistant/components/weatherflow/strings.json
+++ b/homeassistant/components/weatherflow/strings.json
@@ -13,11 +13,11 @@
},
"error": {
"address_in_use": "Unable to open local UDP port 50222.",
- "cannot_connect": "UDP discovery error.",
- "no_device_found": "[%key:common::config_flow::abort::no_devices_found%]"
+ "cannot_connect": "UDP discovery error."
},
"abort": {
- "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
},
"entity": {
diff --git a/homeassistant/components/weatherflow_cloud/config_flow.py b/homeassistant/components/weatherflow_cloud/config_flow.py
index bdd3003e6b6..cbb83b6f25b 100644
--- a/homeassistant/components/weatherflow_cloud/config_flow.py
+++ b/homeassistant/components/weatherflow_cloud/config_flow.py
@@ -49,11 +49,15 @@ class WeatherFlowCloudConfigFlow(ConfigFlow, domain=DOMAIN):
errors = await _validate_api_token(api_token)
if not errors:
# Update the existing entry and abort
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(),
- data={CONF_API_TOKEN: api_token},
- reload_even_if_entry_is_unchanged=False,
- )
+ if existing_entry := self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ ):
+ return self.async_update_reload_and_abort(
+ existing_entry,
+ data={CONF_API_TOKEN: api_token},
+ reason="reauth_successful",
+ reload_even_if_entry_is_unchanged=False,
+ )
return self.async_show_form(
step_id="reauth_confirm",
diff --git a/homeassistant/components/weatherkit/manifest.json b/homeassistant/components/weatherkit/manifest.json
index f86745f330f..a6dd40d5993 100644
--- a/homeassistant/components/weatherkit/manifest.json
+++ b/homeassistant/components/weatherkit/manifest.json
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/weatherkit",
"iot_class": "cloud_polling",
- "requirements": ["apple_weatherkit==1.1.3"]
+ "requirements": ["apple_weatherkit==1.1.2"]
}
diff --git a/homeassistant/components/webmin/config_flow.py b/homeassistant/components/webmin/config_flow.py
index 64f8c684dfa..3f55bbd9110 100644
--- a/homeassistant/components/webmin/config_flow.py
+++ b/homeassistant/components/webmin/config_flow.py
@@ -26,7 +26,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowFormStep,
)
-from .const import DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, LOGGER
+from .const import DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN
from .helpers import get_instance_from_options, get_sorted_mac_addresses
@@ -45,8 +45,9 @@ async def validate_user_input(
raise SchemaFlowError("invalid_auth") from err
raise SchemaFlowError("cannot_connect") from err
except Fault as fault:
- LOGGER.exception(f"Fault {fault.faultCode}: {fault.faultString}")
- raise SchemaFlowError("unknown") from fault
+ raise SchemaFlowError(
+ f"Fault {fault.faultCode}: {fault.faultString}"
+ ) from fault
except ClientConnectionError as err:
raise SchemaFlowError("cannot_connect") from err
except Exception as err:
diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py
index 45395bd282a..4bc2c5ca258 100644
--- a/homeassistant/components/webostv/config_flow.py
+++ b/homeassistant/components/webostv/config_flow.py
@@ -47,6 +47,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
self._host: str = ""
self._name: str = ""
self._uuid: str | None = None
+ self._entry: ConfigEntry | None = None
@staticmethod
@callback
@@ -143,12 +144,15 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Perform reauth upon an WebOsTvPairError."""
self._host = entry_data[CONF_HOST]
+ self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
+ assert self._entry is not None
+
if user_input is not None:
try:
client = await async_control_connect(self._host, None)
@@ -157,9 +161,8 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
except WEBOSTV_EXCEPTIONS:
return self.async_abort(reason="reauth_unsuccessful")
- reauth_entry = self._get_reauth_entry()
- update_client_key(self.hass, reauth_entry, client)
- await self.hass.config_entries.async_reload(reauth_entry.entry_id)
+ update_client_key(self.hass, self._entry, client)
+ await self.hass.config_entries.async_reload(self._entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(step_id="reauth_confirm")
@@ -170,6 +173,8 @@ class OptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
+ self.config_entry = config_entry
+ self.options = config_entry.options
self.host = config_entry.data[CONF_HOST]
self.key = config_entry.data[CONF_CLIENT_SECRET]
@@ -186,8 +191,7 @@ class OptionsFlowHandler(OptionsFlow):
if not sources_list:
errors["base"] = "cannot_retrieve"
- option_sources = self.config_entry.options.get(CONF_SOURCES, [])
- sources = [s for s in option_sources if s in sources_list]
+ sources = [s for s in self.options.get(CONF_SOURCES, []) if s in sources_list]
if not sources:
sources = sources_list
diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py
index 62f1adc39b9..6c0c6f0c587 100644
--- a/homeassistant/components/websocket_api/connection.py
+++ b/homeassistant/components/websocket_api/connection.py
@@ -16,12 +16,6 @@ from homeassistant.helpers.http import current_request
from homeassistant.util.json import JsonValueType
from . import const, messages
-from .messages import (
- error_message,
- event_message,
- message_to_json_bytes,
- result_message,
-)
from .util import describe_request
if TYPE_CHECKING:
@@ -132,12 +126,12 @@ class ActiveConnection:
@callback
def send_result(self, msg_id: int, result: Any | None = None) -> None:
"""Send a result message."""
- self.send_message(message_to_json_bytes(result_message(msg_id, result)))
+ self.send_message(messages.result_message(msg_id, result))
@callback
def send_event(self, msg_id: int, event: Any | None = None) -> None:
"""Send a event message."""
- self.send_message(message_to_json_bytes(event_message(msg_id, event)))
+ self.send_message(messages.event_message(msg_id, event))
@callback
def send_error(
@@ -151,15 +145,13 @@ class ActiveConnection:
) -> None:
"""Send an error message."""
self.send_message(
- message_to_json_bytes(
- error_message(
- msg_id,
- code,
- message,
- translation_key=translation_key,
- translation_domain=translation_domain,
- translation_placeholders=translation_placeholders,
- )
+ messages.error_message(
+ msg_id,
+ code,
+ message,
+ translation_key=translation_key,
+ translation_domain=translation_domain,
+ translation_placeholders=translation_placeholders,
)
)
diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py
index e7d57aebab6..29dc6113350 100644
--- a/homeassistant/components/websocket_api/http.py
+++ b/homeassistant/components/websocket_api/http.py
@@ -36,8 +36,6 @@ from .error import Disconnect
from .messages import message_to_json_bytes
from .util import describe_request
-CLOSE_MSG_TYPES = {WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING}
-
if TYPE_CHECKING:
from .connection import ActiveConnection
@@ -330,7 +328,13 @@ class WebSocketHandler:
if TYPE_CHECKING:
assert writer is not None
- send_bytes_text = partial(writer.send_frame, opcode=WSMsgType.TEXT)
+ # aiohttp 3.11.0 changed the method name from _send_frame to send_frame
+ if hasattr(writer, "send_frame"):
+ send_frame = writer.send_frame # pragma: no cover
+ else:
+ send_frame = writer._send_frame # noqa: SLF001
+
+ send_bytes_text = partial(send_frame, opcode=WSMsgType.TEXT)
auth = AuthPhase(
logger, hass, self._send_message, self._cancel, request, send_bytes_text
)
@@ -340,7 +344,7 @@ class WebSocketHandler:
try:
connection = await self._async_handle_auth_phase(auth, send_bytes_text)
self._async_increase_writer_limit(writer)
- await self._async_websocket_command_phase(connection)
+ await self._async_websocket_command_phase(connection, send_bytes_text)
except asyncio.CancelledError:
logger.debug("%s: Connection cancelled", self.description)
raise
@@ -450,7 +454,9 @@ class WebSocketHandler:
writer._limit = 2**20 # noqa: SLF001
async def _async_websocket_command_phase(
- self, connection: ActiveConnection
+ self,
+ connection: ActiveConnection,
+ send_bytes_text: Callable[[bytes], Coroutine[Any, Any, None]],
) -> None:
"""Handle the command phase of the websocket connection."""
wsock = self._wsock
@@ -461,26 +467,24 @@ class WebSocketHandler:
# Command phase
while not wsock.closed:
msg = await wsock.receive()
- msg_type = msg.type
- msg_data = msg.data
- if msg_type in CLOSE_MSG_TYPES:
+ if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING):
break
- if msg_type is WSMsgType.BINARY:
- if len(msg_data) < 1:
+ if msg.type is WSMsgType.BINARY:
+ if len(msg.data) < 1:
raise Disconnect("Received invalid binary message.")
- handler = msg_data[0]
- payload = msg_data[1:]
+ handler = msg.data[0]
+ payload = msg.data[1:]
async_handle_binary(handler, payload)
continue
- if msg_type is not WSMsgType.TEXT:
+ if msg.type is not WSMsgType.TEXT:
raise Disconnect("Received non-Text message.")
try:
- command_msg_data = json_loads(msg_data)
+ command_msg_data = json_loads(msg.data)
except ValueError as ex:
raise Disconnect("Received invalid JSON.") from ex
diff --git a/homeassistant/components/weheat/api.py b/homeassistant/components/weheat/api.py
index b1f5c0b3eff..1d0828aa41b 100644
--- a/homeassistant/components/weheat/api.py
+++ b/homeassistant/components/weheat/api.py
@@ -23,6 +23,7 @@ class AsyncConfigEntryAuth(AbstractAuth):
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
- await self._oauth_session.async_ensure_token_valid()
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token[CONF_ACCESS_TOKEN]
diff --git a/homeassistant/components/weheat/config_flow.py b/homeassistant/components/weheat/config_flow.py
index b1a0b5dd4ea..c1eccaf6ba7 100644
--- a/homeassistant/components/weheat/config_flow.py
+++ b/homeassistant/components/weheat/config_flow.py
@@ -6,7 +6,7 @@ from typing import Any
from weheat.abstractions.user import get_user_id_from_token
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
@@ -18,6 +18,8 @@ class OAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
DOMAIN = DOMAIN
+ reauth_entry: ConfigEntry | None = None
+
@property
def logger(self) -> logging.Logger:
"""Return logger."""
@@ -36,21 +38,28 @@ class OAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
user_id = await get_user_id_from_token(
API_URL, data[CONF_TOKEN][CONF_ACCESS_TOKEN]
)
- await self.async_set_unique_id(user_id)
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
+ await self.async_set_unique_id(user_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=ENTRY_TITLE, data=data)
- self._abort_if_unique_id_mismatch(reason="wrong_account")
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=data
- )
+ if self.reauth_entry.unique_id == user_id:
+ return self.async_update_reload_and_abort(
+ self.reauth_entry,
+ unique_id=user_id,
+ data={**self.reauth_entry.data, **data},
+ )
+
+ return self.async_abort(reason="wrong_account")
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
diff --git a/homeassistant/components/weheat/icons.json b/homeassistant/components/weheat/icons.json
index 6fdae84cfff..a7579c12ecd 100644
--- a/homeassistant/components/weheat/icons.json
+++ b/homeassistant/components/weheat/icons.json
@@ -10,17 +10,23 @@
"cop": {
"default": "mdi:speedometer"
},
+ "water_inlet_temperature": {
+ "default": "mdi:thermometer"
+ },
+ "water_outlet_temperature": {
+ "default": "mdi:thermometer"
+ },
"ch_inlet_temperature": {
"default": "mdi:radiator"
},
"outside_temperature": {
"default": "mdi:home-thermometer-outline"
},
- "thermostat_room_temperature": {
- "default": "mdi:home-thermometer"
+ "dhw_top_temperature": {
+ "default": "mdi:thermometer"
},
- "thermostat_room_temperature_setpoint": {
- "default": "mdi:home-thermometer"
+ "dhw_bottom_temperature": {
+ "default": "mdi:thermometer"
},
"heat_pump_state": {
"default": "mdi:state-machine"
diff --git a/homeassistant/components/weheat/manifest.json b/homeassistant/components/weheat/manifest.json
index ef89a2f1acb..d32e0ce4047 100644
--- a/homeassistant/components/weheat/manifest.json
+++ b/homeassistant/components/weheat/manifest.json
@@ -6,5 +6,5 @@
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/weheat",
"iot_class": "cloud_polling",
- "requirements": ["weheat==2024.11.02"]
+ "requirements": ["weheat==2024.09.23"]
}
diff --git a/homeassistant/components/weheat/sensor.py b/homeassistant/components/weheat/sensor.py
index ef5be9030b9..fc7d3628a33 100644
--- a/homeassistant/components/weheat/sensor.py
+++ b/homeassistant/components/weheat/sensor.py
@@ -95,33 +95,6 @@ SENSORS = [
suggested_display_precision=DISPLAY_PRECISION_WATER_TEMP,
value_fn=lambda status: status.air_inlet_temperature,
),
- WeHeatSensorEntityDescription(
- translation_key="thermostat_water_setpoint",
- key="thermostat_water_setpoint",
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- device_class=SensorDeviceClass.TEMPERATURE,
- state_class=SensorStateClass.MEASUREMENT,
- suggested_display_precision=DISPLAY_PRECISION_WATER_TEMP,
- value_fn=lambda status: status.thermostat_water_setpoint,
- ),
- WeHeatSensorEntityDescription(
- translation_key="thermostat_room_temperature",
- key="thermostat_room_temperature",
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- device_class=SensorDeviceClass.TEMPERATURE,
- state_class=SensorStateClass.MEASUREMENT,
- suggested_display_precision=DISPLAY_PRECISION_WATER_TEMP,
- value_fn=lambda status: status.thermostat_room_temperature,
- ),
- WeHeatSensorEntityDescription(
- translation_key="thermostat_room_temperature_setpoint",
- key="thermostat_room_temperature_setpoint",
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
- device_class=SensorDeviceClass.TEMPERATURE,
- state_class=SensorStateClass.MEASUREMENT,
- suggested_display_precision=DISPLAY_PRECISION_WATER_TEMP,
- value_fn=lambda status: status.thermostat_room_temperature_setpoint,
- ),
WeHeatSensorEntityDescription(
translation_key="heat_pump_state",
key="heat_pump_state",
diff --git a/homeassistant/components/weheat/strings.json b/homeassistant/components/weheat/strings.json
index 0733024cbed..3982bfd23b3 100644
--- a/homeassistant/components/weheat/strings.json
+++ b/homeassistant/components/weheat/strings.json
@@ -54,15 +54,6 @@
"outside_temperature": {
"name": "Outside temperature"
},
- "thermostat_water_setpoint": {
- "name": "Water target temperature"
- },
- "thermostat_room_temperature": {
- "name": "Current room temperature"
- },
- "thermostat_room_temperature_setpoint": {
- "name": "Room temperature setpoint"
- },
"dhw_top_temperature": {
"name": "DHW top temperature"
},
diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py
index 361c58953c5..10a9bf5604b 100644
--- a/homeassistant/components/wemo/config_flow.py
+++ b/homeassistant/components/wemo/config_flow.py
@@ -32,12 +32,16 @@ class WemoFlow(DiscoveryFlowHandler, domain=DOMAIN):
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
- return WemoOptionsFlow()
+ return WemoOptionsFlow(config_entry)
class WemoOptionsFlow(OptionsFlow):
"""Options flow for the WeMo component."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py
index 069a5ca1e4f..7c39b1fbb29 100644
--- a/homeassistant/components/whirlpool/config_flow.py
+++ b/homeassistant/components/whirlpool/config_flow.py
@@ -12,7 +12,7 @@ from whirlpool.appliancesmanager import AppliancesManager
from whirlpool.auth import Auth
from whirlpool.backendselector import BackendSelector
-from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -71,11 +71,14 @@ class WhirlpoolConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Whirlpool Sixth Sense."""
VERSION = 1
+ entry: ConfigEntry | None
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication with Whirlpool Sixth Sense."""
+
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -85,10 +88,10 @@ class WhirlpoolConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input:
- reauth_entry = self._get_reauth_entry()
+ assert self.entry is not None
password = user_input[CONF_PASSWORD]
brand = user_input[CONF_BRAND]
- data = {**reauth_entry.data, CONF_PASSWORD: password, CONF_BRAND: brand}
+ data = {**self.entry.data, CONF_PASSWORD: password, CONF_BRAND: brand}
try:
await validate_input(self.hass, data)
@@ -97,7 +100,9 @@ class WhirlpoolConfigFlow(ConfigFlow, domain=DOMAIN):
except (CannotConnect, TimeoutError):
errors["base"] = "cannot_connect"
else:
- return self.async_update_reload_and_abort(reauth_entry, data=data)
+ self.hass.config_entries.async_update_entry(self.entry, data=data)
+ await self.hass.config_entries.async_reload(self.entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json
index 09257652ece..4b4673b771e 100644
--- a/homeassistant/components/whirlpool/strings.json
+++ b/homeassistant/components/whirlpool/strings.json
@@ -27,8 +27,7 @@
}
},
"abort": {
- "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
diff --git a/homeassistant/components/whois/__init__.py b/homeassistant/components/whois/__init__.py
index 07116825f29..b9f5938d93b 100644
--- a/homeassistant/components/whois/__init__.py
+++ b/homeassistant/components/whois/__init__.py
@@ -35,7 +35,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator: DataUpdateCoordinator[Domain | None] = DataUpdateCoordinator(
hass,
LOGGER,
- config_entry=entry,
name=f"{DOMAIN}_APK",
update_interval=SCAN_INTERVAL,
update_method=_async_query_domain,
diff --git a/homeassistant/components/wiffi/config_flow.py b/homeassistant/components/wiffi/config_flow.py
index 308923597cd..3fcbef395e6 100644
--- a/homeassistant/components/wiffi/config_flow.py
+++ b/homeassistant/components/wiffi/config_flow.py
@@ -34,7 +34,7 @@ class WiffiFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Create Wiffi server setup option flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -79,6 +79,10 @@ class WiffiFlowHandler(ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(OptionsFlow):
"""Wiffi server setup option flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, int] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py
index d7f07ccc184..150c0d52890 100644
--- a/homeassistant/components/withings/config_flow.py
+++ b/homeassistant/components/withings/config_flow.py
@@ -9,7 +9,7 @@ from typing import Any
from aiowithings import AuthScope
from homeassistant.components.webhook import async_generate_id
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.const import CONF_NAME, CONF_TOKEN, CONF_WEBHOOK_ID
from homeassistant.helpers import config_entry_oauth2_flow
@@ -23,6 +23,8 @@ class WithingsFlowHandler(
DOMAIN = DOMAIN
+ reauth_entry: ConfigEntry | None = None
+
@property
def logger(self) -> logging.Logger:
"""Return logger."""
@@ -40,6 +42,9 @@ class WithingsFlowHandler(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -47,17 +52,18 @@ class WithingsFlowHandler(
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
if user_input is None:
+ assert self.reauth_entry
return self.async_show_form(
step_id="reauth_confirm",
- description_placeholders={CONF_NAME: self._get_reauth_entry().title},
+ description_placeholders={CONF_NAME: self.reauth_entry.title},
)
return await self.async_step_user()
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create an entry for the flow, or update existing entry."""
user_id = str(data[CONF_TOKEN]["userid"])
- await self.async_set_unique_id(user_id)
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
+ await self.async_set_unique_id(user_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
@@ -65,7 +71,9 @@ class WithingsFlowHandler(
data={**data, CONF_WEBHOOK_ID: async_generate_id()},
)
- self._abort_if_unique_id_mismatch(reason="wrong_account")
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=data
- )
+ if self.reauth_entry.unique_id == user_id:
+ return self.async_update_reload_and_abort(
+ self.reauth_entry, data={**self.reauth_entry.data, **data}
+ )
+
+ return self.async_abort(reason="wrong_account")
diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json
index f9e8328ae53..e0d85f207a3 100644
--- a/homeassistant/components/withings/manifest.json
+++ b/homeassistant/components/withings/manifest.json
@@ -9,5 +9,5 @@
"iot_class": "cloud_push",
"loggers": ["aiowithings"],
"quality_scale": "platinum",
- "requirements": ["aiowithings==3.1.3"]
+ "requirements": ["aiowithings==3.1.0"]
}
diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json
index 775ef5cdaab..38592305c3d 100644
--- a/homeassistant/components/withings/strings.json
+++ b/homeassistant/components/withings/strings.json
@@ -21,7 +21,6 @@
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"wrong_account": "Authenticated account does not match the account to be reauthenticated. Please log in with the correct account."
},
"create_entry": {
diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py
index 0e986aaefa2..1bf3188e9e9 100644
--- a/homeassistant/components/wiz/__init__.py
+++ b/homeassistant/components/wiz/__init__.py
@@ -103,7 +103,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass=hass,
logger=_LOGGER,
- config_entry=entry,
name=entry.title,
update_interval=timedelta(seconds=15),
update_method=_async_update,
diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py
index 812a0500d1a..2798e0d46d1 100644
--- a/homeassistant/components/wled/config_flow.py
+++ b/homeassistant/components/wled/config_flow.py
@@ -12,7 +12,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_HOST, CONF_MAC
from homeassistant.core import callback
@@ -30,11 +30,9 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> WLEDOptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> WLEDOptionsFlowHandler:
"""Get the options flow for this handler."""
- return WLEDOptionsFlowHandler()
+ return WLEDOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -119,7 +117,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
return await wled.update()
-class WLEDOptionsFlowHandler(OptionsFlow):
+class WLEDOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle WLED options."""
async def async_step_init(
@@ -135,7 +133,7 @@ class WLEDOptionsFlowHandler(OptionsFlow):
{
vol.Optional(
CONF_KEEP_MAIN_LIGHT,
- default=self.config_entry.options.get(
+ default=self.options.get(
CONF_KEEP_MAIN_LIGHT, DEFAULT_KEEP_MAIN_LIGHT
),
): bool,
diff --git a/homeassistant/components/wled/coordinator.py b/homeassistant/components/wled/coordinator.py
index 8e2855e9f05..cb39fde5e5a 100644
--- a/homeassistant/components/wled/coordinator.py
+++ b/homeassistant/components/wled/coordinator.py
@@ -49,7 +49,6 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]):
super().__init__(
hass,
LOGGER,
- config_entry=entry,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
@@ -134,7 +133,6 @@ class WLEDReleasesDataUpdateCoordinator(DataUpdateCoordinator[Releases]):
super().__init__(
hass,
LOGGER,
- config_entry=None,
name=DOMAIN,
update_interval=RELEASES_SCAN_INTERVAL,
)
diff --git a/homeassistant/components/wmspro/__init__.py b/homeassistant/components/wmspro/__init__.py
index 37bf1495a56..c0c4a9e3950 100644
--- a/homeassistant/components/wmspro/__init__.py
+++ b/homeassistant/components/wmspro/__init__.py
@@ -15,7 +15,7 @@ from homeassistant.helpers.typing import UNDEFINED
from .const import DOMAIN, MANUFACTURER
-PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT, Platform.SCENE]
+PLATFORMS: list[Platform] = [Platform.COVER]
type WebControlProConfigEntry = ConfigEntry[WebControlPro]
diff --git a/homeassistant/components/wmspro/config_flow.py b/homeassistant/components/wmspro/config_flow.py
index 2ce58ec9eca..19b9ab28e6a 100644
--- a/homeassistant/components/wmspro/config_flow.py
+++ b/homeassistant/components/wmspro/config_flow.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-import ipaddress
import logging
from typing import Any
@@ -39,19 +38,7 @@ class WebControlProConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the DHCP discovery step."""
unique_id = format_mac(discovery_info.macaddress)
await self.async_set_unique_id(unique_id)
-
- entry = self.hass.config_entries.async_entry_for_domain_unique_id(
- DOMAIN, unique_id
- )
- if entry:
- try: # Check if current host is a valid IP address
- ipaddress.ip_address(entry.data[CONF_HOST])
- except ValueError: # Do not touch name-based host
- return self.async_abort(reason="already_configured")
- else: # Update existing host with new IP address
- self._abort_if_unique_id_configured(
- updates={CONF_HOST: discovery_info.ip}
- )
+ self._abort_if_unique_id_configured()
for entry in self.hass.config_entries.async_entries(DOMAIN):
if not entry.unique_id and entry.data[CONF_HOST] in (
@@ -84,15 +71,6 @@ class WebControlProConfigFlow(ConfigFlow, domain=DOMAIN):
if not pong:
errors["base"] = "cannot_connect"
else:
- await hub.refresh()
- rooms = set(hub.rooms.keys())
- for entry in self.hass.config_entries.async_loaded_entries(DOMAIN):
- if (
- entry.runtime_data
- and entry.runtime_data.rooms
- and set(entry.runtime_data.rooms.keys()) == rooms
- ):
- return self.async_abort(reason="already_configured")
return self.async_create_entry(title=host, data=user_input)
if self.source == dhcp.DOMAIN:
diff --git a/homeassistant/components/wmspro/const.py b/homeassistant/components/wmspro/const.py
index d92534d9e46..0a1036cf632 100644
--- a/homeassistant/components/wmspro/const.py
+++ b/homeassistant/components/wmspro/const.py
@@ -5,5 +5,3 @@ SUGGESTED_HOST = "webcontrol"
ATTRIBUTION = "Data provided by WMS WebControl pro API"
MANUFACTURER = "WAREMA Renkhoff SE"
-
-BRIGHTNESS_SCALE = (1, 100)
diff --git a/homeassistant/components/wmspro/cover.py b/homeassistant/components/wmspro/cover.py
index a36b34642b7..b8540a5bf08 100644
--- a/homeassistant/components/wmspro/cover.py
+++ b/homeassistant/components/wmspro/cover.py
@@ -46,12 +46,12 @@ class WebControlProAwning(WebControlProGenericEntity, CoverEntity):
def current_cover_position(self) -> int | None:
"""Return current position of cover."""
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.AwningDrive)
- return 100 - action["percentage"]
+ return action["percentage"]
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.AwningDrive)
- await action(percentage=100 - kwargs[ATTR_POSITION])
+ await action(percentage=kwargs[ATTR_POSITION])
@property
def is_closed(self) -> bool | None:
@@ -61,12 +61,12 @@ class WebControlProAwning(WebControlProGenericEntity, CoverEntity):
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.AwningDrive)
- await action(percentage=0)
+ await action(percentage=100)
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.AwningDrive)
- await action(percentage=100)
+ await action(percentage=0)
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the device if in motion."""
diff --git a/homeassistant/components/wmspro/light.py b/homeassistant/components/wmspro/light.py
deleted file mode 100644
index 9242982bcf9..00000000000
--- a/homeassistant/components/wmspro/light.py
+++ /dev/null
@@ -1,89 +0,0 @@
-"""Support for lights connected with WMS WebControl pro."""
-
-from __future__ import annotations
-
-from datetime import timedelta
-from typing import Any
-
-from wmspro.const import WMS_WebControl_pro_API_actionDescription
-
-from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.util.color import brightness_to_value, value_to_brightness
-
-from . import WebControlProConfigEntry
-from .const import BRIGHTNESS_SCALE
-from .entity import WebControlProGenericEntity
-
-SCAN_INTERVAL = timedelta(seconds=5)
-PARALLEL_UPDATES = 1
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: WebControlProConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the WMS based lights from a config entry."""
- hub = config_entry.runtime_data
-
- entities: list[WebControlProGenericEntity] = []
- for dest in hub.dests.values():
- if dest.action(WMS_WebControl_pro_API_actionDescription.LightDimming):
- entities.append(WebControlProDimmer(config_entry.entry_id, dest))
- elif dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch):
- entities.append(WebControlProLight(config_entry.entry_id, dest))
-
- async_add_entities(entities)
-
-
-class WebControlProLight(WebControlProGenericEntity, LightEntity):
- """Representation of a WMS based light."""
-
- _attr_color_mode = ColorMode.ONOFF
- _attr_supported_color_modes = {ColorMode.ONOFF}
-
- @property
- def is_on(self) -> bool:
- """Return true if light is on."""
- action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch)
- return action["onOffState"]
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn the light on."""
- action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch)
- await action(onOffState=True)
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn the light off."""
- action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch)
- await action(onOffState=False)
-
-
-class WebControlProDimmer(WebControlProLight):
- """Representation of a WMS-based dimmable light."""
-
- _attr_color_mode = ColorMode.BRIGHTNESS
- _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
-
- @property
- def brightness(self) -> int:
- """Return the brightness of this light between 1..255."""
- action = self._dest.action(
- WMS_WebControl_pro_API_actionDescription.LightDimming
- )
- return value_to_brightness(BRIGHTNESS_SCALE, action["percentage"])
-
- async def async_turn_on(self, **kwargs: Any) -> None:
- """Turn the dimmer on."""
- if ATTR_BRIGHTNESS not in kwargs:
- await super().async_turn_on(**kwargs)
- return
-
- action = self._dest.action(
- WMS_WebControl_pro_API_actionDescription.LightDimming
- )
- await action(
- percentage=brightness_to_value(BRIGHTNESS_SCALE, kwargs[ATTR_BRIGHTNESS])
- )
diff --git a/homeassistant/components/wmspro/manifest.json b/homeassistant/components/wmspro/manifest.json
index dd65be3e7e7..f174bcc89c7 100644
--- a/homeassistant/components/wmspro/manifest.json
+++ b/homeassistant/components/wmspro/manifest.json
@@ -3,6 +3,7 @@
"name": "WMS WebControl pro",
"codeowners": ["@mback2k"],
"config_flow": true,
+ "dependencies": [],
"dhcp": [
{
"macaddress": "0023D5*"
diff --git a/homeassistant/components/wmspro/scene.py b/homeassistant/components/wmspro/scene.py
deleted file mode 100644
index de18106b7f0..00000000000
--- a/homeassistant/components/wmspro/scene.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""Support for scenes provided by WMS WebControl pro."""
-
-from __future__ import annotations
-
-from typing import Any
-
-from wmspro.scene import Scene as WMS_Scene
-
-from homeassistant.components.scene import Scene
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers.device_registry import DeviceInfo
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-
-from . import WebControlProConfigEntry
-from .const import ATTRIBUTION, DOMAIN, MANUFACTURER
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: WebControlProConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up the WMS based scenes from a config entry."""
- hub = config_entry.runtime_data
-
- async_add_entities(
- WebControlProScene(config_entry.entry_id, scene)
- for scene in hub.scenes.values()
- )
-
-
-class WebControlProScene(Scene):
- """Representation of a WMS based scene."""
-
- _attr_attribution = ATTRIBUTION
- _attr_has_entity_name = True
-
- def __init__(self, config_entry_id: str, scene: WMS_Scene) -> None:
- """Initialize the entity with the configured scene."""
- super().__init__()
-
- # Scene information
- self._scene = scene
- self._attr_name = scene.name
- self._attr_unique_id = str(scene.id)
-
- # Room information
- room = scene.room
- room_name = room.name
- room_id_str = str(room.id)
- self._attr_device_info = DeviceInfo(
- identifiers={(DOMAIN, room_id_str)},
- manufacturer=MANUFACTURER,
- model="Room",
- name=room_name,
- serial_number=room_id_str,
- suggested_area=room_name,
- via_device=(DOMAIN, config_entry_id),
- configuration_url=f"http://{scene.host}/control",
- )
-
- async def async_activate(self, **kwargs: Any) -> None:
- """Activate scene. Try to get entities into requested state."""
- await self._scene()
diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py
index 49197ed7d26..b897debfede 100644
--- a/homeassistant/components/wolflink/__init__.py
+++ b/homeassistant/components/wolflink/__init__.py
@@ -100,7 +100,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=DOMAIN,
update_method=async_update_data,
update_interval=timedelta(seconds=60),
diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py
index 4d93fccb1a7..2552fe849e2 100644
--- a/homeassistant/components/workday/config_flow.py
+++ b/homeassistant/components/workday/config_flow.py
@@ -12,7 +12,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE, CONF_NAME
from homeassistant.core import callback
@@ -219,7 +219,7 @@ class WorkdayConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> WorkdayOptionsFlowHandler:
"""Get the options flow for this handler."""
- return WorkdayOptionsFlowHandler()
+ return WorkdayOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -310,7 +310,7 @@ class WorkdayConfigFlow(ConfigFlow, domain=DOMAIN):
)
-class WorkdayOptionsFlowHandler(OptionsFlow):
+class WorkdayOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle Workday options."""
async def async_step_init(
@@ -320,7 +320,7 @@ class WorkdayOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {}
if user_input is not None:
- combined_input: dict[str, Any] = {**self.config_entry.options, **user_input}
+ combined_input: dict[str, Any] = {**self.options, **user_input}
if CONF_PROVINCE not in user_input:
# Province not present, delete old value (if present) too
combined_input.pop(CONF_PROVINCE, None)
@@ -340,7 +340,7 @@ class WorkdayOptionsFlowHandler(OptionsFlow):
else:
LOGGER.debug("abort_check in options with %s", combined_input)
abort_match = {
- CONF_COUNTRY: self.config_entry.options.get(CONF_COUNTRY),
+ CONF_COUNTRY: self._config_entry.options.get(CONF_COUNTRY),
CONF_EXCLUDES: combined_input[CONF_EXCLUDES],
CONF_OFFSET: combined_input[CONF_OFFSET],
CONF_WORKDAYS: combined_input[CONF_WORKDAYS],
@@ -357,22 +357,23 @@ class WorkdayOptionsFlowHandler(OptionsFlow):
else:
return self.async_create_entry(data=combined_input)
- options = self.config_entry.options
schema: vol.Schema = await self.hass.async_add_executor_job(
add_province_and_language_to_schema,
DATA_SCHEMA_OPT,
- options.get(CONF_COUNTRY),
+ self.options.get(CONF_COUNTRY),
)
- new_schema = self.add_suggested_values_to_schema(schema, user_input or options)
+ new_schema = self.add_suggested_values_to_schema(
+ schema, user_input or self.options
+ )
LOGGER.debug("Errors have occurred in options %s", errors)
return self.async_show_form(
step_id="init",
data_schema=new_schema,
errors=errors,
description_placeholders={
- "name": options[CONF_NAME],
- "country": options.get(CONF_COUNTRY),
+ "name": self.options[CONF_NAME],
+ "country": self.options.get(CONF_COUNTRY),
},
)
diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json
index b02db734729..cf3afb5fc37 100644
--- a/homeassistant/components/workday/manifest.json
+++ b/homeassistant/components/workday/manifest.json
@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
- "requirements": ["holidays==0.60"]
+ "requirements": ["holidays==0.58"]
}
diff --git a/homeassistant/components/ws66i/config_flow.py b/homeassistant/components/ws66i/config_flow.py
index 120b7738d2e..9f6f4ca59c2 100644
--- a/homeassistant/components/ws66i/config_flow.py
+++ b/homeassistant/components/ws66i/config_flow.py
@@ -130,7 +130,7 @@ class WS66iConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> Ws66iOptionsFlowHandler:
"""Define the config flow to handle options."""
- return Ws66iOptionsFlowHandler()
+ return Ws66iOptionsFlowHandler(config_entry)
@callback
@@ -145,6 +145,10 @@ def _key_for_source(
class Ws66iOptionsFlowHandler(OptionsFlow):
"""Handle a WS66i options flow."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> ConfigFlowResult:
diff --git a/homeassistant/components/wyoming/__init__.py b/homeassistant/components/wyoming/__init__.py
index d639933ece6..00d587e2bb4 100644
--- a/homeassistant/components/wyoming/__init__.py
+++ b/homeassistant/components/wyoming/__init__.py
@@ -14,11 +14,11 @@ from .const import ATTR_SPEAKER, DOMAIN
from .data import WyomingService
from .devices import SatelliteDevice
from .models import DomainDataItem
+from .satellite import WyomingSatellite
_LOGGER = logging.getLogger(__name__)
SATELLITE_PLATFORMS = [
- Platform.ASSIST_SATELLITE,
Platform.BINARY_SENSOR,
Platform.SELECT,
Platform.SWITCH,
@@ -47,29 +47,51 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.async_on_unload(entry.add_update_listener(update_listener))
if (satellite_info := service.info.satellite) is not None:
- # Create satellite device
- dev_reg = dr.async_get(hass)
+ # Create satellite device, etc.
+ item.satellite = _make_satellite(hass, entry, service)
- # Use config entry id since only one satellite per entry is supported
- satellite_id = entry.entry_id
- device = dev_reg.async_get_or_create(
- config_entry_id=entry.entry_id,
- identifiers={(DOMAIN, satellite_id)},
- name=satellite_info.name,
- suggested_area=satellite_info.area,
- )
-
- item.device = SatelliteDevice(
- satellite_id=satellite_id,
- device_id=device.id,
- )
-
- # Set up satellite entity, sensors, switches, etc.
+ # Set up satellite sensors, switches, etc.
await hass.config_entries.async_forward_entry_setups(entry, SATELLITE_PLATFORMS)
+ # Start satellite communication
+ entry.async_create_background_task(
+ hass,
+ item.satellite.run(),
+ f"Satellite {satellite_info.name}",
+ )
+
+ entry.async_on_unload(item.satellite.stop)
+
return True
+def _make_satellite(
+ hass: HomeAssistant, config_entry: ConfigEntry, service: WyomingService
+) -> WyomingSatellite:
+ """Create Wyoming satellite/device from config entry and Wyoming service."""
+ satellite_info = service.info.satellite
+ assert satellite_info is not None
+
+ dev_reg = dr.async_get(hass)
+
+ # Use config entry id since only one satellite per entry is supported
+ satellite_id = config_entry.entry_id
+
+ device = dev_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ identifiers={(DOMAIN, satellite_id)},
+ name=satellite_info.name,
+ suggested_area=satellite_info.area,
+ )
+
+ satellite_device = SatelliteDevice(
+ satellite_id=satellite_id,
+ device_id=device.id,
+ )
+
+ return WyomingSatellite(hass, config_entry, service, satellite_device)
+
+
async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
@@ -80,7 +102,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
item: DomainDataItem = hass.data[DOMAIN][entry.entry_id]
platforms = list(item.service.platforms)
- if item.device is not None:
+ if item.satellite is not None:
platforms += SATELLITE_PLATFORMS
unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms)
diff --git a/homeassistant/components/wyoming/binary_sensor.py b/homeassistant/components/wyoming/binary_sensor.py
index 24ee073ec4d..ac5db0cda99 100644
--- a/homeassistant/components/wyoming/binary_sensor.py
+++ b/homeassistant/components/wyoming/binary_sensor.py
@@ -28,9 +28,9 @@ async def async_setup_entry(
item: DomainDataItem = hass.data[DOMAIN][config_entry.entry_id]
# Setup is only forwarded for satellites
- assert item.device is not None
+ assert item.satellite is not None
- async_add_entities([WyomingSatelliteAssistInProgress(item.device)])
+ async_add_entities([WyomingSatelliteAssistInProgress(item.satellite.device)])
class WyomingSatelliteAssistInProgress(WyomingSatelliteEntity, BinarySensorEntity):
diff --git a/homeassistant/components/wyoming/config_flow.py b/homeassistant/components/wyoming/config_flow.py
index 5fdcb1a5484..4ed2d458ad5 100644
--- a/homeassistant/components/wyoming/config_flow.py
+++ b/homeassistant/components/wyoming/config_flow.py
@@ -8,10 +8,9 @@ from urllib.parse import urlparse
import voluptuous as vol
-from homeassistant.components import zeroconf
+from homeassistant.components import hassio, zeroconf
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PORT
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from .const import DOMAIN
from .data import WyomingService
@@ -31,7 +30,7 @@ class WyomingConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
- _hassio_discovery: HassioServiceInfo
+ _hassio_discovery: hassio.HassioServiceInfo
_service: WyomingService | None = None
_name: str | None = None
@@ -62,7 +61,7 @@ class WyomingConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="no_services")
async def async_step_hassio(
- self, discovery_info: HassioServiceInfo
+ self, discovery_info: hassio.HassioServiceInfo
) -> ConfigFlowResult:
"""Handle Supervisor add-on discovery."""
_LOGGER.debug("Supervisor discovery info: %s", discovery_info)
diff --git a/homeassistant/components/wyoming/conversation.py b/homeassistant/components/wyoming/conversation.py
deleted file mode 100644
index 9a17559c1f8..00000000000
--- a/homeassistant/components/wyoming/conversation.py
+++ /dev/null
@@ -1,194 +0,0 @@
-"""Support for Wyoming intent recognition services."""
-
-import logging
-
-from wyoming.asr import Transcript
-from wyoming.client import AsyncTcpClient
-from wyoming.handle import Handled, NotHandled
-from wyoming.info import HandleProgram, IntentProgram
-from wyoming.intent import Intent, NotRecognized
-
-from homeassistant.components import conversation
-from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers import intent
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.util import ulid
-
-from .const import DOMAIN
-from .data import WyomingService
-from .error import WyomingError
-from .models import DomainDataItem
-
-_LOGGER = logging.getLogger(__name__)
-
-
-async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up Wyoming conversation."""
- item: DomainDataItem = hass.data[DOMAIN][config_entry.entry_id]
- async_add_entities(
- [
- WyomingConversationEntity(config_entry, item.service),
- ]
- )
-
-
-class WyomingConversationEntity(
- conversation.ConversationEntity, conversation.AbstractConversationAgent
-):
- """Wyoming conversation agent."""
-
- _attr_has_entity_name = True
-
- def __init__(
- self,
- config_entry: ConfigEntry,
- service: WyomingService,
- ) -> None:
- """Set up provider."""
- super().__init__()
-
- self.service = service
-
- self._intent_service: IntentProgram | None = None
- self._handle_service: HandleProgram | None = None
-
- for maybe_intent in self.service.info.intent:
- if maybe_intent.installed:
- self._intent_service = maybe_intent
- break
-
- for maybe_handle in self.service.info.handle:
- if maybe_handle.installed:
- self._handle_service = maybe_handle
- break
-
- model_languages: set[str] = set()
-
- if self._intent_service is not None:
- for intent_model in self._intent_service.models:
- if intent_model.installed:
- model_languages.update(intent_model.languages)
-
- self._attr_name = self._intent_service.name
- self._attr_supported_features = (
- conversation.ConversationEntityFeature.CONTROL
- )
- elif self._handle_service is not None:
- for handle_model in self._handle_service.models:
- if handle_model.installed:
- model_languages.update(handle_model.languages)
-
- self._attr_name = self._handle_service.name
-
- self._supported_languages = list(model_languages)
- self._attr_unique_id = f"{config_entry.entry_id}-conversation"
-
- @property
- def supported_languages(self) -> list[str]:
- """Return a list of supported languages."""
- return self._supported_languages
-
- async def async_process(
- self, user_input: conversation.ConversationInput
- ) -> conversation.ConversationResult:
- """Process a sentence."""
- conversation_id = user_input.conversation_id or ulid.ulid_now()
- intent_response = intent.IntentResponse(language=user_input.language)
-
- try:
- async with AsyncTcpClient(self.service.host, self.service.port) as client:
- await client.write_event(
- Transcript(
- user_input.text, context={"conversation_id": conversation_id}
- ).event()
- )
-
- while True:
- event = await client.read_event()
- if event is None:
- _LOGGER.debug("Connection lost")
- intent_response.async_set_error(
- intent.IntentResponseErrorCode.UNKNOWN,
- "Connection to service was lost",
- )
- return conversation.ConversationResult(
- response=intent_response,
- conversation_id=user_input.conversation_id,
- )
-
- if Intent.is_type(event.type):
- # Success
- recognized_intent = Intent.from_event(event)
- _LOGGER.debug("Recognized intent: %s", recognized_intent)
-
- intent_type = recognized_intent.name
- intent_slots = {
- e.name: {"value": e.value}
- for e in recognized_intent.entities
- }
- intent_response = await intent.async_handle(
- self.hass,
- DOMAIN,
- intent_type,
- intent_slots,
- text_input=user_input.text,
- language=user_input.language,
- )
-
- if (not intent_response.speech) and recognized_intent.text:
- intent_response.async_set_speech(recognized_intent.text)
-
- break
-
- if NotRecognized.is_type(event.type):
- not_recognized = NotRecognized.from_event(event)
- intent_response.async_set_error(
- intent.IntentResponseErrorCode.NO_INTENT_MATCH,
- not_recognized.text,
- )
- break
-
- if Handled.is_type(event.type):
- # Success
- handled = Handled.from_event(event)
- intent_response.async_set_speech(handled.text)
- break
-
- if NotHandled.is_type(event.type):
- not_handled = NotHandled.from_event(event)
- intent_response.async_set_error(
- intent.IntentResponseErrorCode.FAILED_TO_HANDLE,
- not_handled.text,
- )
- break
-
- except (OSError, WyomingError) as err:
- _LOGGER.exception("Unexpected error while communicating with service")
- intent_response.async_set_error(
- intent.IntentResponseErrorCode.UNKNOWN,
- f"Error communicating with service: {err}",
- )
- return conversation.ConversationResult(
- response=intent_response,
- conversation_id=user_input.conversation_id,
- )
- except intent.IntentError as err:
- _LOGGER.exception("Unexpected error while handling intent")
- intent_response.async_set_error(
- intent.IntentResponseErrorCode.FAILED_TO_HANDLE,
- f"Error handling intent: {err}",
- )
- return conversation.ConversationResult(
- response=intent_response,
- conversation_id=user_input.conversation_id,
- )
-
- # Success
- return conversation.ConversationResult(
- response=intent_response, conversation_id=conversation_id
- )
diff --git a/homeassistant/components/wyoming/data.py b/homeassistant/components/wyoming/data.py
index a16062ab058..1ee0f24f805 100644
--- a/homeassistant/components/wyoming/data.py
+++ b/homeassistant/components/wyoming/data.py
@@ -37,10 +37,6 @@ class WyomingService:
self.platforms.append(Platform.TTS)
if any(wake.installed for wake in info.wake):
self.platforms.append(Platform.WAKE_WORD)
- if any(intent.installed for intent in info.intent) or any(
- handle.installed for handle in info.handle
- ):
- self.platforms.append(Platform.CONVERSATION)
def has_services(self) -> bool:
"""Return True if services are installed that Home Assistant can use."""
@@ -48,8 +44,6 @@ class WyomingService:
any(asr for asr in self.info.asr if asr.installed)
or any(tts for tts in self.info.tts if tts.installed)
or any(wake for wake in self.info.wake if wake.installed)
- or any(intent for intent in self.info.intent if intent.installed)
- or any(handle for handle in self.info.handle if handle.installed)
or ((self.info.satellite is not None) and self.info.satellite.installed)
)
@@ -76,16 +70,6 @@ class WyomingService:
if wake_installed:
return wake_installed[0].name
- # intent recognition (text -> intent)
- intent_installed = [intent for intent in self.info.intent if intent.installed]
- if intent_installed:
- return intent_installed[0].name
-
- # intent handling (text -> text)
- handle_installed = [handle for handle in self.info.handle if handle.installed]
- if handle_installed:
- return handle_installed[0].name
-
return None
@classmethod
diff --git a/homeassistant/components/wyoming/entity.py b/homeassistant/components/wyoming/entity.py
index 1ce105fb860..4591283036f 100644
--- a/homeassistant/components/wyoming/entity.py
+++ b/homeassistant/components/wyoming/entity.py
@@ -6,7 +6,7 @@ from homeassistant.helpers import entity
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from .const import DOMAIN
-from .devices import SatelliteDevice
+from .satellite import SatelliteDevice
class WyomingSatelliteEntity(entity.Entity):
diff --git a/homeassistant/components/wyoming/manifest.json b/homeassistant/components/wyoming/manifest.json
index b837d2a9e76..30104a88dce 100644
--- a/homeassistant/components/wyoming/manifest.json
+++ b/homeassistant/components/wyoming/manifest.json
@@ -3,12 +3,7 @@
"name": "Wyoming Protocol",
"codeowners": ["@balloob", "@synesthesiam"],
"config_flow": true,
- "dependencies": [
- "assist_satellite",
- "assist_pipeline",
- "intent",
- "conversation"
- ],
+ "dependencies": ["assist_pipeline", "intent", "conversation"],
"documentation": "https://www.home-assistant.io/integrations/wyoming",
"integration_type": "service",
"iot_class": "local_push",
diff --git a/homeassistant/components/wyoming/models.py b/homeassistant/components/wyoming/models.py
index b819d06f916..066af144d78 100644
--- a/homeassistant/components/wyoming/models.py
+++ b/homeassistant/components/wyoming/models.py
@@ -3,7 +3,7 @@
from dataclasses import dataclass
from .data import WyomingService
-from .devices import SatelliteDevice
+from .satellite import WyomingSatellite
@dataclass
@@ -11,4 +11,4 @@ class DomainDataItem:
"""Domain data item."""
service: WyomingService
- device: SatelliteDevice | None = None
+ satellite: WyomingSatellite | None = None
diff --git a/homeassistant/components/wyoming/number.py b/homeassistant/components/wyoming/number.py
index d9a58cc3333..5e769eeb06d 100644
--- a/homeassistant/components/wyoming/number.py
+++ b/homeassistant/components/wyoming/number.py
@@ -30,12 +30,13 @@ async def async_setup_entry(
item: DomainDataItem = hass.data[DOMAIN][config_entry.entry_id]
# Setup is only forwarded for satellites
- assert item.device is not None
+ assert item.satellite is not None
+ device = item.satellite.device
async_add_entities(
[
- WyomingSatelliteAutoGainNumber(item.device),
- WyomingSatelliteVolumeMultiplierNumber(item.device),
+ WyomingSatelliteAutoGainNumber(device),
+ WyomingSatelliteVolumeMultiplierNumber(device),
]
)
diff --git a/homeassistant/components/wyoming/assist_satellite.py b/homeassistant/components/wyoming/satellite.py
similarity index 82%
rename from homeassistant/components/wyoming/assist_satellite.py
rename to homeassistant/components/wyoming/satellite.py
index 615084bcbf3..781f0706c68 100644
--- a/homeassistant/components/wyoming/assist_satellite.py
+++ b/homeassistant/components/wyoming/satellite.py
@@ -1,12 +1,12 @@
-"""Assist satellite entity for Wyoming integration."""
-
-from __future__ import annotations
+"""Support for Wyoming satellite services."""
import asyncio
from collections.abc import AsyncGenerator
import io
import logging
-from typing import Any, Final
+import time
+from typing import Final
+from uuid import uuid4
import wave
from wyoming.asr import Transcribe, Transcript
@@ -18,28 +18,20 @@ from wyoming.info import Describe, Info
from wyoming.ping import Ping, Pong
from wyoming.pipeline import PipelineStage, RunPipeline
from wyoming.satellite import PauseSatellite, RunSatellite
-from wyoming.snd import Played
from wyoming.timer import TimerCancelled, TimerFinished, TimerStarted, TimerUpdated
from wyoming.tts import Synthesize, SynthesizeVoice
from wyoming.vad import VoiceStarted, VoiceStopped
from wyoming.wake import Detect, Detection
-from homeassistant.components import assist_pipeline, intent, tts
-from homeassistant.components.assist_pipeline import PipelineEvent
-from homeassistant.components.assist_satellite import (
- AssistSatelliteConfiguration,
- AssistSatelliteEntity,
- AssistSatelliteEntityDescription,
-)
+from homeassistant.components import assist_pipeline, intent, stt, tts
+from homeassistant.components.assist_pipeline import select as pipeline_select
+from homeassistant.components.assist_pipeline.vad import VadSensitivity
from homeassistant.config_entries import ConfigEntry
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.core import Context, HomeAssistant, callback
from .const import DOMAIN
from .data import WyomingService
from .devices import SatelliteDevice
-from .entity import WyomingSatelliteEntity
-from .models import DomainDataItem
_LOGGER = logging.getLogger(__name__)
@@ -49,6 +41,7 @@ _RESTART_SECONDS: Final = 3
_PING_TIMEOUT: Final = 5
_PING_SEND_DELAY: Final = 2
_PIPELINE_FINISH_TIMEOUT: Final = 1
+_CONVERSATION_TIMEOUT_SEC: Final = 5 * 60 # 5 minutes
# Wyoming stage -> Assist stage
_STAGES: dict[PipelineStage, assist_pipeline.PipelineStage] = {
@@ -59,46 +52,21 @@ _STAGES: dict[PipelineStage, assist_pipeline.PipelineStage] = {
}
-async def async_setup_entry(
- hass: HomeAssistant,
- config_entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
-) -> None:
- """Set up Wyoming Assist satellite entity."""
- domain_data: DomainDataItem = hass.data[DOMAIN][config_entry.entry_id]
- assert domain_data.device is not None
-
- async_add_entities(
- [
- WyomingAssistSatellite(
- hass, domain_data.service, domain_data.device, config_entry
- )
- ]
- )
-
-
-class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
- """Assist satellite for Wyoming devices."""
-
- entity_description = AssistSatelliteEntityDescription(key="assist_satellite")
- _attr_translation_key = "assist_satellite"
- _attr_name = None
+class WyomingSatellite:
+ """Remove voice satellite running the Wyoming protocol."""
def __init__(
self,
hass: HomeAssistant,
+ config_entry: ConfigEntry,
service: WyomingService,
device: SatelliteDevice,
- config_entry: ConfigEntry,
) -> None:
- """Initialize an Assist satellite."""
- WyomingSatelliteEntity.__init__(self, device)
- AssistSatelliteEntity.__init__(self)
-
+ """Initialize satellite."""
+ self.hass = hass
+ self.config_entry = config_entry
self.service = service
self.device = device
- self.config_entry = config_entry
-
self.is_running = True
self._client: AsyncTcpClient | None = None
@@ -116,160 +84,6 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
self.device.set_pipeline_listener(self._pipeline_changed)
self.device.set_audio_settings_listener(self._audio_settings_changed)
- @property
- def pipeline_entity_id(self) -> str | None:
- """Return the entity ID of the pipeline to use for the next conversation."""
- return self.device.get_pipeline_entity_id(self.hass)
-
- @property
- def vad_sensitivity_entity_id(self) -> str | None:
- """Return the entity ID of the VAD sensitivity to use for the next conversation."""
- return self.device.get_vad_sensitivity_entity_id(self.hass)
-
- @property
- def tts_options(self) -> dict[str, Any] | None:
- """Options passed for text-to-speech."""
- return {
- tts.ATTR_PREFERRED_FORMAT: "wav",
- tts.ATTR_PREFERRED_SAMPLE_RATE: 16000,
- tts.ATTR_PREFERRED_SAMPLE_CHANNELS: 1,
- tts.ATTR_PREFERRED_SAMPLE_BYTES: 2,
- }
-
- async def async_added_to_hass(self) -> None:
- """Run when entity about to be added to hass."""
- await super().async_added_to_hass()
- self.start_satellite()
-
- async def async_will_remove_from_hass(self) -> None:
- """Run when entity will be removed from hass."""
- await super().async_will_remove_from_hass()
- self.stop_satellite()
-
- @callback
- def async_get_configuration(
- self,
- ) -> AssistSatelliteConfiguration:
- """Get the current satellite configuration."""
- raise NotImplementedError
-
- async def async_set_configuration(
- self, config: AssistSatelliteConfiguration
- ) -> None:
- """Set the current satellite configuration."""
- raise NotImplementedError
-
- def on_pipeline_event(self, event: PipelineEvent) -> None:
- """Set state based on pipeline stage."""
- assert self._client is not None
-
- if event.type == assist_pipeline.PipelineEventType.RUN_END:
- # Pipeline run is complete
- self._is_pipeline_running = False
- self._pipeline_ended_event.set()
- self.device.set_is_active(False)
- elif event.type == assist_pipeline.PipelineEventType.WAKE_WORD_START:
- self.hass.add_job(self._client.write_event(Detect().event()))
- elif event.type == assist_pipeline.PipelineEventType.WAKE_WORD_END:
- # Wake word detection
- # Inform client of wake word detection
- if event.data and (wake_word_output := event.data.get("wake_word_output")):
- detection = Detection(
- name=wake_word_output["wake_word_id"],
- timestamp=wake_word_output.get("timestamp"),
- )
- self.hass.add_job(self._client.write_event(detection.event()))
- elif event.type == assist_pipeline.PipelineEventType.STT_START:
- # Speech-to-text
- self.device.set_is_active(True)
-
- if event.data:
- self.hass.add_job(
- self._client.write_event(
- Transcribe(language=event.data["metadata"]["language"]).event()
- )
- )
- elif event.type == assist_pipeline.PipelineEventType.STT_VAD_START:
- # User started speaking
- if event.data:
- self.hass.add_job(
- self._client.write_event(
- VoiceStarted(timestamp=event.data["timestamp"]).event()
- )
- )
- elif event.type == assist_pipeline.PipelineEventType.STT_VAD_END:
- # User stopped speaking
- if event.data:
- self.hass.add_job(
- self._client.write_event(
- VoiceStopped(timestamp=event.data["timestamp"]).event()
- )
- )
- elif event.type == assist_pipeline.PipelineEventType.STT_END:
- # Speech-to-text transcript
- if event.data:
- # Inform client of transript
- stt_text = event.data["stt_output"]["text"]
- self.hass.add_job(
- self._client.write_event(Transcript(text=stt_text).event())
- )
- elif event.type == assist_pipeline.PipelineEventType.TTS_START:
- # Text-to-speech text
- if event.data:
- # Inform client of text
- self.hass.add_job(
- self._client.write_event(
- Synthesize(
- text=event.data["tts_input"],
- voice=SynthesizeVoice(
- name=event.data.get("voice"),
- language=event.data.get("language"),
- ),
- ).event()
- )
- )
- elif event.type == assist_pipeline.PipelineEventType.TTS_END:
- # TTS stream
- if event.data and (tts_output := event.data["tts_output"]):
- media_id = tts_output["media_id"]
- self.hass.add_job(self._stream_tts(media_id))
- elif event.type == assist_pipeline.PipelineEventType.ERROR:
- # Pipeline error
- if event.data:
- self.hass.add_job(
- self._client.write_event(
- Error(
- text=event.data["message"], code=event.data["code"]
- ).event()
- )
- )
-
- # -------------------------------------------------------------------------
-
- def start_satellite(self) -> None:
- """Start satellite task."""
- self.is_running = True
-
- self.config_entry.async_create_background_task(
- self.hass, self.run(), "wyoming satellite run"
- )
-
- def stop_satellite(self) -> None:
- """Signal satellite task to stop running."""
- # Stop existing pipeline
- self._audio_queue.put_nowait(None)
-
- # Tell satellite to stop running
- self._send_pause()
-
- # Stop task loop
- self.is_running = False
-
- # Unblock waiting for unmuted
- self._muted_changed_event.set()
-
- # -------------------------------------------------------------------------
-
async def run(self) -> None:
"""Run and maintain a connection to satellite."""
_LOGGER.debug("Running satellite task")
@@ -296,9 +110,6 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
except Exception as err: # noqa: BLE001
_LOGGER.debug("%s: %s", err.__class__.__name__, str(err))
- # Stop any existing pipeline
- self._audio_queue.put_nowait(None)
-
# Ensure sensor is off (before restart)
self.device.set_is_active(False)
@@ -312,6 +123,17 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
await self.on_stopped()
+ def stop(self) -> None:
+ """Signal satellite task to stop running."""
+ # Tell satellite to stop running
+ self._send_pause()
+
+ # Stop task loop
+ self.is_running = False
+
+ # Unblock waiting for unmuted
+ self._muted_changed_event.set()
+
async def on_restart(self) -> None:
"""Block until pipeline loop will be restarted."""
_LOGGER.warning(
@@ -329,7 +151,7 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
await asyncio.sleep(_RECONNECT_SECONDS)
async def on_muted(self) -> None:
- """Block until device may be unmuted again."""
+ """Block until device may be unmated again."""
await self._muted_changed_event.wait()
async def on_stopped(self) -> None:
@@ -430,7 +252,6 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
done, pending = await asyncio.wait(
pending, return_when=asyncio.FIRST_COMPLETED
)
-
if pipeline_ended_task in done:
# Pipeline run end event was received
_LOGGER.debug("Pipeline finished")
@@ -481,7 +302,7 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
elif AudioStop.is_type(client_event.type) and self._is_pipeline_running:
# Stop pipeline
_LOGGER.debug("Client requested pipeline to stop")
- self._audio_queue.put_nowait(None)
+ self._audio_queue.put_nowait(b"")
elif Info.is_type(client_event.type):
client_info = Info.from_event(client_event)
_LOGGER.debug("Updated client info: %s", client_info)
@@ -508,9 +329,6 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
break
_LOGGER.debug("Client detected wake word: %s", wake_word_phrase)
- elif Played.is_type(client_event.type):
- # TTS response has finished playing on satellite
- self.tts_response_finished()
else:
_LOGGER.debug("Unexpected event from satellite: %s", client_event)
@@ -535,20 +353,72 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
if end_stage is None:
raise ValueError(f"Invalid end stage: {end_stage}")
+ pipeline_id = pipeline_select.get_chosen_pipeline(
+ self.hass,
+ DOMAIN,
+ self.device.satellite_id,
+ )
+ pipeline = assist_pipeline.async_get_pipeline(self.hass, pipeline_id)
+ assert pipeline is not None
+
# We will push audio in through a queue
self._audio_queue = asyncio.Queue()
+ stt_stream = self._stt_stream()
+
+ # Start pipeline running
+ _LOGGER.debug(
+ "Starting pipeline %s from %s to %s",
+ pipeline.name,
+ start_stage,
+ end_stage,
+ )
+
+ # Reset conversation id, if necessary
+ if (self._conversation_id_time is None) or (
+ (time.monotonic() - self._conversation_id_time) > _CONVERSATION_TIMEOUT_SEC
+ ):
+ self._conversation_id = None
+
+ if self._conversation_id is None:
+ self._conversation_id = str(uuid4())
+
+ # Update timeout
+ self._conversation_id_time = time.monotonic()
self._is_pipeline_running = True
self._pipeline_ended_event.clear()
self.config_entry.async_create_background_task(
self.hass,
- self.async_accept_pipeline_from_satellite(
- audio_stream=self._stt_stream(),
+ assist_pipeline.async_pipeline_from_audio_stream(
+ self.hass,
+ context=Context(),
+ event_callback=self._event_callback,
+ stt_metadata=stt.SpeechMetadata(
+ language=pipeline.language,
+ format=stt.AudioFormats.WAV,
+ codec=stt.AudioCodecs.PCM,
+ bit_rate=stt.AudioBitRates.BITRATE_16,
+ sample_rate=stt.AudioSampleRates.SAMPLERATE_16000,
+ channel=stt.AudioChannels.CHANNEL_MONO,
+ ),
+ stt_stream=stt_stream,
start_stage=start_stage,
end_stage=end_stage,
+ tts_audio_output="wav",
+ pipeline_id=pipeline_id,
+ audio_settings=assist_pipeline.AudioSettings(
+ noise_suppression_level=self.device.noise_suppression_level,
+ auto_gain_dbfs=self.device.auto_gain,
+ volume_multiplier=self.device.volume_multiplier,
+ silence_seconds=VadSensitivity.to_seconds(
+ self.device.vad_sensitivity
+ ),
+ ),
+ device_id=self.device.device_id,
wake_word_phrase=wake_word_phrase,
+ conversation_id=self._conversation_id,
),
- "wyoming satellite pipeline",
+ name="wyoming satellite pipeline",
)
async def _send_delayed_ping(self) -> None:
@@ -561,6 +431,91 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
except ConnectionError:
pass # handled with timeout
+ def _event_callback(self, event: assist_pipeline.PipelineEvent) -> None:
+ """Translate pipeline events into Wyoming events."""
+ assert self._client is not None
+
+ if event.type == assist_pipeline.PipelineEventType.RUN_END:
+ # Pipeline run is complete
+ self._is_pipeline_running = False
+ self._pipeline_ended_event.set()
+ self.device.set_is_active(False)
+ elif event.type == assist_pipeline.PipelineEventType.WAKE_WORD_START:
+ self.hass.add_job(self._client.write_event(Detect().event()))
+ elif event.type == assist_pipeline.PipelineEventType.WAKE_WORD_END:
+ # Wake word detection
+ # Inform client of wake word detection
+ if event.data and (wake_word_output := event.data.get("wake_word_output")):
+ detection = Detection(
+ name=wake_word_output["wake_word_id"],
+ timestamp=wake_word_output.get("timestamp"),
+ )
+ self.hass.add_job(self._client.write_event(detection.event()))
+ elif event.type == assist_pipeline.PipelineEventType.STT_START:
+ # Speech-to-text
+ self.device.set_is_active(True)
+
+ if event.data:
+ self.hass.add_job(
+ self._client.write_event(
+ Transcribe(language=event.data["metadata"]["language"]).event()
+ )
+ )
+ elif event.type == assist_pipeline.PipelineEventType.STT_VAD_START:
+ # User started speaking
+ if event.data:
+ self.hass.add_job(
+ self._client.write_event(
+ VoiceStarted(timestamp=event.data["timestamp"]).event()
+ )
+ )
+ elif event.type == assist_pipeline.PipelineEventType.STT_VAD_END:
+ # User stopped speaking
+ if event.data:
+ self.hass.add_job(
+ self._client.write_event(
+ VoiceStopped(timestamp=event.data["timestamp"]).event()
+ )
+ )
+ elif event.type == assist_pipeline.PipelineEventType.STT_END:
+ # Speech-to-text transcript
+ if event.data:
+ # Inform client of transript
+ stt_text = event.data["stt_output"]["text"]
+ self.hass.add_job(
+ self._client.write_event(Transcript(text=stt_text).event())
+ )
+ elif event.type == assist_pipeline.PipelineEventType.TTS_START:
+ # Text-to-speech text
+ if event.data:
+ # Inform client of text
+ self.hass.add_job(
+ self._client.write_event(
+ Synthesize(
+ text=event.data["tts_input"],
+ voice=SynthesizeVoice(
+ name=event.data.get("voice"),
+ language=event.data.get("language"),
+ ),
+ ).event()
+ )
+ )
+ elif event.type == assist_pipeline.PipelineEventType.TTS_END:
+ # TTS stream
+ if event.data and (tts_output := event.data["tts_output"]):
+ media_id = tts_output["media_id"]
+ self.hass.add_job(self._stream_tts(media_id))
+ elif event.type == assist_pipeline.PipelineEventType.ERROR:
+ # Pipeline error
+ if event.data:
+ self.hass.add_job(
+ self._client.write_event(
+ Error(
+ text=event.data["message"], code=event.data["code"]
+ ).event()
+ )
+ )
+
async def _connect(self) -> None:
"""Connect to satellite over TCP."""
await self._disconnect()
@@ -621,16 +576,16 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
async def _stt_stream(self) -> AsyncGenerator[bytes]:
"""Yield audio chunks from a queue."""
- is_first_chunk = True
- while chunk := await self._audio_queue.get():
- if chunk is None:
- break
+ try:
+ is_first_chunk = True
+ while chunk := await self._audio_queue.get():
+ if is_first_chunk:
+ is_first_chunk = False
+ _LOGGER.debug("Receiving audio from satellite")
- if is_first_chunk:
- is_first_chunk = False
- _LOGGER.debug("Receiving audio from satellite")
-
- yield chunk
+ yield chunk
+ except asyncio.CancelledError:
+ pass # ignore
@callback
def _handle_timer(
diff --git a/homeassistant/components/wyoming/select.py b/homeassistant/components/wyoming/select.py
index bbcaab81710..f852b4d0434 100644
--- a/homeassistant/components/wyoming/select.py
+++ b/homeassistant/components/wyoming/select.py
@@ -42,13 +42,14 @@ async def async_setup_entry(
item: DomainDataItem = hass.data[DOMAIN][config_entry.entry_id]
# Setup is only forwarded for satellites
- assert item.device is not None
+ assert item.satellite is not None
+ device = item.satellite.device
async_add_entities(
[
- WyomingSatellitePipelineSelect(hass, item.device),
- WyomingSatelliteNoiseSuppressionLevelSelect(item.device),
- WyomingSatelliteVadSensitivitySelect(hass, item.device),
+ WyomingSatellitePipelineSelect(hass, device),
+ WyomingSatelliteNoiseSuppressionLevelSelect(device),
+ WyomingSatelliteVadSensitivitySelect(hass, device),
]
)
diff --git a/homeassistant/components/wyoming/switch.py b/homeassistant/components/wyoming/switch.py
index 308429331c3..c012c60bc5a 100644
--- a/homeassistant/components/wyoming/switch.py
+++ b/homeassistant/components/wyoming/switch.py
@@ -27,9 +27,9 @@ async def async_setup_entry(
item: DomainDataItem = hass.data[DOMAIN][config_entry.entry_id]
# Setup is only forwarded for satellites
- assert item.device is not None
+ assert item.satellite is not None
- async_add_entities([WyomingSatelliteMuteSwitch(item.device)])
+ async_add_entities([WyomingSatelliteMuteSwitch(item.satellite.device)])
class WyomingSatelliteMuteSwitch(
@@ -51,7 +51,7 @@ class WyomingSatelliteMuteSwitch(
# Default to off
self._attr_is_on = (state is not None) and (state.state == STATE_ON)
- self._device.set_is_muted(self._attr_is_on)
+ self._device.is_muted = self._attr_is_on
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on."""
diff --git a/homeassistant/components/xeoma/manifest.json b/homeassistant/components/xeoma/manifest.json
index d66177ca214..a73b4bb8671 100644
--- a/homeassistant/components/xeoma/manifest.json
+++ b/homeassistant/components/xeoma/manifest.json
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/xeoma",
"iot_class": "local_polling",
"loggers": ["pyxeoma"],
- "requirements": ["pyxeoma==1.4.2"]
+ "requirements": ["pyxeoma==1.4.1"]
}
diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py
index df2de381d39..8209c9565bd 100644
--- a/homeassistant/components/xiaomi_ble/config_flow.py
+++ b/homeassistant/components/xiaomi_ble/config_flow.py
@@ -4,16 +4,10 @@ from __future__ import annotations
from collections.abc import Mapping
import dataclasses
-import logging
from typing import Any
import voluptuous as vol
-from xiaomi_ble import (
- XiaomiBluetoothDeviceData as DeviceData,
- XiaomiCloudException,
- XiaomiCloudInvalidAuthenticationException,
- XiaomiCloudTokenFetch,
-)
+from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData
from xiaomi_ble.parser import EncryptionScheme
from homeassistant.components import onboarding
@@ -23,18 +17,14 @@ from homeassistant.components.bluetooth import (
async_discovered_service_info,
async_process_advertisements,
)
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
-from homeassistant.const import CONF_ADDRESS, CONF_PASSWORD, CONF_USERNAME
-from homeassistant.data_entry_flow import AbortFlow
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
+from homeassistant.const import CONF_ADDRESS
from .const import DOMAIN
# How long to wait for additional advertisement packets if we don't have the right ones
ADDITIONAL_DISCOVERY_TIMEOUT = 60
-_LOGGER = logging.getLogger(__name__)
-
@dataclasses.dataclass
class Discovery:
@@ -114,7 +104,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY:
return await self.async_step_get_encryption_key_legacy()
if device.encryption_scheme == EncryptionScheme.MIBEACON_4_5:
- return await self.async_step_get_encryption_key_4_5_choose_method()
+ return await self.async_step_get_encryption_key_4_5()
return await self.async_step_bluetooth_confirm()
async def async_step_get_encryption_key_legacy(
@@ -185,67 +175,6 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
- async def async_step_cloud_auth(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle the cloud auth step."""
- assert self._discovery_info
-
- errors: dict[str, str] = {}
- description_placeholders: dict[str, str] = {}
- if user_input is not None:
- session = async_get_clientsession(self.hass)
- fetcher = XiaomiCloudTokenFetch(
- user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session
- )
- try:
- device_details = await fetcher.get_device_info(
- self._discovery_info.address
- )
- except XiaomiCloudInvalidAuthenticationException as ex:
- _LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
- errors = {"base": "auth_failed"}
- description_placeholders = {"error_detail": str(ex)}
- except XiaomiCloudException as ex:
- _LOGGER.debug("Failed to connect to MI API: %s", ex, exc_info=True)
- raise AbortFlow(
- "api_error", description_placeholders={"error_detail": str(ex)}
- ) from ex
- else:
- if device_details:
- return await self.async_step_get_encryption_key_4_5(
- {"bindkey": device_details.bindkey}
- )
- errors = {"base": "api_device_not_found"}
-
- user_input = user_input or {}
- return self.async_show_form(
- step_id="cloud_auth",
- errors=errors,
- data_schema=vol.Schema(
- {
- vol.Required(
- CONF_USERNAME, default=user_input.get(CONF_USERNAME)
- ): str,
- vol.Required(CONF_PASSWORD): str,
- }
- ),
- description_placeholders={
- **self.context["title_placeholders"],
- **description_placeholders,
- },
- )
-
- async def async_step_get_encryption_key_4_5_choose_method(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Choose method to get the bind key for a version 4/5 device."""
- return self.async_show_menu(
- step_id="get_encryption_key_4_5_choose_method",
- menu_options=["cloud_auth", "get_encryption_key_4_5"],
- description_placeholders=self.context["title_placeholders"],
- )
-
async def async_step_bluetooth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -302,7 +231,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
return await self.async_step_get_encryption_key_legacy()
if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5:
- return await self.async_step_get_encryption_key_4_5_choose_method()
+ return await self.async_step_get_encryption_key_4_5()
return self._async_get_or_create_entry()
@@ -335,6 +264,9 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle a flow initialized by a reauth event."""
+ entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+ assert entry is not None
+
device: DeviceData = entry_data["device"]
self._discovered_device = device
@@ -344,7 +276,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
return await self.async_step_get_encryption_key_legacy()
if device.encryption_scheme == EncryptionScheme.MIBEACON_4_5:
- return await self.async_step_get_encryption_key_4_5_choose_method()
+ return await self.async_step_get_encryption_key_4_5()
# Otherwise there wasn't actually encryption so abort
return self.async_abort(reason="reauth_successful")
@@ -357,10 +289,10 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
if bindkey:
data["bindkey"] = bindkey
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=data
- )
+ if entry_id := self.context.get("entry_id"):
+ entry = self.hass.config_entries.async_get_entry(entry_id)
+ assert entry is not None
+ return self.async_update_reload_and_abort(entry, data=data)
return self.async_create_entry(
title=self.context["title_placeholders"]["name"],
diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json
index 26dd82c73bc..e4c643e491e 100644
--- a/homeassistant/components/xiaomi_ble/manifest.json
+++ b/homeassistant/components/xiaomi_ble/manifest.json
@@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble",
"iot_class": "local_push",
- "requirements": ["xiaomi-ble==0.33.0"]
+ "requirements": ["xiaomi-ble==0.32.0"]
}
diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json
index 4ea4a47c61e..048c9bd92e2 100644
--- a/homeassistant/components/xiaomi_ble/strings.json
+++ b/homeassistant/components/xiaomi_ble/strings.json
@@ -25,35 +25,18 @@
"data": {
"bindkey": "Bindkey"
}
- },
- "cloud_auth": {
- "description": "Please provide your Mi app username and password. This data won't be saved and only used to retrieve the device encryption key. Usernames and passwords are case sensitive.",
- "data": {
- "username": "[%key:common::config_flow::data::username%]",
- "password": "[%key:common::config_flow::data::password%]"
- }
- },
- "get_encryption_key_4_5_choose_method": {
- "description": "A Mi device can be set up in Home Assistant in two different ways.\n\nYou can enter the bindkey yourself, or Home Assistant can import them from your Mi account.",
- "menu_options": {
- "cloud_auth": "Mi account (recommended)",
- "get_encryption_key_4_5": "Enter encryption key manually"
- }
}
},
"error": {
"decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.",
"expected_24_characters": "Expected a 24 character hexadecimal bindkey.",
- "expected_32_characters": "Expected a 32 character hexadecimal bindkey.",
- "auth_failed": "Authentication failed: {error_detail}",
- "api_device_not_found": "The device was not found in your Mi account."
+ "expected_32_characters": "Expected a 32 character hexadecimal bindkey."
},
"abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
- "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
- "api_error": "Error while communicating with Mi API: {error_detail}"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"device_automation": {
diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py
index d841045d235..9e14a3c58ba 100644
--- a/homeassistant/components/xiaomi_miio/__init__.py
+++ b/homeassistant/components/xiaomi_miio/__init__.py
@@ -56,7 +56,6 @@ from .const import (
MODEL_FAN_P9,
MODEL_FAN_P10,
MODEL_FAN_P11,
- MODEL_FAN_P18,
MODEL_FAN_ZA5,
MODELS_AIR_MONITOR,
MODELS_FAN,
@@ -119,7 +118,6 @@ MODEL_TO_CLASS_MAP = {
MODEL_FAN_P9: FanMiot,
MODEL_FAN_P10: FanMiot,
MODEL_FAN_P11: FanMiot,
- MODEL_FAN_P18: FanMiot,
MODEL_FAN_P5: FanP5,
MODEL_FAN_ZA5: FanZA5,
}
@@ -308,7 +306,6 @@ async def async_create_miio_device_and_coordinator(
"zhimi.fan.za3": True,
"zhimi.fan.za5": True,
"zhimi.airpurifier.za1": True,
- "dmaker.fan.1c": True,
}
lazy_discover = LAZY_DISCOVER_FOR_MODEL.get(model, False)
@@ -388,7 +385,6 @@ async def async_create_miio_device_and_coordinator(
coordinator = coordinator_class(
hass,
_LOGGER,
- config_entry=entry,
name=name,
update_method=update_method(hass, device),
# Polling interval. Will only be polled if there are subscribers.
@@ -454,7 +450,6 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) ->
coordinator_dict[sub_device.sid] = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name=name,
update_method=update_data_factory(sub_device),
# Polling interval. Will only be polled if there are subscribers.
diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py
index 9c06198bc7e..58d5ed247ad 100644
--- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py
+++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py
@@ -10,9 +10,13 @@ from miio import DeviceException
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMING,
+ STATE_ALARM_DISARMED,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -102,11 +106,11 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity):
self._attr_available = True
if state == XIAOMI_STATE_ARMED_VALUE:
- self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
+ self._attr_state = STATE_ALARM_ARMED_AWAY
elif state == XIAOMI_STATE_DISARMED_VALUE:
- self._attr_alarm_state = AlarmControlPanelState.DISARMED
+ self._attr_state = STATE_ALARM_DISARMED
elif state == XIAOMI_STATE_ARMING_VALUE:
- self._attr_alarm_state = AlarmControlPanelState.ARMING
+ self._attr_state = STATE_ALARM_ARMING
else:
_LOGGER.warning(
"New state (%s) doesn't match expected values: %s/%s/%s",
@@ -115,6 +119,6 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity):
XIAOMI_STATE_DISARMED_VALUE,
XIAOMI_STATE_ARMING_VALUE,
)
- self._attr_alarm_state = None
+ self._attr_state = None
- _LOGGER.debug("State value: %s", self._attr_alarm_state)
+ _LOGGER.debug("State value: %s", self._attr_state)
diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py
index b068f4a1e61..bd925b5fc54 100644
--- a/homeassistant/components/xiaomi_miio/config_flow.py
+++ b/homeassistant/components/xiaomi_miio/config_flow.py
@@ -13,6 +13,7 @@ import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import (
+ SOURCE_REAUTH,
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
@@ -63,6 +64,10 @@ DEVICE_CLOUD_CONFIG = vol.Schema(
class OptionsFlowHandler(OptionsFlow):
"""Options for the component."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Init object."""
+ self.config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -78,7 +83,14 @@ class OptionsFlowHandler(OptionsFlow):
not cloud_username or not cloud_password or not cloud_country
):
errors["base"] = "cloud_credentials_incomplete"
- self.config_entry.async_start_reauth(self.hass)
+ # trigger re-auth flow
+ self.hass.async_create_task(
+ self.hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_REAUTH},
+ data=self.config_entry.data,
+ )
+ )
if not errors:
return self.async_create_entry(title="", data=user_input)
@@ -118,7 +130,7 @@ class XiaomiMiioFlowHandler(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler:
"""Get the options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py
index 2b9cdb2ffdd..7d6cf152d7a 100644
--- a/homeassistant/components/xiaomi_miio/const.py
+++ b/homeassistant/components/xiaomi_miio/const.py
@@ -94,7 +94,6 @@ MODEL_AIRFRESH_T2017 = "dmaker.airfresh.t2017"
MODEL_FAN_1C = "dmaker.fan.1c"
MODEL_FAN_P10 = "dmaker.fan.p10"
MODEL_FAN_P11 = "dmaker.fan.p11"
-MODEL_FAN_P18 = "dmaker.fan.p18"
MODEL_FAN_P5 = "dmaker.fan.p5"
MODEL_FAN_P9 = "dmaker.fan.p9"
MODEL_FAN_SA1 = "zhimi.fan.sa1"
@@ -119,7 +118,6 @@ MODELS_FAN_MIOT = [
MODEL_FAN_1C,
MODEL_FAN_P10,
MODEL_FAN_P11,
- MODEL_FAN_P18,
MODEL_FAN_P9,
MODEL_FAN_ZA5,
]
@@ -493,7 +491,7 @@ FEATURE_FLAGS_FAN_P9 = (
| FEATURE_SET_DELAY_OFF_COUNTDOWN
)
-FEATURE_FLAGS_FAN_P10_P11_P18 = (
+FEATURE_FLAGS_FAN_P10_P11 = (
FEATURE_SET_BUZZER
| FEATURE_SET_CHILD_LOCK
| FEATURE_SET_OSCILLATION_ANGLE
diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py
index 81ca38eb053..b8f92bd89b0 100644
--- a/homeassistant/components/xiaomi_miio/fan.py
+++ b/homeassistant/components/xiaomi_miio/fan.py
@@ -60,7 +60,7 @@ from .const import (
FEATURE_FLAGS_FAN_1C,
FEATURE_FLAGS_FAN_P5,
FEATURE_FLAGS_FAN_P9,
- FEATURE_FLAGS_FAN_P10_P11_P18,
+ FEATURE_FLAGS_FAN_P10_P11,
FEATURE_FLAGS_FAN_ZA5,
FEATURE_RESET_FILTER,
FEATURE_SET_EXTRA_FEATURES,
@@ -85,7 +85,6 @@ from .const import (
MODEL_FAN_P9,
MODEL_FAN_P10,
MODEL_FAN_P11,
- MODEL_FAN_P18,
MODEL_FAN_ZA5,
MODELS_FAN_MIIO,
MODELS_FAN_MIOT,
@@ -118,10 +117,6 @@ ATTR_BUTTON_PRESSED = "button_pressed"
# Air Fresh A1
ATTR_FAVORITE_SPEED = "favorite_speed"
-# Air Purifier 3C
-ATTR_FAVORITE_RPM = "favorite_rpm"
-ATTR_MOTOR_SPEED = "motor_speed"
-
# Map attributes to properties of the state object
AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = {
ATTR_EXTRA_FEATURES: "extra_features",
@@ -613,68 +608,28 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier):
class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier):
"""Representation of a Xiaomi Air Purifier MB4."""
- def __init__(self, device, entry, unique_id, coordinator) -> None:
+ def __init__(self, device, entry, unique_id, coordinator):
"""Initialize Air Purifier MB4."""
super().__init__(device, entry, unique_id, coordinator)
self._device_features = FEATURE_FLAGS_AIRPURIFIER_3C
self._preset_modes = PRESET_MODES_AIRPURIFIER_3C
self._attr_supported_features = (
- FanEntityFeature.SET_SPEED
- | FanEntityFeature.PRESET_MODE
+ FanEntityFeature.PRESET_MODE
| FanEntityFeature.TURN_OFF
| FanEntityFeature.TURN_ON
)
self._state = self.coordinator.data.is_on
self._mode = self.coordinator.data.mode.value
- self._favorite_rpm: int | None = None
- self._speed_range = (300, 2200)
- self._motor_speed = 0
@property
def operation_mode_class(self):
"""Hold operation mode class."""
return AirpurifierMiotOperationMode
- @property
- def percentage(self) -> int | None:
- """Return the current percentage based speed."""
- # show the actual fan speed in silent or auto preset mode
- if self._mode != self.operation_mode_class["Favorite"].value:
- return ranged_value_to_percentage(self._speed_range, self._motor_speed)
- if self._favorite_rpm is None:
- return None
- if self._state:
- return ranged_value_to_percentage(self._speed_range, self._favorite_rpm)
-
- return None
-
- async def async_set_percentage(self, percentage: int) -> None:
- """Set the percentage of the fan. This method is a coroutine."""
- if percentage == 0:
- await self.async_turn_off()
- return
-
- favorite_rpm = int(
- round(percentage_to_ranged_value(self._speed_range, percentage), -1)
- )
- if not favorite_rpm:
- return
- if await self._try_command(
- "Setting fan level of the miio device failed.",
- self._device.set_favorite_rpm,
- favorite_rpm,
- ):
- self._favorite_rpm = favorite_rpm
- self._mode = self.operation_mode_class["Favorite"].value
- self.async_write_ha_state()
-
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
- if not self._state:
- await self.async_turn_on()
-
if await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
@@ -688,14 +643,6 @@ class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier):
"""Fetch state from the device."""
self._state = self.coordinator.data.is_on
self._mode = self.coordinator.data.mode.value
- self._favorite_rpm = getattr(self.coordinator.data, ATTR_FAVORITE_RPM, None)
- self._motor_speed = min(
- self._speed_range[1],
- max(
- self._speed_range[0],
- getattr(self.coordinator.data, ATTR_MOTOR_SPEED, 0),
- ),
- )
self.async_write_ha_state()
@@ -913,8 +860,8 @@ class XiaomiGenericFan(XiaomiGenericDevice):
self._device_features = FEATURE_FLAGS_FAN_1C
elif self._model == MODEL_FAN_P9:
self._device_features = FEATURE_FLAGS_FAN_P9
- elif self._model in (MODEL_FAN_P10, MODEL_FAN_P11, MODEL_FAN_P18):
- self._device_features = FEATURE_FLAGS_FAN_P10_P11_P18
+ elif self._model in (MODEL_FAN_P10, MODEL_FAN_P11):
+ self._device_features = FEATURE_FLAGS_FAN_P10_P11
else:
self._device_features = FEATURE_FLAGS_FAN
self._attr_supported_features = (
diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py
index a3c501aad3f..f8788ba07d6 100644
--- a/homeassistant/components/xiaomi_miio/number.py
+++ b/homeassistant/components/xiaomi_miio/number.py
@@ -50,7 +50,7 @@ from .const import (
FEATURE_FLAGS_FAN_1C,
FEATURE_FLAGS_FAN_P5,
FEATURE_FLAGS_FAN_P9,
- FEATURE_FLAGS_FAN_P10_P11_P18,
+ FEATURE_FLAGS_FAN_P10_P11,
FEATURE_FLAGS_FAN_ZA5,
FEATURE_SET_DELAY_OFF_COUNTDOWN,
FEATURE_SET_FAN_LEVEL,
@@ -87,7 +87,6 @@ from .const import (
MODEL_FAN_P9,
MODEL_FAN_P10,
MODEL_FAN_P11,
- MODEL_FAN_P18,
MODEL_FAN_SA1,
MODEL_FAN_V2,
MODEL_FAN_V3,
@@ -257,9 +256,8 @@ MODEL_TO_FEATURES_MAP = {
MODEL_AIRPURIFIER_4_PRO: FEATURE_FLAGS_AIRPURIFIER_4,
MODEL_AIRPURIFIER_ZA1: FEATURE_FLAGS_AIRPURIFIER_ZA1,
MODEL_FAN_1C: FEATURE_FLAGS_FAN_1C,
- MODEL_FAN_P10: FEATURE_FLAGS_FAN_P10_P11_P18,
- MODEL_FAN_P11: FEATURE_FLAGS_FAN_P10_P11_P18,
- MODEL_FAN_P18: FEATURE_FLAGS_FAN_P10_P11_P18,
+ MODEL_FAN_P10: FEATURE_FLAGS_FAN_P10_P11,
+ MODEL_FAN_P11: FEATURE_FLAGS_FAN_P10_P11,
MODEL_FAN_P5: FEATURE_FLAGS_FAN_P5,
MODEL_FAN_P9: FEATURE_FLAGS_FAN_P9,
MODEL_FAN_SA1: FEATURE_FLAGS_FAN,
@@ -277,7 +275,6 @@ OSCILLATION_ANGLE_VALUES = {
MODEL_FAN_P9: OscillationAngleValues(max_value=150, min_value=30, step=30),
MODEL_FAN_P10: OscillationAngleValues(max_value=140, min_value=30, step=30),
MODEL_FAN_P11: OscillationAngleValues(max_value=140, min_value=30, step=30),
- MODEL_FAN_P18: OscillationAngleValues(max_value=140, min_value=30, step=30),
}
FAVORITE_LEVEL_VALUES = {
diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py
index 02f4d4e94e5..8df3522b2ac 100644
--- a/homeassistant/components/xiaomi_miio/switch.py
+++ b/homeassistant/components/xiaomi_miio/switch.py
@@ -59,7 +59,7 @@ from .const import (
FEATURE_FLAGS_FAN_1C,
FEATURE_FLAGS_FAN_P5,
FEATURE_FLAGS_FAN_P9,
- FEATURE_FLAGS_FAN_P10_P11_P18,
+ FEATURE_FLAGS_FAN_P10_P11,
FEATURE_FLAGS_FAN_ZA5,
FEATURE_SET_ANION,
FEATURE_SET_AUTO_DETECT,
@@ -99,7 +99,6 @@ from .const import (
MODEL_FAN_P9,
MODEL_FAN_P10,
MODEL_FAN_P11,
- MODEL_FAN_P18,
MODEL_FAN_ZA1,
MODEL_FAN_ZA3,
MODEL_FAN_ZA4,
@@ -212,9 +211,8 @@ MODEL_TO_FEATURES_MAP = {
MODEL_AIRPURIFIER_4_PRO: FEATURE_FLAGS_AIRPURIFIER_4,
MODEL_AIRPURIFIER_ZA1: FEATURE_FLAGS_AIRPURIFIER_ZA1,
MODEL_FAN_1C: FEATURE_FLAGS_FAN_1C,
- MODEL_FAN_P10: FEATURE_FLAGS_FAN_P10_P11_P18,
- MODEL_FAN_P11: FEATURE_FLAGS_FAN_P10_P11_P18,
- MODEL_FAN_P18: FEATURE_FLAGS_FAN_P10_P11_P18,
+ MODEL_FAN_P10: FEATURE_FLAGS_FAN_P10_P11,
+ MODEL_FAN_P11: FEATURE_FLAGS_FAN_P10_P11,
MODEL_FAN_P5: FEATURE_FLAGS_FAN_P5,
MODEL_FAN_P9: FEATURE_FLAGS_FAN_P9,
MODEL_FAN_ZA1: FEATURE_FLAGS_FAN,
diff --git a/homeassistant/components/yale/config_flow.py b/homeassistant/components/yale/config_flow.py
index fecf286fdd6..6cbc9543ea4 100644
--- a/homeassistant/components/yale/config_flow.py
+++ b/homeassistant/components/yale/config_flow.py
@@ -6,7 +6,7 @@ from typing import Any
import jwt
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN
@@ -19,6 +19,7 @@ class YaleConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=
VERSION = 1
DOMAIN = DOMAIN
+ reauth_entry: ConfigEntry | None = None
@property
def logger(self) -> logging.Logger:
@@ -29,6 +30,9 @@ class YaleConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_user()
def _async_get_user_id_from_access_token(self, encoded: str) -> str:
@@ -47,11 +51,10 @@ class YaleConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=
user_id = self._async_get_user_id_from_access_token(
data["token"]["access_token"]
)
+ if entry := self.reauth_entry:
+ if entry.unique_id != user_id:
+ return self.async_abort(reason="reauth_invalid_user")
+ return self.async_update_reload_and_abort(entry, data=data)
await self.async_set_unique_id(user_id)
- if self.source == SOURCE_REAUTH:
- self._abort_if_unique_id_mismatch(reason="reauth_invalid_user")
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data=data
- )
self._abort_if_unique_id_configured()
return await super().async_oauth_create_entry(data)
diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py
index 0f5b7d0b8e5..2fc56a9e5dd 100644
--- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py
+++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py
@@ -13,12 +13,12 @@ from yalesmartalarmclient.const import (
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import StateType
from . import YaleConfigEntry
from .const import DOMAIN, STATE_MAP, YALE_ALL_ERRORS
@@ -106,6 +106,6 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity):
return super().available
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> StateType:
"""Return the state of the alarm."""
return STATE_MAP.get(self.coordinator.data["alarm"])
diff --git a/homeassistant/components/yale_smart_alarm/binary_sensor.py b/homeassistant/components/yale_smart_alarm/binary_sensor.py
index 8e68b1f0cb4..a1b94b907de 100644
--- a/homeassistant/components/yale_smart_alarm/binary_sensor.py
+++ b/homeassistant/components/yale_smart_alarm/binary_sensor.py
@@ -49,13 +49,9 @@ async def async_setup_entry(
"""Set up the Yale binary sensor entry."""
coordinator = entry.runtime_data
- sensors: list[YaleDoorSensor | YaleDoorBatterySensor | YaleProblemSensor] = [
+ sensors: list[YaleDoorSensor | YaleProblemSensor] = [
YaleDoorSensor(coordinator, data) for data in coordinator.data["door_windows"]
]
- sensors.extend(
- YaleDoorBatterySensor(coordinator, data)
- for data in coordinator.data["door_windows"]
- )
sensors.extend(
YaleProblemSensor(coordinator, description) for description in SENSOR_TYPES
)
@@ -74,27 +70,6 @@ class YaleDoorSensor(YaleEntity, BinarySensorEntity):
return bool(self.coordinator.data["sensor_map"][self._attr_unique_id] == "open")
-class YaleDoorBatterySensor(YaleEntity, BinarySensorEntity):
- """Representation of a Yale door sensor battery status."""
-
- _attr_device_class = BinarySensorDeviceClass.BATTERY
-
- def __init__(
- self,
- coordinator: YaleDataUpdateCoordinator,
- data: dict,
- ) -> None:
- """Initiate Yale door battery Sensor."""
- super().__init__(coordinator, data)
- self._attr_unique_id = f"{data["address"]}-battery"
-
- @property
- def is_on(self) -> bool:
- """Return true if the battery is low."""
- state: bool = self.coordinator.data["sensor_battery_map"][self._attr_unique_id]
- return state
-
-
class YaleProblemSensor(YaleAlarmEntity, BinarySensorEntity):
"""Representation of a Yale problem sensor."""
diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py
index c71b7b33a08..644160a8d93 100644
--- a/homeassistant/components/yale_smart_alarm/config_flow.py
+++ b/homeassistant/components/yale_smart_alarm/config_flow.py
@@ -23,8 +23,10 @@ from .const import (
CONF_AREA_ID,
CONF_LOCK_CODE_DIGITS,
DEFAULT_AREA_ID,
+ DEFAULT_LOCK_CODE_DIGITS,
DEFAULT_NAME,
DOMAIN,
+ LOGGER,
YALE_BASE_ERRORS,
)
@@ -38,67 +40,66 @@ DATA_SCHEMA = vol.Schema(
DATA_SCHEMA_AUTH = vol.Schema(
{
+ vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}
)
-OPTIONS_SCHEMA = vol.Schema(
- {
- vol.Optional(
- CONF_LOCK_CODE_DIGITS,
- ): int,
- }
-)
-
-
-def validate_credentials(username: str, password: str) -> dict[str, Any]:
- """Validate credentials."""
- errors: dict[str, str] = {}
- try:
- YaleSmartAlarmClient(username, password)
- except AuthenticationError:
- errors = {"base": "invalid_auth"}
- except YALE_BASE_ERRORS:
- errors = {"base": "cannot_connect"}
- return errors
-
class YaleConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Yale integration."""
VERSION = 2
+ entry: ConfigEntry | None
+
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> YaleOptionsFlowHandler:
"""Get the options flow for this handler."""
- return YaleOptionsFlowHandler()
+ return YaleOptionsFlowHandler(config_entry)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle initiation of re-authentication with Yale."""
+ self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
- errors: dict[str, str] = {}
+ errors = {}
if user_input is not None:
- reauth_entry = self._get_reauth_entry()
- username = reauth_entry.data[CONF_USERNAME]
+ username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
- errors = await self.hass.async_add_executor_job(
- validate_credentials, username, password
- )
- if not errors:
- return self.async_update_reload_and_abort(
- reauth_entry,
- data_updates={CONF_PASSWORD: password},
+ try:
+ await self.hass.async_add_executor_job(
+ YaleSmartAlarmClient, username, password
)
+ except AuthenticationError as error:
+ LOGGER.error("Authentication failed. Check credentials %s", error)
+ errors = {"base": "invalid_auth"}
+ except YALE_BASE_ERRORS as error:
+ LOGGER.error("Connection to API failed %s", error)
+ errors = {"base": "cannot_connect"}
+
+ if not errors:
+ existing_entry = await self.async_set_unique_id(username)
+ if existing_entry and self.entry:
+ self.hass.config_entries.async_update_entry(
+ existing_entry,
+ data={
+ **self.entry.data,
+ CONF_USERNAME: username,
+ CONF_PASSWORD: password,
+ },
+ )
+ await self.hass.config_entries.async_reload(existing_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
@@ -106,42 +107,11 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
- async def async_step_reconfigure(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Handle reconfiguration of existing entry."""
- errors: dict[str, str] = {}
-
- if user_input is not None:
- reconfigure_entry = self._get_reconfigure_entry()
- username = user_input[CONF_USERNAME]
-
- errors = await self.hass.async_add_executor_job(
- validate_credentials, username, user_input[CONF_PASSWORD]
- )
- if (
- username != reconfigure_entry.unique_id
- and await self.async_set_unique_id(username)
- ):
- errors["base"] = "unique_id_exists"
- if not errors:
- return self.async_update_reload_and_abort(
- reconfigure_entry,
- unique_id=username,
- data_updates=user_input,
- )
-
- return self.async_show_form(
- step_id="reconfigure",
- data_schema=DATA_SCHEMA,
- errors=errors,
- )
-
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
- errors: dict[str, str] = {}
+ errors = {}
if user_input is not None:
username = user_input[CONF_USERNAME]
@@ -149,9 +119,17 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN):
name = DEFAULT_NAME
area = user_input.get(CONF_AREA_ID, DEFAULT_AREA_ID)
- errors = await self.hass.async_add_executor_job(
- validate_credentials, username, password
- )
+ try:
+ await self.hass.async_add_executor_job(
+ YaleSmartAlarmClient, username, password
+ )
+ except AuthenticationError as error:
+ LOGGER.error("Authentication failed. Check credentials %s", error)
+ errors = {"base": "invalid_auth"}
+ except YALE_BASE_ERRORS as error:
+ LOGGER.error("Connection to API failed %s", error)
+ errors = {"base": "cannot_connect"}
+
if not errors:
await self.async_set_unique_id(username)
self._abort_if_unique_id_configured()
@@ -176,18 +154,32 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN):
class YaleOptionsFlowHandler(OptionsFlow):
"""Handle Yale options."""
+ def __init__(self, entry: ConfigEntry) -> None:
+ """Initialize Yale options flow."""
+ self.entry = entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage Yale options."""
+ errors: dict[str, Any] = {}
- if user_input is not None:
+ if user_input:
return self.async_create_entry(data=user_input)
return self.async_show_form(
step_id="init",
- data_schema=self.add_suggested_values_to_schema(
- OPTIONS_SCHEMA,
- self.config_entry.options,
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_LOCK_CODE_DIGITS,
+ description={
+ "suggested_value": self.entry.options.get(
+ CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS
+ )
+ },
+ ): int,
+ }
),
+ errors=errors,
)
diff --git a/homeassistant/components/yale_smart_alarm/const.py b/homeassistant/components/yale_smart_alarm/const.py
index 14e31268ec9..41a754e4ce7 100644
--- a/homeassistant/components/yale_smart_alarm/const.py
+++ b/homeassistant/components/yale_smart_alarm/const.py
@@ -9,8 +9,12 @@ from yalesmartalarmclient.client import (
)
from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError
-from homeassistant.components.alarm_control_panel import AlarmControlPanelState
-from homeassistant.const import Platform
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
+ Platform,
+)
CONF_AREA_ID = "area_id"
CONF_LOCK_CODE_DIGITS = "lock_code_digits"
@@ -41,9 +45,9 @@ PLATFORMS = [
]
STATE_MAP = {
- YALE_STATE_DISARM: AlarmControlPanelState.DISARMED,
- YALE_STATE_ARM_PARTIAL: AlarmControlPanelState.ARMED_HOME,
- YALE_STATE_ARM_FULL: AlarmControlPanelState.ARMED_AWAY,
+ YALE_STATE_DISARM: STATE_ALARM_DISARMED,
+ YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME,
+ YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY,
}
YALE_BASE_ERRORS = (
diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py
index 66bd71c9f1e..911b4523fc4 100644
--- a/homeassistant/components/yale_smart_alarm/coordinator.py
+++ b/homeassistant/components/yale_smart_alarm/coordinator.py
@@ -60,9 +60,6 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
for device in updates["cycle"]["device_status"]:
state = device["status1"]
if device["type"] == "device_type.door_contact":
- device["_battery"] = False
- if "device_status.low_battery" in state:
- device["_battery"] = True
if "device_status.dc_close" in state:
device["_state"] = "closed"
door_windows.append(device)
@@ -80,10 +77,6 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
_sensor_map = {
contact["address"]: contact["_state"] for contact in door_windows
}
- _sensor_battery_map = {
- f"{contact["address"]}-battery": contact["_battery"]
- for contact in door_windows
- }
_temp_map = {temp["address"]: temp["status_temp"] for temp in temp_sensors}
return {
@@ -93,7 +86,6 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"status": updates["status"],
"online": updates["online"],
"sensor_map": _sensor_map,
- "sensor_battery_map": _sensor_battery_map,
"temp_map": _temp_map,
"panel_info": updates["panel_info"],
}
diff --git a/homeassistant/components/yale_smart_alarm/strings.json b/homeassistant/components/yale_smart_alarm/strings.json
index 7f940e1139e..8bade77f5f6 100644
--- a/homeassistant/components/yale_smart_alarm/strings.json
+++ b/homeassistant/components/yale_smart_alarm/strings.json
@@ -2,13 +2,11 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
- "unique_id_exists": "Another config entry with this username already exist"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"user": {
@@ -20,14 +18,10 @@
}
},
"reauth_confirm": {
- "data": {
- "password": "[%key:common::config_flow::data::password%]"
- }
- },
- "reconfigure": {
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
+ "name": "[%key:common::config_flow::data::name%]",
"area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]"
}
}
diff --git a/homeassistant/components/yalexs_ble/config_flow.py b/homeassistant/components/yalexs_ble/config_flow.py
index 6de74759686..7b69e417de7 100644
--- a/homeassistant/components/yalexs_ble/config_flow.py
+++ b/homeassistant/components/yalexs_ble/config_flow.py
@@ -78,6 +78,7 @@ class YalexsConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovery_info: BluetoothServiceInfoBleak | None = None
self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {}
self._lock_cfg: ValidatedLockConfig | None = None
+ self._reauth_entry: ConfigEntry | None = None
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
@@ -193,6 +194,9 @@ class YalexsConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_validate()
async def async_step_reauth_validate(
@@ -200,7 +204,8 @@ class YalexsConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle reauth and validation."""
errors = {}
- reauth_entry = self._get_reauth_entry()
+ reauth_entry = self._reauth_entry
+ assert reauth_entry is not None
if user_input is not None:
if (
device := async_ble_device_from_address(
@@ -217,7 +222,7 @@ class YalexsConfigFlow(ConfigFlow, domain=DOMAIN):
)
):
return self.async_update_reload_and_abort(
- reauth_entry, data_updates=user_input
+ reauth_entry, data={**reauth_entry.data, **user_input}
)
return self.async_show_form(
@@ -312,12 +317,16 @@ class YalexsConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> YaleXSBLEOptionsFlowHandler:
"""Get the options flow for this handler."""
- return YaleXSBLEOptionsFlowHandler()
+ return YaleXSBLEOptionsFlowHandler(config_entry)
class YaleXSBLEOptionsFlowHandler(OptionsFlow):
"""Handle YaleXSBLE options."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize YaleXSBLE options flow."""
+ self.entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -339,9 +348,7 @@ class YaleXSBLEOptionsFlowHandler(OptionsFlow):
{
vol.Optional(
CONF_ALWAYS_CONNECTED,
- default=self.config_entry.options.get(
- CONF_ALWAYS_CONNECTED, False
- ),
+ default=self.entry.options.get(CONF_ALWAYS_CONNECTED, False),
): bool,
}
),
diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py
index 7a3a0a2f100..5438414ea61 100644
--- a/homeassistant/components/yeelight/config_flow.py
+++ b/homeassistant/components/yeelight/config_flow.py
@@ -58,11 +58,9 @@ class YeelightConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
- def async_get_options_flow(
- config_entry: ConfigEntry,
- ) -> OptionsFlowHandler:
+ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler:
"""Return the options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -298,12 +296,16 @@ class YeelightConfigFlow(ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(OptionsFlow):
"""Handle a option flow for Yeelight."""
+ def __init__(self, config_entry: ConfigEntry) -> None:
+ """Initialize the option flow."""
+ self._config_entry = config_entry
+
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
- data = self.config_entry.data
- options = self.config_entry.options
+ data = self._config_entry.data
+ options = self._config_entry.options
detected_model = data.get(CONF_DETECTED_MODEL)
model = options[CONF_MODEL] or detected_model
diff --git a/homeassistant/components/yolink/config_flow.py b/homeassistant/components/yolink/config_flow.py
index 2e96dcf9f8c..abdac696248 100644
--- a/homeassistant/components/yolink/config_flow.py
+++ b/homeassistant/components/yolink/config_flow.py
@@ -6,7 +6,7 @@ from collections.abc import Mapping
import logging
from typing import Any
-from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
+from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN
@@ -18,6 +18,7 @@ class OAuth2FlowHandler(
"""Config flow to handle yolink OAuth2 authentication."""
DOMAIN = DOMAIN
+ _reauth_entry: ConfigEntry | None = None
@property
def logger(self) -> logging.Logger:
@@ -34,6 +35,9 @@ class OAuth2FlowHandler(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self._reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None) -> ConfigFlowResult:
@@ -44,10 +48,12 @@ class OAuth2FlowHandler(
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
"""Create an oauth config entry or update existing entry for reauth."""
- if self.source == SOURCE_REAUTH:
- return self.async_update_reload_and_abort(
- self._get_reauth_entry(), data_updates=data
+ if existing_entry := self._reauth_entry:
+ self.hass.config_entries.async_update_entry(
+ existing_entry, data=existing_entry.data | data
)
+ await self.hass.config_entries.async_reload(existing_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title="YoLink", data=data)
async def async_step_user(
@@ -55,6 +61,6 @@ class OAuth2FlowHandler(
) -> ConfigFlowResult:
"""Handle a flow start."""
existing_entry = await self.async_set_unique_id(DOMAIN)
- if existing_entry and self.source != SOURCE_REAUTH:
+ if existing_entry and not self._reauth_entry:
return self.async_abort(reason="already_configured")
return await super().async_step_user(user_input)
diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json
index 2f9a9454502..cefc7737a79 100644
--- a/homeassistant/components/yolink/strings.json
+++ b/homeassistant/components/yolink/strings.json
@@ -19,8 +19,7 @@
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
- "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
- "missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]"
+ "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
diff --git a/homeassistant/components/youless/__init__.py b/homeassistant/components/youless/__init__.py
index d475034cc9d..a968d052922 100644
--- a/homeassistant/components/youless/__init__.py
+++ b/homeassistant/components/youless/__init__.py
@@ -36,7 +36,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
- config_entry=entry,
name="youless_gateway",
update_method=async_update_data,
update_interval=timedelta(seconds=10),
diff --git a/homeassistant/components/youtube/config_flow.py b/homeassistant/components/youtube/config_flow.py
index 48336422585..32b37b93eb2 100644
--- a/homeassistant/components/youtube/config_flow.py
+++ b/homeassistant/components/youtube/config_flow.py
@@ -12,10 +12,9 @@ from youtubeaio.types import AuthScope, ForbiddenError
from youtubeaio.youtube import YouTube
from homeassistant.config_entries import (
- SOURCE_REAUTH,
ConfigEntry,
ConfigFlowResult,
- OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.core import callback
@@ -46,6 +45,7 @@ class OAuth2FlowHandler(
DOMAIN = DOMAIN
+ reauth_entry: ConfigEntry | None = None
_youtube: YouTube | None = None
@staticmethod
@@ -54,7 +54,7 @@ class OAuth2FlowHandler(
config_entry: ConfigEntry,
) -> YouTubeOptionsFlowHandler:
"""Get the options flow for this handler."""
- return YouTubeOptionsFlowHandler()
+ return YouTubeOptionsFlowHandler(config_entry)
@property
def logger(self) -> logging.Logger:
@@ -75,6 +75,9 @@ class OAuth2FlowHandler(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
+ self.reauth_entry = self.hass.config_entries.async_get_entry(
+ self.context["entry_id"]
+ )
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
@@ -114,19 +117,22 @@ class OAuth2FlowHandler(
self._title = own_channel.snippet.title
self._data = data
- await self.async_set_unique_id(own_channel.channel_id)
- if self.source != SOURCE_REAUTH:
+ if not self.reauth_entry:
+ await self.async_set_unique_id(own_channel.channel_id)
self._abort_if_unique_id_configured()
return await self.async_step_channels()
- self._abort_if_unique_id_mismatch(
+ if self.reauth_entry.unique_id == own_channel.channel_id:
+ self.hass.config_entries.async_update_entry(self.reauth_entry, data=data)
+ await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+
+ return self.async_abort(
reason="wrong_account",
description_placeholders={"title": self._title},
)
- return self.async_update_reload_and_abort(self._get_reauth_entry(), data=data)
-
async def async_step_channels(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -159,7 +165,7 @@ class OAuth2FlowHandler(
)
-class YouTubeOptionsFlowHandler(OptionsFlow):
+class YouTubeOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""YouTube Options flow handler."""
async def async_step_init(
@@ -194,6 +200,6 @@ class YouTubeOptionsFlowHandler(OptionsFlow):
),
}
),
- self.config_entry.options,
+ self.options,
),
)
diff --git a/homeassistant/components/youtube/strings.json b/homeassistant/components/youtube/strings.json
index 78ca0532459..5902d3a4482 100644
--- a/homeassistant/components/youtube/strings.json
+++ b/homeassistant/components/youtube/strings.json
@@ -10,8 +10,7 @@
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
- "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
- "wrong_account": "Wrong account: please authenticate with the right account."
+ "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]"
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json
index 98b09f1a251..8246085e405 100644
--- a/homeassistant/components/zeroconf/manifest.json
+++ b/homeassistant/components/zeroconf/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["zeroconf"],
"quality_scale": "internal",
- "requirements": ["zeroconf==0.136.0"]
+ "requirements": ["zeroconf==0.135.0"]
}
diff --git a/homeassistant/components/zeroconf/usage.py b/homeassistant/components/zeroconf/usage.py
index 8ddfdbd592d..b9d51cd3c36 100644
--- a/homeassistant/components/zeroconf/usage.py
+++ b/homeassistant/components/zeroconf/usage.py
@@ -4,7 +4,7 @@ from typing import Any
import zeroconf
-from homeassistant.helpers.frame import ReportBehavior, report_usage
+from homeassistant.helpers.frame import report
from .models import HaZeroconf
@@ -16,14 +16,14 @@ def install_multiple_zeroconf_catcher(hass_zc: HaZeroconf) -> None:
"""
def new_zeroconf_new(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> HaZeroconf:
- report_usage(
+ report(
(
"attempted to create another Zeroconf instance. Please use the shared"
" Zeroconf via await"
" homeassistant.components.zeroconf.async_get_instance(hass)"
),
exclude_integrations={"zeroconf"},
- core_behavior=ReportBehavior.LOG,
+ error_if_core=False,
)
return hass_zc
diff --git a/homeassistant/components/zeversolar/manifest.json b/homeassistant/components/zeversolar/manifest.json
index 18bab34c04e..af197b3aa7c 100644
--- a/homeassistant/components/zeversolar/manifest.json
+++ b/homeassistant/components/zeversolar/manifest.json
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/zeversolar",
"integration_type": "device",
"iot_class": "local_polling",
- "requirements": ["zeversolar==0.3.2"]
+ "requirements": ["zeversolar==0.3.1"]
}
diff --git a/homeassistant/components/zha/alarm_control_panel.py b/homeassistant/components/zha/alarm_control_panel.py
index 734683e5497..c54d7c7ab2d 100644
--- a/homeassistant/components/zha/alarm_control_panel.py
+++ b/homeassistant/components/zha/alarm_control_panel.py
@@ -4,14 +4,9 @@ from __future__ import annotations
import functools
-from zha.application.platforms.alarm_control_panel.const import (
- AlarmState as ZHAAlarmState,
-)
-
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
CodeFormat,
)
from homeassistant.config_entries import ConfigEntry
@@ -28,20 +23,6 @@ from .helpers import (
get_zha_data,
)
-ZHA_STATE_TO_ALARM_STATE_MAP = {
- ZHAAlarmState.DISARMED.value: AlarmControlPanelState.DISARMED,
- ZHAAlarmState.ARMED_HOME.value: AlarmControlPanelState.ARMED_HOME,
- ZHAAlarmState.ARMED_AWAY.value: AlarmControlPanelState.ARMED_AWAY,
- ZHAAlarmState.ARMED_NIGHT.value: AlarmControlPanelState.ARMED_NIGHT,
- ZHAAlarmState.ARMED_VACATION.value: AlarmControlPanelState.ARMED_VACATION,
- ZHAAlarmState.ARMED_CUSTOM_BYPASS.value: AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
- ZHAAlarmState.PENDING.value: AlarmControlPanelState.PENDING,
- ZHAAlarmState.ARMING.value: AlarmControlPanelState.ARMING,
- ZHAAlarmState.DISARMING.value: AlarmControlPanelState.DISARMING,
- ZHAAlarmState.TRIGGERED.value: AlarmControlPanelState.TRIGGERED,
- ZHAAlarmState.UNKNOWN.value: None,
-}
-
async def async_setup_entry(
hass: HomeAssistant,
@@ -113,6 +94,6 @@ class ZHAAlarmControlPanel(ZHAEntity, AlarmControlPanelEntity):
self.async_write_ha_state()
@property
- def alarm_state(self) -> AlarmControlPanelState | None:
+ def state(self) -> str | None:
"""Return the state of the entity."""
- return ZHA_STATE_TO_ALARM_STATE_MAP.get(self.entity_data.entity.state["state"])
+ return self.entity_data.entity.state["state"]
diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py
index f3f7f38772d..20eb006eb74 100644
--- a/homeassistant/components/zha/config_flow.py
+++ b/homeassistant/components/zha/config_flow.py
@@ -33,7 +33,6 @@ from homeassistant.config_entries import (
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.selector import FileSelector, FileSelectorConfig
from homeassistant.util import dt as dt_util
@@ -105,26 +104,25 @@ async def list_serial_ports(hass: HomeAssistant) -> list[ListPortInfo]:
yellow_radio.description = "Yellow Zigbee module"
yellow_radio.manufacturer = "Nabu Casa"
- if is_hassio(hass):
- # Present the multi-PAN addon as a setup option, if it's available
- multipan_manager = (
- await silabs_multiprotocol_addon.get_multiprotocol_addon_manager(hass)
+ # Present the multi-PAN addon as a setup option, if it's available
+ multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager(
+ hass
+ )
+
+ try:
+ addon_info = await multipan_manager.async_get_addon_info()
+ except (AddonError, KeyError):
+ addon_info = None
+
+ if addon_info is not None and addon_info.state != AddonState.NOT_INSTALLED:
+ addon_port = ListPortInfo(
+ device=silabs_multiprotocol_addon.get_zigbee_socket(),
+ skip_link_detection=True,
)
- try:
- addon_info = await multipan_manager.async_get_addon_info()
- except (AddonError, KeyError):
- addon_info = None
-
- if addon_info is not None and addon_info.state != AddonState.NOT_INSTALLED:
- addon_port = ListPortInfo(
- device=silabs_multiprotocol_addon.get_zigbee_socket(),
- skip_link_detection=True,
- )
-
- addon_port.description = "Multiprotocol add-on"
- addon_port.manufacturer = "Nabu Casa"
- ports.append(addon_port)
+ addon_port.description = "Multiprotocol add-on"
+ addon_port.manufacturer = "Nabu Casa"
+ ports.append(addon_port)
return ports
@@ -682,6 +680,8 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
super().__init__()
+ self.config_entry = config_entry
+
self._radio_mgr.device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
self._radio_mgr.device_settings = config_entry.data[CONF_DEVICE]
self._radio_mgr.radio_type = RadioType[config_entry.data[CONF_RADIO_TYPE]]
diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py
index 2440e18cf53..06899296991 100644
--- a/homeassistant/components/zha/helpers.py
+++ b/homeassistant/components/zha/helpers.py
@@ -104,7 +104,7 @@ from homeassistant.const import (
ATTR_NAME,
Platform,
)
-from homeassistant.core import Event, HomeAssistant, callback
+from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
@@ -495,7 +495,7 @@ class ZHAGatewayProxy(EventBase):
self.hass = hass
self.config_entry = config_entry
self.gateway = gateway
- self.device_proxies: dict[EUI64, ZHADeviceProxy] = {}
+ self.device_proxies: dict[str, ZHADeviceProxy] = {}
self.group_proxies: dict[int, ZHAGroupProxy] = {}
self._ha_entity_refs: collections.defaultdict[EUI64, list[EntityReference]] = (
collections.defaultdict(list)
@@ -509,12 +509,6 @@ class ZHAGatewayProxy(EventBase):
self._unsubs: list[Callable[[], None]] = []
self._unsubs.append(self.gateway.on_all_events(self._handle_event_protocol))
self._reload_task: asyncio.Task | None = None
- config_entry.async_on_unload(
- self.hass.bus.async_listen(
- er.EVENT_ENTITY_REGISTRY_UPDATED,
- self._handle_entity_registry_updated,
- )
- )
@property
def ha_entity_refs(self) -> collections.defaultdict[EUI64, list[EntityReference]]:
@@ -538,46 +532,6 @@ class ZHAGatewayProxy(EventBase):
)
)
- async def _handle_entity_registry_updated(
- self, event: Event[er.EventEntityRegistryUpdatedData]
- ) -> None:
- """Handle when entity registry updated."""
- entity_id = event.data["entity_id"]
- entity_entry: er.RegistryEntry | None = er.async_get(self.hass).async_get(
- entity_id
- )
- if (
- entity_entry is None
- or entity_entry.config_entry_id != self.config_entry.entry_id
- or entity_entry.device_id is None
- ):
- return
- device_entry: dr.DeviceEntry | None = dr.async_get(self.hass).async_get(
- entity_entry.device_id
- )
- assert device_entry
-
- ieee_address = next(
- identifier
- for domain, identifier in device_entry.identifiers
- if domain == DOMAIN
- )
- assert ieee_address
-
- ieee = EUI64.convert(ieee_address)
-
- assert ieee in self.device_proxies
-
- zha_device_proxy = self.device_proxies[ieee]
- entity_key = (entity_entry.domain, entity_entry.unique_id)
- if entity_key not in zha_device_proxy.device.platform_entities:
- return
- platform_entity = zha_device_proxy.device.platform_entities[entity_key]
- if entity_entry.disabled:
- platform_entity.disable()
- else:
- platform_entity.enable()
-
async def async_initialize_devices_and_entities(self) -> None:
"""Initialize devices and entities."""
for device in self.gateway.devices.values():
@@ -1163,7 +1117,7 @@ def async_add_entities(
if not entities:
return
- entities_to_add: list[ZHAEntity] = []
+ entities_to_add = []
for entity_data in entities:
try:
entities_to_add.append(entity_class(entity_data))
@@ -1175,9 +1129,6 @@ def async_add_entities(
"Error while adding entity from entity data: %s", entity_data
)
_async_add_entities(entities_to_add, update_before_add=False)
- for entity in entities_to_add:
- if not entity.enabled:
- entity.entity_data.entity.disable()
entities.clear()
@@ -1247,7 +1198,7 @@ def create_zha_config(hass: HomeAssistant, ha_zha_data: HAZHAData) -> ZHAData:
# deep copy the yaml config to avoid modifying the original and to safely
# pass it to the ZHA library
app_config = copy.deepcopy(ha_zha_data.yaml_config.get(CONF_ZIGPY, {}))
- database = ha_zha_data.yaml_config.get(
+ database = app_config.get(
CONF_DATABASE,
hass.config.path(DEFAULT_DATABASE_NAME),
)
diff --git a/homeassistant/components/zha/icons.json b/homeassistant/components/zha/icons.json
index 5b3b85ced39..9d5254fe237 100644
--- a/homeassistant/components/zha/icons.json
+++ b/homeassistant/components/zha/icons.json
@@ -45,15 +45,6 @@
"maximum_level": {
"default": "mdi:brightness-percent"
},
- "default_level_local": {
- "default": "mdi:brightness-percent"
- },
- "default_level_remote": {
- "default": "mdi:brightness-percent"
- },
- "state_after_power_restored": {
- "default": "mdi:brightness-percent"
- },
"auto_off_timer": {
"default": "mdi:timer"
},
diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py
index 9a22dfb02e9..fa83ad1cab6 100644
--- a/homeassistant/components/zha/light.py
+++ b/homeassistant/components/zha/light.py
@@ -44,7 +44,12 @@ ZHA_TO_HA_COLOR_MODE = {
ZhaColorMode.ONOFF: ColorMode.ONOFF,
ZhaColorMode.BRIGHTNESS: ColorMode.BRIGHTNESS,
ZhaColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
+ ZhaColorMode.HS: ColorMode.HS,
ZhaColorMode.XY: ColorMode.XY,
+ ZhaColorMode.RGB: ColorMode.RGB,
+ ZhaColorMode.RGBW: ColorMode.RGBW,
+ ZhaColorMode.RGBWW: ColorMode.RGBWW,
+ ZhaColorMode.WHITE: ColorMode.WHITE,
}
HA_TO_ZHA_COLOR_MODE = {v: k for k, v in ZHA_TO_HA_COLOR_MODE.items()}
diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json
index 96c9bc030f6..dd15fb99960 100644
--- a/homeassistant/components/zha/manifest.json
+++ b/homeassistant/components/zha/manifest.json
@@ -1,7 +1,7 @@
{
"domain": "zha",
"name": "Zigbee Home Automation",
- "after_dependencies": ["hassio", "onboarding", "usb"],
+ "after_dependencies": ["onboarding", "usb"],
"codeowners": ["@dmulcahey", "@adminiuga", "@puddly", "@TheJulianJES"],
"config_flow": true,
"dependencies": ["file_upload"],
@@ -21,7 +21,7 @@
"zha",
"universal_silabs_flasher"
],
- "requirements": ["universal-silabs-flasher==0.0.24", "zha==0.0.37"],
+ "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.34"],
"usb": [
{
"vid": "10C4",
diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json
index d0505bf2460..6123081fcd7 100644
--- a/homeassistant/components/zha/strings.json
+++ b/homeassistant/components/zha/strings.json
@@ -708,15 +708,6 @@
"maximum_level": {
"name": "Maximum load dimming level"
},
- "default_level_local": {
- "name": "Local default dimming level"
- },
- "default_level_remote": {
- "name": "Remote default dimming level"
- },
- "state_after_power_restored": {
- "name": "Start-up default dimming level"
- },
"auto_off_timer": {
"name": "Automatic switch shutoff timer"
},
@@ -776,21 +767,6 @@
},
"regulation_setpoint_offset": {
"name": "Regulation setpoint offset"
- },
- "irrigation_cycles": {
- "name": "Irrigation cycles"
- },
- "irrigation_target": {
- "name": "Irrigation target"
- },
- "irrigation_interval": {
- "name": "Irrigation interval"
- },
- "valve_countdown_1": {
- "name": "Irrigation time 1"
- },
- "valve_countdown_2": {
- "name": "Irrigation time 2"
}
},
"select": {
@@ -842,9 +818,6 @@
"increased_non_neutral_output": {
"name": "Non neutral output"
},
- "leading_or_trailing_edge": {
- "name": "Dimming mode"
- },
"feeding_mode": {
"name": "Mode"
},
@@ -880,12 +853,6 @@
},
"setpoint_response_time": {
"name": "Setpoint response time"
- },
- "irrigation_mode": {
- "name": "Irrigation mode"
- },
- "weather_delay": {
- "name": "Weather delay"
}
},
"sensor": {
@@ -931,12 +898,6 @@
"device_temperature": {
"name": "Device temperature"
},
- "internal_temp_monitor": {
- "name": "Internal temperature"
- },
- "overheated": {
- "name": "Overheat protection"
- },
"formaldehyde": {
"name": "Formaldehyde concentration"
},
@@ -1062,27 +1023,6 @@
},
"motor_stepcount": {
"name": "Motor stepcount"
- },
- "irrigation_duration": {
- "name": "Last irrigation duration"
- },
- "irrigation_start_time": {
- "name": "Irrigation start time"
- },
- "irrigation_end_time": {
- "name": "Irrigation end time"
- },
- "irrigation_duration_1": {
- "name": "Irrigation duration 1"
- },
- "irriation_duration_2": {
- "name": "Irrigation duration 2"
- },
- "valve_status_1": {
- "name": "Status 1"
- },
- "valve_status_2": {
- "name": "Status 2"
}
},
"switch": {
@@ -1187,12 +1127,6 @@
},
"adaptation_run_enabled": {
"name": "Adaptation run enabled"
- },
- "valve_on_off_1": {
- "name": "Valve 1"
- },
- "valve_on_off_2": {
- "name": "Valve 2"
}
}
}
diff --git a/homeassistant/components/zhong_hong/manifest.json b/homeassistant/components/zhong_hong/manifest.json
index 9da0e9ab72b..06cc06faf0b 100644
--- a/homeassistant/components/zhong_hong/manifest.json
+++ b/homeassistant/components/zhong_hong/manifest.json
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/zhong_hong",
"iot_class": "local_push",
"loggers": ["zhong_hong_hvac"],
- "requirements": ["zhong-hong-hvac==1.0.13"]
+ "requirements": ["zhong-hong-hvac==1.0.12"]
}
diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py
index bd49e85b601..b43528fe358 100644
--- a/homeassistant/components/zwave_js/api.py
+++ b/homeassistant/components/zwave_js/api.py
@@ -13,10 +13,8 @@ from zwave_js_server.client import Client
from zwave_js_server.const import (
CommandClass,
ExclusionStrategy,
- InclusionState,
InclusionStrategy,
LogLevel,
- NodeStatus,
Protocols,
ProvisioningEntryStatus,
QRCodeVersion,
@@ -43,7 +41,6 @@ from zwave_js_server.model.controller.firmware import (
ControllerFirmwareUpdateResult,
)
from zwave_js_server.model.driver import Driver
-from zwave_js_server.model.endpoint import Endpoint
from zwave_js_server.model.log_config import LogConfig
from zwave_js_server.model.log_message import LogMessage
from zwave_js_server.model.node import Node, NodeStatistics
@@ -56,7 +53,6 @@ from zwave_js_server.model.utils import (
async_parse_qr_code_string,
async_try_parse_dsk_from_qr_code_string,
)
-from zwave_js_server.model.value import ConfigurationValueFormat
from zwave_js_server.util.node import async_set_config_parameter
from homeassistant.components import websocket_api
@@ -77,11 +73,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .config_validation import BITMASK_SCHEMA
from .const import (
- ATTR_COMMAND_CLASS,
- ATTR_ENDPOINT,
- ATTR_METHOD_NAME,
- ATTR_PARAMETERS,
- ATTR_WAIT_FOR_RESULT,
CONF_DATA_COLLECTION_OPTED_IN,
DATA_CLIENT,
EVENT_DEVICE_ADDED_TO_REGISTRY,
@@ -107,8 +98,6 @@ PROPERTY = "property"
PROPERTY_KEY = "property_key"
ENDPOINT = "endpoint"
VALUE = "value"
-VALUE_SIZE = "value_size"
-VALUE_FORMAT = "value_format"
# constants for log config commands
CONFIG = "config"
@@ -419,8 +408,6 @@ def async_register_api(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, websocket_rebuild_node_routes)
websocket_api.async_register_command(hass, websocket_set_config_parameter)
websocket_api.async_register_command(hass, websocket_get_config_parameters)
- websocket_api.async_register_command(hass, websocket_get_raw_config_parameter)
- websocket_api.async_register_command(hass, websocket_set_raw_config_parameter)
websocket_api.async_register_command(hass, websocket_subscribe_log_updates)
websocket_api.async_register_command(hass, websocket_update_log_config)
websocket_api.async_register_command(hass, websocket_get_log_config)
@@ -448,8 +435,6 @@ def async_register_api(hass: HomeAssistant) -> None:
)
websocket_api.async_register_command(hass, websocket_subscribe_node_statistics)
websocket_api.async_register_command(hass, websocket_hard_reset_controller)
- websocket_api.async_register_command(hass, websocket_node_capabilities)
- websocket_api.async_register_command(hass, websocket_invoke_cc_api)
hass.http.register_view(FirmwareUploadView(dr.async_get(hass)))
@@ -708,30 +693,6 @@ async def websocket_add_node(
)
)
- @callback
- def forward_node_added(
- node: Node, low_security: bool, low_security_reason: str | None
- ) -> None:
- interview_unsubs = [
- node.on("interview started", forward_event),
- node.on("interview completed", forward_event),
- node.on("interview stage completed", forward_stage),
- node.on("interview failed", forward_event),
- ]
- unsubs.extend(interview_unsubs)
- node_details = {
- "node_id": node.node_id,
- "status": node.status,
- "ready": node.ready,
- "low_security": low_security,
- "low_security_reason": low_security_reason,
- }
- connection.send_message(
- websocket_api.event_message(
- msg[ID], {"event": "node added", "node": node_details}
- )
- )
-
@callback
def forward_requested_grant(event: dict) -> None:
connection.send_message(
@@ -766,10 +727,24 @@ async def websocket_add_node(
@callback
def node_added(event: dict) -> None:
- forward_node_added(
- event["node"],
- event["result"].get("lowSecurity", False),
- event["result"].get("lowSecurityReason"),
+ node = event["node"]
+ interview_unsubs = [
+ node.on("interview started", forward_event),
+ node.on("interview completed", forward_event),
+ node.on("interview stage completed", forward_stage),
+ node.on("interview failed", forward_event),
+ ]
+ unsubs.extend(interview_unsubs)
+ node_details = {
+ "node_id": node.node_id,
+ "status": node.status,
+ "ready": node.ready,
+ "low_security": event["result"].get("lowSecurity", False),
+ }
+ connection.send_message(
+ websocket_api.event_message(
+ msg[ID], {"event": "node added", "node": node_details}
+ )
)
@callback
@@ -801,39 +776,25 @@ async def websocket_add_node(
]
msg[DATA_UNSUBSCRIBE] = unsubs
- if controller.inclusion_state == InclusionState.INCLUDING:
- connection.send_result(
- msg[ID],
- True, # Inclusion is already in progress
+ try:
+ result = await controller.async_begin_inclusion(
+ INCLUSION_STRATEGY_NOT_SMART_START[inclusion_strategy.value],
+ force_security=force_security,
+ provisioning=provisioning,
+ dsk=dsk,
)
- # Check for nodes that have been added but not fully included
- for node in controller.nodes.values():
- if node.status != NodeStatus.DEAD and not node.ready:
- forward_node_added(
- node,
- not node.is_secure,
- None,
- )
- else:
- try:
- result = await controller.async_begin_inclusion(
- INCLUSION_STRATEGY_NOT_SMART_START[inclusion_strategy.value],
- force_security=force_security,
- provisioning=provisioning,
- dsk=dsk,
- )
- except ValueError as err:
- connection.send_error(
- msg[ID],
- ERR_INVALID_FORMAT,
- err.args[0],
- )
- return
+ except ValueError as err:
+ connection.send_error(
+ msg[ID],
+ ERR_INVALID_FORMAT,
+ err.args[0],
+ )
+ return
- connection.send_result(
- msg[ID],
- result,
- )
+ connection.send_result(
+ msg[ID],
+ result,
+ )
@websocket_api.require_admin
@@ -1765,72 +1726,6 @@ async def websocket_get_config_parameters(
)
-@websocket_api.require_admin
-@websocket_api.websocket_command(
- {
- vol.Required(TYPE): "zwave_js/set_raw_config_parameter",
- vol.Required(DEVICE_ID): str,
- vol.Required(PROPERTY): int,
- vol.Required(VALUE): int,
- vol.Required(VALUE_SIZE): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
- vol.Required(VALUE_FORMAT): vol.Coerce(ConfigurationValueFormat),
- }
-)
-@websocket_api.async_response
-@async_handle_failed_command
-@async_get_node
-async def websocket_set_raw_config_parameter(
- hass: HomeAssistant,
- connection: ActiveConnection,
- msg: dict[str, Any],
- node: Node,
-) -> None:
- """Set a custom config parameter value for a Z-Wave node."""
- result = await node.async_set_raw_config_parameter_value(
- msg[VALUE],
- msg[PROPERTY],
- value_size=msg[VALUE_SIZE],
- value_format=msg[VALUE_FORMAT],
- )
-
- connection.send_result(
- msg[ID],
- {
- STATUS: result.status,
- },
- )
-
-
-@websocket_api.require_admin
-@websocket_api.websocket_command(
- {
- vol.Required(TYPE): "zwave_js/get_raw_config_parameter",
- vol.Required(DEVICE_ID): str,
- vol.Required(PROPERTY): int,
- }
-)
-@websocket_api.async_response
-@async_handle_failed_command
-@async_get_node
-async def websocket_get_raw_config_parameter(
- hass: HomeAssistant,
- connection: ActiveConnection,
- msg: dict[str, Any],
- node: Node,
-) -> None:
- """Get a custom config parameter value for a Z-Wave node."""
- value = await node.async_get_raw_config_parameter_value(
- msg[PROPERTY],
- )
-
- connection.send_result(
- msg[ID],
- {
- VALUE: value,
- },
- )
-
-
def filename_is_present_if_logging_to_file(obj: dict) -> dict:
"""Validate that filename is provided if log_to_file is True."""
if obj.get(LOG_TO_FILE, False) and FILENAME not in obj:
@@ -2604,81 +2499,3 @@ async def websocket_hard_reset_controller(
)
]
await driver.async_hard_reset()
-
-
-@websocket_api.websocket_command(
- {
- vol.Required(TYPE): "zwave_js/node_capabilities",
- vol.Required(DEVICE_ID): str,
- }
-)
-@websocket_api.async_response
-@async_handle_failed_command
-@async_get_node
-async def websocket_node_capabilities(
- hass: HomeAssistant,
- connection: ActiveConnection,
- msg: dict[str, Any],
- node: Node,
-) -> None:
- """Get node endpoints with their support command classes."""
- # consumers expect snake_case at the moment
- # remove that addition when consumers are updated
- connection.send_result(
- msg[ID],
- {
- idx: [
- command_class.to_dict() | {"is_secure": command_class.is_secure}
- for command_class in endpoint.command_classes
- ]
- for idx, endpoint in node.endpoints.items()
- },
- )
-
-
-@websocket_api.require_admin
-@websocket_api.websocket_command(
- {
- vol.Required(TYPE): "zwave_js/invoke_cc_api",
- vol.Required(DEVICE_ID): str,
- vol.Required(ATTR_COMMAND_CLASS): vol.All(
- vol.Coerce(int), vol.Coerce(CommandClass)
- ),
- vol.Optional(ATTR_ENDPOINT): vol.Coerce(int),
- vol.Required(ATTR_METHOD_NAME): cv.string,
- vol.Required(ATTR_PARAMETERS): list,
- vol.Optional(ATTR_WAIT_FOR_RESULT): cv.boolean,
- }
-)
-@websocket_api.async_response
-@async_handle_failed_command
-@async_get_node
-async def websocket_invoke_cc_api(
- hass: HomeAssistant,
- connection: ActiveConnection,
- msg: dict[str, Any],
- node: Node,
-) -> None:
- """Call invokeCCAPI on the node or provided endpoint."""
- command_class: CommandClass = msg[ATTR_COMMAND_CLASS]
- method_name: str = msg[ATTR_METHOD_NAME]
- parameters: list[Any] = msg[ATTR_PARAMETERS]
-
- node_or_endpoint: Node | Endpoint = node
- if (endpoint := msg.get(ATTR_ENDPOINT)) is not None:
- node_or_endpoint = node.endpoints[endpoint]
-
- try:
- result = await node_or_endpoint.async_invoke_cc_api(
- command_class,
- method_name,
- *parameters,
- wait_for_result=msg.get(ATTR_WAIT_FOR_RESULT, False),
- )
- except BaseZwaveJSServerError as err:
- connection.send_error(msg[ID], err.__class__.__name__, str(err))
- else:
- connection.send_result(
- msg[ID],
- result,
- )
diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py
index c7ab579c2cb..14a3fe579c4 100644
--- a/homeassistant/components/zwave_js/climate.py
+++ b/homeassistant/components/zwave_js/climate.py
@@ -24,6 +24,8 @@ from homeassistant.components.climate import (
ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
+ DEFAULT_MAX_TEMP,
+ DEFAULT_MIN_TEMP,
DOMAIN as CLIMATE_DOMAIN,
PRESET_NONE,
ClimateEntity,
@@ -419,7 +421,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
@property
def min_temp(self) -> float:
"""Return the minimum temperature."""
- min_temp = 0.0 # Not using DEFAULT_MIN_TEMP to allow wider range
+ min_temp = DEFAULT_MIN_TEMP
base_unit: str = UnitOfTemperature.CELSIUS
try:
temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0])
@@ -435,7 +437,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
@property
def max_temp(self) -> float:
"""Return the maximum temperature."""
- max_temp = 50.0 # Not using DEFAULT_MAX_TEMP to allow wider range
+ max_temp = DEFAULT_MAX_TEMP
base_unit: str = UnitOfTemperature.CELSIUS
try:
temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0])
diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py
index 36f208e18d5..5668f90f4c5 100644
--- a/homeassistant/components/zwave_js/config_flow.py
+++ b/homeassistant/components/zwave_js/config_flow.py
@@ -18,6 +18,8 @@ from homeassistant.components.hassio import (
AddonInfo,
AddonManager,
AddonState,
+ HassioServiceInfo,
+ is_hassio,
)
from homeassistant.components.zeroconf import ZeroconfServiceInfo
from homeassistant.config_entries import (
@@ -37,8 +39,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow, FlowManager
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.helpers.hassio import is_hassio
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from homeassistant.helpers.typing import VolDictType
from . import disconnect_client
@@ -366,7 +366,7 @@ class ZWaveJSConfigFlow(BaseZwaveJSFlow, ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Return the options flow."""
- return OptionsFlowHandler()
+ return OptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -725,9 +725,10 @@ class ZWaveJSConfigFlow(BaseZwaveJSFlow, ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(BaseZwaveJSFlow, OptionsFlow):
"""Handle an options flow for Z-Wave JS."""
- def __init__(self) -> None:
+ def __init__(self, config_entry: ConfigEntry) -> None:
"""Set up the options flow."""
super().__init__()
+ self.config_entry = config_entry
self.original_addon_config: dict[str, Any] | None = None
self.revert_reason: str | None = None
diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py
index 5c79c668afc..cff0eb434e0 100644
--- a/homeassistant/components/zwave_js/discovery.py
+++ b/homeassistant/components/zwave_js/discovery.py
@@ -238,12 +238,6 @@ SWITCH_BINARY_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema(
command_class={CommandClass.SWITCH_BINARY}, property={CURRENT_VALUE_PROPERTY}
)
-COLOR_SWITCH_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema(
- command_class={CommandClass.SWITCH_COLOR},
- property={CURRENT_COLOR_PROPERTY},
- property_key={None},
-)
-
SIREN_TONE_SCHEMA = ZWaveValueDiscoverySchema(
command_class={CommandClass.SOUND_SWITCH},
property={TONE_ID_PROPERTY},
@@ -768,6 +762,33 @@ DISCOVERY_SCHEMAS = [
},
),
),
+ # HomeSeer HSM-200 v1
+ ZWaveDiscoverySchema(
+ platform=Platform.LIGHT,
+ hint="black_is_off",
+ manufacturer_id={0x001E},
+ product_id={0x0001},
+ product_type={0x0004},
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={CommandClass.SWITCH_COLOR},
+ property={CURRENT_COLOR_PROPERTY},
+ property_key={None},
+ ),
+ absent_values=[SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA],
+ ),
+ # Logic Group ZDB5100
+ ZWaveDiscoverySchema(
+ platform=Platform.LIGHT,
+ hint="black_is_off",
+ manufacturer_id={0x0234},
+ product_id={0x0121},
+ product_type={0x0003},
+ primary_value=ZWaveValueDiscoverySchema(
+ command_class={CommandClass.SWITCH_COLOR},
+ property={CURRENT_COLOR_PROPERTY},
+ property_key={None},
+ ),
+ ),
# ====== START OF GENERIC MAPPING SCHEMAS =======
# locks
# Door Lock CC
@@ -969,6 +990,11 @@ DISCOVERY_SCHEMAS = [
),
entity_category=EntityCategory.CONFIG,
),
+ # binary switches
+ ZWaveDiscoverySchema(
+ platform=Platform.SWITCH,
+ primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA,
+ ),
# switch for Indicator CC
ZWaveDiscoverySchema(
platform=Platform.SWITCH,
@@ -1056,51 +1082,15 @@ DISCOVERY_SCHEMAS = [
device_class_generic={"Thermostat"},
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
),
- # Handle the different combinations of Binary Switch, Multilevel Switch and Color Switch
- # to create switches and/or (colored) lights. The goal is to:
- # - couple Color Switch CC with Multilevel Switch CC if possible
- # - couple Color Switch CC with Binary Switch CC as the first fallback
- # - use Color Switch CC standalone as the last fallback
- #
- # Multilevel Switch CC (+ Color Switch CC) -> Dimmable light with or without color support.
+ # lights
+ # primary value is the currentValue (brightness)
+ # catch any device with multilevel CC as light
+ # NOTE: keep this at the bottom of the discovery scheme,
+ # to handle all others that need the multilevel CC first
ZWaveDiscoverySchema(
platform=Platform.LIGHT,
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
),
- # Binary Switch CC when Multilevel Switch and Color Switch CC exist ->
- # On/Off switch, assign color to light entity instead
- ZWaveDiscoverySchema(
- platform=Platform.SWITCH,
- primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA,
- required_values=[
- SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
- COLOR_SWITCH_CURRENT_VALUE_SCHEMA,
- ],
- ),
- # Binary Switch CC and Color Switch CC ->
- # Colored light that uses Binary Switch CC for turning on/off.
- ZWaveDiscoverySchema(
- platform=Platform.LIGHT,
- hint="color_onoff",
- primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA,
- required_values=[COLOR_SWITCH_CURRENT_VALUE_SCHEMA],
- ),
- # Binary Switch CC without Color Switch CC -> On/Off switch
- ZWaveDiscoverySchema(
- platform=Platform.SWITCH,
- primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA,
- absent_values=[COLOR_SWITCH_CURRENT_VALUE_SCHEMA],
- ),
- # Colored light (legacy device) that can only be controlled through Color Switch CC.
- ZWaveDiscoverySchema(
- platform=Platform.LIGHT,
- hint="color_onoff",
- primary_value=COLOR_SWITCH_CURRENT_VALUE_SCHEMA,
- absent_values=[
- SWITCH_BINARY_CURRENT_VALUE_SCHEMA,
- SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
- ],
- ),
# light for Basic CC with target
ZWaveDiscoverySchema(
platform=Platform.LIGHT,
@@ -1325,20 +1315,14 @@ def async_discover_single_value(
# check additional required values
if schema.required_values is not None and not all(
- any(
- check_value(val, val_scheme, primary_value=value)
- for val in value.node.values.values()
- )
+ any(check_value(val, val_scheme) for val in value.node.values.values())
for val_scheme in schema.required_values
):
continue
# check for values that may not be present
if schema.absent_values is not None and any(
- any(
- check_value(val, val_scheme, primary_value=value)
- for val in value.node.values.values()
- )
+ any(check_value(val, val_scheme) for val in value.node.values.values())
for val_scheme in schema.absent_values
):
continue
@@ -1457,11 +1441,7 @@ def async_discover_single_configuration_value(
@callback
-def check_value(
- value: ZwaveValue,
- schema: ZWaveValueDiscoverySchema,
- primary_value: ZwaveValue | None = None,
-) -> bool:
+def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool:
"""Check if value matches scheme."""
# check command_class
if (
@@ -1472,14 +1452,6 @@ def check_value(
# check endpoint
if schema.endpoint is not None and value.endpoint not in schema.endpoint:
return False
- # If the schema does not require an endpoint, make sure the value is on the
- # same endpoint as the primary value
- if (
- schema.endpoint is None
- and primary_value is not None
- and value.endpoint != primary_value.endpoint
- ):
- return False
# check property
if schema.property is not None and value.property_ not in schema.property:
return False
diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py
index 4a044ca3f52..020f1b66b3d 100644
--- a/homeassistant/components/zwave_js/light.py
+++ b/homeassistant/components/zwave_js/light.py
@@ -76,8 +76,8 @@ async def async_setup_entry(
driver = client.driver
assert driver is not None # Driver is ready before platforms are loaded.
- if info.platform_hint == "color_onoff":
- async_add_entities([ZwaveColorOnOffLight(config_entry, driver, info)])
+ if info.platform_hint == "black_is_off":
+ async_add_entities([ZwaveBlackIsOffLight(config_entry, driver, info)])
else:
async_add_entities([ZwaveLight(config_entry, driver, info)])
@@ -111,10 +111,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
self._supports_color = False
self._supports_rgbw = False
self._supports_color_temp = False
- self._supports_dimming = False
- self._color_mode: str | None = None
self._hs_color: tuple[float, float] | None = None
self._rgbw_color: tuple[int, int, int, int] | None = None
+ self._color_mode: str | None = None
self._color_temp: int | None = None
self._min_mireds = 153 # 6500K as a safe default
self._max_mireds = 370 # 2700K as a safe default
@@ -130,28 +129,15 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
)
self._supported_color_modes: set[ColorMode] = set()
- self._target_brightness: Value | None = None
-
# get additional (optional) values and set features
- if self.info.primary_value.command_class == CommandClass.SWITCH_BINARY:
- # This light can not be dimmed separately from the color channels
- self._target_brightness = self.get_zwave_value(
- TARGET_VALUE_PROPERTY,
- CommandClass.SWITCH_BINARY,
- add_to_watched_value_ids=False,
- )
- self._supports_dimming = False
- elif self.info.primary_value.command_class == CommandClass.SWITCH_MULTILEVEL:
- # This light can be dimmed separately from the color channels
- self._target_brightness = self.get_zwave_value(
- TARGET_VALUE_PROPERTY,
- CommandClass.SWITCH_MULTILEVEL,
- add_to_watched_value_ids=False,
- )
- self._supports_dimming = True
- elif self.info.primary_value.command_class == CommandClass.BASIC:
- # If the command class is Basic, we must generate a name that includes
- # the command class name to avoid ambiguity
+ # If the command class is Basic, we must geenerate a name that includes
+ # the command class name to avoid ambiguity
+ self._target_brightness = self.get_zwave_value(
+ TARGET_VALUE_PROPERTY,
+ CommandClass.SWITCH_MULTILEVEL,
+ add_to_watched_value_ids=False,
+ )
+ if self.info.primary_value.command_class == CommandClass.BASIC:
self._attr_name = self.generate_name(
include_value_name=True, alternate_value_name="Basic"
)
@@ -160,13 +146,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
CommandClass.BASIC,
add_to_watched_value_ids=False,
)
- self._supports_dimming = True
-
- self._current_color = self.get_zwave_value(
- CURRENT_COLOR_PROPERTY,
- CommandClass.SWITCH_COLOR,
- value_property_key=None,
- )
self._target_color = self.get_zwave_value(
TARGET_COLOR_PROPERTY,
CommandClass.SWITCH_COLOR,
@@ -237,7 +216,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
@property
def rgbw_color(self) -> tuple[int, int, int, int] | None:
- """Return the RGBW color."""
+ """Return the hs color."""
return self._rgbw_color
@property
@@ -264,39 +243,11 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
"""Turn the device on."""
transition = kwargs.get(ATTR_TRANSITION)
- brightness = kwargs.get(ATTR_BRIGHTNESS)
-
- hs_color = kwargs.get(ATTR_HS_COLOR)
- color_temp = kwargs.get(ATTR_COLOR_TEMP)
- rgbw = kwargs.get(ATTR_RGBW_COLOR)
-
- new_colors = self._get_new_colors(hs_color, color_temp, rgbw)
- if new_colors is not None:
- await self._async_set_colors(new_colors, transition)
-
- # set brightness (or turn on if dimming is not supported)
- await self._async_set_brightness(brightness, transition)
-
- async def async_turn_off(self, **kwargs: Any) -> None:
- """Turn the light off."""
- await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION))
-
- def _get_new_colors(
- self,
- hs_color: tuple[float, float] | None,
- color_temp: int | None,
- rgbw: tuple[int, int, int, int] | None,
- brightness_scale: float | None = None,
- ) -> dict[ColorComponent, int] | None:
- """Determine the new color dict to set."""
# RGB/HS color
+ hs_color = kwargs.get(ATTR_HS_COLOR)
if hs_color is not None and self._supports_color:
red, green, blue = color_util.color_hs_to_RGB(*hs_color)
- if brightness_scale is not None:
- red = round(red * brightness_scale)
- green = round(green * brightness_scale)
- blue = round(blue * brightness_scale)
colors = {
ColorComponent.RED: red,
ColorComponent.GREEN: green,
@@ -306,9 +257,10 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
# turn of white leds when setting rgb
colors[ColorComponent.WARM_WHITE] = 0
colors[ColorComponent.COLD_WHITE] = 0
- return colors
+ await self._async_set_colors(colors, transition)
# Color temperature
+ color_temp = kwargs.get(ATTR_COLOR_TEMP)
if color_temp is not None and self._supports_color_temp:
# Limit color temp to min/max values
cold = max(
@@ -323,18 +275,20 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
),
)
warm = 255 - cold
- colors = {
- ColorComponent.WARM_WHITE: warm,
- ColorComponent.COLD_WHITE: cold,
- }
- if self._supports_color:
- # turn off color leds when setting color temperature
- colors[ColorComponent.RED] = 0
- colors[ColorComponent.GREEN] = 0
- colors[ColorComponent.BLUE] = 0
- return colors
+ await self._async_set_colors(
+ {
+ # turn off color leds when setting color temperature
+ ColorComponent.RED: 0,
+ ColorComponent.GREEN: 0,
+ ColorComponent.BLUE: 0,
+ ColorComponent.WARM_WHITE: warm,
+ ColorComponent.COLD_WHITE: cold,
+ },
+ transition,
+ )
# RGBW
+ rgbw = kwargs.get(ATTR_RGBW_COLOR)
if rgbw is not None and self._supports_rgbw:
rgbw_channels = {
ColorComponent.RED: rgbw[0],
@@ -346,15 +300,17 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
if self._cold_white:
rgbw_channels[ColorComponent.COLD_WHITE] = rgbw[3]
+ await self._async_set_colors(rgbw_channels, transition)
- return rgbw_channels
+ # set brightness
+ await self._async_set_brightness(kwargs.get(ATTR_BRIGHTNESS), transition)
- return None
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Turn the light off."""
+ await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION))
async def _async_set_colors(
- self,
- colors: dict[ColorComponent, int],
- transition: float | None = None,
+ self, colors: dict[ColorComponent, int], transition: float | None = None
) -> None:
"""Set (multiple) defined colors to given value(s)."""
# prefer the (new) combined color property
@@ -405,14 +361,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
zwave_transition = {TRANSITION_DURATION_OPTION: "default"}
# setting a value requires setting targetValue
- if self._supports_dimming:
- await self._async_set_value(
- self._target_brightness, zwave_brightness, zwave_transition
- )
- else:
- await self._async_set_value(
- self._target_brightness, zwave_brightness > 0, zwave_transition
- )
+ await self._async_set_value(
+ self._target_brightness, zwave_brightness, zwave_transition
+ )
# We do an optimistic state update when setting to a previous value
# to avoid waiting for the value to be updated from the device which is
# typically delayed and causes a confusing UX.
@@ -476,8 +427,15 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
"""Calculate light colors."""
(red_val, green_val, blue_val, ww_val, cw_val) = self._get_color_values()
- if self._current_color and isinstance(self._current_color.value, dict):
- multi_color = self._current_color.value
+ # prefer the (new) combined color property
+ # https://github.com/zwave-js/node-zwave-js/pull/1782
+ combined_color_val = self.get_zwave_value(
+ CURRENT_COLOR_PROPERTY,
+ CommandClass.SWITCH_COLOR,
+ value_property_key=None,
+ )
+ if combined_color_val and isinstance(combined_color_val.value, dict):
+ multi_color = combined_color_val.value
else:
multi_color = {}
@@ -528,10 +486,11 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
self._color_mode = ColorMode.RGBW
-class ZwaveColorOnOffLight(ZwaveLight):
- """Representation of a colored Z-Wave light with an optional binary switch to turn on/off.
+class ZwaveBlackIsOffLight(ZwaveLight):
+ """Representation of a Z-Wave light where setting the color to black turns it off.
- Dimming for RGB lights is realized by scaling the color channels.
+ Currently only supports lights with RGB, no color temperature, and no white
+ channels.
"""
def __init__(
@@ -540,137 +499,61 @@ class ZwaveColorOnOffLight(ZwaveLight):
"""Initialize the light."""
super().__init__(config_entry, driver, info)
- self._last_on_color: dict[ColorComponent, int] | None = None
- self._last_brightness: int | None = None
+ self._last_color: dict[str, int] | None = None
+ self._supported_color_modes.discard(ColorMode.BRIGHTNESS)
@property
- def brightness(self) -> int | None:
- """Return the brightness of this light between 0..255.
+ def brightness(self) -> int:
+ """Return the brightness of this light between 0..255."""
+ return 255
- Z-Wave multilevel switches use a range of [0, 99] to control brightness.
- """
+ @property
+ def is_on(self) -> bool | None:
+ """Return true if device is on (brightness above 0)."""
if self.info.primary_value.value is None:
return None
- if self._target_brightness and self.info.primary_value.value is False:
- # Binary switch exists and is turned off
- return 0
-
- # Brightness is encoded in the color channels by scaling them lower than 255
- color_values = [
- v.value
- for v in self._get_color_values()
- if v is not None and v.value is not None
- ]
- return max(color_values) if color_values else 0
+ return any(value != 0 for value in self.info.primary_value.value.values())
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
-
if (
kwargs.get(ATTR_RGBW_COLOR) is not None
or kwargs.get(ATTR_COLOR_TEMP) is not None
+ or kwargs.get(ATTR_HS_COLOR) is not None
):
- # RGBW and color temp are not supported in this mode,
- # delegate to the parent class
await super().async_turn_on(**kwargs)
return
transition = kwargs.get(ATTR_TRANSITION)
- brightness = kwargs.get(ATTR_BRIGHTNESS)
- hs_color = kwargs.get(ATTR_HS_COLOR)
- new_colors: dict[ColorComponent, int] | None = None
- scale: float | None = None
-
- if brightness is None and hs_color is None:
- # Turned on without specifying brightness or color
- if self._last_on_color is not None:
- if self._target_brightness:
- # Color is already set, use the binary switch to turn on
- await self._async_set_brightness(None, transition)
- return
-
- # Preserve the previous color
- new_colors = self._last_on_color
- elif self._supports_color:
- # Turned on for the first time. Make it white
- new_colors = {
+ # turn on light to last color if known, otherwise set to white
+ if self._last_color is not None:
+ await self._async_set_colors(
+ {
+ ColorComponent.RED: self._last_color["red"],
+ ColorComponent.GREEN: self._last_color["green"],
+ ColorComponent.BLUE: self._last_color["blue"],
+ },
+ transition,
+ )
+ else:
+ await self._async_set_colors(
+ {
ColorComponent.RED: 255,
ColorComponent.GREEN: 255,
ColorComponent.BLUE: 255,
- }
- elif brightness is not None:
- # If brightness gets set, preserve the color and mix it with the new brightness
- if self.color_mode == ColorMode.HS:
- scale = brightness / 255
- if (
- self._last_on_color is not None
- and None not in self._last_on_color.values()
- ):
- # Changed brightness from 0 to >0
- old_brightness = max(self._last_on_color.values())
- new_scale = brightness / old_brightness
- scale = new_scale
- new_colors = {}
- for color, value in self._last_on_color.items():
- new_colors[color] = round(value * new_scale)
- elif hs_color is None and self._color_mode == ColorMode.HS:
- hs_color = self._hs_color
- elif hs_color is not None and brightness is None:
- # Turned on by using the color controls
- current_brightness = self.brightness
- if current_brightness == 0 and self._last_brightness is not None:
- # Use the last brightness value if the light is currently off
- scale = self._last_brightness / 255
- elif current_brightness is not None:
- scale = current_brightness / 255
-
- # Reset last color until turning off again
- self._last_on_color = None
-
- if new_colors is None:
- new_colors = self._get_new_colors(
- hs_color=hs_color, color_temp=None, rgbw=None, brightness_scale=scale
+ },
+ transition,
)
- if new_colors is not None:
- await self._async_set_colors(new_colors, transition)
-
- # Turn the binary switch on if there is one
- await self._async_set_brightness(brightness, transition)
-
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
-
- # Remember last color and brightness to restore it when turning on
- self._last_brightness = self.brightness
- if self._current_color and isinstance(self._current_color.value, dict):
- red = self._current_color.value.get(COLOR_SWITCH_COMBINED_RED)
- green = self._current_color.value.get(COLOR_SWITCH_COMBINED_GREEN)
- blue = self._current_color.value.get(COLOR_SWITCH_COMBINED_BLUE)
-
- last_color: dict[ColorComponent, int] = {}
- if red is not None:
- last_color[ColorComponent.RED] = red
- if green is not None:
- last_color[ColorComponent.GREEN] = green
- if blue is not None:
- last_color[ColorComponent.BLUE] = blue
-
- if last_color:
- self._last_on_color = last_color
-
- if self._target_brightness:
- # Turn off the binary switch only
- await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION))
- else:
- # turn off all color channels
- colors = {
+ self._last_color = self.info.primary_value.value
+ await self._async_set_colors(
+ {
ColorComponent.RED: 0,
ColorComponent.GREEN: 0,
ColorComponent.BLUE: 0,
- }
-
- await self._async_set_colors(
- colors,
- kwargs.get(ATTR_TRANSITION),
- )
+ },
+ kwargs.get(ATTR_TRANSITION),
+ )
+ await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION))
diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json
index 3631bf1163b..0fee480b093 100644
--- a/homeassistant/components/zwave_js/manifest.json
+++ b/homeassistant/components/zwave_js/manifest.json
@@ -1,7 +1,6 @@
{
"domain": "zwave_js",
"name": "Z-Wave",
- "after_dependencies": ["hassio"],
"codeowners": ["@home-assistant/z-wave"],
"config_flow": true,
"dependencies": ["http", "repairs", "usb", "websocket_api"],
@@ -10,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["zwave_js_server"],
"quality_scale": "platinum",
- "requirements": ["pyserial==3.5", "zwave-js-server-python==0.59.1"],
+ "requirements": ["pyserial==3.5", "zwave-js-server-python==0.58.1"],
"usb": [
{
"vid": "0658",
diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py
index d1cb66ceafc..969a235bb41 100644
--- a/homeassistant/components/zwave_js/services.py
+++ b/homeassistant/components/zwave_js/services.py
@@ -529,15 +529,8 @@ class ZWaveServices:
for node_or_endpoint, result in get_valid_responses_from_results(
nodes_or_endpoints_list, _results
):
- if value_size is None:
- # async_set_config_parameter still returns (Value, SetConfigParameterResult)
- zwave_value = result[0]
- cmd_status = result[1]
- else:
- # async_set_raw_config_parameter_value now returns just SetConfigParameterResult
- cmd_status = result
- zwave_value = f"parameter {property_or_property_name}"
-
+ zwave_value = result[0]
+ cmd_status = result[1]
if cmd_status.status == CommandStatus.ACCEPTED:
msg = "Set configuration parameter %s on Node %s with value %s"
else:
diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml
index acf6e9a0665..f5063fdfd93 100644
--- a/homeassistant/components/zwave_js/services.yaml
+++ b/homeassistant/components/zwave_js/services.yaml
@@ -51,6 +51,16 @@ set_lock_configuration:
min: 0
max: 65535
unit_of_measurement: sec
+ outside_handles_can_open_door_configuration:
+ required: false
+ example: [true, true, true, false]
+ selector:
+ object:
+ inside_handles_can_open_door_configuration:
+ required: false
+ example: [true, true, true, false]
+ selector:
+ object:
auto_relock_time:
required: false
example: 1
diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json
index 28789bbf9f4..ca7d5153e6e 100644
--- a/homeassistant/components/zwave_js/strings.json
+++ b/homeassistant/components/zwave_js/strings.json
@@ -523,6 +523,10 @@
"description": "Duration in seconds the latch stays retracted.",
"name": "Hold and release time"
},
+ "inside_handles_can_open_door_configuration": {
+ "description": "A list of four booleans which indicate which inside handles can open the door.",
+ "name": "Inside handles can open door configuration"
+ },
"lock_timeout": {
"description": "Seconds until lock mode times out. Should only be used if operation type is `timed`.",
"name": "Lock timeout"
@@ -531,6 +535,10 @@
"description": "The operation type of the lock.",
"name": "Operation Type"
},
+ "outside_handles_can_open_door_configuration": {
+ "description": "A list of four booleans which indicate which outside handles can open the door.",
+ "name": "Outside handles can open door configuration"
+ },
"twist_assist": {
"description": "Enable Twist Assist.",
"name": "Twist assist"
diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py
index d060abe007d..02c59d220e1 100644
--- a/homeassistant/components/zwave_js/update.py
+++ b/homeassistant/components/zwave_js/update.py
@@ -155,8 +155,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
progress: NodeFirmwareUpdateProgress = event["firmware_update_progress"]
if not self._latest_version_firmware:
return
- self._attr_in_progress = True
- self._attr_update_percentage = int(progress.progress)
+ self._attr_in_progress = int(progress.progress)
self.async_write_ha_state()
@callback
@@ -182,7 +181,6 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
self._result = None
self._finished_event.clear()
self._attr_in_progress = False
- self._attr_update_percentage = None
if write_state:
self.async_write_ha_state()
@@ -269,7 +267,6 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
assert firmware
self._unsub_firmware_events_and_reset_progress(False)
self._attr_in_progress = True
- self._attr_update_percentage = None
self.async_write_ha_state()
self._progress_unsub = self.node.on(
diff --git a/homeassistant/config.py b/homeassistant/config.py
index cab4d0c7aff..9063429ca91 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -17,23 +17,62 @@ import re
import shutil
from types import ModuleType
from typing import TYPE_CHECKING, Any
+from urllib.parse import urlparse
from awesomeversion import AwesomeVersion
import voluptuous as vol
from voluptuous.humanize import MAX_VALIDATION_ERROR_ITEM_LENGTH
from yaml.error import MarkedYAMLError
-from .const import CONF_PACKAGES, CONF_PLATFORM, __version__
-from .core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
-from .core_config import _PACKAGE_DEFINITION_SCHEMA, _PACKAGES_CONFIG_SCHEMA
+from . import auth
+from .auth import mfa_modules as auth_mfa_modules, providers as auth_providers
+from .const import (
+ ATTR_ASSUMED_STATE,
+ ATTR_FRIENDLY_NAME,
+ ATTR_HIDDEN,
+ CONF_ALLOWLIST_EXTERNAL_DIRS,
+ CONF_ALLOWLIST_EXTERNAL_URLS,
+ CONF_AUTH_MFA_MODULES,
+ CONF_AUTH_PROVIDERS,
+ CONF_COUNTRY,
+ CONF_CURRENCY,
+ CONF_CUSTOMIZE,
+ CONF_CUSTOMIZE_DOMAIN,
+ CONF_CUSTOMIZE_GLOB,
+ CONF_DEBUG,
+ CONF_ELEVATION,
+ CONF_EXTERNAL_URL,
+ CONF_ID,
+ CONF_INTERNAL_URL,
+ CONF_LANGUAGE,
+ CONF_LATITUDE,
+ CONF_LEGACY_TEMPLATES,
+ CONF_LONGITUDE,
+ CONF_MEDIA_DIRS,
+ CONF_NAME,
+ CONF_PACKAGES,
+ CONF_PLATFORM,
+ CONF_RADIUS,
+ CONF_TEMPERATURE_UNIT,
+ CONF_TIME_ZONE,
+ CONF_TYPE,
+ CONF_UNIT_SYSTEM,
+ LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
+ __version__,
+)
+from .core import DOMAIN as HOMEASSISTANT_DOMAIN, ConfigSource, HomeAssistant, callback
from .exceptions import ConfigValidationError, HomeAssistantError
-from .helpers import config_validation as cv
+from .generated.currencies import HISTORIC_CURRENCIES
+from .helpers import config_validation as cv, issue_registry as ir
+from .helpers.entity_values import EntityValues
from .helpers.translation import async_get_exception_message
from .helpers.typing import ConfigType
from .loader import ComponentProtocol, Integration, IntegrationNotFound
from .requirements import RequirementsNotFound, async_get_integration_with_requirements
from .util.async_ import create_eager_task
+from .util.hass_dict import HassKey
from .util.package import is_docker_env
+from .util.unit_system import get_unit_system, validate_unit_system
from .util.yaml import SECRET_YAML, Secrets, YamlTypeError, load_yaml_dict
from .util.yaml.objects import NodeStrClass
@@ -44,6 +83,7 @@ RE_ASCII = re.compile(r"\033\[[^m]*m")
YAML_CONFIG_FILE = "configuration.yaml"
VERSION_FILE = ".HA_VERSION"
CONFIG_DIR_NAME = ".homeassistant"
+DATA_CUSTOMIZE: HassKey[EntityValues] = HassKey("hass_customize")
AUTOMATION_CONFIG_PATH = "automations.yaml"
SCRIPT_CONFIG_PATH = "scripts.yaml"
@@ -132,6 +172,201 @@ class IntegrationConfigInfo:
exception_info_list: list[ConfigExceptionInfo]
+def _no_duplicate_auth_provider(
+ configs: Sequence[dict[str, Any]],
+) -> Sequence[dict[str, Any]]:
+ """No duplicate auth provider config allowed in a list.
+
+ Each type of auth provider can only have one config without optional id.
+ Unique id is required if same type of auth provider used multiple times.
+ """
+ config_keys: set[tuple[str, str | None]] = set()
+ for config in configs:
+ key = (config[CONF_TYPE], config.get(CONF_ID))
+ if key in config_keys:
+ raise vol.Invalid(
+ f"Duplicate auth provider {config[CONF_TYPE]} found. "
+ "Please add unique IDs "
+ "if you want to have the same auth provider twice"
+ )
+ config_keys.add(key)
+ return configs
+
+
+def _no_duplicate_auth_mfa_module(
+ configs: Sequence[dict[str, Any]],
+) -> Sequence[dict[str, Any]]:
+ """No duplicate auth mfa module item allowed in a list.
+
+ Each type of mfa module can only have one config without optional id.
+ A global unique id is required if same type of mfa module used multiple
+ times.
+ Note: this is different than auth provider
+ """
+ config_keys: set[str] = set()
+ for config in configs:
+ key = config.get(CONF_ID, config[CONF_TYPE])
+ if key in config_keys:
+ raise vol.Invalid(
+ f"Duplicate mfa module {config[CONF_TYPE]} found. "
+ "Please add unique IDs "
+ "if you want to have the same mfa module twice"
+ )
+ config_keys.add(key)
+ return configs
+
+
+def _filter_bad_internal_external_urls(conf: dict) -> dict:
+ """Filter internal/external URL with a path."""
+ for key in CONF_INTERNAL_URL, CONF_EXTERNAL_URL:
+ if key in conf and urlparse(conf[key]).path not in ("", "/"):
+ # We warn but do not fix, because if this was incorrectly configured,
+ # adjusting this value might impact security.
+ _LOGGER.warning(
+ "Invalid %s set. It's not allowed to have a path (/bla)", key
+ )
+
+ return conf
+
+
+# Schema for all packages element
+PACKAGES_CONFIG_SCHEMA = vol.Schema({cv.string: vol.Any(dict, list)})
+
+# Schema for individual package definition
+PACKAGE_DEFINITION_SCHEMA = vol.Schema({cv.string: vol.Any(dict, list, None)})
+
+CUSTOMIZE_DICT_SCHEMA = vol.Schema(
+ {
+ vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
+ vol.Optional(ATTR_HIDDEN): cv.boolean,
+ vol.Optional(ATTR_ASSUMED_STATE): cv.boolean,
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+CUSTOMIZE_CONFIG_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_CUSTOMIZE, default={}): vol.Schema(
+ {cv.entity_id: CUSTOMIZE_DICT_SCHEMA}
+ ),
+ vol.Optional(CONF_CUSTOMIZE_DOMAIN, default={}): vol.Schema(
+ {cv.string: CUSTOMIZE_DICT_SCHEMA}
+ ),
+ vol.Optional(CONF_CUSTOMIZE_GLOB, default={}): vol.Schema(
+ {cv.string: CUSTOMIZE_DICT_SCHEMA}
+ ),
+ }
+)
+
+
+def _raise_issue_if_historic_currency(hass: HomeAssistant, currency: str) -> None:
+ if currency not in HISTORIC_CURRENCIES:
+ ir.async_delete_issue(hass, HOMEASSISTANT_DOMAIN, "historic_currency")
+ return
+
+ ir.async_create_issue(
+ hass,
+ HOMEASSISTANT_DOMAIN,
+ "historic_currency",
+ is_fixable=False,
+ learn_more_url="homeassistant://config/general",
+ severity=ir.IssueSeverity.WARNING,
+ translation_key="historic_currency",
+ translation_placeholders={"currency": currency},
+ )
+
+
+def _raise_issue_if_no_country(hass: HomeAssistant, country: str | None) -> None:
+ if country is not None:
+ ir.async_delete_issue(hass, HOMEASSISTANT_DOMAIN, "country_not_configured")
+ return
+
+ ir.async_create_issue(
+ hass,
+ HOMEASSISTANT_DOMAIN,
+ "country_not_configured",
+ is_fixable=False,
+ learn_more_url="homeassistant://config/general",
+ severity=ir.IssueSeverity.WARNING,
+ translation_key="country_not_configured",
+ )
+
+
+def _validate_currency(data: Any) -> Any:
+ try:
+ return cv.currency(data)
+ except vol.InInvalid:
+ with suppress(vol.InInvalid):
+ return cv.historic_currency(data)
+ raise
+
+
+CORE_CONFIG_SCHEMA = vol.All(
+ CUSTOMIZE_CONFIG_SCHEMA.extend(
+ {
+ CONF_NAME: vol.Coerce(str),
+ CONF_LATITUDE: cv.latitude,
+ CONF_LONGITUDE: cv.longitude,
+ CONF_ELEVATION: vol.Coerce(int),
+ CONF_RADIUS: cv.positive_int,
+ vol.Remove(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
+ CONF_UNIT_SYSTEM: validate_unit_system,
+ CONF_TIME_ZONE: cv.time_zone,
+ vol.Optional(CONF_INTERNAL_URL): cv.url,
+ vol.Optional(CONF_EXTERNAL_URL): cv.url,
+ vol.Optional(CONF_ALLOWLIST_EXTERNAL_DIRS): vol.All(
+ cv.ensure_list, [vol.IsDir()]
+ ),
+ vol.Optional(LEGACY_CONF_WHITELIST_EXTERNAL_DIRS): vol.All(
+ cv.ensure_list, [vol.IsDir()]
+ ),
+ vol.Optional(CONF_ALLOWLIST_EXTERNAL_URLS): vol.All(
+ cv.ensure_list, [cv.url]
+ ),
+ vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA,
+ vol.Optional(CONF_AUTH_PROVIDERS): vol.All(
+ cv.ensure_list,
+ [
+ auth_providers.AUTH_PROVIDER_SCHEMA.extend(
+ {
+ CONF_TYPE: vol.NotIn(
+ ["insecure_example"],
+ (
+ "The insecure_example auth provider"
+ " is for testing only."
+ ),
+ )
+ }
+ )
+ ],
+ _no_duplicate_auth_provider,
+ ),
+ vol.Optional(CONF_AUTH_MFA_MODULES): vol.All(
+ cv.ensure_list,
+ [
+ auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend(
+ {
+ CONF_TYPE: vol.NotIn(
+ ["insecure_example"],
+ "The insecure_example mfa module is for testing only.",
+ )
+ }
+ )
+ ],
+ _no_duplicate_auth_mfa_module,
+ ),
+ vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()),
+ vol.Remove(CONF_LEGACY_TEMPLATES): cv.boolean,
+ vol.Optional(CONF_CURRENCY): _validate_currency,
+ vol.Optional(CONF_COUNTRY): cv.country,
+ vol.Optional(CONF_LANGUAGE): cv.language,
+ vol.Optional(CONF_DEBUG): cv.boolean,
+ }
+ ),
+ _filter_bad_internal_external_urls,
+)
+
+
def get_default_config_dir() -> str:
"""Put together the default configuration directory based on the OS."""
data_dir = os.path.expanduser("~")
@@ -577,6 +812,131 @@ def format_schema_error(
return humanize_error(hass, exc, domain, config, link)
+async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> None:
+ """Process the [homeassistant] section from the configuration.
+
+ This method is a coroutine.
+ """
+ # CORE_CONFIG_SCHEMA is not async safe since it uses vol.IsDir
+ # so we need to run it in an executor job.
+ config = await hass.async_add_executor_job(CORE_CONFIG_SCHEMA, config)
+
+ # Only load auth during startup.
+ if not hasattr(hass, "auth"):
+ if (auth_conf := config.get(CONF_AUTH_PROVIDERS)) is None:
+ auth_conf = [{"type": "homeassistant"}]
+
+ mfa_conf = config.get(
+ CONF_AUTH_MFA_MODULES,
+ [{"type": "totp", "id": "totp", "name": "Authenticator app"}],
+ )
+
+ setattr(
+ hass, "auth", await auth.auth_manager_from_config(hass, auth_conf, mfa_conf)
+ )
+
+ await hass.config.async_load()
+
+ hac = hass.config
+
+ if any(
+ k in config
+ for k in (
+ CONF_LATITUDE,
+ CONF_LONGITUDE,
+ CONF_NAME,
+ CONF_ELEVATION,
+ CONF_TIME_ZONE,
+ CONF_UNIT_SYSTEM,
+ CONF_EXTERNAL_URL,
+ CONF_INTERNAL_URL,
+ CONF_CURRENCY,
+ CONF_COUNTRY,
+ CONF_LANGUAGE,
+ CONF_RADIUS,
+ )
+ ):
+ hac.config_source = ConfigSource.YAML
+
+ for key, attr in (
+ (CONF_LATITUDE, "latitude"),
+ (CONF_LONGITUDE, "longitude"),
+ (CONF_NAME, "location_name"),
+ (CONF_ELEVATION, "elevation"),
+ (CONF_INTERNAL_URL, "internal_url"),
+ (CONF_EXTERNAL_URL, "external_url"),
+ (CONF_MEDIA_DIRS, "media_dirs"),
+ (CONF_CURRENCY, "currency"),
+ (CONF_COUNTRY, "country"),
+ (CONF_LANGUAGE, "language"),
+ (CONF_RADIUS, "radius"),
+ ):
+ if key in config:
+ setattr(hac, attr, config[key])
+
+ if config.get(CONF_DEBUG):
+ hac.debug = True
+
+ _raise_issue_if_historic_currency(hass, hass.config.currency)
+ _raise_issue_if_no_country(hass, hass.config.country)
+
+ if CONF_TIME_ZONE in config:
+ await hac.async_set_time_zone(config[CONF_TIME_ZONE])
+
+ if CONF_MEDIA_DIRS not in config:
+ if is_docker_env():
+ hac.media_dirs = {"local": "/media"}
+ else:
+ hac.media_dirs = {"local": hass.config.path("media")}
+
+ # Init whitelist external dir
+ hac.allowlist_external_dirs = {hass.config.path("www"), *hac.media_dirs.values()}
+ if CONF_ALLOWLIST_EXTERNAL_DIRS in config:
+ hac.allowlist_external_dirs.update(set(config[CONF_ALLOWLIST_EXTERNAL_DIRS]))
+
+ elif LEGACY_CONF_WHITELIST_EXTERNAL_DIRS in config:
+ _LOGGER.warning(
+ "Key %s has been replaced with %s. Please update your config",
+ LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
+ CONF_ALLOWLIST_EXTERNAL_DIRS,
+ )
+ hac.allowlist_external_dirs.update(
+ set(config[LEGACY_CONF_WHITELIST_EXTERNAL_DIRS])
+ )
+
+ # Init whitelist external URL list – make sure to add / to every URL that doesn't
+ # already have it so that we can properly test "path ownership"
+ if CONF_ALLOWLIST_EXTERNAL_URLS in config:
+ hac.allowlist_external_urls.update(
+ url if url.endswith("/") else f"{url}/"
+ for url in config[CONF_ALLOWLIST_EXTERNAL_URLS]
+ )
+
+ # Customize
+ cust_exact = dict(config[CONF_CUSTOMIZE])
+ cust_domain = dict(config[CONF_CUSTOMIZE_DOMAIN])
+ cust_glob = OrderedDict(config[CONF_CUSTOMIZE_GLOB])
+
+ for name, pkg in config[CONF_PACKAGES].items():
+ if (pkg_cust := pkg.get(HOMEASSISTANT_DOMAIN)) is None:
+ continue
+
+ try:
+ pkg_cust = CUSTOMIZE_CONFIG_SCHEMA(pkg_cust)
+ except vol.Invalid:
+ _LOGGER.warning("Package %s contains invalid customize", name)
+ continue
+
+ cust_exact.update(pkg_cust[CONF_CUSTOMIZE])
+ cust_domain.update(pkg_cust[CONF_CUSTOMIZE_DOMAIN])
+ cust_glob.update(pkg_cust[CONF_CUSTOMIZE_GLOB])
+
+ hass.data[DATA_CUSTOMIZE] = EntityValues(cust_exact, cust_domain, cust_glob)
+
+ if CONF_UNIT_SYSTEM in config:
+ hac.units = get_unit_system(config[CONF_UNIT_SYSTEM])
+
+
def _log_pkg_error(
hass: HomeAssistant, package: str, component: str | None, config: dict, message: str
) -> None:
@@ -641,7 +1001,7 @@ def _identify_config_schema(module: ComponentProtocol) -> str | None:
def _validate_package_definition(name: str, conf: Any) -> None:
"""Validate basic package definition properties."""
cv.slug(name)
- _PACKAGE_DEFINITION_SCHEMA(conf)
+ PACKAGE_DEFINITION_SCHEMA(conf)
def _recursive_merge(conf: dict[str, Any], package: dict[str, Any]) -> str | None:
@@ -680,7 +1040,7 @@ async def merge_packages_config(
vol.Invalid if whole package config is invalid.
"""
- _PACKAGES_CONFIG_SCHEMA(packages)
+ PACKAGES_CONFIG_SCHEMA(packages)
invalid_packages = []
for pack_name, pack_conf in packages.items():
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index f1748c6b7fb..506f223e8f0 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -27,16 +27,10 @@ from typing import TYPE_CHECKING, Any, Generic, Self, cast
from async_interrupt import interrupt
from propcache import cached_property
from typing_extensions import TypeVar
-import voluptuous as vol
from . import data_entry_flow, loader
from .components import persistent_notification
-from .const import (
- CONF_NAME,
- EVENT_HOMEASSISTANT_STARTED,
- EVENT_HOMEASSISTANT_STOP,
- Platform,
-)
+from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, Platform
from .core import (
CALLBACK_TYPE,
DOMAIN as HOMEASSISTANT_DOMAIN,
@@ -63,7 +57,7 @@ from .helpers.event import (
RANDOM_MICROSECOND_MIN,
async_call_later,
)
-from .helpers.frame import ReportBehavior, report, report_usage
+from .helpers.frame import report
from .helpers.json import json_bytes, json_bytes_sorted, json_fragment
from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType
from .loader import async_suggest_report_issue
@@ -84,10 +78,10 @@ from .util.enum import try_parse_enum
if TYPE_CHECKING:
from .components.bluetooth import BluetoothServiceInfoBleak
from .components.dhcp import DhcpServiceInfo
+ from .components.hassio import HassioServiceInfo
from .components.ssdp import SsdpServiceInfo
from .components.usb import UsbServiceInfo
from .components.zeroconf import ZeroconfServiceInfo
- from .helpers.service_info.hassio import HassioServiceInfo
from .helpers.service_info.mqtt import MqttServiceInfo
@@ -129,9 +123,6 @@ SAVE_DELAY = 1
DISCOVERY_COOLDOWN = 1
-ISSUE_UNIQUE_ID_COLLISION = "config_entry_unique_id_collision"
-UNIQUE_ID_COLLISION_TITLE_LIMIT = 5
-
_DataT = TypeVar("_DataT", default=Any)
@@ -179,13 +170,11 @@ DISCOVERY_SOURCES = {
SOURCE_DHCP,
SOURCE_DISCOVERY,
SOURCE_HARDWARE,
- SOURCE_HASSIO,
SOURCE_HOMEKIT,
SOURCE_IMPORT,
SOURCE_INTEGRATION_DISCOVERY,
SOURCE_MQTT,
SOURCE_SSDP,
- SOURCE_SYSTEM,
SOURCE_USB,
SOURCE_ZEROCONF,
}
@@ -538,21 +527,10 @@ class ConfigEntry(Generic[_DataT]):
integration: loader.Integration | None = None,
) -> None:
"""Set up an entry."""
+ current_entry.set(self)
if self.source == SOURCE_IGNORE or self.disabled_by:
return
- current_entry.set(self)
- try:
- await self.__async_setup_with_context(hass, integration)
- finally:
- current_entry.set(None)
-
- async def __async_setup_with_context(
- self,
- hass: HomeAssistant,
- integration: loader.Integration | None,
- ) -> None:
- """Set up an entry, with current_entry set."""
if integration is None and not (integration := self._integration_for_domain):
integration = await loader.async_get_integration(hass, self.domain)
self._integration_for_domain = integration
@@ -1260,31 +1238,14 @@ class ConfigEntriesFlowManager(
if not context or "source" not in context:
raise KeyError("Context not set or doesn't have a source set")
- # reauth/reconfigure flows should be linked to a config entry
- if (source := context["source"]) in {
- SOURCE_REAUTH,
- SOURCE_RECONFIGURE,
- } and "entry_id" not in context:
- # Deprecated in 2024.12, should fail in 2025.12
- report(
- f"initialises a {source} flow without a link to the config entry",
- error_if_integration=False,
- error_if_core=True,
- )
-
flow_id = ulid_util.ulid_now()
# Avoid starting a config flow on an integration that only supports
# a single config entry, but which already has an entry
if (
- source not in {SOURCE_IGNORE, SOURCE_REAUTH, SOURCE_RECONFIGURE}
- and (
- self.config_entries.async_has_entries(handler, include_ignore=False)
- or (
- self.config_entries.async_has_entries(handler, include_ignore=True)
- and source != SOURCE_USER
- )
- )
+ context.get("source")
+ not in {SOURCE_IGNORE, SOURCE_REAUTH, SOURCE_RECONFIGURE}
+ and self.config_entries.async_has_entries(handler, include_ignore=False)
and await _support_single_config_entry_only(self.hass, handler)
):
return ConfigFlowResult(
@@ -1297,7 +1258,7 @@ class ConfigEntriesFlowManager(
loop = self.hass.loop
- if source == SOURCE_IMPORT:
+ if context["source"] == SOURCE_IMPORT:
self._pending_import_flows[handler][flow_id] = loop.create_future()
cancel_init_future = loop.create_future()
@@ -1463,7 +1424,6 @@ class ConfigEntriesFlowManager(
or progress_unique_id == DEFAULT_DISCOVERY_UNIQUE_ID
):
self.async_abort(progress_flow_id)
- continue
# Abort any flows in progress for the same handler
# when integration allows only one config entry
@@ -1507,14 +1467,10 @@ class ConfigEntriesFlowManager(
version=result["version"],
)
- if existing_entry is not None:
- # Unload and remove the existing entry
- await self.config_entries._async_remove(existing_entry.entry_id) # noqa: SLF001
await self.config_entries.async_add(entry)
if existing_entry is not None:
- # Clean up devices and entities belonging to the existing entry
- self.config_entries._async_clean_up(existing_entry) # noqa: SLF001
+ await self.config_entries.async_remove(existing_entry.entry_id)
result["result"] = entry
return result
@@ -1630,7 +1586,6 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]):
def __setitem__(self, entry_id: str, entry: ConfigEntry) -> None:
"""Add an item."""
data = self.data
- self.check_unique_id(entry)
if entry_id in data:
# This is likely a bug in a test that is adding the same entry twice.
# In the future, once we have fixed the tests, this will raise HomeAssistantError.
@@ -1639,49 +1594,34 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]):
data[entry_id] = entry
self._index_entry(entry)
- def check_unique_id(self, entry: ConfigEntry) -> None:
- """Check config entry unique id.
-
- For a string unique id (this is the correct case): return
- For a hashable non string unique id: log warning
- For a non-hashable unique id: raise error
- """
- if (unique_id := entry.unique_id) is None:
- return
- if isinstance(unique_id, str):
- # Unique id should be a string
- return
- if isinstance(unique_id, Hashable): # type: ignore[unreachable]
- # Checks for other non-string was added in HA Core 2024.10
- # In HA Core 2025.10, we should remove the error and instead fail
- report_issue = async_suggest_report_issue(
- self._hass, integration_domain=entry.domain
- )
- _LOGGER.error(
- (
- "Config entry '%s' from integration %s has an invalid unique_id"
- " '%s' of type %s when a string is expected, please %s"
- ),
- entry.title,
- entry.domain,
- entry.unique_id,
- type(entry.unique_id).__name__,
- report_issue,
- )
- else:
- # Guard against integrations using unhashable unique_id
- # In HA Core 2024.11, the guard was changed from warning to failing
- raise HomeAssistantError(
- f"The entry unique id {unique_id} is not a string."
- )
-
def _index_entry(self, entry: ConfigEntry) -> None:
"""Index an entry."""
- self.check_unique_id(entry)
self._domain_index.setdefault(entry.domain, []).append(entry)
if entry.unique_id is not None:
+ unique_id_hash = entry.unique_id
+ if not isinstance(entry.unique_id, str):
+ # Guard against integrations using unhashable unique_id
+ # In HA Core 2024.9, we should remove the guard and instead fail
+ if not isinstance(entry.unique_id, Hashable): # type: ignore[unreachable]
+ unique_id_hash = str(entry.unique_id)
+ # Checks for other non-string was added in HA Core 2024.10
+ # In HA Core 2025.10, we should remove the error and instead fail
+ report_issue = async_suggest_report_issue(
+ self._hass, integration_domain=entry.domain
+ )
+ _LOGGER.error(
+ (
+ "Config entry '%s' from integration %s has an invalid unique_id"
+ " '%s', please %s"
+ ),
+ entry.title,
+ entry.domain,
+ entry.unique_id,
+ report_issue,
+ )
+
self._domain_unique_id_index.setdefault(entry.domain, {}).setdefault(
- entry.unique_id, []
+ unique_id_hash, []
).append(entry)
def _unindex_entry(self, entry_id: str) -> None:
@@ -1692,6 +1632,9 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]):
if not self._domain_index[domain]:
del self._domain_index[domain]
if (unique_id := entry.unique_id) is not None:
+ # Check type first to avoid expensive isinstance call
+ if type(unique_id) is not str and not isinstance(unique_id, Hashable): # noqa: E721
+ unique_id = str(entry.unique_id) # type: ignore[unreachable]
self._domain_unique_id_index[domain][unique_id].remove(entry)
if not self._domain_unique_id_index[domain][unique_id]:
del self._domain_unique_id_index[domain][unique_id]
@@ -1710,7 +1653,6 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]):
"""
entry_id = entry.entry_id
self._unindex_entry(entry_id)
- self.check_unique_id(entry)
object.__setattr__(entry, "unique_id", new_unique_id)
self._index_entry(entry)
entry.clear_state_cache()
@@ -1724,12 +1666,9 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]):
self, domain: str, unique_id: str
) -> ConfigEntry | None:
"""Get entry by domain and unique id."""
- if unique_id is None:
- return None # type: ignore[unreachable]
- if not isinstance(unique_id, Hashable):
- raise HomeAssistantError(
- f"The entry unique id {unique_id} is not a string."
- )
+ # Check type first to avoid expensive isinstance call
+ if type(unique_id) is not str and not isinstance(unique_id, Hashable): # noqa: E721
+ unique_id = str(unique_id) # type: ignore[unreachable]
entries = self._domain_unique_id_index.get(domain, {}).get(unique_id)
if not entries:
return None
@@ -1898,27 +1837,12 @@ class ConfigEntries:
)
self._entries[entry.entry_id] = entry
- self.async_update_issues()
self._async_dispatch(ConfigEntryChange.ADDED, entry)
await self.async_setup(entry.entry_id)
self._async_schedule_save()
async def async_remove(self, entry_id: str) -> dict[str, Any]:
- """Remove, unload and clean up after an entry."""
- unload_success, entry = await self._async_remove(entry_id)
- self._async_clean_up(entry)
-
- for discovery_domain in entry.discovery_keys:
- async_dispatcher_send_internal(
- self.hass,
- signal_discovered_config_entry_removed(discovery_domain),
- entry,
- )
-
- return {"require_restart": not unload_success}
-
- async def _async_remove(self, entry_id: str) -> tuple[bool, ConfigEntry]:
- """Remove and unload an entry."""
+ """Remove an entry."""
if (entry := self.async_get_entry(entry_id)) is None:
raise UnknownEntry
@@ -1931,16 +1855,8 @@ class ConfigEntries:
await entry.async_remove(self.hass)
del self._entries[entry.entry_id]
- self.async_update_issues()
self._async_schedule_save()
- return (unload_success, entry)
-
- @callback
- def _async_clean_up(self, entry: ConfigEntry) -> None:
- """Clean up after an entry."""
- entry_id = entry.entry_id
-
dev_reg = device_registry.async_get(self.hass)
ent_reg = entity_registry.async_get(self.hass)
@@ -1959,6 +1875,13 @@ class ConfigEntries:
ir.async_delete_issue(self.hass, HOMEASSISTANT_DOMAIN, issue_id)
self._async_dispatch(ConfigEntryChange.REMOVED, entry)
+ for discovery_domain in entry.discovery_keys:
+ async_dispatcher_send_internal(
+ self.hass,
+ signal_discovered_config_entry_removed(discovery_domain),
+ entry,
+ )
+ return {"require_restart": not unload_success}
@callback
def _async_shutdown(self, event: Event) -> None:
@@ -2006,7 +1929,6 @@ class ConfigEntries:
entries[entry_id] = config_entry
self._entries = entries
- self.async_update_issues()
async def async_setup(self, entry_id: str, _lock: bool = True) -> bool:
"""Set up a config entry.
@@ -2176,12 +2098,7 @@ class ConfigEntries:
if unique_id is not UNDEFINED and entry.unique_id != unique_id:
# Deprecated in 2024.11, should fail in 2025.11
if (
- # flipr creates duplicates during migration, and asks users to
- # remove the duplicate. We don't need warn about it here too.
- # We should remove the special case for "flipr" in HA Core 2025.4,
- # when the flipr migration period ends
- entry.domain != "flipr"
- and unique_id is not None
+ unique_id is not None
and self.async_entry_for_domain_unique_id(entry.domain, unique_id)
is not None
):
@@ -2200,7 +2117,6 @@ class ConfigEntries:
)
# Reindex the entry if the unique_id has changed
self._entries.update_unique_id(entry, unique_id)
- self.async_update_issues()
changed = True
for attr, value in (
@@ -2443,84 +2359,6 @@ class ConfigEntries:
return False
return entry.state is ConfigEntryState.LOADED
- @callback
- def async_update_issues(self) -> None:
- """Update unique id collision issues."""
- issue_registry = ir.async_get(self.hass)
- issues: set[str] = set()
-
- for issue in issue_registry.issues.values():
- if (
- issue.domain != HOMEASSISTANT_DOMAIN
- or not (issue_data := issue.data)
- or issue_data.get("issue_type") != ISSUE_UNIQUE_ID_COLLISION
- ):
- continue
- issues.add(issue.issue_id)
-
- for domain, unique_ids in self._entries._domain_unique_id_index.items(): # noqa: SLF001
- # flipr creates duplicates during migration, and asks users to
- # remove the duplicate. We don't need warn about it here too.
- # We should remove the special case for "flipr" in HA Core 2025.4,
- # when the flipr migration period ends
- if domain == "flipr":
- continue
- for unique_id, entries in unique_ids.items():
- # We might mutate the list of entries, so we need a copy to not mess up
- # the index
- entries = list(entries)
-
- # There's no need to raise an issue for ignored entries, we can
- # safely remove them once we no longer allow unique id collisions.
- # Iterate over a copy of the copy to allow mutating while iterating
- for entry in list(entries):
- if entry.source == SOURCE_IGNORE:
- entries.remove(entry)
-
- if len(entries) < 2:
- continue
- issue_id = f"{ISSUE_UNIQUE_ID_COLLISION}_{domain}_{unique_id}"
- issues.discard(issue_id)
- titles = [f"'{entry.title}'" for entry in entries]
- translation_placeholders = {
- "domain": domain,
- "configure_url": f"/config/integrations/integration/{domain}",
- "unique_id": str(unique_id),
- }
- if len(titles) <= UNIQUE_ID_COLLISION_TITLE_LIMIT:
- translation_key = "config_entry_unique_id_collision"
- translation_placeholders["titles"] = ", ".join(titles)
- else:
- translation_key = "config_entry_unique_id_collision_many"
- translation_placeholders["number_of_entries"] = str(len(titles))
- translation_placeholders["titles"] = ", ".join(
- titles[:UNIQUE_ID_COLLISION_TITLE_LIMIT]
- )
- translation_placeholders["title_limit"] = str(
- UNIQUE_ID_COLLISION_TITLE_LIMIT
- )
-
- ir.async_create_issue(
- self.hass,
- HOMEASSISTANT_DOMAIN,
- issue_id,
- breaks_in_ha_version="2025.11.0",
- data={
- "issue_type": ISSUE_UNIQUE_ID_COLLISION,
- "unique_id": unique_id,
- },
- is_fixable=False,
- issue_domain=domain,
- severity=ir.IssueSeverity.ERROR,
- translation_key=translation_key,
- translation_placeholders=translation_placeholders,
- )
-
- break # Only create one issue per domain
-
- for issue_id in issues:
- ir.async_delete_issue(self.hass, HOMEASSISTANT_DOMAIN, issue_id)
-
@callback
def _async_abort_entries_match(
@@ -2596,7 +2434,6 @@ class ConfigFlow(ConfigEntryBaseFlow):
self,
*,
reason: str = "unique_id_mismatch",
- description_placeholders: Mapping[str, str] | None = None,
) -> None:
"""Abort if the unique ID does not match the reauth/reconfigure context.
@@ -2610,7 +2447,7 @@ class ConfigFlow(ConfigEntryBaseFlow):
self.source == SOURCE_RECONFIGURE
and self._get_reconfigure_entry().unique_id != self.unique_id
):
- raise data_entry_flow.AbortFlow(reason, description_placeholders)
+ raise data_entry_flow.AbortFlow(reason)
@callback
def _abort_if_unique_id_configured(
@@ -2963,38 +2800,6 @@ class ConfigFlow(ConfigEntryBaseFlow):
reason = "reconfigure_successful"
return self.async_abort(reason=reason)
- @callback
- def async_show_form(
- self,
- *,
- step_id: str | None = None,
- data_schema: vol.Schema | None = None,
- errors: dict[str, str] | None = None,
- description_placeholders: Mapping[str, str | None] | None = None,
- last_step: bool | None = None,
- preview: str | None = None,
- ) -> ConfigFlowResult:
- """Return the definition of a form to gather user input.
-
- The step_id parameter is deprecated and will be removed in a future release.
- """
- if self.source == SOURCE_REAUTH and "entry_id" in self.context:
- # If the integration does not provide a name for the reauth title,
- # we append it to the description placeholders.
- # We also need to check entry_id as some integrations bypass the
- # reauth helpers and create a flow without it.
- description_placeholders = dict(description_placeholders or {})
- if description_placeholders.get(CONF_NAME) is None:
- description_placeholders[CONF_NAME] = self._get_reauth_entry().title
- return super().async_show_form(
- step_id=step_id,
- data_schema=data_schema,
- errors=errors,
- description_placeholders=description_placeholders,
- last_step=last_step,
- preview=preview,
- )
-
def is_matching(self, other_flow: Self) -> bool:
"""Return True if other_flow is matching this flow."""
raise NotImplementedError
@@ -3102,9 +2907,6 @@ class OptionsFlow(ConfigEntryBaseFlow):
handler: str
- _config_entry: ConfigEntry
- """For compatibility only - to be removed in 2025.12"""
-
@callback
def _async_abort_entries_match(
self, match_dict: dict[str, Any] | None = None
@@ -3113,78 +2915,32 @@ class OptionsFlow(ConfigEntryBaseFlow):
Requires `already_configured` in strings.json in user visible flows.
"""
+
+ config_entry = cast(
+ ConfigEntry, self.hass.config_entries.async_get_entry(self.handler)
+ )
_async_abort_entries_match(
[
entry
- for entry in self.hass.config_entries.async_entries(
- self.config_entry.domain
- )
- if entry is not self.config_entry and entry.source != SOURCE_IGNORE
+ for entry in self.hass.config_entries.async_entries(config_entry.domain)
+ if entry is not config_entry and entry.source != SOURCE_IGNORE
],
match_dict,
)
- @property
- def _config_entry_id(self) -> str:
- """Return config entry id.
-
- Please note that this is not available inside `__init__` method, and
- can only be referenced after initialisation.
- """
- # This is the same as handler, but that's an implementation detail
- if self.handler is None:
- raise ValueError(
- "The config entry id is not available during initialisation"
- )
- return self.handler
-
- @property
- def config_entry(self) -> ConfigEntry:
- """Return the config entry linked to the current options flow.
-
- Please note that this is not available inside `__init__` method, and
- can only be referenced after initialisation.
- """
- # For compatibility only - to be removed in 2025.12
- if hasattr(self, "_config_entry"):
- return self._config_entry
-
- if self.hass is None:
- raise ValueError("The config entry is not available during initialisation")
- if entry := self.hass.config_entries.async_get_entry(self._config_entry_id):
- return entry
- raise UnknownEntry
-
- @config_entry.setter
- def config_entry(self, value: ConfigEntry) -> None:
- """Set the config entry value."""
- report_usage(
- "sets option flow config_entry explicitly, which is deprecated "
- "and will stop working in 2025.12",
- core_behavior=ReportBehavior.ERROR,
- core_integration_behavior=ReportBehavior.ERROR,
- custom_integration_behavior=ReportBehavior.LOG,
- )
- self._config_entry = value
-
class OptionsFlowWithConfigEntry(OptionsFlow):
- """Base class for options flows with config entry and options.
-
- This class is being phased out, and should not be referenced in new code.
- It is kept only for backward compatibility, and only for custom integrations.
- """
+ """Base class for options flows with config entry and options."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self._config_entry = config_entry
self._options = deepcopy(dict(config_entry.options))
- report_usage(
- "inherits from OptionsFlowWithConfigEntry",
- core_behavior=ReportBehavior.ERROR,
- core_integration_behavior=ReportBehavior.ERROR,
- custom_integration_behavior=ReportBehavior.IGNORE,
- )
+
+ @property
+ def config_entry(self) -> ConfigEntry:
+ """Return the config entry."""
+ return self._config_entry
@property
def options(self) -> dict[str, Any]:
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 4082a076b94..33c4f228430 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -24,14 +24,14 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
-MINOR_VERSION: Final = 12
+MINOR_VERSION: Final = 11
PATCH_VERSION: Final = "0.dev0"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
-REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0)
+REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
# Truthy date string triggers showing related deprecation warning messages.
-REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "2025.2"
+REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = ""
# Format for platform files
PLATFORM_FORMAT: Final = "{platform}.{domain}"
@@ -479,6 +479,16 @@ STATE_PLAYING: Final = "playing"
STATE_PAUSED: Final = "paused"
STATE_IDLE: Final = "idle"
STATE_STANDBY: Final = "standby"
+STATE_ALARM_DISARMED: Final = "disarmed"
+STATE_ALARM_ARMED_HOME: Final = "armed_home"
+STATE_ALARM_ARMED_AWAY: Final = "armed_away"
+STATE_ALARM_ARMED_NIGHT: Final = "armed_night"
+STATE_ALARM_ARMED_VACATION: Final = "armed_vacation"
+STATE_ALARM_ARMED_CUSTOM_BYPASS: Final = "armed_custom_bypass"
+STATE_ALARM_PENDING: Final = "pending"
+STATE_ALARM_ARMING: Final = "arming"
+STATE_ALARM_DISARMING: Final = "disarming"
+STATE_ALARM_TRIGGERED: Final = "triggered"
STATE_UNAVAILABLE: Final = "unavailable"
STATE_OK: Final = "ok"
STATE_PROBLEM: Final = "problem"
@@ -512,60 +522,6 @@ _DEPRECATED_STATE_JAMMED: Final = DeprecatedConstant(
"2025.10",
)
-# #### ALARM CONTROL PANEL STATES ####
-# STATE_ALARM_* below are deprecated as of 2024.11
-# use the AlarmControlPanelState enum instead.
-_DEPRECATED_STATE_ALARM_DISARMED: Final = DeprecatedConstant(
- "disarmed",
- "AlarmControlPanelState.DISARMED",
- "2025.11",
-)
-_DEPRECATED_STATE_ALARM_ARMED_HOME: Final = DeprecatedConstant(
- "armed_home",
- "AlarmControlPanelState.ARMED_HOME",
- "2025.11",
-)
-_DEPRECATED_STATE_ALARM_ARMED_AWAY: Final = DeprecatedConstant(
- "armed_away",
- "AlarmControlPanelState.ARMED_AWAY",
- "2025.11",
-)
-_DEPRECATED_STATE_ALARM_ARMED_NIGHT: Final = DeprecatedConstant(
- "armed_night",
- "AlarmControlPanelState.ARMED_NIGHT",
- "2025.11",
-)
-_DEPRECATED_STATE_ALARM_ARMED_VACATION: Final = DeprecatedConstant(
- "armed_vacation",
- "AlarmControlPanelState.ARMED_VACATION",
- "2025.11",
-)
-_DEPRECATED_STATE_ALARM_ARMED_CUSTOM_BYPASS: Final = DeprecatedConstant(
- "armed_custom_bypass",
- "AlarmControlPanelState.ARMED_CUSTOM_BYPASS",
- "2025.11",
-)
-_DEPRECATED_STATE_ALARM_PENDING: Final = DeprecatedConstant(
- "pending",
- "AlarmControlPanelState.PENDING",
- "2025.11",
-)
-_DEPRECATED_STATE_ALARM_ARMING: Final = DeprecatedConstant(
- "arming",
- "AlarmControlPanelState.ARMING",
- "2025.11",
-)
-_DEPRECATED_STATE_ALARM_DISARMING: Final = DeprecatedConstant(
- "disarming",
- "AlarmControlPanelState.DISARMING",
- "2025.11",
-)
-_DEPRECATED_STATE_ALARM_TRIGGERED: Final = DeprecatedConstant(
- "triggered",
- "AlarmControlPanelState.TRIGGERED",
- "2025.11",
-)
-
# #### STATE AND EVENT ATTRIBUTES ####
# Attribution
ATTR_ATTRIBUTION: Final = "attribution"
@@ -725,9 +681,6 @@ class UnitOfPower(StrEnum):
WATT = "W"
KILO_WATT = "kW"
- MEGA_WATT = "MW"
- GIGA_WATT = "GW"
- TERA_WATT = "TW"
BTU_PER_HOUR = "BTU/h"
@@ -773,8 +726,6 @@ class UnitOfEnergy(StrEnum):
WATT_HOUR = "Wh"
KILO_WATT_HOUR = "kWh"
MEGA_WATT_HOUR = "MWh"
- GIGA_WATT_HOUR = "GWh"
- TERA_WATT_HOUR = "TWh"
CALORIE = "cal"
KILO_CALORIE = "kcal"
MEGA_CALORIE = "Mcal"
@@ -1231,9 +1182,9 @@ class UnitOfConductivity(
StrEnum,
metaclass=EnumWithDeprecatedMembers,
deprecated={
- "SIEMENS": ("UnitOfConductivity.SIEMENS_PER_CM", "2025.11.0"),
- "MICROSIEMENS": ("UnitOfConductivity.MICROSIEMENS_PER_CM", "2025.11.0"),
- "MILLISIEMENS": ("UnitOfConductivity.MILLISIEMENS_PER_CM", "2025.11.0"),
+ "SIEMENS": ("SIEMENS_PER_CM", "2025.11.0"),
+ "MICROSIEMENS": ("MICROSIEMENS_PER_CM", "2025.11.0"),
+ "MILLISIEMENS": ("MILLISIEMENS_PER_CM", "2025.11.0"),
},
):
"""Conductivity units."""
@@ -1358,13 +1309,6 @@ CONCENTRATION_PARTS_PER_MILLION: Final = "ppm"
CONCENTRATION_PARTS_PER_BILLION: Final = "ppb"
-class UnitOfBloodGlucoseConcentration(StrEnum):
- """Blood glucose concentration units."""
-
- MILLIGRAMS_PER_DECILITER = "mg/dL"
- MILLIMOLE_PER_LITER = "mmol/L"
-
-
# Speed units
class UnitOfSpeed(StrEnum):
"""Speed units."""
diff --git a/homeassistant/core.py b/homeassistant/core.py
index cdfb5570b44..82ec4956a94 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -18,12 +18,15 @@ from collections.abc import (
ValuesView,
)
import concurrent.futures
+from contextlib import suppress
from dataclasses import dataclass
import datetime
import enum
import functools
import inspect
import logging
+import os
+import pathlib
import re
import threading
import time
@@ -39,10 +42,12 @@ from typing import (
cast,
overload,
)
+from urllib.parse import urlparse
from propcache import cached_property, under_cached_property
from typing_extensions import TypeVar
import voluptuous as vol
+import yarl
from . import util
from .const import (
@@ -50,6 +55,7 @@ from .const import (
ATTR_FRIENDLY_NAME,
ATTR_SERVICE,
ATTR_SERVICE_DATA,
+ BASE_PLATFORMS,
COMPRESSED_STATE_ATTRIBUTES,
COMPRESSED_STATE_CONTEXT,
COMPRESSED_STATE_LAST_CHANGED,
@@ -71,6 +77,7 @@ from .const import (
MAX_EXPECTED_ENTITY_IDS,
MAX_LENGTH_EVENT_EVENT_TYPE,
MAX_LENGTH_STATE_STATE,
+ UnitOfLength,
__version__,
)
from .exceptions import (
@@ -83,16 +90,14 @@ from .exceptions import (
Unauthorized,
)
from .helpers.deprecation import (
- DeferredDeprecatedAlias,
DeprecatedConstantEnum,
- EnumWithDeprecatedMembers,
all_with_deprecated_constants,
check_if_deprecated_constant,
dir_with_deprecated_constants,
)
from .helpers.json import json_bytes, json_fragment
-from .helpers.typing import VolSchemaType
-from .util import dt as dt_util
+from .helpers.typing import UNDEFINED, UndefinedType, VolSchemaType
+from .util import dt as dt_util, location
from .util.async_ import (
cancelling,
create_eager_task,
@@ -107,11 +112,18 @@ from .util.json import JsonObjectType
from .util.read_only_dict import ReadOnlyDict
from .util.timeout import TimeoutManager
from .util.ulid import ulid_at_time, ulid_now
+from .util.unit_system import (
+ _CONF_UNIT_SYSTEM_IMPERIAL,
+ _CONF_UNIT_SYSTEM_US_CUSTOMARY,
+ METRIC_SYSTEM,
+ UnitSystem,
+ get_unit_system,
+)
# Typing imports that create a circular dependency
if TYPE_CHECKING:
from .auth import AuthManager
- from .components.http import HomeAssistantHTTP
+ from .components.http import ApiConfig, HomeAssistantHTTP
from .config_entries import ConfigEntries
from .helpers.entity import StateInfo
@@ -125,6 +137,10 @@ _SENTINEL = object()
_DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=Mapping[str, Any])
type CALLBACK_TYPE = Callable[[], None]
+CORE_STORAGE_KEY = "core.config"
+CORE_STORAGE_VERSION = 1
+CORE_STORAGE_MINOR_VERSION = 4
+
DOMAIN = "homeassistant"
# How long to wait to log tasks that are blocking
@@ -134,16 +150,7 @@ type ServiceResponse = JsonObjectType | None
type EntityServiceResponse = dict[str, ServiceResponse]
-class ConfigSource(
- enum.StrEnum,
- metaclass=EnumWithDeprecatedMembers,
- deprecated={
- "DEFAULT": ("core_config.ConfigSource.DEFAULT", "2025.11.0"),
- "DISCOVERED": ("core_config.ConfigSource.DISCOVERED", "2025.11.0"),
- "STORAGE": ("core_config.ConfigSource.STORAGE", "2025.11.0"),
- "YAML": ("core_config.ConfigSource.YAML", "2025.11.0"),
- },
-):
+class ConfigSource(enum.StrEnum):
"""Source of core configuration."""
DEFAULT = "default"
@@ -185,19 +192,6 @@ _DEPRECATED_SOURCE_STORAGE = DeprecatedConstantEnum(ConfigSource.STORAGE, "2025.
_DEPRECATED_SOURCE_YAML = DeprecatedConstantEnum(ConfigSource.YAML, "2025.1")
-def _deprecated_core_config() -> Any:
- # pylint: disable-next=import-outside-toplevel
- from . import core_config
-
- return core_config.Config
-
-
-# The Config class was moved to core_config in Home Assistant 2024.11
-_DEPRECATED_Config = DeferredDeprecatedAlias(
- _deprecated_core_config, "homeassistant.core_config.Config", "2025.11"
-)
-
-
# How long to wait until things that run on startup have to finish.
TIMEOUT_EVENT_START = 15
@@ -437,9 +431,6 @@ class HomeAssistant:
# pylint: disable-next=import-outside-toplevel
from . import loader
- # pylint: disable-next=import-outside-toplevel
- from .core_config import Config
-
# This is a dictionary that any component can store any data on.
self.data = HassDict()
self.loop = asyncio.get_running_loop()
@@ -656,12 +647,12 @@ class HomeAssistant:
# late import to avoid circular imports
from .helpers import frame # pylint: disable=import-outside-toplevel
- frame.report_usage(
+ frame.report(
"calls `async_add_job`, which is deprecated and will be removed in Home "
"Assistant 2025.4; Please review "
"https://developers.home-assistant.io/blog/2024/03/13/deprecate_add_run_job"
" for replacement options",
- core_behavior=frame.ReportBehavior.LOG,
+ error_if_core=False,
)
if target is None:
@@ -712,12 +703,12 @@ class HomeAssistant:
# late import to avoid circular imports
from .helpers import frame # pylint: disable=import-outside-toplevel
- frame.report_usage(
+ frame.report(
"calls `async_add_hass_job`, which is deprecated and will be removed in Home "
"Assistant 2025.5; Please review "
"https://developers.home-assistant.io/blog/2024/04/07/deprecate_add_hass_job"
" for replacement options",
- core_behavior=frame.ReportBehavior.LOG,
+ error_if_core=False,
)
return self._async_add_hass_job(hassjob, *args, background=background)
@@ -986,12 +977,12 @@ class HomeAssistant:
# late import to avoid circular imports
from .helpers import frame # pylint: disable=import-outside-toplevel
- frame.report_usage(
+ frame.report(
"calls `async_run_job`, which is deprecated and will be removed in Home "
"Assistant 2025.4; Please review "
"https://developers.home-assistant.io/blog/2024/03/13/deprecate_add_run_job"
" for replacement options",
- core_behavior=frame.ReportBehavior.LOG,
+ error_if_core=False,
)
if asyncio.iscoroutine(target):
@@ -1635,10 +1626,10 @@ class EventBus:
# late import to avoid circular imports
from .helpers import frame # pylint: disable=import-outside-toplevel
- frame.report_usage(
+ frame.report(
"calls `async_listen` with run_immediately, which is"
" deprecated and will be removed in Home Assistant 2025.5",
- core_behavior=frame.ReportBehavior.LOG,
+ error_if_core=False,
)
if event_filter is not None and not is_callback_check_partial(event_filter):
@@ -1705,10 +1696,10 @@ class EventBus:
# late import to avoid circular imports
from .helpers import frame # pylint: disable=import-outside-toplevel
- frame.report_usage(
+ frame.report(
"calls `async_listen_once` with run_immediately, which is "
"deprecated and will be removed in Home Assistant 2025.5",
- core_behavior=frame.ReportBehavior.LOG,
+ error_if_core=False,
)
one_time_listener: _OneTimeListener[_DataT] = _OneTimeListener(
@@ -2852,6 +2843,452 @@ class ServiceRegistry:
return await self._hass.async_add_executor_job(target, service_call)
+class _ComponentSet(set[str]):
+ """Set of loaded components.
+
+ This set contains both top level components and platforms.
+
+ Examples:
+ `light`, `switch`, `hue`, `mjpeg.camera`, `universal.media_player`,
+ `homeassistant.scene`
+
+ The top level components set only contains the top level components.
+
+ The all components set contains all components, including platform
+ based components.
+
+ """
+
+ def __init__(
+ self, top_level_components: set[str], all_components: set[str]
+ ) -> None:
+ """Initialize the component set."""
+ self._top_level_components = top_level_components
+ self._all_components = all_components
+
+ def add(self, component: str) -> None:
+ """Add a component to the store."""
+ if "." not in component:
+ self._top_level_components.add(component)
+ self._all_components.add(component)
+ else:
+ platform, _, domain = component.partition(".")
+ if domain in BASE_PLATFORMS:
+ self._all_components.add(platform)
+ return super().add(component)
+
+ def remove(self, component: str) -> None:
+ """Remove a component from the store."""
+ if "." in component:
+ raise ValueError("_ComponentSet does not support removing sub-components")
+ self._top_level_components.remove(component)
+ return super().remove(component)
+
+ def discard(self, component: str) -> None:
+ """Remove a component from the store."""
+ raise NotImplementedError("_ComponentSet does not support discard, use remove")
+
+
+class Config:
+ """Configuration settings for Home Assistant."""
+
+ _store: Config._ConfigStore
+
+ def __init__(self, hass: HomeAssistant, config_dir: str) -> None:
+ """Initialize a new config object."""
+ # pylint: disable-next=import-outside-toplevel
+ from .components.zone import DEFAULT_RADIUS
+
+ self.hass = hass
+
+ self.latitude: float = 0
+ self.longitude: float = 0
+
+ self.elevation: int = 0
+ """Elevation (always in meters regardless of the unit system)."""
+
+ self.radius: int = DEFAULT_RADIUS
+ """Radius of the Home Zone (always in meters regardless of the unit system)."""
+
+ self.debug: bool = False
+ self.location_name: str = "Home"
+ self.time_zone: str = "UTC"
+ self.units: UnitSystem = METRIC_SYSTEM
+ self.internal_url: str | None = None
+ self.external_url: str | None = None
+ self.currency: str = "EUR"
+ self.country: str | None = None
+ self.language: str = "en"
+
+ self.config_source: ConfigSource = ConfigSource.DEFAULT
+
+ # If True, pip install is skipped for requirements on startup
+ self.skip_pip: bool = False
+
+ # List of packages to skip when installing requirements on startup
+ self.skip_pip_packages: list[str] = []
+
+ # Set of loaded top level components
+ # This set is updated by _ComponentSet
+ # and should not be modified directly
+ self.top_level_components: set[str] = set()
+
+ # Set of all loaded components including platform
+ # based components
+ self.all_components: set[str] = set()
+
+ # Set of loaded components
+ self.components: _ComponentSet = _ComponentSet(
+ self.top_level_components, self.all_components
+ )
+
+ # API (HTTP) server configuration
+ self.api: ApiConfig | None = None
+
+ # Directory that holds the configuration
+ self.config_dir: str = config_dir
+
+ # List of allowed external dirs to access
+ self.allowlist_external_dirs: set[str] = set()
+
+ # List of allowed external URLs that integrations may use
+ self.allowlist_external_urls: set[str] = set()
+
+ # Dictionary of Media folders that integrations may use
+ self.media_dirs: dict[str, str] = {}
+
+ # If Home Assistant is running in recovery mode
+ self.recovery_mode: bool = False
+
+ # Use legacy template behavior
+ self.legacy_templates: bool = False
+
+ # If Home Assistant is running in safe mode
+ self.safe_mode: bool = False
+
+ def async_initialize(self) -> None:
+ """Finish initializing a config object.
+
+ This must be called before the config object is used.
+ """
+ self._store = self._ConfigStore(self.hass)
+
+ def distance(self, lat: float, lon: float) -> float | None:
+ """Calculate distance from Home Assistant.
+
+ Async friendly.
+ """
+ return self.units.length(
+ location.distance(self.latitude, self.longitude, lat, lon),
+ UnitOfLength.METERS,
+ )
+
+ def path(self, *path: str) -> str:
+ """Generate path to the file within the configuration directory.
+
+ Async friendly.
+ """
+ return os.path.join(self.config_dir, *path)
+
+ def is_allowed_external_url(self, url: str) -> bool:
+ """Check if an external URL is allowed."""
+ parsed_url = f"{yarl.URL(url)!s}/"
+
+ return any(
+ allowed
+ for allowed in self.allowlist_external_urls
+ if parsed_url.startswith(allowed)
+ )
+
+ def is_allowed_path(self, path: str) -> bool:
+ """Check if the path is valid for access from outside.
+
+ This function does blocking I/O and should not be called from the event loop.
+ Use hass.async_add_executor_job to schedule it on the executor.
+ """
+ assert path is not None
+
+ thepath = pathlib.Path(path)
+ try:
+ # The file path does not have to exist (it's parent should)
+ if thepath.exists():
+ thepath = thepath.resolve()
+ else:
+ thepath = thepath.parent.resolve()
+ except (FileNotFoundError, RuntimeError, PermissionError):
+ return False
+
+ for allowed_path in self.allowlist_external_dirs:
+ try:
+ thepath.relative_to(allowed_path)
+ except ValueError:
+ pass
+ else:
+ return True
+
+ return False
+
+ def as_dict(self) -> dict[str, Any]:
+ """Create a dictionary representation of the configuration.
+
+ Async friendly.
+ """
+ allowlist_external_dirs = list(self.allowlist_external_dirs)
+ return {
+ "latitude": self.latitude,
+ "longitude": self.longitude,
+ "elevation": self.elevation,
+ "unit_system": self.units.as_dict(),
+ "location_name": self.location_name,
+ "time_zone": self.time_zone,
+ "components": list(self.components),
+ "config_dir": self.config_dir,
+ # legacy, backwards compat
+ "whitelist_external_dirs": allowlist_external_dirs,
+ "allowlist_external_dirs": allowlist_external_dirs,
+ "allowlist_external_urls": list(self.allowlist_external_urls),
+ "version": __version__,
+ "config_source": self.config_source,
+ "recovery_mode": self.recovery_mode,
+ "state": self.hass.state.value,
+ "external_url": self.external_url,
+ "internal_url": self.internal_url,
+ "currency": self.currency,
+ "country": self.country,
+ "language": self.language,
+ "safe_mode": self.safe_mode,
+ "debug": self.debug,
+ "radius": self.radius,
+ }
+
+ async def async_set_time_zone(self, time_zone_str: str) -> None:
+ """Help to set the time zone."""
+ if time_zone := await dt_util.async_get_time_zone(time_zone_str):
+ self.time_zone = time_zone_str
+ dt_util.set_default_time_zone(time_zone)
+ else:
+ raise ValueError(f"Received invalid time zone {time_zone_str}")
+
+ def set_time_zone(self, time_zone_str: str) -> None:
+ """Set the time zone.
+
+ This is a legacy method that should not be used in new code.
+ Use async_set_time_zone instead.
+
+ It will be removed in Home Assistant 2025.6.
+ """
+ # report is imported here to avoid a circular import
+ from .helpers.frame import report # pylint: disable=import-outside-toplevel
+
+ report(
+ "set the time zone using set_time_zone instead of async_set_time_zone"
+ " which will stop working in Home Assistant 2025.6",
+ error_if_core=True,
+ error_if_integration=True,
+ )
+ if time_zone := dt_util.get_time_zone(time_zone_str):
+ self.time_zone = time_zone_str
+ dt_util.set_default_time_zone(time_zone)
+ else:
+ raise ValueError(f"Received invalid time zone {time_zone_str}")
+
+ async def _async_update(
+ self,
+ *,
+ source: ConfigSource,
+ latitude: float | None = None,
+ longitude: float | None = None,
+ elevation: int | None = None,
+ unit_system: str | None = None,
+ location_name: str | None = None,
+ time_zone: str | None = None,
+ external_url: str | UndefinedType | None = UNDEFINED,
+ internal_url: str | UndefinedType | None = UNDEFINED,
+ currency: str | None = None,
+ country: str | UndefinedType | None = UNDEFINED,
+ language: str | None = None,
+ radius: int | None = None,
+ ) -> None:
+ """Update the configuration from a dictionary."""
+ self.config_source = source
+ if latitude is not None:
+ self.latitude = latitude
+ if longitude is not None:
+ self.longitude = longitude
+ if elevation is not None:
+ self.elevation = elevation
+ if unit_system is not None:
+ try:
+ self.units = get_unit_system(unit_system)
+ except ValueError:
+ self.units = METRIC_SYSTEM
+ if location_name is not None:
+ self.location_name = location_name
+ if time_zone is not None:
+ await self.async_set_time_zone(time_zone)
+ if external_url is not UNDEFINED:
+ self.external_url = external_url
+ if internal_url is not UNDEFINED:
+ self.internal_url = internal_url
+ if currency is not None:
+ self.currency = currency
+ if country is not UNDEFINED:
+ self.country = country
+ if language is not None:
+ self.language = language
+ if radius is not None:
+ self.radius = radius
+
+ async def async_update(self, **kwargs: Any) -> None:
+ """Update the configuration from a dictionary."""
+ # pylint: disable-next=import-outside-toplevel
+ from .config import (
+ _raise_issue_if_historic_currency,
+ _raise_issue_if_no_country,
+ )
+
+ await self._async_update(source=ConfigSource.STORAGE, **kwargs)
+ await self._async_store()
+ self.hass.bus.async_fire_internal(EVENT_CORE_CONFIG_UPDATE, kwargs)
+
+ _raise_issue_if_historic_currency(self.hass, self.currency)
+ _raise_issue_if_no_country(self.hass, self.country)
+
+ async def async_load(self) -> None:
+ """Load [homeassistant] core config."""
+ if not (data := await self._store.async_load()):
+ return
+
+ # In 2021.9 we fixed validation to disallow a path (because that's never
+ # correct) but this data still lives in storage, so we print a warning.
+ if data.get("external_url") and urlparse(data["external_url"]).path not in (
+ "",
+ "/",
+ ):
+ _LOGGER.warning("Invalid external_url set. It's not allowed to have a path")
+
+ if data.get("internal_url") and urlparse(data["internal_url"]).path not in (
+ "",
+ "/",
+ ):
+ _LOGGER.warning("Invalid internal_url set. It's not allowed to have a path")
+
+ await self._async_update(
+ source=ConfigSource.STORAGE,
+ latitude=data.get("latitude"),
+ longitude=data.get("longitude"),
+ elevation=data.get("elevation"),
+ unit_system=data.get("unit_system_v2"),
+ location_name=data.get("location_name"),
+ time_zone=data.get("time_zone"),
+ external_url=data.get("external_url", UNDEFINED),
+ internal_url=data.get("internal_url", UNDEFINED),
+ currency=data.get("currency"),
+ country=data.get("country"),
+ language=data.get("language"),
+ radius=data["radius"],
+ )
+
+ async def _async_store(self) -> None:
+ """Store [homeassistant] core config."""
+ data = {
+ "latitude": self.latitude,
+ "longitude": self.longitude,
+ "elevation": self.elevation,
+ # We don't want any integrations to use the name of the unit system
+ # so we are using the private attribute here
+ "unit_system_v2": self.units._name, # noqa: SLF001
+ "location_name": self.location_name,
+ "time_zone": self.time_zone,
+ "external_url": self.external_url,
+ "internal_url": self.internal_url,
+ "currency": self.currency,
+ "country": self.country,
+ "language": self.language,
+ "radius": self.radius,
+ }
+ await self._store.async_save(data)
+
+ # Circular dependency prevents us from generating the class at top level
+ # pylint: disable-next=import-outside-toplevel
+ from .helpers.storage import Store
+
+ class _ConfigStore(Store[dict[str, Any]]):
+ """Class to help storing Config data."""
+
+ def __init__(self, hass: HomeAssistant) -> None:
+ """Initialize storage class."""
+ super().__init__(
+ hass,
+ CORE_STORAGE_VERSION,
+ CORE_STORAGE_KEY,
+ private=True,
+ atomic_writes=True,
+ minor_version=CORE_STORAGE_MINOR_VERSION,
+ )
+ self._original_unit_system: str | None = None # from old store 1.1
+
+ async def _async_migrate_func(
+ self,
+ old_major_version: int,
+ old_minor_version: int,
+ old_data: dict[str, Any],
+ ) -> dict[str, Any]:
+ """Migrate to the new version."""
+
+ # pylint: disable-next=import-outside-toplevel
+ from .components.zone import DEFAULT_RADIUS
+
+ data = old_data
+ if old_major_version == 1 and old_minor_version < 2:
+ # In 1.2, we remove support for "imperial", replaced by "us_customary"
+ # Using a new key to allow rollback
+ self._original_unit_system = data.get("unit_system")
+ data["unit_system_v2"] = self._original_unit_system
+ if data["unit_system_v2"] == _CONF_UNIT_SYSTEM_IMPERIAL:
+ data["unit_system_v2"] = _CONF_UNIT_SYSTEM_US_CUSTOMARY
+ if old_major_version == 1 and old_minor_version < 3:
+ # In 1.3, we add the key "language", initialize it from the
+ # owner account.
+ data["language"] = "en"
+ try:
+ owner = await self.hass.auth.async_get_owner()
+ if owner is not None:
+ # pylint: disable-next=import-outside-toplevel
+ from .components.frontend import storage as frontend_store
+
+ # pylint: disable-next=import-outside-toplevel
+ from .helpers import config_validation as cv
+
+ _, owner_data = await frontend_store.async_user_store(
+ self.hass, owner.id
+ )
+
+ if (
+ "language" in owner_data
+ and "language" in owner_data["language"]
+ ):
+ with suppress(vol.InInvalid):
+ data["language"] = cv.language(
+ owner_data["language"]["language"]
+ )
+ # pylint: disable-next=broad-except
+ except Exception:
+ _LOGGER.exception("Unexpected error during core config migration")
+ if old_major_version == 1 and old_minor_version < 4:
+ # In 1.4, we add the key "radius", initialize it with the default.
+ data.setdefault("radius", DEFAULT_RADIUS)
+
+ if old_major_version > 1:
+ raise NotImplementedError
+ return data
+
+ async def async_save(self, data: dict[str, Any]) -> None:
+ if self._original_unit_system:
+ data["unit_system"] = self._original_unit_system
+ return await super().async_save(data)
+
+
# These can be removed if no deprecated constant are in this module anymore
__getattr__ = functools.partial(check_if_deprecated_constant, module_globals=globals())
__dir__ = functools.partial(
diff --git a/homeassistant/core_config.py b/homeassistant/core_config.py
deleted file mode 100644
index 5c773c57bc4..00000000000
--- a/homeassistant/core_config.py
+++ /dev/null
@@ -1,891 +0,0 @@
-"""Module to help with parsing and generating configuration files."""
-
-from __future__ import annotations
-
-from collections import OrderedDict
-from collections.abc import Sequence
-from contextlib import suppress
-import enum
-import logging
-import os
-import pathlib
-from typing import TYPE_CHECKING, Any, Final
-from urllib.parse import urlparse
-
-import voluptuous as vol
-from webrtc_models import RTCConfiguration, RTCIceServer
-import yarl
-
-from . import auth
-from .auth import mfa_modules as auth_mfa_modules, providers as auth_providers
-from .const import (
- ATTR_ASSUMED_STATE,
- ATTR_FRIENDLY_NAME,
- ATTR_HIDDEN,
- BASE_PLATFORMS,
- CONF_ALLOWLIST_EXTERNAL_DIRS,
- CONF_ALLOWLIST_EXTERNAL_URLS,
- CONF_AUTH_MFA_MODULES,
- CONF_AUTH_PROVIDERS,
- CONF_COUNTRY,
- CONF_CURRENCY,
- CONF_CUSTOMIZE,
- CONF_CUSTOMIZE_DOMAIN,
- CONF_CUSTOMIZE_GLOB,
- CONF_DEBUG,
- CONF_ELEVATION,
- CONF_EXTERNAL_URL,
- CONF_ID,
- CONF_INTERNAL_URL,
- CONF_LANGUAGE,
- CONF_LATITUDE,
- CONF_LEGACY_TEMPLATES,
- CONF_LONGITUDE,
- CONF_MEDIA_DIRS,
- CONF_NAME,
- CONF_PACKAGES,
- CONF_RADIUS,
- CONF_TEMPERATURE_UNIT,
- CONF_TIME_ZONE,
- CONF_TYPE,
- CONF_UNIT_SYSTEM,
- CONF_URL,
- CONF_USERNAME,
- EVENT_CORE_CONFIG_UPDATE,
- LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
- UnitOfLength,
- __version__,
-)
-from .core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
-from .generated.currencies import HISTORIC_CURRENCIES
-from .helpers import config_validation as cv, issue_registry as ir
-from .helpers.entity_values import EntityValues
-from .helpers.frame import ReportBehavior, report_usage
-from .helpers.storage import Store
-from .helpers.typing import UNDEFINED, UndefinedType
-from .util import dt as dt_util, location
-from .util.hass_dict import HassKey
-from .util.package import is_docker_env
-from .util.unit_system import (
- _CONF_UNIT_SYSTEM_IMPERIAL,
- _CONF_UNIT_SYSTEM_US_CUSTOMARY,
- METRIC_SYSTEM,
- UnitSystem,
- get_unit_system,
- validate_unit_system,
-)
-
-# Typing imports that create a circular dependency
-if TYPE_CHECKING:
- from .components.http import ApiConfig
-
-_LOGGER = logging.getLogger(__name__)
-
-DATA_CUSTOMIZE: HassKey[EntityValues] = HassKey("hass_customize")
-
-CONF_CREDENTIAL: Final = "credential"
-CONF_ICE_SERVERS: Final = "ice_servers"
-CONF_WEBRTC: Final = "webrtc"
-
-CORE_STORAGE_KEY = "core.config"
-CORE_STORAGE_VERSION = 1
-CORE_STORAGE_MINOR_VERSION = 4
-
-
-class ConfigSource(enum.StrEnum):
- """Source of core configuration."""
-
- DEFAULT = "default"
- DISCOVERED = "discovered"
- STORAGE = "storage"
- YAML = "yaml"
-
-
-def _no_duplicate_auth_provider(
- configs: Sequence[dict[str, Any]],
-) -> Sequence[dict[str, Any]]:
- """No duplicate auth provider config allowed in a list.
-
- Each type of auth provider can only have one config without optional id.
- Unique id is required if same type of auth provider used multiple times.
- """
- config_keys: set[tuple[str, str | None]] = set()
- for config in configs:
- key = (config[CONF_TYPE], config.get(CONF_ID))
- if key in config_keys:
- raise vol.Invalid(
- f"Duplicate auth provider {config[CONF_TYPE]} found. "
- "Please add unique IDs "
- "if you want to have the same auth provider twice"
- )
- config_keys.add(key)
- return configs
-
-
-def _no_duplicate_auth_mfa_module(
- configs: Sequence[dict[str, Any]],
-) -> Sequence[dict[str, Any]]:
- """No duplicate auth mfa module item allowed in a list.
-
- Each type of mfa module can only have one config without optional id.
- A global unique id is required if same type of mfa module used multiple
- times.
- Note: this is different than auth provider
- """
- config_keys: set[str] = set()
- for config in configs:
- key = config.get(CONF_ID, config[CONF_TYPE])
- if key in config_keys:
- raise vol.Invalid(
- f"Duplicate mfa module {config[CONF_TYPE]} found. "
- "Please add unique IDs "
- "if you want to have the same mfa module twice"
- )
- config_keys.add(key)
- return configs
-
-
-def _filter_bad_internal_external_urls(conf: dict) -> dict:
- """Filter internal/external URL with a path."""
- for key in CONF_INTERNAL_URL, CONF_EXTERNAL_URL:
- if key in conf and urlparse(conf[key]).path not in ("", "/"):
- # We warn but do not fix, because if this was incorrectly configured,
- # adjusting this value might impact security.
- _LOGGER.warning(
- "Invalid %s set. It's not allowed to have a path (/bla)", key
- )
-
- return conf
-
-
-# Schema for all packages element
-_PACKAGES_CONFIG_SCHEMA = vol.Schema({cv.string: vol.Any(dict, list)})
-
-# Schema for individual package definition
-_PACKAGE_DEFINITION_SCHEMA = vol.Schema({cv.string: vol.Any(dict, list, None)})
-
-_CUSTOMIZE_DICT_SCHEMA = vol.Schema(
- {
- vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
- vol.Optional(ATTR_HIDDEN): cv.boolean,
- vol.Optional(ATTR_ASSUMED_STATE): cv.boolean,
- },
- extra=vol.ALLOW_EXTRA,
-)
-
-_CUSTOMIZE_CONFIG_SCHEMA = vol.Schema(
- {
- vol.Optional(CONF_CUSTOMIZE, default={}): vol.Schema(
- {cv.entity_id: _CUSTOMIZE_DICT_SCHEMA}
- ),
- vol.Optional(CONF_CUSTOMIZE_DOMAIN, default={}): vol.Schema(
- {cv.string: _CUSTOMIZE_DICT_SCHEMA}
- ),
- vol.Optional(CONF_CUSTOMIZE_GLOB, default={}): vol.Schema(
- {cv.string: _CUSTOMIZE_DICT_SCHEMA}
- ),
- }
-)
-
-
-def _raise_issue_if_historic_currency(hass: HomeAssistant, currency: str) -> None:
- if currency not in HISTORIC_CURRENCIES:
- ir.async_delete_issue(hass, HOMEASSISTANT_DOMAIN, "historic_currency")
- return
-
- ir.async_create_issue(
- hass,
- HOMEASSISTANT_DOMAIN,
- "historic_currency",
- is_fixable=False,
- learn_more_url="homeassistant://config/general",
- severity=ir.IssueSeverity.WARNING,
- translation_key="historic_currency",
- translation_placeholders={"currency": currency},
- )
-
-
-def _raise_issue_if_no_country(hass: HomeAssistant, country: str | None) -> None:
- if country is not None:
- ir.async_delete_issue(hass, HOMEASSISTANT_DOMAIN, "country_not_configured")
- return
-
- ir.async_create_issue(
- hass,
- HOMEASSISTANT_DOMAIN,
- "country_not_configured",
- is_fixable=False,
- learn_more_url="homeassistant://config/general",
- severity=ir.IssueSeverity.WARNING,
- translation_key="country_not_configured",
- )
-
-
-def _validate_currency(data: Any) -> Any:
- try:
- return cv.currency(data)
- except vol.InInvalid:
- with suppress(vol.InInvalid):
- return cv.historic_currency(data)
- raise
-
-
-def _validate_stun_or_turn_url(value: Any) -> str:
- """Validate an URL."""
- url_in = str(value)
- url = urlparse(url_in)
-
- if url.scheme not in ("stun", "stuns", "turn", "turns"):
- raise vol.Invalid("invalid url")
- return url_in
-
-
-CORE_CONFIG_SCHEMA = vol.All(
- _CUSTOMIZE_CONFIG_SCHEMA.extend(
- {
- CONF_NAME: vol.Coerce(str),
- CONF_LATITUDE: cv.latitude,
- CONF_LONGITUDE: cv.longitude,
- CONF_ELEVATION: vol.Coerce(int),
- CONF_RADIUS: cv.positive_int,
- vol.Remove(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
- CONF_UNIT_SYSTEM: validate_unit_system,
- CONF_TIME_ZONE: cv.time_zone,
- vol.Optional(CONF_INTERNAL_URL): cv.url,
- vol.Optional(CONF_EXTERNAL_URL): cv.url,
- vol.Optional(CONF_ALLOWLIST_EXTERNAL_DIRS): vol.All(
- cv.ensure_list, [vol.IsDir()]
- ),
- vol.Optional(LEGACY_CONF_WHITELIST_EXTERNAL_DIRS): vol.All(
- cv.ensure_list, [vol.IsDir()]
- ),
- vol.Optional(CONF_ALLOWLIST_EXTERNAL_URLS): vol.All(
- cv.ensure_list, [cv.url]
- ),
- vol.Optional(CONF_PACKAGES, default={}): _PACKAGES_CONFIG_SCHEMA,
- vol.Optional(CONF_AUTH_PROVIDERS): vol.All(
- cv.ensure_list,
- [
- auth_providers.AUTH_PROVIDER_SCHEMA.extend(
- {
- CONF_TYPE: vol.NotIn(
- ["insecure_example"],
- (
- "The insecure_example auth provider"
- " is for testing only."
- ),
- )
- }
- )
- ],
- _no_duplicate_auth_provider,
- ),
- vol.Optional(CONF_AUTH_MFA_MODULES): vol.All(
- cv.ensure_list,
- [
- auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend(
- {
- CONF_TYPE: vol.NotIn(
- ["insecure_example"],
- "The insecure_example mfa module is for testing only.",
- )
- }
- )
- ],
- _no_duplicate_auth_mfa_module,
- ),
- vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()),
- vol.Remove(CONF_LEGACY_TEMPLATES): cv.boolean,
- vol.Optional(CONF_CURRENCY): _validate_currency,
- vol.Optional(CONF_COUNTRY): cv.country,
- vol.Optional(CONF_LANGUAGE): cv.language,
- vol.Optional(CONF_DEBUG): cv.boolean,
- vol.Optional(CONF_WEBRTC): vol.Schema(
- {
- vol.Required(CONF_ICE_SERVERS): vol.All(
- cv.ensure_list,
- [
- vol.Schema(
- {
- vol.Required(CONF_URL): vol.All(
- cv.ensure_list, [_validate_stun_or_turn_url]
- ),
- vol.Optional(CONF_USERNAME): cv.string,
- vol.Optional(CONF_CREDENTIAL): cv.string,
- }
- )
- ],
- )
- }
- ),
- }
- ),
- _filter_bad_internal_external_urls,
-)
-
-
-async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> None:
- """Process the [homeassistant] section from the configuration.
-
- This method is a coroutine.
- """
- # CORE_CONFIG_SCHEMA is not async safe since it uses vol.IsDir
- # so we need to run it in an executor job.
- config = await hass.async_add_executor_job(CORE_CONFIG_SCHEMA, config)
-
- # Only load auth during startup.
- if not hasattr(hass, "auth"):
- if (auth_conf := config.get(CONF_AUTH_PROVIDERS)) is None:
- auth_conf = [{"type": "homeassistant"}]
-
- mfa_conf = config.get(
- CONF_AUTH_MFA_MODULES,
- [{"type": "totp", "id": "totp", "name": "Authenticator app"}],
- )
-
- setattr(
- hass, "auth", await auth.auth_manager_from_config(hass, auth_conf, mfa_conf)
- )
-
- await hass.config.async_load()
-
- hac = hass.config
-
- if any(
- k in config
- for k in (
- CONF_COUNTRY,
- CONF_CURRENCY,
- CONF_ELEVATION,
- CONF_EXTERNAL_URL,
- CONF_INTERNAL_URL,
- CONF_LANGUAGE,
- CONF_LATITUDE,
- CONF_LONGITUDE,
- CONF_NAME,
- CONF_RADIUS,
- CONF_TIME_ZONE,
- CONF_UNIT_SYSTEM,
- )
- ):
- hac.config_source = ConfigSource.YAML
-
- for key, attr in (
- (CONF_COUNTRY, "country"),
- (CONF_CURRENCY, "currency"),
- (CONF_ELEVATION, "elevation"),
- (CONF_EXTERNAL_URL, "external_url"),
- (CONF_INTERNAL_URL, "internal_url"),
- (CONF_LANGUAGE, "language"),
- (CONF_LATITUDE, "latitude"),
- (CONF_LONGITUDE, "longitude"),
- (CONF_MEDIA_DIRS, "media_dirs"),
- (CONF_NAME, "location_name"),
- (CONF_RADIUS, "radius"),
- ):
- if key in config:
- setattr(hac, attr, config[key])
-
- if config.get(CONF_DEBUG):
- hac.debug = True
-
- if CONF_WEBRTC in config:
- hac.webrtc.ice_servers = [
- RTCIceServer(
- server[CONF_URL],
- server.get(CONF_USERNAME),
- server.get(CONF_CREDENTIAL),
- )
- for server in config[CONF_WEBRTC][CONF_ICE_SERVERS]
- ]
-
- _raise_issue_if_historic_currency(hass, hass.config.currency)
- _raise_issue_if_no_country(hass, hass.config.country)
-
- if CONF_TIME_ZONE in config:
- await hac.async_set_time_zone(config[CONF_TIME_ZONE])
-
- if CONF_MEDIA_DIRS not in config:
- if is_docker_env():
- hac.media_dirs = {"local": "/media"}
- else:
- hac.media_dirs = {"local": hass.config.path("media")}
-
- # Init whitelist external dir
- hac.allowlist_external_dirs = {hass.config.path("www"), *hac.media_dirs.values()}
- if CONF_ALLOWLIST_EXTERNAL_DIRS in config:
- hac.allowlist_external_dirs.update(set(config[CONF_ALLOWLIST_EXTERNAL_DIRS]))
-
- elif LEGACY_CONF_WHITELIST_EXTERNAL_DIRS in config:
- _LOGGER.warning(
- "Key %s has been replaced with %s. Please update your config",
- LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
- CONF_ALLOWLIST_EXTERNAL_DIRS,
- )
- hac.allowlist_external_dirs.update(
- set(config[LEGACY_CONF_WHITELIST_EXTERNAL_DIRS])
- )
-
- # Init whitelist external URL list – make sure to add / to every URL that doesn't
- # already have it so that we can properly test "path ownership"
- if CONF_ALLOWLIST_EXTERNAL_URLS in config:
- hac.allowlist_external_urls.update(
- url if url.endswith("/") else f"{url}/"
- for url in config[CONF_ALLOWLIST_EXTERNAL_URLS]
- )
-
- # Customize
- cust_exact = dict(config[CONF_CUSTOMIZE])
- cust_domain = dict(config[CONF_CUSTOMIZE_DOMAIN])
- cust_glob = OrderedDict(config[CONF_CUSTOMIZE_GLOB])
-
- for name, pkg in config[CONF_PACKAGES].items():
- if (pkg_cust := pkg.get(HOMEASSISTANT_DOMAIN)) is None:
- continue
-
- try:
- pkg_cust = _CUSTOMIZE_CONFIG_SCHEMA(pkg_cust)
- except vol.Invalid:
- _LOGGER.warning("Package %s contains invalid customize", name)
- continue
-
- cust_exact.update(pkg_cust[CONF_CUSTOMIZE])
- cust_domain.update(pkg_cust[CONF_CUSTOMIZE_DOMAIN])
- cust_glob.update(pkg_cust[CONF_CUSTOMIZE_GLOB])
-
- hass.data[DATA_CUSTOMIZE] = EntityValues(cust_exact, cust_domain, cust_glob)
-
- if CONF_UNIT_SYSTEM in config:
- hac.units = get_unit_system(config[CONF_UNIT_SYSTEM])
-
-
-class _ComponentSet(set[str]):
- """Set of loaded components.
-
- This set contains both top level components and platforms.
-
- Examples:
- `light`, `switch`, `hue`, `mjpeg.camera`, `universal.media_player`,
- `homeassistant.scene`
-
- The top level components set only contains the top level components.
-
- The all components set contains all components, including platform
- based components.
-
- """
-
- def __init__(
- self, top_level_components: set[str], all_components: set[str]
- ) -> None:
- """Initialize the component set."""
- self._top_level_components = top_level_components
- self._all_components = all_components
-
- def add(self, component: str) -> None:
- """Add a component to the store."""
- if "." not in component:
- self._top_level_components.add(component)
- self._all_components.add(component)
- else:
- platform, _, domain = component.partition(".")
- if domain in BASE_PLATFORMS:
- self._all_components.add(platform)
- return super().add(component)
-
- def remove(self, component: str) -> None:
- """Remove a component from the store."""
- if "." in component:
- raise ValueError("_ComponentSet does not support removing sub-components")
- self._top_level_components.remove(component)
- return super().remove(component)
-
- def discard(self, component: str) -> None:
- """Remove a component from the store."""
- raise NotImplementedError("_ComponentSet does not support discard, use remove")
-
-
-class Config:
- """Configuration settings for Home Assistant."""
-
- _store: Config._ConfigStore
-
- def __init__(self, hass: HomeAssistant, config_dir: str) -> None:
- """Initialize a new config object."""
- # pylint: disable-next=import-outside-toplevel
- from .components.zone import DEFAULT_RADIUS
-
- self.hass = hass
-
- self.latitude: float = 0
- self.longitude: float = 0
-
- self.elevation: int = 0
- """Elevation (always in meters regardless of the unit system)."""
-
- self.radius: int = DEFAULT_RADIUS
- """Radius of the Home Zone (always in meters regardless of the unit system)."""
-
- self.debug: bool = False
- self.location_name: str = "Home"
- self.time_zone: str = "UTC"
- self.units: UnitSystem = METRIC_SYSTEM
- self.internal_url: str | None = None
- self.external_url: str | None = None
- self.currency: str = "EUR"
- self.country: str | None = None
- self.language: str = "en"
-
- self.config_source: ConfigSource = ConfigSource.DEFAULT
-
- # If True, pip install is skipped for requirements on startup
- self.skip_pip: bool = False
-
- # List of packages to skip when installing requirements on startup
- self.skip_pip_packages: list[str] = []
-
- # Set of loaded top level components
- # This set is updated by _ComponentSet
- # and should not be modified directly
- self.top_level_components: set[str] = set()
-
- # Set of all loaded components including platform
- # based components
- self.all_components: set[str] = set()
-
- # Set of loaded components
- self.components: _ComponentSet = _ComponentSet(
- self.top_level_components, self.all_components
- )
-
- # API (HTTP) server configuration
- self.api: ApiConfig | None = None
-
- # Directory that holds the configuration
- self.config_dir: str = config_dir
-
- # List of allowed external dirs to access
- self.allowlist_external_dirs: set[str] = set()
-
- # List of allowed external URLs that integrations may use
- self.allowlist_external_urls: set[str] = set()
-
- # Dictionary of Media folders that integrations may use
- self.media_dirs: dict[str, str] = {}
-
- # If Home Assistant is running in recovery mode
- self.recovery_mode: bool = False
-
- # Use legacy template behavior
- self.legacy_templates: bool = False
-
- # If Home Assistant is running in safe mode
- self.safe_mode: bool = False
-
- self.webrtc = RTCConfiguration()
-
- def async_initialize(self) -> None:
- """Finish initializing a config object.
-
- This must be called before the config object is used.
- """
- self._store = self._ConfigStore(self.hass)
-
- def distance(self, lat: float, lon: float) -> float | None:
- """Calculate distance from Home Assistant.
-
- Async friendly.
- """
- return self.units.length(
- location.distance(self.latitude, self.longitude, lat, lon),
- UnitOfLength.METERS,
- )
-
- def path(self, *path: str) -> str:
- """Generate path to the file within the configuration directory.
-
- Async friendly.
- """
- return os.path.join(self.config_dir, *path)
-
- def is_allowed_external_url(self, url: str) -> bool:
- """Check if an external URL is allowed."""
- parsed_url = f"{yarl.URL(url)!s}/"
-
- return any(
- allowed
- for allowed in self.allowlist_external_urls
- if parsed_url.startswith(allowed)
- )
-
- def is_allowed_path(self, path: str) -> bool:
- """Check if the path is valid for access from outside.
-
- This function does blocking I/O and should not be called from the event loop.
- Use hass.async_add_executor_job to schedule it on the executor.
- """
- assert path is not None
-
- thepath = pathlib.Path(path)
- try:
- # The file path does not have to exist (it's parent should)
- if thepath.exists():
- thepath = thepath.resolve()
- else:
- thepath = thepath.parent.resolve()
- except (FileNotFoundError, RuntimeError, PermissionError):
- return False
-
- for allowed_path in self.allowlist_external_dirs:
- try:
- thepath.relative_to(allowed_path)
- except ValueError:
- pass
- else:
- return True
-
- return False
-
- def as_dict(self) -> dict[str, Any]:
- """Return a dictionary representation of the configuration.
-
- Async friendly.
- """
- allowlist_external_dirs = list(self.allowlist_external_dirs)
- return {
- "allowlist_external_dirs": allowlist_external_dirs,
- "allowlist_external_urls": list(self.allowlist_external_urls),
- "components": list(self.components),
- "config_dir": self.config_dir,
- "config_source": self.config_source,
- "country": self.country,
- "currency": self.currency,
- "debug": self.debug,
- "elevation": self.elevation,
- "external_url": self.external_url,
- "internal_url": self.internal_url,
- "language": self.language,
- "latitude": self.latitude,
- "location_name": self.location_name,
- "longitude": self.longitude,
- "radius": self.radius,
- "recovery_mode": self.recovery_mode,
- "safe_mode": self.safe_mode,
- "state": self.hass.state.value,
- "time_zone": self.time_zone,
- "unit_system": self.units.as_dict(),
- "version": __version__,
- # legacy, backwards compat
- "whitelist_external_dirs": allowlist_external_dirs,
- }
-
- async def async_set_time_zone(self, time_zone_str: str) -> None:
- """Help to set the time zone."""
- if time_zone := await dt_util.async_get_time_zone(time_zone_str):
- self.time_zone = time_zone_str
- dt_util.set_default_time_zone(time_zone)
- else:
- raise ValueError(f"Received invalid time zone {time_zone_str}")
-
- def set_time_zone(self, time_zone_str: str) -> None:
- """Set the time zone.
-
- This is a legacy method that should not be used in new code.
- Use async_set_time_zone instead.
-
- It will be removed in Home Assistant 2025.6.
- """
- report_usage(
- "set the time zone using set_time_zone instead of async_set_time_zone"
- " which will stop working in Home Assistant 2025.6",
- core_integration_behavior=ReportBehavior.ERROR,
- custom_integration_behavior=ReportBehavior.ERROR,
- )
- if time_zone := dt_util.get_time_zone(time_zone_str):
- self.time_zone = time_zone_str
- dt_util.set_default_time_zone(time_zone)
- else:
- raise ValueError(f"Received invalid time zone {time_zone_str}")
-
- async def _async_update(
- self,
- *,
- country: str | UndefinedType | None = UNDEFINED,
- currency: str | None = None,
- elevation: int | None = None,
- external_url: str | UndefinedType | None = UNDEFINED,
- internal_url: str | UndefinedType | None = UNDEFINED,
- language: str | None = None,
- latitude: float | None = None,
- location_name: str | None = None,
- longitude: float | None = None,
- radius: int | None = None,
- source: ConfigSource,
- time_zone: str | None = None,
- unit_system: str | None = None,
- ) -> None:
- """Update the configuration from a dictionary."""
- self.config_source = source
- if country is not UNDEFINED:
- self.country = country
- if currency is not None:
- self.currency = currency
- if elevation is not None:
- self.elevation = elevation
- if external_url is not UNDEFINED:
- self.external_url = external_url
- if internal_url is not UNDEFINED:
- self.internal_url = internal_url
- if language is not None:
- self.language = language
- if latitude is not None:
- self.latitude = latitude
- if location_name is not None:
- self.location_name = location_name
- if longitude is not None:
- self.longitude = longitude
- if radius is not None:
- self.radius = radius
- if time_zone is not None:
- await self.async_set_time_zone(time_zone)
- if unit_system is not None:
- try:
- self.units = get_unit_system(unit_system)
- except ValueError:
- self.units = METRIC_SYSTEM
-
- async def async_update(self, **kwargs: Any) -> None:
- """Update the configuration from a dictionary."""
- await self._async_update(source=ConfigSource.STORAGE, **kwargs)
- await self._async_store()
- self.hass.bus.async_fire_internal(EVENT_CORE_CONFIG_UPDATE, kwargs)
-
- _raise_issue_if_historic_currency(self.hass, self.currency)
- _raise_issue_if_no_country(self.hass, self.country)
-
- async def async_load(self) -> None:
- """Load [homeassistant] core config."""
- if not (data := await self._store.async_load()):
- return
-
- # In 2021.9 we fixed validation to disallow a path (because that's never
- # correct) but this data still lives in storage, so we print a warning.
- if data.get("external_url") and urlparse(data["external_url"]).path not in (
- "",
- "/",
- ):
- _LOGGER.warning("Invalid external_url set. It's not allowed to have a path")
-
- if data.get("internal_url") and urlparse(data["internal_url"]).path not in (
- "",
- "/",
- ):
- _LOGGER.warning("Invalid internal_url set. It's not allowed to have a path")
-
- await self._async_update(
- source=ConfigSource.STORAGE,
- latitude=data.get("latitude"),
- longitude=data.get("longitude"),
- elevation=data.get("elevation"),
- unit_system=data.get("unit_system_v2"),
- location_name=data.get("location_name"),
- time_zone=data.get("time_zone"),
- external_url=data.get("external_url", UNDEFINED),
- internal_url=data.get("internal_url", UNDEFINED),
- currency=data.get("currency"),
- country=data.get("country"),
- language=data.get("language"),
- radius=data["radius"],
- )
-
- async def _async_store(self) -> None:
- """Store [homeassistant] core config."""
- data = {
- "latitude": self.latitude,
- "longitude": self.longitude,
- "elevation": self.elevation,
- # We don't want any integrations to use the name of the unit system
- # so we are using the private attribute here
- "unit_system_v2": self.units._name, # noqa: SLF001
- "location_name": self.location_name,
- "time_zone": self.time_zone,
- "external_url": self.external_url,
- "internal_url": self.internal_url,
- "currency": self.currency,
- "country": self.country,
- "language": self.language,
- "radius": self.radius,
- }
- await self._store.async_save(data)
-
- class _ConfigStore(Store[dict[str, Any]]):
- """Class to help storing Config data."""
-
- def __init__(self, hass: HomeAssistant) -> None:
- """Initialize storage class."""
- super().__init__(
- hass,
- CORE_STORAGE_VERSION,
- CORE_STORAGE_KEY,
- private=True,
- atomic_writes=True,
- minor_version=CORE_STORAGE_MINOR_VERSION,
- )
- self._original_unit_system: str | None = None # from old store 1.1
-
- async def _async_migrate_func(
- self,
- old_major_version: int,
- old_minor_version: int,
- old_data: dict[str, Any],
- ) -> dict[str, Any]:
- """Migrate to the new version."""
-
- # pylint: disable-next=import-outside-toplevel
- from .components.zone import DEFAULT_RADIUS
-
- data = old_data
- if old_major_version == 1 and old_minor_version < 2:
- # In 1.2, we remove support for "imperial", replaced by "us_customary"
- # Using a new key to allow rollback
- self._original_unit_system = data.get("unit_system")
- data["unit_system_v2"] = self._original_unit_system
- if data["unit_system_v2"] == _CONF_UNIT_SYSTEM_IMPERIAL:
- data["unit_system_v2"] = _CONF_UNIT_SYSTEM_US_CUSTOMARY
- if old_major_version == 1 and old_minor_version < 3:
- # In 1.3, we add the key "language", initialize it from the
- # owner account.
- data["language"] = "en"
- try:
- owner = await self.hass.auth.async_get_owner()
- if owner is not None:
- # pylint: disable-next=import-outside-toplevel
- from .components.frontend import storage as frontend_store
-
- _, owner_data = await frontend_store.async_user_store(
- self.hass, owner.id
- )
-
- if (
- "language" in owner_data
- and "language" in owner_data["language"]
- ):
- with suppress(vol.InInvalid):
- data["language"] = cv.language(
- owner_data["language"]["language"]
- )
- # pylint: disable-next=broad-except
- except Exception:
- _LOGGER.exception("Unexpected error during core config migration")
- if old_major_version == 1 and old_minor_version < 4:
- # In 1.4, we add the key "radius", initialize it with the default.
- data.setdefault("radius", DEFAULT_RADIUS)
-
- if old_major_version > 1:
- raise NotImplementedError
- return data
-
- async def async_save(self, data: dict[str, Any]) -> None:
- if self._original_unit_system:
- data["unit_system"] = self._original_unit_system
- return await super().async_save(data)
diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py
index 9d041c9b8d3..1fb6439a8c4 100644
--- a/homeassistant/data_entry_flow.py
+++ b/homeassistant/data_entry_flow.py
@@ -26,7 +26,7 @@ from .helpers.deprecation import (
check_if_deprecated_constant,
dir_with_deprecated_constants,
)
-from .helpers.frame import ReportBehavior, report_usage
+from .helpers.frame import report
from .loader import async_suggest_report_issue
from .util import uuid as uuid_util
@@ -530,12 +530,12 @@ class FlowManager(abc.ABC, Generic[_FlowContextT, _FlowResultT, _HandlerT]):
if not isinstance(result["type"], FlowResultType):
result["type"] = FlowResultType(result["type"]) # type: ignore[unreachable]
- report_usage(
+ report(
(
"does not use FlowResultType enum for data entry flow result type. "
"This is deprecated and will stop working in Home Assistant 2025.1"
),
- core_behavior=ReportBehavior.LOG,
+ error_if_core=False,
)
if (
diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py
index a105efc2685..2ea604a91a2 100644
--- a/homeassistant/generated/bluetooth.py
+++ b/homeassistant/generated/bluetooth.py
@@ -8,26 +8,6 @@ from __future__ import annotations
from typing import Final
BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
- {
- "domain": "acaia",
- "manufacturer_id": 16962,
- },
- {
- "domain": "acaia",
- "local_name": "ACAIA*",
- },
- {
- "domain": "acaia",
- "local_name": "PYXIS-*",
- },
- {
- "domain": "acaia",
- "local_name": "LUNAR-*",
- },
- {
- "domain": "acaia",
- "local_name": "PROCHBT001",
- },
{
"domain": "airthings_ble",
"manufacturer_id": 820,
@@ -299,11 +279,6 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
],
"manufacturer_id": 76,
},
- {
- "connectable": True,
- "domain": "husqvarna_automower_ble",
- "service_uuid": "98bd0001-0b0e-421a-84e5-ddbf75dc6de4",
- },
{
"domain": "ibeacon",
"manufacturer_data_start": [
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index ffe61b915c6..f399b0922f1 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -24,7 +24,6 @@ FLOWS = {
],
"integration": [
"abode",
- "acaia",
"accuweather",
"acmeda",
"adax",
@@ -222,6 +221,7 @@ FLOWS = {
"gios",
"github",
"glances",
+ "go2rtc",
"goalzero",
"gogogate2",
"goodwe",
@@ -265,7 +265,6 @@ FLOWS = {
"huisbaasje",
"hunterdouglas_powerview",
"husqvarna_automower",
- "husqvarna_automower_ble",
"huum",
"hvv_departures",
"hydrawise",
@@ -328,7 +327,6 @@ FLOWS = {
"lektrico",
"lg_netcast",
"lg_soundbar",
- "lg_thinq",
"lidarr",
"lifx",
"linear_garage_door",
@@ -337,7 +335,6 @@ FLOWS = {
"litterrobot",
"livisi",
"local_calendar",
- "local_file",
"local_ip",
"local_todo",
"locative",
@@ -385,14 +382,12 @@ FLOWS = {
"mpd",
"mqtt",
"mullvad",
- "music_assistant",
"mutesync",
"mysensors",
"mystrom",
"myuplink",
"nam",
"nanoleaf",
- "nasweb",
"neato",
"nest",
"netatmo",
@@ -409,7 +404,6 @@ FLOWS = {
"nina",
"nmap_tracker",
"nobo_hub",
- "nordpool",
"notion",
"nuheat",
"nuki",
@@ -424,7 +418,6 @@ FLOWS = {
"oncue",
"ondilo_ico",
"onewire",
- "onkyo",
"onvif",
"open_meteo",
"openai_conversation",
@@ -445,7 +438,6 @@ FLOWS = {
"ovo_energy",
"owntracks",
"p1_monitor",
- "palazzetti",
"panasonic_viera",
"peco",
"pegel_online",
@@ -538,7 +530,6 @@ FLOWS = {
"simplefin",
"simplepush",
"simplisafe",
- "sky_remote",
"skybell",
"slack",
"sleepiq",
@@ -548,7 +539,6 @@ FLOWS = {
"smart_meter_texas",
"smartthings",
"smarttub",
- "smarty",
"smhi",
"smlight",
"sms",
diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py
index 7dacf9a0bca..154ca93545c 100644
--- a/homeassistant/generated/dhcp.py
+++ b/homeassistant/generated/dhcp.py
@@ -37,6 +37,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [
"hostname": "august*",
"macaddress": "E076D0*",
},
+ {
+ "domain": "awair",
+ "macaddress": "70886B1*",
+ },
{
"domain": "axis",
"registered_devices": True,
@@ -276,18 +280,6 @@ DHCP: Final[list[dict[str, str | bool]]] = [
"hostname": "polisy*",
"macaddress": "000DB9*",
},
- {
- "domain": "lamarzocco",
- "hostname": "gs[0-9][0-9][0-9][0-9][0-9][0-9]",
- },
- {
- "domain": "lamarzocco",
- "hostname": "lm[0-9][0-9][0-9][0-9][0-9][0-9]",
- },
- {
- "domain": "lamarzocco",
- "hostname": "mr[0-9][0-9][0-9][0-9][0-9][0-9]",
- },
{
"domain": "lametric",
"registered_devices": True,
@@ -379,15 +371,6 @@ DHCP: Final[list[dict[str, str | bool]]] = [
"hostname": "gateway*",
"macaddress": "F8811A*",
},
- {
- "domain": "palazzetti",
- "hostname": "connbox*",
- "macaddress": "40F3857*",
- },
- {
- "domain": "palazzetti",
- "registered_devices": True,
- },
{
"domain": "powerwall",
"hostname": "1118431-*",
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index f007db87868..3243d1677ae 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -11,12 +11,6 @@
"config_flow": true,
"iot_class": "cloud_push"
},
- "acaia": {
- "name": "Acaia",
- "integration_type": "device",
- "config_flow": true,
- "iot_class": "local_push"
- },
"accuweather": {
"name": "AccuWeather",
"integration_type": "service",
@@ -704,6 +698,12 @@
"config_flow": false,
"iot_class": "cloud_polling"
},
+ "bloomsky": {
+ "name": "BloomSky",
+ "integration_type": "hub",
+ "config_flow": false,
+ "iot_class": "cloud_polling"
+ },
"blue_current": {
"name": "Blue Current",
"integration_type": "hub",
@@ -958,8 +958,7 @@
"name": "Cloudflare",
"integration_type": "hub",
"config_flow": true,
- "iot_class": "cloud_push",
- "single_config_entry": true
+ "iot_class": "cloud_push"
},
"cmus": {
"name": "cmus",
@@ -1161,8 +1160,7 @@
"demo": {
"integration_type": "hub",
"config_flow": false,
- "iot_class": "calculated",
- "single_config_entry": true
+ "iot_class": "calculated"
},
"denon": {
"name": "Denon",
@@ -1405,8 +1403,7 @@
"name": "Duotecno",
"integration_type": "hub",
"config_flow": true,
- "iot_class": "local_push",
- "single_config_entry": true
+ "iot_class": "local_push"
},
"duquesne_light": {
"name": "Duquesne Light",
@@ -1464,8 +1461,7 @@
"name": "ecobee",
"integration_type": "hub",
"config_flow": true,
- "iot_class": "cloud_polling",
- "single_config_entry": true
+ "iot_class": "cloud_polling"
},
"ecoforest": {
"name": "Ecoforest",
@@ -1663,8 +1659,7 @@
"name": "EnOcean",
"integration_type": "hub",
"config_flow": true,
- "iot_class": "local_push",
- "single_config_entry": true
+ "iot_class": "local_push"
},
"enphase_envoy": {
"name": "Enphase Envoy",
@@ -2252,6 +2247,13 @@
}
}
},
+ "go2rtc": {
+ "name": "go2rtc",
+ "integration_type": "hub",
+ "config_flow": true,
+ "iot_class": "local_polling",
+ "single_config_entry": true
+ },
"goalzero": {
"name": "Goal Zero Yeti",
"integration_type": "device",
@@ -2468,8 +2470,7 @@
"name": "Home Assistant Supervisor",
"integration_type": "hub",
"config_flow": false,
- "iot_class": "local_polling",
- "single_config_entry": true
+ "iot_class": "local_polling"
},
"havana_shade": {
"name": "Havana Shade",
@@ -2684,22 +2685,11 @@
"integration_type": "virtual",
"supported_by": "motion_blinds"
},
- "husqvarna": {
- "name": "Husqvarna",
- "integrations": {
- "husqvarna_automower": {
- "integration_type": "hub",
- "config_flow": true,
- "iot_class": "cloud_push",
- "name": "Husqvarna Automower"
- },
- "husqvarna_automower_ble": {
- "integration_type": "hub",
- "config_flow": true,
- "iot_class": "local_polling",
- "name": "Husqvarna Automower BLE"
- }
- }
+ "husqvarna_automower": {
+ "name": "Husqvarna Automower",
+ "integration_type": "hub",
+ "config_flow": true,
+ "iot_class": "cloud_push"
},
"huum": {
"name": "Huum",
@@ -2741,8 +2731,7 @@
"name": "Jandy iAqualink",
"integration_type": "hub",
"config_flow": true,
- "iot_class": "cloud_polling",
- "single_config_entry": true
+ "iot_class": "cloud_polling"
},
"ibm": {
"name": "IBM",
@@ -2871,8 +2860,7 @@
"name": "Insteon",
"integration_type": "hub",
"config_flow": true,
- "iot_class": "local_push",
- "single_config_entry": true
+ "iot_class": "local_push"
},
"intellifire": {
"name": "IntelliFire",
@@ -2971,8 +2959,7 @@
"name": "International Space Station (ISS)",
"integration_type": "service",
"config_flow": true,
- "iot_class": "cloud_polling",
- "single_config_entry": true
+ "iot_class": "cloud_polling"
},
"ista_ecotrend": {
"name": "ista EcoTrend",
@@ -3111,8 +3098,7 @@
"name": "Everything but the Kitchen Sink",
"integration_type": "hub",
"config_flow": false,
- "iot_class": "calculated",
- "single_config_entry": true
+ "iot_class": "calculated"
},
"kiwi": {
"name": "KIWI",
@@ -3226,8 +3212,7 @@
"name": "Launch Library",
"integration_type": "service",
"config_flow": true,
- "iot_class": "cloud_polling",
- "single_config_entry": true
+ "iot_class": "cloud_polling"
},
"laundrify": {
"name": "laundrify",
@@ -3291,12 +3276,6 @@
"iot_class": "local_polling",
"name": "LG Soundbars"
},
- "lg_thinq": {
- "integration_type": "hub",
- "config_flow": true,
- "iot_class": "cloud_push",
- "name": "LG ThinQ"
- },
"webostv": {
"integration_type": "hub",
"config_flow": true,
@@ -3375,8 +3354,7 @@
"name": "LiteJet",
"integration_type": "hub",
"config_flow": true,
- "iot_class": "local_push",
- "single_config_entry": true
+ "iot_class": "local_push"
},
"litterrobot": {
"name": "Litter-Robot",
@@ -3404,14 +3382,13 @@
"local_file": {
"name": "Local File",
"integration_type": "hub",
- "config_flow": true,
+ "config_flow": false,
"iot_class": "local_polling"
},
"local_ip": {
"integration_type": "hub",
"config_flow": true,
- "iot_class": "local_polling",
- "single_config_entry": true
+ "iot_class": "local_polling"
},
"local_todo": {
"integration_type": "hub",
@@ -3950,12 +3927,6 @@
"iot_class": "cloud_polling",
"single_config_entry": true
},
- "music_assistant": {
- "name": "Music Assistant",
- "integration_type": "hub",
- "config_flow": true,
- "iot_class": "local_push"
- },
"mutesync": {
"name": "mutesync",
"integration_type": "hub",
@@ -4022,12 +3993,6 @@
"config_flow": true,
"iot_class": "local_push"
},
- "nasweb": {
- "name": "NASweb",
- "integration_type": "hub",
- "config_flow": true,
- "iot_class": "local_push"
- },
"neato": {
"name": "Neato Botvac",
"integration_type": "hub",
@@ -4193,13 +4158,6 @@
"config_flow": true,
"iot_class": "local_push"
},
- "nordpool": {
- "name": "Nord Pool",
- "integration_type": "hub",
- "config_flow": true,
- "iot_class": "cloud_polling",
- "single_config_entry": true
- },
"norway_air": {
"name": "Om Luftkvalitet i Norge (Norway Air)",
"integration_type": "hub",
@@ -4281,8 +4239,7 @@
"name": "NZBGet",
"integration_type": "hub",
"config_flow": true,
- "iot_class": "local_polling",
- "single_config_entry": true
+ "iot_class": "local_polling"
},
"oasa_telematics": {
"name": "OASA Telematics",
@@ -4330,8 +4287,7 @@
"name": "Hayward Omnilogic",
"integration_type": "hub",
"config_flow": true,
- "iot_class": "cloud_polling",
- "single_config_entry": true
+ "iot_class": "cloud_polling"
},
"oncue": {
"name": "Oncue by Kohler",
@@ -4343,8 +4299,7 @@
"name": "Ondilo ICO",
"integration_type": "hub",
"config_flow": true,
- "iot_class": "cloud_polling",
- "single_config_entry": true
+ "iot_class": "cloud_polling"
},
"onewire": {
"name": "1-Wire",
@@ -4354,8 +4309,8 @@
},
"onkyo": {
"name": "Onkyo",
- "integration_type": "device",
- "config_flow": true,
+ "integration_type": "hub",
+ "config_flow": false,
"iot_class": "local_push"
},
"onvif": {
@@ -4552,8 +4507,7 @@
"name": "OwnTracks",
"integration_type": "hub",
"config_flow": true,
- "iot_class": "local_push",
- "single_config_entry": true
+ "iot_class": "local_push"
},
"p1_monitor": {
"name": "P1 Monitor",
@@ -4561,12 +4515,6 @@
"config_flow": true,
"iot_class": "local_polling"
},
- "palazzetti": {
- "name": "Palazzetti",
- "integration_type": "device",
- "config_flow": true,
- "iot_class": "local_polling"
- },
"panasonic": {
"name": "Panasonic",
"integrations": {
@@ -4590,6 +4538,11 @@
"config_flow": false,
"iot_class": "local_polling"
},
+ "panel_iframe": {
+ "name": "iframe Panel",
+ "integration_type": "hub",
+ "config_flow": false
+ },
"pcs_lighting": {
"name": "PCS Lighting",
"integration_type": "virtual",
@@ -4769,8 +4722,7 @@
"profiler": {
"name": "Profiler",
"integration_type": "hub",
- "config_flow": true,
- "single_config_entry": true
+ "config_flow": true
},
"progettihwsw": {
"name": "ProgettiHWSW Automation",
@@ -4985,8 +4937,7 @@
"name": "Radio Browser",
"integration_type": "service",
"config_flow": true,
- "iot_class": "cloud_polling",
- "single_config_entry": true
+ "iot_class": "cloud_polling"
},
"radiotherm": {
"name": "Radio Thermostat",
@@ -5161,8 +5112,7 @@
"name": "Rhasspy",
"integration_type": "hub",
"config_flow": true,
- "iot_class": "local_push",
- "single_config_entry": true
+ "iot_class": "local_push"
},
"ridwell": {
"name": "Ridwell",
@@ -5614,22 +5564,11 @@
"config_flow": false,
"iot_class": "local_push"
},
- "sky": {
- "name": "Sky",
- "integrations": {
- "sky_hub": {
- "integration_type": "hub",
- "config_flow": false,
- "iot_class": "local_polling",
- "name": "Sky Hub"
- },
- "sky_remote": {
- "integration_type": "device",
- "config_flow": true,
- "iot_class": "assumed_state",
- "name": "Sky Remote Control"
- }
- }
+ "sky_hub": {
+ "name": "Sky Hub",
+ "integration_type": "hub",
+ "config_flow": false,
+ "iot_class": "local_polling"
},
"skybeacon": {
"name": "Skybeacon",
@@ -5715,7 +5654,7 @@
"smarty": {
"name": "Salda Smarty",
"integration_type": "hub",
- "config_flow": true,
+ "config_flow": false,
"iot_class": "local_polling"
},
"smhi": {
@@ -7349,6 +7288,7 @@
"iot_class": "calculated"
},
"history_stats": {
+ "name": "History Stats",
"integration_type": "helper",
"config_flow": true,
"iot_class": "local_polling"
@@ -7394,11 +7334,13 @@
"iot_class": "calculated"
},
"mold_indicator": {
+ "name": "Mold Indicator",
"integration_type": "helper",
"config_flow": true,
"iot_class": "calculated"
},
"random": {
+ "name": "Random",
"integration_type": "helper",
"config_flow": true,
"iot_class": "calculated"
@@ -7408,6 +7350,7 @@
"config_flow": false
},
"statistics": {
+ "name": "Statistics",
"integration_type": "helper",
"config_flow": true,
"iot_class": "local_polling"
@@ -7429,6 +7372,7 @@
"iot_class": "local_polling"
},
"timer": {
+ "name": "Timer",
"integration_type": "helper",
"config_flow": false
},
@@ -7438,6 +7382,7 @@
"iot_class": "calculated"
},
"trend": {
+ "name": "Trend",
"integration_type": "helper",
"config_flow": true,
"iot_class": "calculated"
@@ -7466,7 +7411,6 @@
"google_travel_time",
"group",
"growatt_server",
- "history_stats",
"holiday",
"homekit_controller",
"input_boolean",
@@ -7483,25 +7427,20 @@
"min_max",
"mobile_app",
"moehlenhoff_alpha2",
- "mold_indicator",
"moon",
"nextbus",
"nmap_tracker",
"plant",
"proximity",
- "random",
"rpi_power",
"schedule",
"season",
"shopping_list",
- "statistics",
"sun",
"switch_as_x",
"threshold",
"time_date",
- "timer",
"tod",
- "trend",
"uptime",
"utility_meter",
"version",
diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py
index 1fbd6337fdb..f627f1f0f47 100644
--- a/homeassistant/generated/zeroconf.py
+++ b/homeassistant/generated/zeroconf.py
@@ -267,11 +267,6 @@ HOMEKIT = {
}
ZEROCONF = {
- "_PowerView-G3._tcp.local.": [
- {
- "domain": "hunterdouglas_powerview",
- },
- ],
"_Volumio._tcp.local.": [
{
"domain": "volumio",
@@ -614,12 +609,6 @@ ZEROCONF = {
},
],
"_lutron._tcp.local.": [
- {
- "domain": "lutron_caseta",
- "properties": {
- "SYSTYPE": "hwqs*",
- },
- },
{
"domain": "lutron_caseta",
"properties": {
@@ -639,11 +628,6 @@ ZEROCONF = {
},
},
],
- "_mass._tcp.local.": [
- {
- "domain": "music_assistant",
- },
- ],
"_matter._tcp.local.": [
{
"domain": "matter",
@@ -711,6 +695,11 @@ ZEROCONF = {
"domain": "plugwise",
},
],
+ "_powerview-g3._tcp.local.": [
+ {
+ "domain": "hunterdouglas_powerview",
+ },
+ ],
"_powerview._tcp.local.": [
{
"domain": "hunterdouglas_powerview",
diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py
index f01ae325875..2f4c1980468 100644
--- a/homeassistant/helpers/aiohttp_client.py
+++ b/homeassistant/helpers/aiohttp_client.py
@@ -44,13 +44,11 @@ SERVER_SOFTWARE = (
f"aiohttp/{aiohttp.__version__} Python/{sys.version_info[0]}.{sys.version_info[1]}"
)
-ENABLE_CLEANUP_CLOSED = (3, 13, 0) <= sys.version_info < (
- 3,
- 13,
- 1,
-) or sys.version_info < (3, 12, 7)
-# Cleanup closed is no longer needed after https://github.com/python/cpython/pull/118960
-# which first appeared in Python 3.12.7 and 3.13.1
+ENABLE_CLEANUP_CLOSED = not (3, 11, 1) <= sys.version_info < (3, 11, 4)
+# Enabling cleanup closed on python 3.11.1+ leaks memory relatively quickly
+# see https://github.com/aio-libs/aiohttp/issues/7252
+# aiohttp interacts poorly with https://github.com/python/cpython/pull/98540
+# The issue was fixed in 3.11.4 via https://github.com/python/cpython/pull/104485
WARN_CLOSE_MSG = "closes the Home Assistant aiohttp session"
diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py
index 4b5e2f277a0..43021fffac5 100644
--- a/homeassistant/helpers/check_config.py
+++ b/homeassistant/helpers/check_config.py
@@ -13,6 +13,7 @@ import voluptuous as vol
from homeassistant import loader
from homeassistant.config import ( # type: ignore[attr-defined]
CONF_PACKAGES,
+ CORE_CONFIG_SCHEMA,
YAML_CONFIG_FILE,
config_per_platform,
extract_domain_configs,
@@ -22,7 +23,6 @@ from homeassistant.config import ( # type: ignore[attr-defined]
merge_packages_config,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
-from homeassistant.core_config import CORE_CONFIG_SCHEMA
from homeassistant.exceptions import HomeAssistantError
from homeassistant.requirements import (
RequirementsNotFound,
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index 2b35ebade76..98a2cd71931 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -719,14 +719,14 @@ def template(value: Any | None) -> template_helper.Template:
raise vol.Invalid("template value should be a string")
if not (hass := _async_get_hass_or_none()):
# pylint: disable-next=import-outside-toplevel
- from .frame import ReportBehavior, report_usage
+ from .frame import report
- report_usage(
+ report(
(
"validates schema outside the event loop, "
"which will stop working in HA Core 2025.10"
),
- core_behavior=ReportBehavior.LOG,
+ error_if_core=False,
)
template_value = template_helper.Template(str(value), hass)
@@ -748,14 +748,14 @@ def dynamic_template(value: Any | None) -> template_helper.Template:
raise vol.Invalid("template value does not contain a dynamic template")
if not (hass := _async_get_hass_or_none()):
# pylint: disable-next=import-outside-toplevel
- from .frame import ReportBehavior, report_usage
+ from .frame import report
- report_usage(
+ report(
(
"validates schema outside the event loop, "
"which will stop working in HA Core 2025.10"
),
- core_behavior=ReportBehavior.LOG,
+ error_if_core=False,
)
template_value = template_helper.Template(str(value), hass)
@@ -874,7 +874,7 @@ def url_no_path(value: Any) -> str:
url_in = url(value)
if urlparse(url_in).path not in ("", "/"):
- raise vol.Invalid("url is not allowed to have a path component")
+ raise vol.Invalid("url it not allowed to have a path component")
return url_in
diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py
index 81f7821ec79..df65546986b 100644
--- a/homeassistant/helpers/deprecation.py
+++ b/homeassistant/helpers/deprecation.py
@@ -3,7 +3,6 @@
from __future__ import annotations
from collections.abc import Callable
-from contextlib import suppress
from enum import Enum, EnumType, _EnumDict
import functools
import inspect
@@ -165,30 +164,6 @@ def _print_deprecation_warning_internal(
breaks_in_ha_version: str | None,
*,
log_when_no_integration_is_found: bool,
-) -> None:
- # Suppress ImportError due to use of deprecated enum in core.py
- # Can be removed in HA Core 2025.1
- with suppress(ImportError):
- _print_deprecation_warning_internal_impl(
- obj_name,
- module_name,
- replacement,
- description,
- verb,
- breaks_in_ha_version,
- log_when_no_integration_is_found=log_when_no_integration_is_found,
- )
-
-
-def _print_deprecation_warning_internal_impl(
- obj_name: str,
- module_name: str,
- replacement: str,
- description: str,
- verb: str,
- breaks_in_ha_version: str | None,
- *,
- log_when_no_integration_is_found: bool,
) -> None:
# pylint: disable=import-outside-toplevel
from homeassistant.core import async_get_hass_or_none
@@ -388,7 +363,7 @@ class EnumWithDeprecatedMembers(EnumType):
_print_deprecation_warning_internal(
f"{cls.__name__}.{name}",
cls.__module__,
- f"{deprecated[name][0]}",
+ f"{cls.__name__}.{deprecated[name][0]}",
"enum member",
"used",
deprecated[name][1],
diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py
index faf4257577d..f05179ccf0a 100644
--- a/homeassistant/helpers/device_registry.py
+++ b/homeassistant/helpers/device_registry.py
@@ -842,6 +842,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
device.id,
allow_collisions=True,
add_config_entry_id=config_entry_id,
+ add_config_entry=config_entry,
configuration_url=configuration_url,
device_info_type=device_info_type,
disabled_by=disabled_by,
@@ -869,6 +870,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
self,
device_id: str,
*,
+ add_config_entry: ConfigEntry | UndefinedType = UNDEFINED,
add_config_entry_id: str | UndefinedType = UNDEFINED,
# Temporary flag so we don't blow up when collisions are implicitly introduced
# by calls to async_get_or_create. Must not be set by integrations.
@@ -903,11 +905,13 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
config_entries = old.config_entries
- if add_config_entry_id is not UNDEFINED:
- if self.hass.config_entries.async_get_entry(add_config_entry_id) is None:
+ if add_config_entry_id is not UNDEFINED and add_config_entry is UNDEFINED:
+ config_entry = self.hass.config_entries.async_get_entry(add_config_entry_id)
+ if config_entry is None:
raise HomeAssistantError(
f"Can't link device to unknown config entry {add_config_entry_id}"
)
+ add_config_entry = config_entry
if not new_connections and not new_identifiers:
raise HomeAssistantError(
@@ -951,11 +955,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
area = ar.async_get(self.hass).async_get_or_create(suggested_area)
area_id = area.id
- if add_config_entry_id is not UNDEFINED:
+ if add_config_entry is not UNDEFINED:
primary_entry_id = old.primary_config_entry
if (
device_info_type == "primary"
- and add_config_entry_id != primary_entry_id
+ and add_config_entry.entry_id != primary_entry_id
):
if (
primary_entry_id is None
@@ -966,11 +970,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
)
or primary_entry.domain in LOW_PRIO_CONFIG_ENTRY_DOMAINS
):
- new_values["primary_config_entry"] = add_config_entry_id
- old_values["primary_config_entry"] = primary_entry_id
+ new_values["primary_config_entry"] = add_config_entry.entry_id
+ old_values["primary_config_entry"] = old.primary_config_entry
- if add_config_entry_id not in old.config_entries:
- config_entries = old.config_entries | {add_config_entry_id}
+ if add_config_entry.entry_id not in old.config_entries:
+ config_entries = old.config_entries | {add_config_entry.entry_id}
if (
remove_config_entry_id is not UNDEFINED
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index 1f77dd3f95c..cc843b6d9b1 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Any, Final, Literal, NotRequired, TypedDict, f
from propcache import cached_property
import voluptuous as vol
+from homeassistant.config import DATA_CUSTOMIZE
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_ATTRIBUTION,
@@ -48,7 +49,6 @@ from homeassistant.core import (
get_hassjob_callable_job_type,
get_release_channel,
)
-from homeassistant.core_config import DATA_CUSTOMIZE
from homeassistant.exceptions import (
HomeAssistantError,
InvalidStateError,
@@ -337,9 +337,7 @@ class CachedProperties(type):
Also invalidates the corresponding cached_property by calling
delattr on it.
"""
- if (
- old_val := getattr(o, private_attr_name, _SENTINEL)
- ) == val and type(old_val) is type(val):
+ if getattr(o, private_attr_name, _SENTINEL) == val:
return
setattr(o, private_attr_name, val)
# Invalidate the cache of the cached property
diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py
index 1be7289401c..76abb3020d1 100644
--- a/homeassistant/helpers/entity_component.py
+++ b/homeassistant/helpers/entity_component.py
@@ -65,13 +65,10 @@ async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None:
class EntityComponent(Generic[_EntityT]):
- """The EntityComponent manages platforms that manage entities.
-
- An example of an entity component is 'light', which manages platforms such
- as 'hue.light'.
+ """The EntityComponent manages platforms that manages entities.
This class has the following responsibilities:
- - Process the configuration and set up a platform based component, for example light.
+ - Process the configuration and set up a platform based component.
- Manage the platforms and their entities.
- Help extract the entities from a service call.
- Listen for discovery events for platforms related to the domain.
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
index 62eed213b2a..fe852e2114b 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -111,11 +111,7 @@ class EntityPlatformModule(Protocol):
class EntityPlatform:
- """Manage the entities for a single platform.
-
- An example of an entity platform is 'hue.light', which is managed by
- the entity component 'light'.
- """
+ """Manage the entities for a single platform."""
def __init__(
self,
diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py
index 61a798dbd75..97a85fdde89 100644
--- a/homeassistant/helpers/event.py
+++ b/homeassistant/helpers/event.py
@@ -322,10 +322,6 @@ def async_track_state_change_event(
for each one, we keep a dict of entity ids that
care about the state change events so we can
do a fast dict lookup to route events.
- The passed in entity_ids will be automatically lower cased.
-
- EVENT_STATE_CHANGED is fired on each occasion the state is updated
- and changed, opposite of EVENT_STATE_REPORTED.
"""
if not (entity_ids := _async_string_to_lower_list(entity_ids)):
return _remove_empty_listener
@@ -387,10 +383,7 @@ def _async_track_state_change_event(
action: Callable[[Event[EventStateChangedData]], Any],
job_type: HassJobType | None,
) -> CALLBACK_TYPE:
- """Faster version of async_track_state_change_event.
-
- The passed in entity_ids will not be automatically lower cased.
- """
+ """async_track_state_change_event without lowercasing."""
return _async_track_event(
_KEYED_TRACK_STATE_CHANGE, hass, entity_ids, action, job_type
)
@@ -410,11 +403,7 @@ def async_track_state_report_event(
action: Callable[[Event[EventStateReportedData]], Any],
job_type: HassJobType | None = None,
) -> CALLBACK_TYPE:
- """Track EVENT_STATE_REPORTED by entity_ids.
-
- EVENT_STATE_REPORTED is fired on each occasion the state is updated
- but not changed, opposite of EVENT_STATE_CHANGED.
- """
+ """Track EVENT_STATE_REPORTED by entity_id without lowercasing."""
return _async_track_event(
_KEYED_TRACK_STATE_REPORT, hass, entity_ids, action, job_type
)
@@ -997,14 +986,14 @@ class TrackTemplateResultInfo:
continue
# pylint: disable-next=import-outside-toplevel
- from .frame import ReportBehavior, report_usage
+ from .frame import report
- report_usage(
+ report(
(
"calls async_track_template_result with template without hass, "
"which will stop working in HA Core 2025.10"
),
- core_behavior=ReportBehavior.LOG,
+ error_if_core=False,
)
track_template_.template.hass = hass
diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py
index eda98099713..fd7e014b2ff 100644
--- a/homeassistant/helpers/frame.py
+++ b/homeassistant/helpers/frame.py
@@ -5,7 +5,6 @@ from __future__ import annotations
import asyncio
from collections.abc import Callable
from dataclasses import dataclass
-import enum
import functools
import linecache
import logging
@@ -145,72 +144,24 @@ def report(
If error_if_integration is True, raise instead of log if an integration is found
when unwinding the stack frame.
"""
- core_behavior = ReportBehavior.ERROR if error_if_core else ReportBehavior.LOG
- core_integration_behavior = (
- ReportBehavior.ERROR if error_if_integration else ReportBehavior.LOG
- )
- custom_integration_behavior = core_integration_behavior
-
- if log_custom_component_only:
- if core_behavior is ReportBehavior.LOG:
- core_behavior = ReportBehavior.IGNORE
- if core_integration_behavior is ReportBehavior.LOG:
- core_integration_behavior = ReportBehavior.IGNORE
-
- report_usage(
- what,
- core_behavior=core_behavior,
- core_integration_behavior=core_integration_behavior,
- custom_integration_behavior=custom_integration_behavior,
- exclude_integrations=exclude_integrations,
- level=level,
- )
-
-
-class ReportBehavior(enum.Enum):
- """Enum for behavior on code usage."""
-
- IGNORE = enum.auto()
- """Ignore the code usage."""
- LOG = enum.auto()
- """Log the code usage."""
- ERROR = enum.auto()
- """Raise an error on code usage."""
-
-
-def report_usage(
- what: str,
- *,
- core_behavior: ReportBehavior = ReportBehavior.ERROR,
- core_integration_behavior: ReportBehavior = ReportBehavior.LOG,
- custom_integration_behavior: ReportBehavior = ReportBehavior.LOG,
- exclude_integrations: set[str] | None = None,
- level: int = logging.WARNING,
-) -> None:
- """Report incorrect code usage.
-
- Similar to `report` but allows more fine-grained reporting.
- """
try:
integration_frame = get_integration_frame(
exclude_integrations=exclude_integrations
)
except MissingIntegrationFrame as err:
msg = f"Detected code that {what}. Please report this issue."
- if core_behavior is ReportBehavior.ERROR:
+ if error_if_core:
raise RuntimeError(msg) from err
- if core_behavior is ReportBehavior.LOG:
+ if not log_custom_component_only:
_LOGGER.warning(msg, stack_info=True)
return
- integration_behavior = core_integration_behavior
- if integration_frame.custom_integration:
- integration_behavior = custom_integration_behavior
-
- if integration_behavior is not ReportBehavior.IGNORE:
- _report_integration(
- what, integration_frame, level, integration_behavior is ReportBehavior.ERROR
- )
+ if (
+ error_if_integration
+ or not log_custom_component_only
+ or integration_frame.custom_integration
+ ):
+ _report_integration(what, integration_frame, level, error_if_integration)
def _report_integration(
diff --git a/homeassistant/helpers/hassio.py b/homeassistant/helpers/hassio.py
deleted file mode 100644
index 51503f709d6..00000000000
--- a/homeassistant/helpers/hassio.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""Hass.io helper."""
-
-import os
-
-from homeassistant.core import HomeAssistant, callback
-
-
-@callback
-def is_hassio(hass: HomeAssistant) -> bool:
- """Return true if Hass.io is loaded.
-
- Async friendly.
- """
- return "hassio" in hass.config.components
-
-
-@callback
-def get_supervisor_ip() -> str | None:
- """Return the supervisor ip address."""
- if "SUPERVISOR" not in os.environ:
- return None
- return os.environ["SUPERVISOR"].partition(":")[0]
diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py
index b38f769b302..15e38d39dda 100644
--- a/homeassistant/helpers/intent.py
+++ b/homeassistant/helpers/intent.py
@@ -56,7 +56,6 @@ INTENT_UNPAUSE_TIMER = "HassUnpauseTimer"
INTENT_TIMER_STATUS = "HassTimerStatus"
INTENT_GET_CURRENT_DATE = "HassGetCurrentDate"
INTENT_GET_CURRENT_TIME = "HassGetCurrentTime"
-INTENT_RESPOND = "HassRespond"
SLOT_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA)
@@ -352,7 +351,6 @@ class MatchTargetsCandidate:
"""Candidate for async_match_targets."""
state: State
- is_exposed: bool
entity: entity_registry.RegistryEntry | None = None
area: area_registry.AreaEntry | None = None
floor: floor_registry.FloorEntry | None = None
@@ -516,31 +514,29 @@ def async_match_targets( # noqa: C901
if not states:
return MatchTargetsResult(False, MatchFailedReason.DOMAIN)
- candidates = [
- MatchTargetsCandidate(
- state=state,
- is_exposed=(
- async_should_expose(hass, constraints.assistant, state.entity_id)
- if constraints.assistant
- else True
- ),
- )
- for state in states
- ]
+ if constraints.assistant:
+ # Filter by exposure
+ states = [
+ s
+ for s in states
+ if async_should_expose(hass, constraints.assistant, s.entity_id)
+ ]
+ if not states:
+ return MatchTargetsResult(False, MatchFailedReason.ASSISTANT)
if constraints.domains and (not filtered_by_domain):
# Filter by domain (if we didn't already do it)
- candidates = [c for c in candidates if c.state.domain in constraints.domains]
- if not candidates:
+ states = [s for s in states if s.domain in constraints.domains]
+ if not states:
return MatchTargetsResult(False, MatchFailedReason.DOMAIN)
if constraints.states:
# Filter by state
- candidates = [c for c in candidates if c.state.state in constraints.states]
- if not candidates:
+ states = [s for s in states if s.state in constraints.states]
+ if not states:
return MatchTargetsResult(False, MatchFailedReason.STATE)
- # Try to exit early so we can avoid registry lookups
+ # Exit early so we can avoid registry lookups
if not (
constraints.name
or constraints.features
@@ -548,18 +544,11 @@ def async_match_targets( # noqa: C901
or constraints.area_name
or constraints.floor_name
):
- if constraints.assistant:
- # Check exposure
- candidates = [c for c in candidates if c.is_exposed]
- if not candidates:
- return MatchTargetsResult(False, MatchFailedReason.ASSISTANT)
-
- return MatchTargetsResult(True, states=[c.state for c in candidates])
+ return MatchTargetsResult(True, states=states)
# We need entity registry entries now
er = entity_registry.async_get(hass)
- for candidate in candidates:
- candidate.entity = er.async_get(candidate.state.entity_id)
+ candidates = [MatchTargetsCandidate(s, er.async_get(s.entity_id)) for s in states]
if constraints.name:
# Filter by entity name or alias
@@ -648,12 +637,6 @@ def async_match_targets( # noqa: C901
False, MatchFailedReason.AREA, areas=targeted_areas
)
- if constraints.assistant:
- # Check exposure
- candidates = [c for c in candidates if c.is_exposed]
- if not candidates:
- return MatchTargetsResult(False, MatchFailedReason.ASSISTANT)
-
if constraints.name and (not constraints.allow_duplicate_names):
# Check for duplicates
if not areas_added:
diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py
index d322810b0ef..8b2e0660687 100644
--- a/homeassistant/helpers/llm.py
+++ b/homeassistant/helpers/llm.py
@@ -177,6 +177,11 @@ class APIInstance:
else:
raise HomeAssistantError(f'Tool "{tool_input.tool_name}" not found')
+ tool_input = ToolInput(
+ tool_name=tool_input.tool_name,
+ tool_args=tool.parameters(tool_input.tool_args),
+ )
+
return await tool.async_call(self.api.hass, tool_input, self.llm_context)
@@ -279,7 +284,6 @@ class AssistAPI(API):
intent.INTENT_TOGGLE,
intent.INTENT_GET_CURRENT_DATE,
intent.INTENT_GET_CURRENT_TIME,
- intent.INTENT_RESPOND,
}
def __init__(self, hass: HomeAssistant) -> None:
@@ -416,9 +420,7 @@ class AssistAPI(API):
):
continue
- script_tool = ScriptTool(self.hass, state.entity_id)
- if script_tool.parameters.schema:
- tools.append(script_tool)
+ tools.append(ScriptTool(self.hass, state.entity_id))
return tools
@@ -449,16 +451,11 @@ def _get_exposed_entities(
entities = {}
for state in hass.states.async_all():
- if not async_should_expose(hass, assistant, state.entity_id):
+ if state.domain == SCRIPT_DOMAIN:
continue
- description: str | None = None
- if state.domain == SCRIPT_DOMAIN:
- description, parameters = _get_cached_script_parameters(
- hass, state.entity_id
- )
- if parameters.schema: # Only list scripts without input fields here
- continue
+ if not async_should_expose(hass, assistant, state.entity_id):
+ continue
entity_entry = entity_registry.async_get(state.entity_id)
names = [state.name]
@@ -488,9 +485,6 @@ def _get_exposed_entities(
"state": state.state,
}
- if description:
- info["description"] = description
-
if area_names:
info["areas"] = ", ".join(area_names)
@@ -616,83 +610,6 @@ def _selector_serializer(schema: Any) -> Any: # noqa: C901
return {"type": "string"}
-def _get_cached_script_parameters(
- hass: HomeAssistant, entity_id: str
-) -> tuple[str | None, vol.Schema]:
- """Get script description and schema."""
- entity_registry = er.async_get(hass)
-
- description = None
- parameters = vol.Schema({})
- entity_entry = entity_registry.async_get(entity_id)
- if entity_entry and entity_entry.unique_id:
- parameters_cache = hass.data.get(SCRIPT_PARAMETERS_CACHE)
-
- if parameters_cache is None:
- parameters_cache = hass.data[SCRIPT_PARAMETERS_CACHE] = {}
-
- @callback
- def clear_cache(event: Event) -> None:
- """Clear script parameter cache on script reload or delete."""
- if (
- event.data[ATTR_DOMAIN] == SCRIPT_DOMAIN
- and event.data[ATTR_SERVICE] in parameters_cache
- ):
- parameters_cache.pop(event.data[ATTR_SERVICE])
-
- cancel = hass.bus.async_listen(EVENT_SERVICE_REMOVED, clear_cache)
-
- @callback
- def on_homeassistant_close(event: Event) -> None:
- """Cleanup."""
- cancel()
-
- hass.bus.async_listen_once(
- EVENT_HOMEASSISTANT_CLOSE, on_homeassistant_close
- )
-
- if entity_entry.unique_id in parameters_cache:
- return parameters_cache[entity_entry.unique_id]
-
- if service_desc := service.async_get_cached_service_description(
- hass, SCRIPT_DOMAIN, entity_entry.unique_id
- ):
- description = service_desc.get("description")
- schema: dict[vol.Marker, Any] = {}
- fields = service_desc.get("fields", {})
-
- for field, config in fields.items():
- field_description = config.get("description")
- if not field_description:
- field_description = config.get("name")
- key: vol.Marker
- if config.get("required"):
- key = vol.Required(field, description=field_description)
- else:
- key = vol.Optional(field, description=field_description)
- if "selector" in config:
- schema[key] = selector.selector(config["selector"])
- else:
- schema[key] = cv.string
-
- parameters = vol.Schema(schema)
-
- aliases: list[str] = []
- if entity_entry.name:
- aliases.append(entity_entry.name)
- if entity_entry.aliases:
- aliases.extend(entity_entry.aliases)
- if aliases:
- if description:
- description = description + ". Aliases: " + str(list(aliases))
- else:
- description = "Aliases: " + str(list(aliases))
-
- parameters_cache[entity_entry.unique_id] = (description, parameters)
-
- return description, parameters
-
-
class ScriptTool(Tool):
"""LLM Tool representing a Script."""
@@ -702,14 +619,86 @@ class ScriptTool(Tool):
script_entity_id: str,
) -> None:
"""Init the class."""
+ entity_registry = er.async_get(hass)
+
self.name = split_entity_id(script_entity_id)[1]
if self.name[0].isdigit():
self.name = "_" + self.name
self._entity_id = script_entity_id
+ self.parameters = vol.Schema({})
+ entity_entry = entity_registry.async_get(script_entity_id)
+ if entity_entry and entity_entry.unique_id:
+ parameters_cache = hass.data.get(SCRIPT_PARAMETERS_CACHE)
- self.description, self.parameters = _get_cached_script_parameters(
- hass, script_entity_id
- )
+ if parameters_cache is None:
+ parameters_cache = hass.data[SCRIPT_PARAMETERS_CACHE] = {}
+
+ @callback
+ def clear_cache(event: Event) -> None:
+ """Clear script parameter cache on script reload or delete."""
+ if (
+ event.data[ATTR_DOMAIN] == SCRIPT_DOMAIN
+ and event.data[ATTR_SERVICE] in parameters_cache
+ ):
+ parameters_cache.pop(event.data[ATTR_SERVICE])
+
+ cancel = hass.bus.async_listen(EVENT_SERVICE_REMOVED, clear_cache)
+
+ @callback
+ def on_homeassistant_close(event: Event) -> None:
+ """Cleanup."""
+ cancel()
+
+ hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_CLOSE, on_homeassistant_close
+ )
+
+ if entity_entry.unique_id in parameters_cache:
+ self.description, self.parameters = parameters_cache[
+ entity_entry.unique_id
+ ]
+ return
+
+ if service_desc := service.async_get_cached_service_description(
+ hass, SCRIPT_DOMAIN, entity_entry.unique_id
+ ):
+ self.description = service_desc.get("description")
+ schema: dict[vol.Marker, Any] = {}
+ fields = service_desc.get("fields", {})
+
+ for field, config in fields.items():
+ description = config.get("description")
+ if not description:
+ description = config.get("name")
+ key: vol.Marker
+ if config.get("required"):
+ key = vol.Required(field, description=description)
+ else:
+ key = vol.Optional(field, description=description)
+ if "selector" in config:
+ schema[key] = selector.selector(config["selector"])
+ else:
+ schema[key] = cv.string
+
+ self.parameters = vol.Schema(schema)
+
+ aliases: list[str] = []
+ if entity_entry.name:
+ aliases.append(entity_entry.name)
+ if entity_entry.aliases:
+ aliases.extend(entity_entry.aliases)
+ if aliases:
+ if self.description:
+ self.description = (
+ self.description + ". Aliases: " + str(list(aliases))
+ )
+ else:
+ self.description = "Aliases: " + str(list(aliases))
+
+ parameters_cache[entity_entry.unique_id] = (
+ self.description,
+ self.parameters,
+ )
async def async_call(
self, hass: HomeAssistant, tool_input: ToolInput, llm_context: LLMContext
diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py
index e39cc2de547..fa7fec9faea 100644
--- a/homeassistant/helpers/network.py
+++ b/homeassistant/helpers/network.py
@@ -16,8 +16,6 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass
from homeassistant.util.network import is_ip_address, is_loopback, normalize_url
-from .hassio import is_hassio
-
TYPE_URL_INTERNAL = "internal_url"
TYPE_URL_EXTERNAL = "external_url"
SUPERVISOR_NETWORK_HOST = "homeassistant"
@@ -44,6 +42,10 @@ def get_supervisor_network_url(
hass: HomeAssistant, *, allow_ssl: bool = False
) -> str | None:
"""Get URL for home assistant within supervisor network."""
+ # Local import to avoid circular dependencies
+ # pylint: disable-next=import-outside-toplevel
+ from homeassistant.components.hassio import is_hassio
+
if hass.config.api is None or not is_hassio(hass):
return None
@@ -178,21 +180,20 @@ def get_url(
and request_host is not None
and hass.config.api is not None
):
+ # Local import to avoid circular dependencies
+ # pylint: disable-next=import-outside-toplevel
+ from homeassistant.components.hassio import get_host_info, is_hassio
+
scheme = "https" if hass.config.api.use_ssl else "http"
current_url = yarl.URL.build(
scheme=scheme, host=request_host, port=hass.config.api.port
)
known_hostnames = ["localhost"]
- if is_hassio(hass):
- # Local import to avoid circular dependencies
- # pylint: disable-next=import-outside-toplevel
- from homeassistant.components.hassio import get_host_info
-
- if host_info := get_host_info(hass):
- known_hostnames.extend(
- [host_info["hostname"], f"{host_info['hostname']}.local"]
- )
+ if is_hassio(hass) and (host_info := get_host_info(hass)):
+ known_hostnames.extend(
+ [host_info["hostname"], f"{host_info['hostname']}.local"]
+ )
if (
(
diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py
index af8c4c6402d..7463c9945b2 100644
--- a/homeassistant/helpers/schema_config_entry_flow.py
+++ b/homeassistant/helpers/schema_config_entry_flow.py
@@ -16,6 +16,7 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
+ OptionsFlowWithConfigEntry,
)
from homeassistant.core import HomeAssistant, callback, split_entity_id
from homeassistant.data_entry_flow import UnknownHandler
@@ -402,7 +403,7 @@ class SchemaConfigFlowHandler(ConfigFlow, ABC):
)
-class SchemaOptionsFlowHandler(OptionsFlow):
+class SchemaOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle a schema based options flow."""
def __init__(
@@ -421,8 +422,10 @@ class SchemaOptionsFlowHandler(OptionsFlow):
options, which is the union of stored options and user input from the options
flow steps.
"""
- self._options = copy.deepcopy(dict(config_entry.options))
- self._common_handler = SchemaCommonFlowHandler(self, options_flow, self.options)
+ super().__init__(config_entry)
+ self._common_handler = SchemaCommonFlowHandler(
+ self, options_flow, self._options
+ )
self._async_options_flow_finished = async_options_flow_finished
for step in options_flow:
@@ -435,11 +438,6 @@ class SchemaOptionsFlowHandler(OptionsFlow):
if async_setup_preview:
setattr(self, "async_setup_preview", async_setup_preview)
- @property
- def options(self) -> dict[str, Any]:
- """Return a mutable copy of the config entry options."""
- return self._options
-
@staticmethod
def _async_step(
step_id: str,
diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py
index 86dcd858c1b..ee2c4c64773 100644
--- a/homeassistant/helpers/script.py
+++ b/homeassistant/helpers/script.py
@@ -1133,11 +1133,7 @@ class _ScriptRun:
self._step_log("wait for trigger", timeout)
variables = {**self._variables}
- self._variables["wait"] = {
- "remaining": timeout,
- "completed": False,
- "trigger": None,
- }
+ self._variables["wait"] = {"remaining": timeout, "trigger": None}
trace_set_result(wait=self._variables["wait"])
if timeout == 0:
@@ -1155,7 +1151,6 @@ class _ScriptRun:
variables: dict[str, Any], context: Context | None = None
) -> None:
self._async_set_remaining_time_var(timeout_handle)
- self._variables["wait"]["completed"] = True
self._variables["wait"]["trigger"] = variables["trigger"]
_set_result_unless_done(done)
diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py
index e3da52604cb..ac21f1da3fc 100644
--- a/homeassistant/helpers/service.py
+++ b/homeassistant/helpers/service.py
@@ -571,31 +571,19 @@ def async_extract_referenced_entity_ids( # noqa: C901
for area_entry in area_reg.areas.get_areas_for_floor(floor_id)
)
- selected.referenced_areas.update(selector.area_ids)
+ # Find devices for targeted areas
selected.referenced_devices.update(selector.device_ids)
- if not selected.referenced_areas and not selected.referenced_devices:
- return selected
-
- # Add indirectly referenced by device
- selected.indirectly_referenced.update(
- entry.entity_id
- for device_id in selected.referenced_devices
- for entry in entities.get_entries_for_device_id(device_id)
- # Do not add entities which are hidden or which are config
- # or diagnostic entities.
- if (entry.entity_category is None and entry.hidden_by is None)
- )
-
- # Find devices for targeted areas
- referenced_devices_by_area: set[str] = set()
+ selected.referenced_areas.update(selector.area_ids)
if selected.referenced_areas:
for area_id in selected.referenced_areas:
- referenced_devices_by_area.update(
+ selected.referenced_devices.update(
device_entry.id
for device_entry in dev_reg.devices.get_devices_for_area_id(area_id)
)
- selected.referenced_devices.update(referenced_devices_by_area)
+
+ if not selected.referenced_areas and not selected.referenced_devices:
+ return selected
# Add indirectly referenced by area
selected.indirectly_referenced.update(
@@ -607,10 +595,10 @@ def async_extract_referenced_entity_ids( # noqa: C901
# or diagnostic entities.
if entry.entity_category is None and entry.hidden_by is None
)
- # Add indirectly referenced by area through device
+ # Add indirectly referenced by device
selected.indirectly_referenced.update(
entry.entity_id
- for device_id in referenced_devices_by_area
+ for device_id in selected.referenced_devices
for entry in entities.get_entries_for_device_id(device_id)
# Do not add entities which are hidden or which are config
# or diagnostic entities.
@@ -622,10 +610,11 @@ def async_extract_referenced_entity_ids( # noqa: C901
# by an area and the entity
# has no explicitly set area
not entry.area_id
+ # The entity's device matches a targeted device
+ or device_id in selector.device_ids
)
)
)
-
return selected
@@ -1277,14 +1266,14 @@ def async_register_entity_service(
schema = cv.make_entity_service_schema(schema)
elif not cv.is_entity_service_schema(schema):
# pylint: disable-next=import-outside-toplevel
- from .frame import ReportBehavior, report_usage
+ from .frame import report
- report_usage(
+ report(
(
"registers an entity service with a non entity service schema "
"which will stop working in HA Core 2025.9"
),
- core_behavior=ReportBehavior.LOG,
+ error_if_core=False,
)
service_func: str | HassJob[..., Any]
diff --git a/homeassistant/helpers/service_info/hassio.py b/homeassistant/helpers/service_info/hassio.py
deleted file mode 100644
index 0125fef3017..00000000000
--- a/homeassistant/helpers/service_info/hassio.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""Hassio Discovery data."""
-
-from dataclasses import dataclass
-from typing import Any
-
-from homeassistant.data_entry_flow import BaseServiceInfo
-
-
-@dataclass(slots=True)
-class HassioServiceInfo(BaseServiceInfo):
- """Prepared info from hassio entries."""
-
- config: dict[str, Any]
- name: str
- slug: str
- uuid: str
diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py
index df4c45cd5ed..69e03904caa 100644
--- a/homeassistant/helpers/system_info.py
+++ b/homeassistant/helpers/system_info.py
@@ -14,7 +14,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.loader import bind_hass
from homeassistant.util.package import is_docker_env, is_virtual_env
-from .hassio import is_hassio
from .importlib import async_import_module
from .singleton import singleton
@@ -53,13 +52,13 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]:
else:
hassio = await async_import_module(hass, "homeassistant.components.hassio")
- is_hassio_ = is_hassio(hass)
+ is_hassio = hassio.is_hassio(hass)
info_object = {
"installation_type": "Unknown",
"version": current_version,
"dev": "dev" in current_version,
- "hassio": is_hassio_,
+ "hassio": is_hassio,
"virtualenv": is_virtual_env(),
"python_version": platform.python_version(),
"docker": False,
@@ -90,7 +89,7 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]:
info_object["installation_type"] = "Home Assistant Core"
# Enrich with Supervisor information
- if is_hassio_:
+ if is_hassio:
if not (info := hassio.get_info(hass)):
_LOGGER.warning("No Home Assistant Supervisor info available")
info = {}
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index 2eab666bbd4..928ef2e791d 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -515,18 +515,18 @@ class Template:
will be non optional in Home Assistant Core 2025.10.
"""
# pylint: disable-next=import-outside-toplevel
- from .frame import ReportBehavior, report_usage
+ from .frame import report
if not isinstance(template, str):
raise TypeError("Expected template to be a string")
if not hass:
- report_usage(
+ report(
(
"creates a template object without passing hass, "
"which will stop working in HA Core 2025.10"
),
- core_behavior=ReportBehavior.LOG,
+ error_if_core=False,
)
self.template: str = template.strip()
@@ -1281,7 +1281,7 @@ def result_as_boolean(template_result: Any | None) -> bool:
True/not 0/'1'/'true'/'yes'/'on'/'enable' are considered truthy
False/0/None/'0'/'false'/'no'/'off'/'disable' are considered falsy
- All other values are falsy
+
"""
if template_result is None:
return False
diff --git a/homeassistant/helpers/trigger_template_entity.py b/homeassistant/helpers/trigger_template_entity.py
index 7f8ad41d7bb..9df263207eb 100644
--- a/homeassistant/helpers/trigger_template_entity.py
+++ b/homeassistant/helpers/trigger_template_entity.py
@@ -176,18 +176,43 @@ class TriggerBaseEntity(Entity):
extra_state_attributes[attr] = last_state.attributes[attr]
self._rendered[CONF_ATTRIBUTES] = extra_state_attributes
+ def _render_availability_template(self, variables: dict[str, Any]) -> None:
+ """Render availability template."""
+ rendered = dict(self._static_rendered)
+ self._rendered = self._static_rendered
+ try:
+ key = CONF_AVAILABILITY
+ if key in self._to_render_simple:
+ rendered[key] = self._config[key].async_render(
+ variables,
+ parse_result=key in self._parse_result,
+ )
+ elif key in self._to_render_complex:
+ rendered[key] = render_complex(
+ self._config[key],
+ variables,
+ )
+ except TemplateError as err:
+ logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error(
+ "Error rendering %s template for %s: %s", key, self.entity_id, err
+ )
+ self._rendered = rendered
+
def _render_templates(self, variables: dict[str, Any]) -> None:
"""Render templates."""
+ rendered = dict(self._rendered)
try:
- rendered = dict(self._static_rendered)
-
for key in self._to_render_simple:
+ if key == CONF_AVAILABILITY:
+ continue
rendered[key] = self._config[key].async_render(
variables,
parse_result=key in self._parse_result,
)
for key in self._to_render_complex:
+ if key == CONF_AVAILABILITY:
+ continue
rendered[key] = render_complex(
self._config[key],
variables,
@@ -204,7 +229,6 @@ class TriggerBaseEntity(Entity):
logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error(
"Error rendering %s template for %s: %s", key, self.entity_id, err
)
- self._rendered = self._static_rendered
class ManualTriggerEntity(TriggerBaseEntity):
@@ -231,16 +255,22 @@ class ManualTriggerEntity(TriggerBaseEntity):
Ex: self._process_manual_data(payload)
"""
+ run_variables: dict[str, Any] = {"value": value}
+ this = None
+ if state := self.hass.states.get(self.entity_id):
+ this = state.as_dict()
+ # Silently try if variable is a json and store result in `value_json` if it is.
+ with contextlib.suppress(*JSON_DECODE_EXCEPTIONS):
+ run_variables["value_json"] = json_loads(run_variables["value"])
+ variables = {"this": this, **(run_variables or {})}
+ self._render_availability_template(variables)
+
self.async_write_ha_state()
this = None
if state := self.hass.states.get(self.entity_id):
this = state.as_dict()
- run_variables: dict[str, Any] = {"value": value}
- # Silently try if variable is a json and store result in `value_json` if it is.
- with contextlib.suppress(*JSON_DECODE_EXCEPTIONS):
- run_variables["value_json"] = json_loads(run_variables["value"])
- variables = {"this": this, **(run_variables or {})}
+ variables["this"] = this
self._render_templates(variables)
diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py
index 87d55891e90..25cd4bc4d90 100644
--- a/homeassistant/helpers/update_coordinator.py
+++ b/homeassistant/helpers/update_coordinator.py
@@ -29,8 +29,6 @@ from homeassistant.util.dt import utcnow
from . import entity, event
from .debounce import Debouncer
-from .frame import report_usage
-from .typing import UNDEFINED, UndefinedType
REQUEST_REFRESH_DEFAULT_COOLDOWN = 10
REQUEST_REFRESH_DEFAULT_IMMEDIATE = True
@@ -70,7 +68,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
hass: HomeAssistant,
logger: logging.Logger,
*,
- config_entry: config_entries.ConfigEntry | None | UndefinedType = UNDEFINED,
name: str,
update_interval: timedelta | None = None,
update_method: Callable[[], Awaitable[_DataT]] | None = None,
@@ -87,12 +84,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
self._update_interval_seconds: float | None = None
self.update_interval = update_interval
self._shutdown_requested = False
- if config_entry is UNDEFINED:
- self.config_entry = config_entries.current_entry.get()
- # This should be deprecated once all core integrations are updated
- # to pass in the config entry explicitly.
- else:
- self.config_entry = config_entry
+ self.config_entry = config_entries.current_entry.get()
self.always_update = always_update
# It's None before the first successful update.
@@ -285,22 +277,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
fails. Additionally logging is handled by config entry setup
to ensure that multiple retries do not cause log spam.
"""
- if self.config_entry is None:
- report_usage(
- "uses `async_config_entry_first_refresh`, which is only supported "
- "for coordinators with a config entry and will stop working in "
- "Home Assistant 2025.11"
- )
- elif (
- self.config_entry.state
- is not config_entries.ConfigEntryState.SETUP_IN_PROGRESS
- ):
- report_usage(
- "uses `async_config_entry_first_refresh`, which is only supported "
- f"when entry state is {config_entries.ConfigEntryState.SETUP_IN_PROGRESS}, "
- f"but it is in state {self.config_entry.state}, "
- "This will stop working in Home Assistant 2025.11",
- )
if await self.__wrap_async_setup():
await self._async_refresh(
log_failures=False, raise_on_auth_failed=True, raise_on_entry_error=True
diff --git a/homeassistant/loader.py b/homeassistant/loader.py
index d2e04df04c4..dd38271070d 100644
--- a/homeassistant/loader.py
+++ b/homeassistant/loader.py
@@ -255,7 +255,6 @@ class Manifest(TypedDict, total=False):
usb: list[dict[str, str]]
homekit: dict[str, list[str]]
is_built_in: bool
- overwrites_built_in: bool
version: str
codeowners: list[str]
loggers: list[str]
@@ -283,7 +282,9 @@ def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest:
}
-def _get_custom_components(hass: HomeAssistant) -> dict[str, Integration]:
+async def _async_get_custom_components(
+ hass: HomeAssistant,
+) -> dict[str, Integration]:
"""Return list of custom integrations."""
if hass.config.recovery_mode or hass.config.safe_mode:
return {}
@@ -293,14 +294,21 @@ def _get_custom_components(hass: HomeAssistant) -> dict[str, Integration]:
except ImportError:
return {}
- dirs = [
- entry
- for path in custom_components.__path__
- for entry in pathlib.Path(path).iterdir()
- if entry.is_dir()
- ]
+ def get_sub_directories(paths: list[str]) -> list[pathlib.Path]:
+ """Return all sub directories in a set of paths."""
+ return [
+ entry
+ for path in paths
+ for entry in pathlib.Path(path).iterdir()
+ if entry.is_dir()
+ ]
- integrations = _resolve_integrations_from_root(
+ dirs = await hass.async_add_executor_job(
+ get_sub_directories, custom_components.__path__
+ )
+
+ integrations = await hass.async_add_executor_job(
+ _resolve_integrations_from_root,
hass,
custom_components,
[comp.name for comp in dirs],
@@ -321,7 +329,7 @@ async def async_get_custom_components(
if comps_or_future is None:
future = hass.data[DATA_CUSTOM_COMPONENTS] = hass.loop.create_future()
- comps = await hass.async_add_executor_job(_get_custom_components, hass)
+ comps = await _async_get_custom_components(hass)
hass.data[DATA_CUSTOM_COMPONENTS] = comps
future.set_result(comps)
@@ -443,7 +451,6 @@ async def async_get_integration_descriptions(
"single_config_entry": integration.manifest.get(
"single_config_entry", False
),
- "overwrites_built_in": integration.overwrites_built_in,
}
custom_flows[integration_key][integration.domain] = metadata
@@ -755,7 +762,6 @@ class Integration:
self.file_path = file_path
self.manifest = manifest
manifest["is_built_in"] = self.is_built_in
- manifest["overwrites_built_in"] = self.overwrites_built_in
if self.dependencies:
self._all_dependencies_resolved: bool | None = None
@@ -903,16 +909,6 @@ class Integration:
"""Test if package is a built-in integration."""
return self.pkg_path.startswith(PACKAGE_BUILTIN)
- @property
- def overwrites_built_in(self) -> bool:
- """Return if package overwrites a built-in integration."""
- if self.is_built_in:
- return False
- core_comp_path = (
- pathlib.Path(__file__).parent / "components" / self.domain / "manifest.json"
- )
- return core_comp_path.is_file()
-
@property
def version(self) -> AwesomeVersion | None:
"""Return the version of the integration."""
@@ -1556,18 +1552,16 @@ class Components:
raise ImportError(f"Unable to load {comp_name}")
# Local import to avoid circular dependencies
- # pylint: disable-next=import-outside-toplevel
- from .helpers.frame import ReportBehavior, report_usage
+ from .helpers.frame import report # pylint: disable=import-outside-toplevel
- report_usage(
+ report(
(
f"accesses hass.components.{comp_name}."
" This is deprecated and will stop working in Home Assistant 2025.3, it"
f" should be updated to import functions used from {comp_name} directly"
),
- core_behavior=ReportBehavior.IGNORE,
- core_integration_behavior=ReportBehavior.IGNORE,
- custom_integration_behavior=ReportBehavior.LOG,
+ error_if_core=False,
+ log_custom_component_only=True,
)
wrapped = ModuleWrapper(self._hass, component)
@@ -1587,18 +1581,16 @@ class Helpers:
helper = importlib.import_module(f"homeassistant.helpers.{helper_name}")
# Local import to avoid circular dependencies
- # pylint: disable-next=import-outside-toplevel
- from .helpers.frame import ReportBehavior, report_usage
+ from .helpers.frame import report # pylint: disable=import-outside-toplevel
- report_usage(
+ report(
(
f"accesses hass.helpers.{helper_name}."
- " This is deprecated and will stop working in Home Assistant 2025.5, it"
+ " This is deprecated and will stop working in Home Assistant 2024.11, it"
f" should be updated to import functions used from {helper_name} directly"
),
- core_behavior=ReportBehavior.IGNORE,
- core_integration_behavior=ReportBehavior.IGNORE,
- custom_integration_behavior=ReportBehavior.LOG,
+ error_if_core=False,
+ log_custom_component_only=True,
)
wrapped = ModuleWrapper(self._hass, helper)
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 5bc539beb86..d1a09ceb648 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -3,18 +3,16 @@
aiodhcpwatcher==1.0.2
aiodiscover==2.1.0
aiodns==3.2.0
-aiohasupervisor==0.2.1
+aiohasupervisor==0.1.0
aiohttp-fast-zlib==0.1.1
-aiohttp==3.11.0
+aiohttp==3.10.9
aiohttp_cors==0.7.0
aiozoneinfo==0.2.1
astral==2.2
async-interrupt==1.2.0
async-upnp-client==0.41.0
atomicwrites-homeassistant==1.4.1
-attrs==24.2.0
-audioop-lts==0.2.1;python_version>='3.13'
-av==13.1.0
+attrs==23.2.0
awesomeversion==24.6.0
bcrypt==4.2.0
bleak-retry-connector==3.6.0
@@ -28,23 +26,24 @@ ciso8601==2.3.1
cryptography==43.0.1
dbus-fast==2.24.3
fnv-hash-fast==1.0.2
-go2rtc-client==0.1.1
-ha-ffmpeg==3.2.2
-habluetooth==3.6.0
-hass-nabucasa==0.84.0
-hassil==2.0.1
+ha-av==10.1.1
+ha-ffmpeg==3.2.0
+habluetooth==3.5.0
+hass-nabucasa==0.81.1
+hassil==1.7.4
home-assistant-bluetooth==1.13.0
-home-assistant-frontend==20241106.2
-home-assistant-intents==2024.11.13
+home-assistant-frontend==20241002.2
+home-assistant-intents==2024.10.2
httpx==0.27.2
ifaddr==0.2.0
Jinja2==3.1.4
lru-dict==1.3.0
+mashumaro==3.13.1
mutagen==1.47.0
-orjson==3.10.11
+orjson==3.10.7
packaging>=23.1
paho-mqtt==1.6.1
-Pillow==11.0.0
+Pillow==10.4.0
propcache==0.2.0
psutil-home-assistant==0.0.1
PyJWT==2.9.0
@@ -58,20 +57,16 @@ PyTurboJPEG==1.7.5
pyudev==0.24.1
PyYAML==6.0.2
requests==2.32.3
-securetar==2024.2.1
SQLAlchemy==2.0.31
-standard-aifc==3.13.0;python_version>='3.13'
-standard-telnetlib==3.13.0;python_version>='3.13'
typing-extensions>=4.12.2,<5.0
ulid-transform==1.0.2
urllib3>=1.26.5,<2
-uv==0.5.0
+uv==0.4.17
voluptuous-openapi==0.0.5
voluptuous-serialize==2.6.0
voluptuous==0.15.2
-webrtc-models==0.2.0
-yarl==1.17.1
-zeroconf==0.136.0
+yarl==1.14.0
+zeroconf==0.135.0
# Constrain pycryptodome to avoid vulnerability
# see https://github.com/home-assistant/core/pull/16238
@@ -84,9 +79,9 @@ httplib2>=0.19.0
# gRPC is an implicit dependency that we want to make explicit so we manage
# upgrades intentionally. It is a large package to build from source and we
# want to ensure we have wheels built.
-grpcio==1.67.1
-grpcio-status==1.67.1
-grpcio-reflection==1.67.1
+grpcio==1.66.2
+grpcio-status==1.66.2
+grpcio-reflection==1.66.2
# This is a old unmaintained library and is replaced with pycryptodome
pycrypto==1000000000.0.0
@@ -106,7 +101,7 @@ uuid==1000000000.0.0
# these requirements are quite loose. As the entire stack has some outstanding issues, and
# even newer versions seem to introduce new issues, it's useful for us to pin all these
# requirements so we can directly link HA versions to these library versions.
-anyio==4.6.2.post1
+anyio==4.6.0
h11==0.14.0
httpcore==1.0.5
@@ -115,8 +110,7 @@ httpcore==1.0.5
hyperframe>=5.2.0
# Ensure we run compatible with musllinux build env
-numpy==2.1.3
-pandas~=2.2.3
+numpy==1.26.4
# Constrain multidict to avoid typing issues
# https://github.com/home-assistant/core/pull/67046
@@ -127,10 +121,7 @@ backoff>=2.0
# Required to avoid breaking (#101042).
# v2 has breaking changes (#99218).
-pydantic==1.10.19
-
-# Required for Python 3.12.4 compatibility (#119223).
-mashumaro>=3.13.1
+pydantic==1.10.18
# Breaks asyncio
# https://github.com/pubnub/python/issues/130
@@ -146,7 +137,7 @@ pyOpenSSL>=24.0.0
# protobuf must be in package constraints for the wheel
# builder to build binary wheels
-protobuf==5.28.3
+protobuf==5.28.2
# faust-cchardet: Ensure we have a version we can build wheels
# 2.1.18 is the first version that works with our wheel builder
@@ -168,12 +159,15 @@ get-mac==1000000000.0.0
# We want to skip the binary wheels for the 'charset-normalizer' packages.
# They are build with mypyc, but causes issues with our wheel builder.
# In order to do so, we need to constrain the version.
-charset-normalizer==3.4.0
+charset-normalizer==3.2.0
# dacite: Ensure we have a version that is able to handle type unions for
-# NAM, Brother, and GIOS.
+# Roborock, NAM, Brother, and GIOS.
dacite>=1.7.0
+# Musle wheels for pandas 2.2.0 cannot be build for any architecture.
+pandas==2.1.4
+
# chacha20poly1305-reuseable==0.12.x is incompatible with cryptography==43.0.x
chacha20poly1305-reuseable>=0.13.0
@@ -181,8 +175,8 @@ chacha20poly1305-reuseable>=0.13.0
# https://github.com/pycountry/pycountry/blob/ea69bab36f00df58624a0e490fdad4ccdc14268b/HISTORY.txt#L39
pycountry>=23.12.11
-# scapy==2.6.0 causes CI failures due to a race condition
-scapy>=2.6.1
+# scapy<2.5.0 will not work with python3.12
+scapy>=2.5.0
# tuf isn't updated to deal with breaking changes in securesystemslib==1.0.
# Only tuf>=4 includes a constraint to <1.0.
@@ -191,7 +185,3 @@ tuf>=4.0.0
# https://github.com/jd/tenacity/issues/471
tenacity!=8.4.0
-
-# 5.0.0 breaks Timeout as a context manager
-# TypeError: 'Timeout' object does not support the context manager protocol
-async-timeout==4.0.3
diff --git a/homeassistant/runner.py b/homeassistant/runner.py
index 59775655854..102dbafe147 100644
--- a/homeassistant/runner.py
+++ b/homeassistant/runner.py
@@ -3,8 +3,10 @@
from __future__ import annotations
import asyncio
+from asyncio import events
import dataclasses
import logging
+import os
import subprocess
import threading
from time import monotonic
@@ -56,6 +58,22 @@ class RuntimeConfig:
safe_mode: bool = False
+def can_use_pidfd() -> bool:
+ """Check if pidfd_open is available.
+
+ Back ported from cpython 3.12
+ """
+ if not hasattr(os, "pidfd_open"):
+ return False
+ try:
+ pid = os.getpid()
+ os.close(os.pidfd_open(pid, 0))
+ except OSError:
+ # blocked by security policy like SECCOMP
+ return False
+ return True
+
+
class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
"""Event loop policy for Home Assistant."""
@@ -63,6 +81,23 @@ class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
"""Init the event loop policy."""
super().__init__()
self.debug = debug
+ self._watcher: asyncio.AbstractChildWatcher | None = None
+
+ def _init_watcher(self) -> None:
+ """Initialize the watcher for child processes.
+
+ Back ported from cpython 3.12
+ """
+ with events._lock: # type: ignore[attr-defined] # noqa: SLF001
+ if self._watcher is None: # pragma: no branch
+ if can_use_pidfd():
+ self._watcher = asyncio.PidfdChildWatcher()
+ else:
+ self._watcher = asyncio.ThreadedChildWatcher()
+ if threading.current_thread() is threading.main_thread():
+ self._watcher.attach_loop(
+ self._local._loop # type: ignore[attr-defined] # noqa: SLF001
+ )
@property
def loop_name(self) -> str:
diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py
index ee2b6c762d8..30cf7222f3a 100644
--- a/homeassistant/util/dt.py
+++ b/homeassistant/util/dt.py
@@ -95,7 +95,7 @@ def set_default_time_zone(time_zone: dt.tzinfo) -> None:
get_default_time_zone.cache_clear()
-def get_time_zone(time_zone_str: str) -> zoneinfo.ZoneInfo | None:
+def get_time_zone(time_zone_str: str) -> dt.tzinfo | None:
"""Get time zone from string. Return None if unable to determine.
Must be run in the executor if the ZoneInfo is not already
@@ -107,7 +107,7 @@ def get_time_zone(time_zone_str: str) -> zoneinfo.ZoneInfo | None:
return None
-async def async_get_time_zone(time_zone_str: str) -> zoneinfo.ZoneInfo | None:
+async def async_get_time_zone(time_zone_str: str) -> dt.tzinfo | None:
"""Get time zone from string. Return None if unable to determine.
Async friendly.
diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py
index da0666290a1..3796bf35cd7 100644
--- a/homeassistant/util/package.py
+++ b/homeassistant/util/package.py
@@ -104,8 +104,6 @@ def install_package(
_LOGGER.info("Attempting install of %s", package)
env = os.environ.copy()
args = [
- sys.executable,
- "-m",
"uv",
"pip",
"install",
diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py
index ddabdf2746d..821f502694b 100644
--- a/homeassistant/util/timeout.py
+++ b/homeassistant/util/timeout.py
@@ -16,7 +16,7 @@ from .async_ import run_callback_threadsafe
ZONE_GLOBAL = "global"
-class _State(enum.Enum):
+class _State(str, enum.Enum):
"""States of a task."""
INIT = "INIT"
@@ -160,16 +160,11 @@ class _GlobalTaskContext:
self._wait_zone: asyncio.Event = asyncio.Event()
self._state: _State = _State.INIT
self._cool_down: float = cool_down
- self._cancelling = 0
async def __aenter__(self) -> Self:
self._manager.global_tasks.append(self)
self._start_timer()
self._state = _State.ACTIVE
- # Remember if the task was already cancelling
- # so when we __aexit__ we can decide if we should
- # raise asyncio.TimeoutError or let the cancellation propagate
- self._cancelling = self._task.cancelling()
return self
async def __aexit__(
@@ -182,15 +177,7 @@ class _GlobalTaskContext:
self._manager.global_tasks.remove(self)
# Timeout on exit
- if exc_type is asyncio.CancelledError and self.state is _State.TIMEOUT:
- # The timeout was hit, and the task was cancelled
- # so we need to uncancel the task since the cancellation
- # should not leak out of the context manager
- if self._task.uncancel() > self._cancelling:
- # If the task was already cancelling don't raise
- # asyncio.TimeoutError and instead return None
- # to allow the cancellation to propagate
- return None
+ if exc_type is asyncio.CancelledError and self.state == _State.TIMEOUT:
raise TimeoutError
self._state = _State.EXIT
@@ -279,7 +266,6 @@ class _ZoneTaskContext:
self._time_left: float = timeout
self._expiration_time: float | None = None
self._timeout_handler: asyncio.Handle | None = None
- self._cancelling = 0
@property
def state(self) -> _State:
@@ -294,11 +280,6 @@ class _ZoneTaskContext:
if self._zone.freezes_done:
self._start_timer()
- # Remember if the task was already cancelling
- # so when we __aexit__ we can decide if we should
- # raise asyncio.TimeoutError or let the cancellation propagate
- self._cancelling = self._task.cancelling()
-
return self
async def __aexit__(
@@ -311,15 +292,7 @@ class _ZoneTaskContext:
self._stop_timer()
# Timeout on exit
- if exc_type is asyncio.CancelledError and self.state is _State.TIMEOUT:
- # The timeout was hit, and the task was cancelled
- # so we need to uncancel the task since the cancellation
- # should not leak out of the context manager
- if self._task.uncancel() > self._cancelling:
- # If the task was already cancelling don't raise
- # asyncio.TimeoutError and instead return None
- # to allow the cancellation to propagate
- return None
+ if exc_type is asyncio.CancelledError and self.state == _State.TIMEOUT:
raise TimeoutError
self._state = _State.EXIT
diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py
index 1bf3561e66a..6bc595bd487 100644
--- a/homeassistant/util/unit_conversion.py
+++ b/homeassistant/util/unit_conversion.py
@@ -10,7 +10,6 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
UNIT_NOT_RECOGNIZED_TEMPLATE,
- UnitOfBloodGlucoseConcentration,
UnitOfConductivity,
UnitOfDataRate,
UnitOfElectricCurrent,
@@ -174,17 +173,6 @@ class DistanceConverter(BaseUnitConverter):
}
-class BloodGlucoseConcentrationConverter(BaseUnitConverter):
- """Utility to convert blood glucose concentration values."""
-
- UNIT_CLASS = "blood_glucose_concentration"
- _UNIT_CONVERSION: dict[str | None, float] = {
- UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER: 18,
- UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER: 1,
- }
- VALID_UNITS = set(UnitOfBloodGlucoseConcentration)
-
-
class ConductivityConverter(BaseUnitConverter):
"""Utility to convert electric current values."""
@@ -234,8 +222,6 @@ class EnergyConverter(BaseUnitConverter):
UnitOfEnergy.WATT_HOUR: 1e3,
UnitOfEnergy.KILO_WATT_HOUR: 1,
UnitOfEnergy.MEGA_WATT_HOUR: 1 / 1e3,
- UnitOfEnergy.GIGA_WATT_HOUR: 1 / 1e6,
- UnitOfEnergy.TERA_WATT_HOUR: 1 / 1e9,
UnitOfEnergy.CALORIE: _WH_TO_CAL * 1e3,
UnitOfEnergy.KILO_CALORIE: _WH_TO_CAL,
UnitOfEnergy.MEGA_CALORIE: _WH_TO_CAL / 1e3,
@@ -306,16 +292,10 @@ class PowerConverter(BaseUnitConverter):
_UNIT_CONVERSION: dict[str | None, float] = {
UnitOfPower.WATT: 1,
UnitOfPower.KILO_WATT: 1 / 1000,
- UnitOfPower.MEGA_WATT: 1 / 1e6,
- UnitOfPower.GIGA_WATT: 1 / 1e9,
- UnitOfPower.TERA_WATT: 1 / 1e12,
}
VALID_UNITS = {
UnitOfPower.WATT,
UnitOfPower.KILO_WATT,
- UnitOfPower.MEGA_WATT,
- UnitOfPower.GIGA_WATT,
- UnitOfPower.TERA_WATT,
}
diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py
index 39d38a8f47d..39ac17d94f9 100644
--- a/homeassistant/util/yaml/loader.py
+++ b/homeassistant/util/yaml/loader.py
@@ -25,6 +25,7 @@ except ImportError:
from propcache import cached_property
from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.frame import report
from .const import SECRET_YAML
from .objects import Input, NodeDictClass, NodeListClass, NodeStrClass
@@ -143,6 +144,37 @@ class FastSafeLoader(FastestAvailableSafeLoader, _LoaderMixin):
self.secrets = secrets
+class SafeLoader(FastSafeLoader):
+ """Provided for backwards compatibility. Logs when instantiated."""
+
+ def __init__(*args: Any, **kwargs: Any) -> None:
+ """Log a warning and call super."""
+ SafeLoader.__report_deprecated()
+ FastSafeLoader.__init__(*args, **kwargs)
+
+ @classmethod
+ def add_constructor(cls, tag: str, constructor: Callable) -> None:
+ """Log a warning and call super."""
+ SafeLoader.__report_deprecated()
+ FastSafeLoader.add_constructor(tag, constructor)
+
+ @classmethod
+ def add_multi_constructor(
+ cls, tag_prefix: str, multi_constructor: Callable
+ ) -> None:
+ """Log a warning and call super."""
+ SafeLoader.__report_deprecated()
+ FastSafeLoader.add_multi_constructor(tag_prefix, multi_constructor)
+
+ @staticmethod
+ def __report_deprecated() -> None:
+ """Log deprecation warning."""
+ report(
+ "uses deprecated 'SafeLoader' instead of 'FastSafeLoader', "
+ "which will stop working in HA Core 2024.6,"
+ )
+
+
class PythonSafeLoader(yaml.SafeLoader, _LoaderMixin):
"""Python safe loader."""
@@ -152,6 +184,37 @@ class PythonSafeLoader(yaml.SafeLoader, _LoaderMixin):
self.secrets = secrets
+class SafeLineLoader(PythonSafeLoader):
+ """Provided for backwards compatibility. Logs when instantiated."""
+
+ def __init__(*args: Any, **kwargs: Any) -> None:
+ """Log a warning and call super."""
+ SafeLineLoader.__report_deprecated()
+ PythonSafeLoader.__init__(*args, **kwargs)
+
+ @classmethod
+ def add_constructor(cls, tag: str, constructor: Callable) -> None:
+ """Log a warning and call super."""
+ SafeLineLoader.__report_deprecated()
+ PythonSafeLoader.add_constructor(tag, constructor)
+
+ @classmethod
+ def add_multi_constructor(
+ cls, tag_prefix: str, multi_constructor: Callable
+ ) -> None:
+ """Log a warning and call super."""
+ SafeLineLoader.__report_deprecated()
+ PythonSafeLoader.add_multi_constructor(tag_prefix, multi_constructor)
+
+ @staticmethod
+ def __report_deprecated() -> None:
+ """Log deprecation warning."""
+ report(
+ "uses deprecated 'SafeLineLoader' instead of 'PythonSafeLoader', "
+ "which will stop working in HA Core 2024.6,"
+ )
+
+
type LoaderType = FastSafeLoader | PythonSafeLoader
diff --git a/mypy.ini b/mypy.ini
index 4d33f16d968..700bcb23f2a 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -11,7 +11,6 @@ follow_imports = normal
local_partial_types = true
strict_equality = true
no_implicit_optional = true
-report_deprecated_as_error = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_unused_configs = true
@@ -995,16 +994,6 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
-[mypy-homeassistant.components.cambridge_audio.*]
-check_untyped_defs = true
-disallow_incomplete_defs = true
-disallow_subclassing_any = true
-disallow_untyped_calls = true
-disallow_untyped_decorators = true
-disallow_untyped_defs = true
-warn_return_any = true
-warn_unreachable = true
-
[mypy-homeassistant.components.camera.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -1846,16 +1835,6 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
-[mypy-homeassistant.components.go2rtc.*]
-check_untyped_defs = true
-disallow_incomplete_defs = true
-disallow_subclassing_any = true
-disallow_untyped_calls = true
-disallow_untyped_decorators = true
-disallow_untyped_defs = true
-warn_return_any = true
-warn_unreachable = true
-
[mypy-homeassistant.components.goalzero.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -2796,6 +2775,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
+[mypy-homeassistant.components.map.*]
+check_untyped_defs = true
+disallow_incomplete_defs = true
+disallow_subclassing_any = true
+disallow_untyped_calls = true
+disallow_untyped_decorators = true
+disallow_untyped_defs = true
+warn_return_any = true
+warn_unreachable = true
+
[mypy-homeassistant.components.mastodon.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -2996,16 +2985,6 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
-[mypy-homeassistant.components.music_assistant.*]
-check_untyped_defs = true
-disallow_incomplete_defs = true
-disallow_subclassing_any = true
-disallow_untyped_calls = true
-disallow_untyped_decorators = true
-disallow_untyped_defs = true
-warn_return_any = true
-warn_unreachable = true
-
[mypy-homeassistant.components.my.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -3056,16 +3035,6 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
-[mypy-homeassistant.components.nasweb.*]
-check_untyped_defs = true
-disallow_incomplete_defs = true
-disallow_subclassing_any = true
-disallow_untyped_calls = true
-disallow_untyped_decorators = true
-disallow_untyped_defs = true
-warn_return_any = true
-warn_unreachable = true
-
[mypy-homeassistant.components.neato.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -3156,16 +3125,6 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
-[mypy-homeassistant.components.nordpool.*]
-check_untyped_defs = true
-disallow_incomplete_defs = true
-disallow_subclassing_any = true
-disallow_untyped_calls = true
-disallow_untyped_decorators = true
-disallow_untyped_defs = true
-warn_return_any = true
-warn_unreachable = true
-
[mypy-homeassistant.components.notify.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -4046,17 +4005,6 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
-[mypy-homeassistant.components.spotify.*]
-check_untyped_defs = true
-disallow_incomplete_defs = true
-disallow_subclassing_any = true
-disallow_untyped_calls = true
-disallow_untyped_decorators = true
-disallow_untyped_defs = true
-warn_return_any = true
-warn_unreachable = true
-no_implicit_reexport = true
-
[mypy-homeassistant.components.sql.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@@ -4992,6 +4940,9 @@ warn_unreachable = true
[mypy-homeassistant.components.application_credentials.*]
no_implicit_reexport = true
+[mypy-homeassistant.components.spotify.*]
+no_implicit_reexport = true
+
[mypy-tests.*]
check_untyped_defs = false
disallow_incomplete_defs = false
diff --git a/pyproject.toml b/pyproject.toml
index ebf22a93d7d..4e4d7c69538 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,10 @@
[build-system]
-requires = ["setuptools==75.1.0"]
+requires = ["setuptools==69.2.0", "wheel~=0.43.0"]
build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
-version = "2024.12.0.dev0"
+version = "2024.11.0.dev0"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@@ -19,25 +19,22 @@ classifiers = [
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.12",
- "Programming Language :: Python :: 3.13",
"Topic :: Home Automation",
]
requires-python = ">=3.12.0"
dependencies = [
"aiodns==3.2.0",
# Integrations may depend on hassio integration without listing it to
- # change behavior based on presence of supervisor. Deprecated with #127228
- # Lib can be removed with 2025.11
- "aiohasupervisor==0.2.1",
- "aiohttp==3.11.0",
+ # change behavior based on presence of supervisor
+ "aiohasupervisor==0.1.0",
+ "aiohttp==3.10.9",
"aiohttp_cors==0.7.0",
"aiohttp-fast-zlib==0.1.1",
"aiozoneinfo==0.2.1",
"astral==2.2",
"async-interrupt==1.2.0",
- "attrs==24.2.0",
+ "attrs==23.2.0",
"atomicwrites-homeassistant==1.4.1",
- "audioop-lts==0.2.1;python_version>='3.13'",
"awesomeversion==24.6.0",
"bcrypt==4.2.0",
"certifi>=2021.5.30",
@@ -45,7 +42,7 @@ dependencies = [
"fnv-hash-fast==1.0.2",
# hass-nabucasa is imported by helpers which don't depend on the cloud
# integration
- "hass-nabucasa==0.84.0",
+ "hass-nabucasa==0.81.1",
# When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.27.2",
@@ -53,34 +50,31 @@ dependencies = [
"ifaddr==0.2.0",
"Jinja2==3.1.4",
"lru-dict==1.3.0",
+ "mashumaro==3.13.1",
"PyJWT==2.9.0",
# PyJWT has loose dependency. We want the latest one.
"cryptography==43.0.1",
- "Pillow==11.0.0",
+ "Pillow==10.4.0",
"propcache==0.2.0",
"pyOpenSSL==24.2.1",
- "orjson==3.10.11",
+ "orjson==3.10.7",
"packaging>=23.1",
"psutil-home-assistant==0.0.1",
"python-slugify==8.0.4",
"PyYAML==6.0.2",
"requests==2.32.3",
- "securetar==2024.2.1",
"SQLAlchemy==2.0.31",
- "standard-aifc==3.13.0;python_version>='3.13'",
- "standard-telnetlib==3.13.0;python_version>='3.13'",
"typing-extensions>=4.12.2,<5.0",
"ulid-transform==1.0.2",
# Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503
# Temporary setting an upper bound, to prevent compat issues with urllib3>=2
# https://github.com/home-assistant/core/issues/97248
"urllib3>=1.26.5,<2",
- "uv==0.5.0",
+ "uv==0.4.17",
"voluptuous==0.15.2",
"voluptuous-serialize==2.6.0",
"voluptuous-openapi==0.0.5",
- "yarl==1.17.1",
- "webrtc-models==0.2.0",
+ "yarl==1.14.0",
]
[project.urls]
@@ -471,14 +465,14 @@ filterwarnings = [
# Ignore custom pytest marks
"ignore:Unknown pytest.mark.disable_autouse_fixture:pytest.PytestUnknownMarkWarning:tests.components.met",
"ignore:Unknown pytest.mark.dataset:pytest.PytestUnknownMarkWarning:tests.components.screenlogic",
- # https://github.com/rokam/sunweg/blob/3.1.0/sunweg/plant.py#L96 - v3.1.0 - 2024-10-02
+ # https://github.com/rokam/sunweg/blob/3.0.2/sunweg/plant.py#L96 - v3.0.2 - 2024-07-10
"ignore:The '(kwh_per_kwp|performance_rate)' property is deprecated and will return 0:DeprecationWarning:tests.components.sunweg.test_init",
# -- design choice 3rd party
- # https://github.com/gwww/elkm1/blob/2.2.10/elkm1_lib/util.py#L8-L19
+ # https://github.com/gwww/elkm1/blob/2.2.7/elkm1_lib/util.py#L8-L19
"ignore:ssl.TLSVersion.TLSv1 is deprecated:DeprecationWarning:elkm1_lib.util",
# https://github.com/allenporter/ical/pull/215
- # https://github.com/allenporter/ical/blob/8.2.0/ical/util.py#L21-L23
+ # https://github.com/allenporter/ical/blob/8.1.1/ical/util.py#L21-L23
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:ical.util",
# https://github.com/bachya/regenmaschine/blob/2024.03.0/regenmaschine/client.py#L52
"ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client",
@@ -490,13 +484,10 @@ filterwarnings = [
"ignore:Deprecated call to `pkg_resources.declare_namespace\\(('azure'|'google.*'|'pywinusb'|'repoze'|'xbox'|'zope')\\)`:DeprecationWarning:pkg_resources",
# -- tracked upstream / open PRs
- # - pyOpenSSL v24.2.1
# https://github.com/certbot/certbot/issues/9828 - v2.11.0
- # https://github.com/certbot/certbot/issues/9992
"ignore:X509Extension support in pyOpenSSL is deprecated. You should use the APIs in cryptography:DeprecationWarning:acme.crypto_util",
- "ignore:CSR support in pyOpenSSL is deprecated. You should use the APIs in cryptography:DeprecationWarning:acme.crypto_util",
- "ignore:CSR support in pyOpenSSL is deprecated. You should use the APIs in cryptography:DeprecationWarning:josepy.util",
- # - other
+ # https://github.com/beetbox/mediafile/issues/67 - v0.12.0
+ "ignore:'imghdr' is deprecated and slated for removal in Python 3.13:DeprecationWarning:mediafile",
# https://github.com/foxel/python_ndms2_client/issues/6 - v0.1.3
# https://github.com/foxel/python_ndms2_client/pull/8
"ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:ndms2_client.connection",
@@ -533,8 +524,8 @@ filterwarnings = [
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:onvif.client",
# https://github.com/okunishinishi/python-stringcase/commit/6a5c5bbd3fe5337862abc7fd0853a0f36e18b2e1 - >1.2.0
"ignore:invalid escape sequence:SyntaxWarning:.*stringcase",
- # https://github.com/cereal2nd/velbus-aio/pull/126 - >2024.10.0
- "ignore:pkg_resources is deprecated as an API:DeprecationWarning:velbusaio.handler",
+ # https://github.com/mvantellingen/python-zeep/pull/1364 - >4.2.1
+ "ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning:zeep.utils",
# -- fixed for Python 3.13
# https://github.com/rhasspy/wyoming/commit/e34af30d455b6f2bb9e5cfb25fad8d276914bc54 - >=1.4.2
@@ -542,9 +533,8 @@ filterwarnings = [
# -- other
# Locale changes might take some time to resolve upstream
- # https://github.com/Squachen/micloud/blob/v_0.6/micloud/micloud.py#L35 - v0.6 - 2022-12-08
"ignore:'locale.getdefaultlocale' is deprecated and slated for removal in Python 3.15:DeprecationWarning:micloud.micloud",
- # https://github.com/MatsNl/pyatag/issues/11 - v0.3.7.1 - 2023-10-09
+ # https://github.com/MatsNl/pyatag/issues/11 - v0.3.7.1
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pyatag.gateway",
# https://github.com/lidatong/dataclasses-json/issues/328
# https://github.com/lidatong/dataclasses-json/pull/351
@@ -552,19 +542,14 @@ filterwarnings = [
# https://pypi.org/project/emulated-roku/ - v0.3.0 - 2023-12-19
# https://github.com/martonperei/emulated_roku
"ignore:loop argument is deprecated:DeprecationWarning:emulated_roku",
- # https://github.com/w1ll1am23/pyeconet/blob/v0.1.23/src/pyeconet/api.py#L38 - v0.1.23 - 2024-10-08
- "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:pyeconet.api",
- # https://github.com/thecynic/pylutron - v0.2.16 - 2024-10-22
+ # https://github.com/thecynic/pylutron - v0.2.15
"ignore:setDaemon\\(\\) is deprecated, set the daemon attribute instead:DeprecationWarning:pylutron",
- # https://github.com/pschmitt/pynuki/blob/1.6.3/pynuki/utils.py#L21 - v1.6.3 - 2024-02-24
+ # https://github.com/pschmitt/pynuki/blob/1.6.3/pynuki/utils.py#L21 - v1.6.3
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pynuki.utils",
- # https://github.com/lextudio/pysnmp/blob/v7.1.10/pysnmp/smi/compiler.py#L23-L31 - v7.1.10 - 2024-11-04
- "ignore:smiV1Relaxed is deprecated. Please use smi_v1_relaxed instead:DeprecationWarning:pysnmp.smi.compiler",
- "ignore:getReadersFromUrls is deprecated. Please use get_readers_from_urls instead:DeprecationWarning:pysmi.reader.url", # wrong stacklevel
# https://github.com/briis/pyweatherflowudp/blob/v1.4.5/pyweatherflowudp/const.py#L20 - v1.4.5 - 2023-10-10
"ignore:This function will be removed in future versions of pint:DeprecationWarning:pyweatherflowudp.const",
# Wrong stacklevel
- # https://bugs.launchpad.net/beautifulsoup/+bug/2034451 fixed in >4.12.3
+ # https://bugs.launchpad.net/beautifulsoup/+bug/2034451
"ignore:It looks like you're parsing an XML document using an HTML parser:UserWarning:html.parser",
# New in aiohttp - v3.9.0
"ignore:It is recommended to use web.AppKey instances for keys:UserWarning:(homeassistant|tests|aiohttp_cors)",
@@ -585,10 +570,13 @@ filterwarnings = [
"ignore:invalid escape sequence:SyntaxWarning:.*sanix",
# https://pypi.org/project/sleekxmppfs/ - v1.4.1 - 2022-08-18
"ignore:invalid escape sequence:SyntaxWarning:.*sleekxmppfs.thirdparty.mini_dateutil", # codespell:ignore thirdparty
+ # https://pypi.org/project/vobject/ - v0.9.7 - 2024-03-25
+ # https://github.com/py-vobject/vobject
+ "ignore:invalid escape sequence:SyntaxWarning:.*vobject.base",
# - pkg_resources
# https://pypi.org/project/aiomusiccast/ - v0.14.8 - 2023-03-20
"ignore:pkg_resources is deprecated as an API:DeprecationWarning:aiomusiccast",
- # https://pypi.org/project/habitipy/ - v0.3.3 - 2024-10-28
+ # https://pypi.org/project/habitipy/ - v0.3.1 - 2019-01-14 / 2024-04-28
"ignore:pkg_resources is deprecated as an API:DeprecationWarning:habitipy.api",
# https://github.com/eavanvalkenburg/pysiaalarm/blob/v3.1.1/src/pysiaalarm/data/data.py#L7 - v3.1.1 - 2023-04-17
"ignore:pkg_resources is deprecated as an API:DeprecationWarning:pysiaalarm.data.data",
@@ -596,6 +584,14 @@ filterwarnings = [
"ignore:pkg_resources is deprecated as an API:DeprecationWarning:pybotvac.version",
# https://github.com/home-assistant-ecosystem/python-mystrom/blob/2.2.0/pymystrom/__init__.py#L10 - v2.2.0 - 2023-05-21
"ignore:pkg_resources is deprecated as an API:DeprecationWarning:pymystrom",
+ # https://pypi.org/project/velbus-aio/ - v2024.7.6 - 2024-07-31
+ # https://github.com/Cereal2nd/velbus-aio/blob/2024.7.6/velbusaio/handler.py#L22
+ "ignore:pkg_resources is deprecated as an API:DeprecationWarning:velbusaio.handler",
+ # - pyOpenSSL v24.2.1
+ # https://pypi.org/project/acme/ - v2.11.0 - 2024-06-06
+ "ignore:CSR support in pyOpenSSL is deprecated. You should use the APIs in cryptography:DeprecationWarning:acme.crypto_util",
+ # https://pypi.org/project/josepy/ - v1.14.0 - 2023-11-01
+ "ignore:CSR support in pyOpenSSL is deprecated. You should use the APIs in cryptography:DeprecationWarning:josepy.util",
# -- Python 3.13
# HomeAssistant
@@ -605,11 +601,11 @@ filterwarnings = [
# https://github.com/nextcord/nextcord/issues/1174
# https://github.com/nextcord/nextcord/blob/v2.6.1/nextcord/player.py#L5
"ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:nextcord.player",
- # https://pypi.org/project/SpeechRecognition/ - v3.11.0 - 2024-05-05
- # https://github.com/Uberi/speech_recognition/blob/3.11.0/speech_recognition/__init__.py#L7
+ # https://pypi.org/project/SpeechRecognition/ - v3.10.4 - 2024-05-05
+ # https://github.com/Uberi/speech_recognition/blob/3.10.4/speech_recognition/__init__.py#L7
"ignore:'aifc' is deprecated and slated for removal in Python 3.13:DeprecationWarning:speech_recognition",
# https://pypi.org/project/voip-utils/ - v0.2.0 - 2024-09-06
- # https://github.com/home-assistant-libs/voip-utils/blob/0.2.0/voip_utils/rtp_audio.py#L3
+ # https://github.com/home-assistant-libs/voip-utils/blob/v0.2.0/voip_utils/rtp_audio.py#L3
"ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:voip_utils.rtp_audio",
# -- Python 3.13 - unmaintained projects, last release about 2+ years
@@ -621,17 +617,6 @@ filterwarnings = [
# https://github.com/ssaenger/pyws66i/blob/v1.1/pyws66i/__init__.py#L2
"ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:pyws66i",
- # -- New in Python 3.13
- # https://github.com/kurtmckee/feedparser/pull/389 - >6.0.11
- # https://github.com/kurtmckee/feedparser/issues/481
- "ignore:'count' is passed as positional argument:DeprecationWarning:feedparser.html",
- # https://github.com/youknowone/python-deadlib - Backports for aifc, telnetlib
- "ignore:aifc was removed in Python 3.13.*'standard-aifc':DeprecationWarning:speech_recognition",
- "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:homeassistant.components.hddtemp.sensor",
- "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:ndms2_client.connection",
- "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:plumlightpad.lightpad",
- "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:pyws66i",
-
# -- unmaintained projects, last release about 2+ years
# https://pypi.org/project/agent-py/ - v0.0.23 - 2020-06-04
"ignore:with timeout\\(\\) is deprecated:DeprecationWarning:agent.a",
@@ -642,7 +627,7 @@ filterwarnings = [
# https://pypi.org/project/directv/ - v0.4.0 - 2020-09-12
"ignore:with timeout\\(\\) is deprecated:DeprecationWarning:directv.directv",
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:directv.models",
- # https://pypi.org/project/foobot_async/ - v1.0.1 - 2024-08-16
+ # https://pypi.org/project/foobot_async/ - v1.0.0 - 2020-11-24
"ignore:with timeout\\(\\) is deprecated:DeprecationWarning:foobot_async",
# https://pypi.org/project/httpsig/ - v1.3.0 - 2018-11-28
"ignore:pkg_resources is deprecated as an API:DeprecationWarning:httpsig",
diff --git a/requirements.txt b/requirements.txt
index b97c8dc57a0..7c40ac6236e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,48 +4,44 @@
# Home Assistant Core
aiodns==3.2.0
-aiohasupervisor==0.2.1
-aiohttp==3.11.0
+aiohasupervisor==0.1.0
+aiohttp==3.10.9
aiohttp_cors==0.7.0
aiohttp-fast-zlib==0.1.1
aiozoneinfo==0.2.1
astral==2.2
async-interrupt==1.2.0
-attrs==24.2.0
+attrs==23.2.0
atomicwrites-homeassistant==1.4.1
-audioop-lts==0.2.1;python_version>='3.13'
awesomeversion==24.6.0
bcrypt==4.2.0
certifi>=2021.5.30
ciso8601==2.3.1
fnv-hash-fast==1.0.2
-hass-nabucasa==0.84.0
+hass-nabucasa==0.81.1
httpx==0.27.2
home-assistant-bluetooth==1.13.0
ifaddr==0.2.0
Jinja2==3.1.4
lru-dict==1.3.0
+mashumaro==3.13.1
PyJWT==2.9.0
cryptography==43.0.1
-Pillow==11.0.0
+Pillow==10.4.0
propcache==0.2.0
pyOpenSSL==24.2.1
-orjson==3.10.11
+orjson==3.10.7
packaging>=23.1
psutil-home-assistant==0.0.1
python-slugify==8.0.4
PyYAML==6.0.2
requests==2.32.3
-securetar==2024.2.1
SQLAlchemy==2.0.31
-standard-aifc==3.13.0;python_version>='3.13'
-standard-telnetlib==3.13.0;python_version>='3.13'
typing-extensions>=4.12.2,<5.0
ulid-transform==1.0.2
urllib3>=1.26.5,<2
-uv==0.5.0
+uv==0.4.17
voluptuous==0.15.2
voluptuous-serialize==2.6.0
voluptuous-openapi==0.0.5
-yarl==1.17.1
-webrtc-models==0.2.0
+yarl==1.14.0
diff --git a/requirements_all.txt b/requirements_all.txt
index 65ef5f1ebf2..d69e33c65d7 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -13,10 +13,10 @@ AIOSomecomfort==0.0.25
Adax-local==0.1.5
# homeassistant.components.doorbird
-DoorBirdPy==3.0.8
+DoorBirdPy==3.0.4
# homeassistant.components.homekit
-HAP-python==4.9.2
+HAP-python==4.9.1
# homeassistant.components.tasmota
HATasmota==0.9.2
@@ -33,7 +33,7 @@ Mastodon.py==1.8.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
-Pillow==11.0.0
+Pillow==10.4.0
# homeassistant.components.plex
PlexAPI==4.15.16
@@ -45,7 +45,7 @@ ProgettiHWSW==0.1.3
# PyBluez==0.22
# homeassistant.components.cast
-PyChromecast==14.0.5
+PyChromecast==14.0.4
# homeassistant.components.flick_electric
PyFlick==0.0.2
@@ -84,7 +84,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3
# homeassistant.components.switchbot
-PySwitchbot==0.51.0
+PySwitchbot==0.48.2
# homeassistant.components.switchmate
PySwitchmate==0.5.1
@@ -100,7 +100,7 @@ PyTransportNSW==0.1.1
PyTurboJPEG==1.7.5
# homeassistant.components.vicare
-PyViCare==2.35.0
+PyViCare==2.34.0
# homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.14.3
@@ -109,7 +109,7 @@ PyXiaomiGateway==0.14.3
RachioPy==1.1.0
# homeassistant.components.python_script
-RestrictedPython==7.4
+RestrictedPython==7.3
# homeassistant.components.remember_the_milk
RtmAPI==0.7.2
@@ -152,7 +152,7 @@ advantage-air==0.4.4
afsapi==0.2.7
# homeassistant.components.agent_dvr
-agent-py==0.0.24
+agent-py==0.0.23
# homeassistant.components.geo_json_events
aio-geojson-generic-client==0.4
@@ -172,17 +172,14 @@ aio-geojson-usgs-earthquakes==0.3
# homeassistant.components.gdacs
aio-georss-gdacs==0.10
-# homeassistant.components.acaia
-aioacaia==0.1.6
-
# homeassistant.components.airq
aioairq==0.3.2
# homeassistant.components.airzone_cloud
-aioairzone-cloud==0.6.10
+aioairzone-cloud==0.6.6
# homeassistant.components.airzone
-aioairzone==0.9.6
+aioairzone==0.9.3
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -201,7 +198,7 @@ aioaseko==1.0.0
aioasuswrt==1.4.0
# homeassistant.components.husqvarna_automower
-aioautomower==2024.10.3
+aioautomower==2024.9.3
# homeassistant.components.azure_devops
aioazuredevops==2.2.1
@@ -213,7 +210,7 @@ aiobafi6==0.9.0
aiobotocore==2.13.1
# homeassistant.components.comelit
-aiocomelit==0.9.1
+aiocomelit==0.9.0
# homeassistant.components.dhcp
aiodhcpwatcher==1.0.2
@@ -243,7 +240,7 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5
# homeassistant.components.esphome
-aioesphomeapi==27.0.1
+aioesphomeapi==27.0.0
# homeassistant.components.flo
aioflo==2021.11.0
@@ -252,7 +249,6 @@ aioflo==2021.11.0
aioftp==0.21.3
# homeassistant.components.github
-# homeassistant.components.iron_os
aiogithubapi==24.6.0
# homeassistant.components.guardian
@@ -262,10 +258,10 @@ aioguardian==2022.07.0
aioharmony==0.2.10
# homeassistant.components.hassio
-aiohasupervisor==0.2.1
+aiohasupervisor==0.1.0
# homeassistant.components.homekit_controller
-aiohomekit==3.2.6
+aiohomekit==3.2.3
# homeassistant.components.hue
aiohue==4.7.3
@@ -319,10 +315,10 @@ aionut==4.3.3
aiooncue==0.3.7
# homeassistant.components.openexchangerates
-aioopenexchangerates==0.6.8
+aioopenexchangerates==0.6.2
# homeassistant.components.nmap_tracker
-aiooui==0.1.7
+aiooui==0.1.6
# homeassistant.components.pegel_online
aiopegelonline==0.0.10
@@ -357,10 +353,10 @@ aiorecollect==2023.09.0
aioridwell==2024.01.0
# homeassistant.components.ruckus_unleashed
-aioruckus==0.42
+aioruckus==0.41
# homeassistant.components.russound_rio
-aiorussound==4.1.0
+aiorussound==4.0.5
# homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0
@@ -369,7 +365,7 @@ aioruuvigateway==0.1.0
aiosenz==1.0.0
# homeassistant.components.shelly
-aioshelly==12.0.1
+aioshelly==12.0.0
# homeassistant.components.skybell
aioskybell==22.7.0
@@ -384,10 +380,10 @@ aiosolaredge==0.2.0
aiosteamist==1.0.0
# homeassistant.components.cambridge_audio
-aiostreammagic==2.8.5
+aiostreammagic==2.5.0
# homeassistant.components.switcher_kis
-aioswitcher==4.4.0
+aioswitcher==4.0.3
# homeassistant.components.syncthing
aiosyncthing==0.5.1
@@ -395,9 +391,6 @@ aiosyncthing==0.5.1
# homeassistant.components.tankerkoenig
aiotankerkoenig==0.4.2
-# homeassistant.components.tedee
-aiotedee==0.2.20
-
# homeassistant.components.tractive
aiotractive==0.6.0
@@ -408,7 +401,7 @@ aiounifi==80
aiovlc==0.5.1
# homeassistant.components.vodafone_station
-aiovodafone==0.6.1
+aiovodafone==0.6.0
# homeassistant.components.waqi
aiowaqi==3.1.0
@@ -420,7 +413,7 @@ aiowatttime==0.1.1
aiowebostv==0.4.2
# homeassistant.components.withings
-aiowithings==3.1.3
+aiowithings==3.1.0
# homeassistant.components.yandex_transport
aioymaps==1.2.5
@@ -432,7 +425,7 @@ airgradient==0.9.1
airly==1.1.0
# homeassistant.components.airthings_ble
-airthings-ble==0.9.2
+airthings-ble==0.9.1
# homeassistant.components.airthings
airthings-cloud==0.2.0
@@ -471,7 +464,7 @@ anthemav==1.4.1
anthropic==0.31.2
# homeassistant.components.weatherkit
-apple_weatherkit==1.1.3
+apple_weatherkit==1.1.2
# homeassistant.components.apprise
apprise==1.9.0
@@ -527,14 +520,7 @@ auroranoaa==0.0.5
aurorapy==0.2.7
# homeassistant.components.autarco
-autarco==3.1.0
-
-# homeassistant.components.husqvarna_automower_ble
-automower-ble==0.2.0
-
-# homeassistant.components.generic
-# homeassistant.components.stream
-av==13.1.0
+autarco==3.0.0
# homeassistant.components.avea
# avea==1.5.1
@@ -543,10 +529,10 @@ av==13.1.0
# avion==0.10
# homeassistant.components.axis
-axis==63
+axis==62
# homeassistant.components.fujitsu_fglair
-ayla-iot-unofficial==1.4.3
+ayla-iot-unofficial==1.4.1
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@@ -582,7 +568,7 @@ beautifulsoup4==4.12.3
# beewi-smartclim==0.0.10
# homeassistant.components.bmw_connected_drive
-bimmer-connected[china]==0.16.4
+bimmer-connected[china]==0.16.3
# homeassistant.components.bizkaibus
bizkaibus==0.1.1
@@ -642,7 +628,7 @@ boto3==1.34.131
botocore==1.34.131
# homeassistant.components.bring
-bring-api==0.9.1
+bring-api==0.9.0
# homeassistant.components.broadlink
broadlink==0.19.0
@@ -708,7 +694,7 @@ connect-box==0.3.1
construct==2.10.68
# homeassistant.components.utility_meter
-cronsim==2.6
+croniter==2.0.2
# homeassistant.components.crownstone
crownstone-cloud==1.4.11
@@ -738,7 +724,7 @@ debugpy==1.8.6
# decora==0.6
# homeassistant.components.ecovacs
-deebot-client==8.4.1
+deebot-client==8.4.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -863,7 +849,7 @@ epion==0.0.3
epson-projector==0.5.1
# homeassistant.components.eq3btsmart
-eq3btsmart==1.4.1
+eq3btsmart==1.1.9
# homeassistant.components.esphome
esphome-dashboard-api==1.2.3
@@ -947,19 +933,19 @@ freesms==0.2.0
fritzconnection[qr]==1.14.0
# homeassistant.components.fyta
-fyta_cli==0.6.10
+fyta_cli==0.6.7
# homeassistant.components.google_translate
gTTS==2.2.4
# homeassistant.components.gardena_bluetooth
-gardena-bluetooth==1.4.4
+gardena-bluetooth==1.4.3
# homeassistant.components.google_assistant_sdk
gassist-text==0.0.11
# homeassistant.components.google
-gcal-sync==6.2.0
+gcal-sync==6.1.5
# homeassistant.components.geniushub
geniushub-client==0.7.1
@@ -987,7 +973,7 @@ georss-qld-bushfire-alert-client==0.8
getmac==0.9.4
# homeassistant.components.gios
-gios==5.0.0
+gios==4.0.0
# homeassistant.components.gitter
gitterpy==0.1.7
@@ -996,7 +982,7 @@ gitterpy==0.1.7
glances-api==0.8.0
# homeassistant.components.go2rtc
-go2rtc-client==0.1.1
+go2rtc-client==0.0.1b0
# homeassistant.components.goalzero
goalzero==0.2.2
@@ -1021,7 +1007,7 @@ google-cloud-texttospeech==2.17.2
google-generativeai==0.8.2
# homeassistant.components.nest
-google-nest-sdm==6.1.5
+google-nest-sdm==5.0.1
# homeassistant.components.google_photos
google-photos-library-api==0.12.1
@@ -1030,7 +1016,7 @@ google-photos-library-api==0.12.1
googlemaps==2.5.1
# homeassistant.components.slide
-goslide-api==0.7.0
+goslide-api==0.5.1
# homeassistant.components.tailwind
gotailwind==0.2.4
@@ -1039,7 +1025,7 @@ gotailwind==0.2.4
govee-ble==0.40.0
# homeassistant.components.govee_light_local
-govee-local-api==1.5.3
+govee-local-api==1.5.2
# homeassistant.components.remote_rpi_gpio
gpiozero==1.6.2
@@ -1069,13 +1055,17 @@ gspread==5.5.0
gstreamer-player==1.1.2
# homeassistant.components.profiler
-guppy3==3.1.4.post1;python_version<'3.13'
+guppy3==3.1.4.post1
# homeassistant.components.iaqualink
h2==4.1.0
+# homeassistant.components.generic
+# homeassistant.components.stream
+ha-av==10.1.1
+
# homeassistant.components.ffmpeg
-ha-ffmpeg==3.2.2
+ha-ffmpeg==3.2.0
# homeassistant.components.iotawatt
ha-iotawattpy==0.1.2
@@ -1084,25 +1074,25 @@ ha-iotawattpy==0.1.2
ha-philipsjs==3.2.2
# homeassistant.components.habitica
-habitipy==0.3.3
+habitipy==0.3.1
# homeassistant.components.bluetooth
-habluetooth==3.6.0
+habluetooth==3.5.0
# homeassistant.components.cloud
-hass-nabucasa==0.84.0
+hass-nabucasa==0.81.1
# homeassistant.components.splunk
hass-splunk==0.1.1
# homeassistant.components.conversation
-hassil==2.0.1
+hassil==1.7.4
# homeassistant.components.jewish_calendar
hdate==0.10.9
# homeassistant.components.heatmiser
-heatmiserV3==2.0.3
+heatmiserV3==1.1.18
# homeassistant.components.here_travel_time
here-routing==1.0.1
@@ -1127,13 +1117,13 @@ hole==0.8.0
# homeassistant.components.holiday
# homeassistant.components.workday
-holidays==0.60
+holidays==0.58
# homeassistant.components.frontend
-home-assistant-frontend==20241106.2
+home-assistant-frontend==20241002.2
# homeassistant.components.conversation
-home-assistant-intents==2024.11.13
+home-assistant-intents==2024.10.2
# homeassistant.components.home_connect
homeconnect==0.8.0
@@ -1148,10 +1138,10 @@ horimote==0.4.1
httplib2==0.20.4
# homeassistant.components.huawei_lte
-huawei-lte-api==1.10.0
+huawei-lte-api==1.7.3
# homeassistant.components.huum
-huum==0.7.12
+huum==0.7.10
# homeassistant.components.hyperion
hyperion-py==0.7.5
@@ -1259,10 +1249,10 @@ knx-frontend==2024.9.10.221729
konnected==1.2.0
# homeassistant.components.kraken
-krakenex==2.2.2
+krakenex==2.1.0
# homeassistant.components.lacrosse_view
-lacrosse-view==1.0.3
+lacrosse-view==1.0.2
# homeassistant.components.eufy
lakeside==0.13
@@ -1271,7 +1261,7 @@ lakeside==0.13
laundrify-aio==1.2.2
# homeassistant.components.lcn
-lcn-frontend==0.2.2
+lcn-frontend==0.1.6
# homeassistant.components.ld2410_ble
ld2410-ble==0.1.1
@@ -1283,7 +1273,7 @@ leaone-ble==0.1.0
led-ble==1.0.2
# homeassistant.components.lektrico
-lektricowifi==0.0.43
+lektricowifi==0.0.42
# homeassistant.components.foscam
libpyfoscam==1.2.2
@@ -1312,6 +1302,9 @@ linear-garage-door==0.2.9
# homeassistant.components.linode
linode-api==4.1.9b1
+# homeassistant.components.lamarzocco
+lmcloud==1.2.3
+
# homeassistant.components.google_maps
locationsharinglib==5.0.1
@@ -1376,7 +1369,7 @@ microBeesPy==0.3.2
mill-local==0.3.0
# homeassistant.components.mill
-millheater==0.12.2
+millheater==0.11.8
# homeassistant.components.minio
minio==7.1.12
@@ -1388,7 +1381,7 @@ moat-ble==0.1.1
moehlenhoff-alpha2==1.3.1
# homeassistant.components.monzo
-monzopy==1.4.2
+monzopy==1.3.2
# homeassistant.components.mopeka
mopeka-iot-ble==0.8.0
@@ -1403,14 +1396,11 @@ motionblindsble==0.1.2
motioneye-client==0.3.14
# homeassistant.components.bang_olufsen
-mozart-api==4.1.1.116.0
+mozart-api==3.4.1.8.8
# homeassistant.components.mullvad
mullvad-api==1.0.0
-# homeassistant.components.music_assistant
-music-assistant-client==1.0.5
-
# homeassistant.components.tts
mutagen==1.47.0
@@ -1460,7 +1450,7 @@ nextdns==3.3.0
nibe==2.11.0
# homeassistant.components.nice_go
-nice-go==0.3.10
+nice-go==0.3.9
# homeassistant.components.niko_home_control
niko-home-control==0.2.1
@@ -1494,10 +1484,10 @@ numato-gpio==0.13.0
# homeassistant.components.stream
# homeassistant.components.tensorflow
# homeassistant.components.trend
-numpy==2.1.3
+numpy==1.26.4
# homeassistant.components.nyt_games
-nyt_games==0.4.4
+nyt_games==0.4.3
# homeassistant.components.oasa_telematics
oasatelematics==0.3
@@ -1557,7 +1547,7 @@ openwrt-luci-rpc==1.1.17
openwrt-ubus-rpc==0.0.2
# homeassistant.components.opower
-opower==0.8.6
+opower==0.8.2
# homeassistant.components.oralb
oralb-ble==0.17.6
@@ -1622,7 +1612,7 @@ plexauth==0.0.6
plexwebsocket==0.0.14
# homeassistant.components.plugwise
-plugwise==1.5.0
+plugwise==1.4.0
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@@ -1654,7 +1644,7 @@ proxmoxer==2.0.1
psutil-home-assistant==0.0.1
# homeassistant.components.systemmonitor
-psutil==6.1.0
+psutil==6.0.0
# homeassistant.components.pulseaudio_loopback
pulsectl==23.5.2
@@ -1723,7 +1713,7 @@ pyCEC==0.5.2
pyControl4==1.2.0
# homeassistant.components.duotecno
-pyDuotecno==2024.10.1
+pyDuotecno==2024.10.0
# homeassistant.components.electrasmart
pyElectra==1.2.4
@@ -1741,7 +1731,7 @@ pyRFXtrx==0.31.1
pySDCP==1
# homeassistant.components.tibber
-pyTibber==0.30.8
+pyTibber==0.30.2
# homeassistant.components.dlink
pyW215==0.7.0
@@ -1793,7 +1783,7 @@ pybbox==0.0.5-alpha
pyblackbird==0.6
# homeassistant.components.bluesound
-pyblu==1.0.4
+pyblu==1.0.3
# homeassistant.components.neato
pybotvac==0.0.25
@@ -1826,7 +1816,7 @@ pycomfoconnect==0.5.1
pycoolmasternet-async==0.2.2
# homeassistant.components.radio_browser
-pycountry==24.6.1
+pycountry==23.12.11
# homeassistant.components.microsoft
pycsspeechtts==1.0.8
@@ -1841,10 +1831,10 @@ pydaikin==2.13.7
pydanfossair==0.1.0
# homeassistant.components.deako
-pydeako==0.5.4
+pydeako==0.4.0
# homeassistant.components.deconz
-pydeconz==118
+pydeconz==116
# homeassistant.components.delijn
pydelijn==1.1.0
@@ -1889,7 +1879,7 @@ pyegps==0.2.5
pyeiscp==0.0.7
# homeassistant.components.emoncms
-pyemoncms==0.1.1
+pyemoncms==0.0.7
# homeassistant.components.enphase_envoy
pyenphase==1.22.0
@@ -1910,7 +1900,7 @@ pyevilgenius==2.0.0
pyezviz==0.2.1.2
# homeassistant.components.fibaro
-pyfibaro==0.8.0
+pyfibaro==0.7.8
# homeassistant.components.fido
pyfido==2.1.2
@@ -1976,7 +1966,7 @@ pyintesishome==1.8.0
pyipma==3.0.7
# homeassistant.components.ipp
-pyipp==0.17.0
+pyipp==0.16.0
# homeassistant.components.iqvia
pyiqvia==2022.04.0
@@ -2026,9 +2016,6 @@ pykwb==0.0.8
# homeassistant.components.lacrosse
pylacrosse==0.4
-# homeassistant.components.lamarzocco
-pylamarzocco==1.2.3
-
# homeassistant.components.lastfm
pylast==5.1.0
@@ -2051,7 +2038,7 @@ pylitterbot==2023.5.0
pylutron-caseta==0.21.1
# homeassistant.components.lutron
-pylutron==0.2.16
+pylutron==0.2.15
# homeassistant.components.mailgun
pymailgunner==1.4
@@ -2090,7 +2077,7 @@ pymsteams==0.1.12
pymysensors==0.24.0
# homeassistant.components.iron_os
-pynecil==0.2.1
+pynecil==0.2.0
# homeassistant.components.netgear
pynetgear==0.10.10
@@ -2101,9 +2088,6 @@ pynetio==0.1.9.1
# homeassistant.components.nobo_hub
pynobo==1.8.1
-# homeassistant.components.nordpool
-pynordpool==0.2.2
-
# homeassistant.components.nuki
pynuki==1.6.3
@@ -2129,7 +2113,7 @@ pyombi==0.1.10
pyopenuv==2023.02.0
# homeassistant.components.openweathermap
-pyopenweathermap==0.2.1
+pyopenweathermap==0.1.1
# homeassistant.components.opnsense
pyopnsense==0.4.0
@@ -2141,7 +2125,7 @@ pyoppleio-legacy==1.0.8
pyosoenergyapi==1.1.4
# homeassistant.components.opentherm_gw
-pyotgw==2.2.2
+pyotgw==2.2.1
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
@@ -2154,9 +2138,6 @@ pyoverkiz==1.14.1
# homeassistant.components.onewire
pyownet==0.10.0.post1
-# homeassistant.components.palazzetti
-pypalazzetti==0.1.11
-
# homeassistant.components.elv
pypca==0.0.7
@@ -2287,13 +2268,13 @@ pyspcwebgw==0.7.0
pyspeex-noise==1.0.2
# homeassistant.components.squeezebox
-pysqueezebox==0.10.0
+pysqueezebox==0.9.3
# homeassistant.components.stiebel_eltron
pystiebeleltron==0.0.1.dev2
# homeassistant.components.suez_water
-pysuezV2==1.3.1
+pysuez==0.2.0
# homeassistant.components.switchbee
pyswitchbee==1.8.3
@@ -2301,6 +2282,9 @@ pyswitchbee==1.8.3
# homeassistant.components.tautulli
pytautulli==23.1.1
+# homeassistant.components.tedee
+pytedee-async==0.2.20
+
# homeassistant.components.thinkingcleaner
pythinkingcleaner==0.0.3
@@ -2314,7 +2298,7 @@ python-awair==0.2.4
python-blockchain-api==0.0.2
# homeassistant.components.bsblan
-python-bsblan==1.2.1
+python-bsblan==0.6.2
# homeassistant.components.clementine
python-clementine-remote==1.0.1
@@ -2362,10 +2346,10 @@ python-join-api==0.0.9
python-juicenet==1.1.0
# homeassistant.components.tplink
-python-kasa[speedups]==0.7.7
+python-kasa[speedups]==0.7.5
# homeassistant.components.linkplay
-python-linkplay==0.0.20
+python-linkplay==0.0.15
# homeassistant.components.lirc
# python-lirc==1.2.3
@@ -2402,7 +2386,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
-python-roborock==2.7.2
+python-roborock==2.6.0
# homeassistant.components.smarttub
python-smarttub==0.0.36
@@ -2411,7 +2395,7 @@ python-smarttub==0.0.36
python-songpal==0.16.2
# homeassistant.components.tado
-python-tado==0.17.7
+python-tado==0.17.6
# homeassistant.components.technove
python-technove==1.3.1
@@ -2505,7 +2489,7 @@ pywmspro==0.2.1
pyws66i==1.1
# homeassistant.components.xeoma
-pyxeoma==1.4.2
+pyxeoma==1.4.1
# homeassistant.components.yardian
pyyardian==1.1.1
@@ -2529,7 +2513,7 @@ qnapstats==0.4.0
quantum-gateway==0.0.8
# homeassistant.components.radio_browser
-radios==0.3.2
+radios==0.3.1
# homeassistant.components.radiotherm
radiotherm==2.1.0
@@ -2556,7 +2540,7 @@ renault-api==0.2.7
renson-endura-delta==1.7.1
# homeassistant.components.reolink
-reolink-aio==0.11.1
+reolink-aio==0.9.11
# homeassistant.components.idteck_prox
rfk101py==0.0.1
@@ -2565,7 +2549,7 @@ rfk101py==0.0.1
rflink==0.0.66
# homeassistant.components.ring
-ring-doorbell==0.9.12
+ring-doorbell==0.9.7
# homeassistant.components.fleetgo
ritassist==0.9.2
@@ -2632,7 +2616,7 @@ sendgrid==6.8.2
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
-sense-energy==0.13.3
+sense-energy==0.12.4
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.1
@@ -2641,7 +2625,7 @@ sensirion-ble==0.1.1
sensorpro-ble==0.5.3
# homeassistant.components.sensorpush
-sensorpush-ble==1.7.1
+sensorpush-ble==1.6.2
# homeassistant.components.sensoterra
sensoterra==2.0.1
@@ -2676,9 +2660,6 @@ simplisafe-python==2024.01.0
# homeassistant.components.sisyphus
sisyphus-control==3.1.4
-# homeassistant.components.sky_remote
-skyboxremote==0.0.6
-
# homeassistant.components.slack
slackclient==2.5.0
@@ -2695,13 +2676,13 @@ smhi-pkg==1.0.18
snapcast==2.3.6
# homeassistant.components.sonos
-soco==0.30.6
+soco==0.30.4
# homeassistant.components.solaredge_local
solaredge-local==0.2.3
# homeassistant.components.solarlog
-solarlog_cli==0.3.2
+solarlog_cli==0.3.1
# homeassistant.components.solax
solax==3.1.1
@@ -2719,7 +2700,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3
# homeassistant.components.spotify
-spotifyaio==0.8.8
+spotipy==2.23.0
# homeassistant.components.sql
sqlparse==0.5.0
@@ -2810,7 +2791,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry
# homeassistant.components.tessie
-tesla-fleet-api==0.8.4
+tesla-fleet-api==0.7.8
# homeassistant.components.powerwall
tesla-powerwall==0.5.2
@@ -2818,9 +2799,6 @@ tesla-powerwall==0.5.2
# homeassistant.components.tesla_wall_connector
tesla-wall-connector==1.0.2
-# homeassistant.components.teslemetry
-teslemetry-stream==0.4.2
-
# homeassistant.components.tessie
tessie-api==0.1.1
@@ -2836,9 +2814,6 @@ thermopro-ble==0.10.0
# homeassistant.components.thingspeak
thingspeak==1.0.0
-# homeassistant.components.lg_thinq
-thinqconnect==1.0.0
-
# homeassistant.components.tikteck
tikteck==0.4
@@ -2864,7 +2839,7 @@ total-connect-client==2024.5
tp-connected==0.0.4
# homeassistant.components.tplink_omada
-tplink-omada-client==1.4.3
+tplink-omada-client==1.4.2
# homeassistant.components.transmission
transmission-rpc==7.0.3
@@ -2879,7 +2854,7 @@ ttls==1.8.3
ttn_client==1.2.0
# homeassistant.components.tuya
-tuya-device-sharing-sdk==0.2.1
+tuya-device-sharing-sdk==0.1.9
# homeassistant.components.twentemilieu
twentemilieu==2.0.1
@@ -2897,7 +2872,7 @@ typedmonarchmoney==0.3.1
uasiren==0.0.1
# homeassistant.components.unifiprotect
-uiprotect==6.4.0
+uiprotect==6.3.1
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7
@@ -2912,13 +2887,13 @@ unifi_ap==0.0.1
unifiled==0.11
# homeassistant.components.zha
-universal-silabs-flasher==0.0.24
+universal-silabs-flasher==0.0.22
# homeassistant.components.upb
upb-lib==0.5.8
# homeassistant.components.upcloud
-upcloud-api==2.6.0
+upcloud-api==2.5.1
# homeassistant.components.huawei_lte
# homeassistant.components.syncthru
@@ -2938,7 +2913,7 @@ vallox-websocket-api==5.3.0
vehicle==2.2.2
# homeassistant.components.velbus
-velbus-aio==2024.10.0
+velbus-aio==2024.7.6
# homeassistant.components.venstar
venstarcolortouch==0.19
@@ -2983,17 +2958,11 @@ waterfurnace==1.1.0
# homeassistant.components.weatherflow_cloud
weatherflow4py==1.0.6
-# homeassistant.components.cisco_webex_teams
-webexpythonsdk==2.0.1
-
-# homeassistant.components.nasweb
-webio-api==0.1.8
-
# homeassistant.components.webmin
webmin-xmlrpc==0.0.2
# homeassistant.components.weheat
-weheat==2024.11.02
+weheat==2024.09.23
# homeassistant.components.whirlpool
whirlpool-sixth-sense==0.18.8
@@ -3020,13 +2989,13 @@ wyoming==1.5.4
xbox-webapi==2.0.11
# homeassistant.components.xiaomi_ble
-xiaomi-ble==0.33.0
+xiaomi-ble==0.32.0
# homeassistant.components.knx
-xknx==3.3.0
+xknx==3.2.0
# homeassistant.components.knx
-xknxproject==3.8.1
+xknxproject==3.8.0
# homeassistant.components.fritz
# homeassistant.components.rest
@@ -3066,7 +3035,7 @@ youless-api==2.1.2
youtubeaio==1.1.5
# homeassistant.components.media_extractor
-yt-dlp[default]==2024.11.04
+yt-dlp==2024.09.27
# homeassistant.components.zamg
zamg==0.3.6
@@ -3075,16 +3044,16 @@ zamg==0.3.6
zengge==0.2
# homeassistant.components.zeroconf
-zeroconf==0.136.0
+zeroconf==0.135.0
# homeassistant.components.zeversolar
-zeversolar==0.3.2
+zeversolar==0.3.1
# homeassistant.components.zha
-zha==0.0.37
+zha==0.0.34
# homeassistant.components.zhong_hong
-zhong-hong-hvac==1.0.13
+zhong-hong-hvac==1.0.12
# homeassistant.components.ziggo_mediabox_xl
ziggo-mediabox-xl==1.1.0
@@ -3093,7 +3062,7 @@ ziggo-mediabox-xl==1.1.0
zm-py==0.5.4
# homeassistant.components.zwave_js
-zwave-js-server-python==0.59.1
+zwave-js-server-python==0.58.1
# homeassistant.components.zwave_me
zwave-me-ws==0.4.3
diff --git a/requirements_test.txt b/requirements_test.txt
index 166fd965e2c..56e4b0e2eb2 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -7,17 +7,17 @@
-c homeassistant/package_constraints.txt
-r requirements_test_pre_commit.txt
-astroid==3.3.5
+astroid==3.3.4
coverage==7.6.1
freezegun==1.5.1
-license-expression==30.4.0
mock-open==1.4.0
-mypy-dev==1.14.0a2
+mypy-dev==1.12.0a5
pre-commit==4.0.0
-pydantic==1.10.19
+pydantic==1.10.18
pylint==3.3.1
pylint-per-file-ignores==1.3.2
pipdeptree==2.23.4
+pip-licenses==5.0.0
pytest-asyncio==0.24.0
pytest-aiohttp==1.0.5
pytest-cov==5.0.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index b61e65f3c68..7d0365ffe92 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -13,10 +13,10 @@ AIOSomecomfort==0.0.25
Adax-local==0.1.5
# homeassistant.components.doorbird
-DoorBirdPy==3.0.8
+DoorBirdPy==3.0.4
# homeassistant.components.homekit
-HAP-python==4.9.2
+HAP-python==4.9.1
# homeassistant.components.tasmota
HATasmota==0.9.2
@@ -33,7 +33,7 @@ Mastodon.py==1.8.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
-Pillow==11.0.0
+Pillow==10.4.0
# homeassistant.components.plex
PlexAPI==4.15.16
@@ -42,7 +42,7 @@ PlexAPI==4.15.16
ProgettiHWSW==0.1.3
# homeassistant.components.cast
-PyChromecast==14.0.5
+PyChromecast==14.0.4
# homeassistant.components.flick_electric
PyFlick==0.0.2
@@ -81,7 +81,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3
# homeassistant.components.switchbot
-PySwitchbot==0.51.0
+PySwitchbot==0.48.2
# homeassistant.components.syncthru
PySyncThru==0.7.10
@@ -94,7 +94,7 @@ PyTransportNSW==0.1.1
PyTurboJPEG==1.7.5
# homeassistant.components.vicare
-PyViCare==2.35.0
+PyViCare==2.34.0
# homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.14.3
@@ -103,7 +103,7 @@ PyXiaomiGateway==0.14.3
RachioPy==1.1.0
# homeassistant.components.python_script
-RestrictedPython==7.4
+RestrictedPython==7.3
# homeassistant.components.remember_the_milk
RtmAPI==0.7.2
@@ -140,7 +140,7 @@ advantage-air==0.4.4
afsapi==0.2.7
# homeassistant.components.agent_dvr
-agent-py==0.0.24
+agent-py==0.0.23
# homeassistant.components.geo_json_events
aio-geojson-generic-client==0.4
@@ -160,17 +160,14 @@ aio-geojson-usgs-earthquakes==0.3
# homeassistant.components.gdacs
aio-georss-gdacs==0.10
-# homeassistant.components.acaia
-aioacaia==0.1.6
-
# homeassistant.components.airq
aioairq==0.3.2
# homeassistant.components.airzone_cloud
-aioairzone-cloud==0.6.10
+aioairzone-cloud==0.6.6
# homeassistant.components.airzone
-aioairzone==0.9.6
+aioairzone==0.9.3
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -189,7 +186,7 @@ aioaseko==1.0.0
aioasuswrt==1.4.0
# homeassistant.components.husqvarna_automower
-aioautomower==2024.10.3
+aioautomower==2024.9.3
# homeassistant.components.azure_devops
aioazuredevops==2.2.1
@@ -201,7 +198,7 @@ aiobafi6==0.9.0
aiobotocore==2.13.1
# homeassistant.components.comelit
-aiocomelit==0.9.1
+aiocomelit==0.9.0
# homeassistant.components.dhcp
aiodhcpwatcher==1.0.2
@@ -231,13 +228,12 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5
# homeassistant.components.esphome
-aioesphomeapi==27.0.1
+aioesphomeapi==27.0.0
# homeassistant.components.flo
aioflo==2021.11.0
# homeassistant.components.github
-# homeassistant.components.iron_os
aiogithubapi==24.6.0
# homeassistant.components.guardian
@@ -247,10 +243,10 @@ aioguardian==2022.07.0
aioharmony==0.2.10
# homeassistant.components.hassio
-aiohasupervisor==0.2.1
+aiohasupervisor==0.1.0
# homeassistant.components.homekit_controller
-aiohomekit==3.2.6
+aiohomekit==3.2.3
# homeassistant.components.hue
aiohue==4.7.3
@@ -301,10 +297,10 @@ aionut==4.3.3
aiooncue==0.3.7
# homeassistant.components.openexchangerates
-aioopenexchangerates==0.6.8
+aioopenexchangerates==0.6.2
# homeassistant.components.nmap_tracker
-aiooui==0.1.7
+aiooui==0.1.6
# homeassistant.components.pegel_online
aiopegelonline==0.0.10
@@ -339,10 +335,10 @@ aiorecollect==2023.09.0
aioridwell==2024.01.0
# homeassistant.components.ruckus_unleashed
-aioruckus==0.42
+aioruckus==0.41
# homeassistant.components.russound_rio
-aiorussound==4.1.0
+aiorussound==4.0.5
# homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0
@@ -351,7 +347,7 @@ aioruuvigateway==0.1.0
aiosenz==1.0.0
# homeassistant.components.shelly
-aioshelly==12.0.1
+aioshelly==12.0.0
# homeassistant.components.skybell
aioskybell==22.7.0
@@ -366,10 +362,10 @@ aiosolaredge==0.2.0
aiosteamist==1.0.0
# homeassistant.components.cambridge_audio
-aiostreammagic==2.8.5
+aiostreammagic==2.5.0
# homeassistant.components.switcher_kis
-aioswitcher==4.4.0
+aioswitcher==4.0.3
# homeassistant.components.syncthing
aiosyncthing==0.5.1
@@ -377,9 +373,6 @@ aiosyncthing==0.5.1
# homeassistant.components.tankerkoenig
aiotankerkoenig==0.4.2
-# homeassistant.components.tedee
-aiotedee==0.2.20
-
# homeassistant.components.tractive
aiotractive==0.6.0
@@ -390,7 +383,7 @@ aiounifi==80
aiovlc==0.5.1
# homeassistant.components.vodafone_station
-aiovodafone==0.6.1
+aiovodafone==0.6.0
# homeassistant.components.waqi
aiowaqi==3.1.0
@@ -402,7 +395,7 @@ aiowatttime==0.1.1
aiowebostv==0.4.2
# homeassistant.components.withings
-aiowithings==3.1.3
+aiowithings==3.1.0
# homeassistant.components.yandex_transport
aioymaps==1.2.5
@@ -414,7 +407,7 @@ airgradient==0.9.1
airly==1.1.0
# homeassistant.components.airthings_ble
-airthings-ble==0.9.2
+airthings-ble==0.9.1
# homeassistant.components.airthings
airthings-cloud==0.2.0
@@ -444,7 +437,7 @@ anthemav==1.4.1
anthropic==0.31.2
# homeassistant.components.weatherkit
-apple_weatherkit==1.1.3
+apple_weatherkit==1.1.2
# homeassistant.components.apprise
apprise==1.9.0
@@ -482,20 +475,13 @@ auroranoaa==0.0.5
aurorapy==0.2.7
# homeassistant.components.autarco
-autarco==3.1.0
-
-# homeassistant.components.husqvarna_automower_ble
-automower-ble==0.2.0
-
-# homeassistant.components.generic
-# homeassistant.components.stream
-av==13.1.0
+autarco==3.0.0
# homeassistant.components.axis
-axis==63
+axis==62
# homeassistant.components.fujitsu_fglair
-ayla-iot-unofficial==1.4.3
+ayla-iot-unofficial==1.4.1
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@@ -516,7 +502,7 @@ base36==0.1.1
beautifulsoup4==4.12.3
# homeassistant.components.bmw_connected_drive
-bimmer-connected[china]==0.16.4
+bimmer-connected[china]==0.16.3
# homeassistant.components.eq3btsmart
# homeassistant.components.esphome
@@ -562,7 +548,7 @@ boschshcpy==0.2.91
botocore==1.34.131
# homeassistant.components.bring
-bring-api==0.9.1
+bring-api==0.9.0
# homeassistant.components.broadlink
broadlink==0.19.0
@@ -604,7 +590,7 @@ colorthief==0.2.1
construct==2.10.68
# homeassistant.components.utility_meter
-cronsim==2.6
+croniter==2.0.2
# homeassistant.components.crownstone
crownstone-cloud==1.4.11
@@ -628,7 +614,7 @@ dbus-fast==2.24.3
debugpy==1.8.6
# homeassistant.components.ecovacs
-deebot-client==8.4.1
+deebot-client==8.4.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -732,7 +718,7 @@ epion==0.0.3
epson-projector==0.5.1
# homeassistant.components.eq3btsmart
-eq3btsmart==1.4.1
+eq3btsmart==1.1.9
# homeassistant.components.esphome
esphome-dashboard-api==1.2.3
@@ -800,19 +786,19 @@ freebox-api==1.1.0
fritzconnection[qr]==1.14.0
# homeassistant.components.fyta
-fyta_cli==0.6.10
+fyta_cli==0.6.7
# homeassistant.components.google_translate
gTTS==2.2.4
# homeassistant.components.gardena_bluetooth
-gardena-bluetooth==1.4.4
+gardena-bluetooth==1.4.3
# homeassistant.components.google_assistant_sdk
gassist-text==0.0.11
# homeassistant.components.google
-gcal-sync==6.2.0
+gcal-sync==6.1.5
# homeassistant.components.geniushub
geniushub-client==0.7.1
@@ -840,13 +826,13 @@ georss-qld-bushfire-alert-client==0.8
getmac==0.9.4
# homeassistant.components.gios
-gios==5.0.0
+gios==4.0.0
# homeassistant.components.glances
glances-api==0.8.0
# homeassistant.components.go2rtc
-go2rtc-client==0.1.1
+go2rtc-client==0.0.1b0
# homeassistant.components.goalzero
goalzero==0.2.2
@@ -871,7 +857,7 @@ google-cloud-texttospeech==2.17.2
google-generativeai==0.8.2
# homeassistant.components.nest
-google-nest-sdm==6.1.5
+google-nest-sdm==5.0.1
# homeassistant.components.google_photos
google-photos-library-api==0.12.1
@@ -886,7 +872,7 @@ gotailwind==0.2.4
govee-ble==0.40.0
# homeassistant.components.govee_light_local
-govee-local-api==1.5.3
+govee-local-api==1.5.2
# homeassistant.components.gpsd
gps3==0.33.3
@@ -907,13 +893,17 @@ growattServer==1.5.0
gspread==5.5.0
# homeassistant.components.profiler
-guppy3==3.1.4.post1;python_version<'3.13'
+guppy3==3.1.4.post1
# homeassistant.components.iaqualink
h2==4.1.0
+# homeassistant.components.generic
+# homeassistant.components.stream
+ha-av==10.1.1
+
# homeassistant.components.ffmpeg
-ha-ffmpeg==3.2.2
+ha-ffmpeg==3.2.0
# homeassistant.components.iotawatt
ha-iotawattpy==0.1.2
@@ -922,16 +912,16 @@ ha-iotawattpy==0.1.2
ha-philipsjs==3.2.2
# homeassistant.components.habitica
-habitipy==0.3.3
+habitipy==0.3.1
# homeassistant.components.bluetooth
-habluetooth==3.6.0
+habluetooth==3.5.0
# homeassistant.components.cloud
-hass-nabucasa==0.84.0
+hass-nabucasa==0.81.1
# homeassistant.components.conversation
-hassil==2.0.1
+hassil==1.7.4
# homeassistant.components.jewish_calendar
hdate==0.10.9
@@ -953,13 +943,13 @@ hole==0.8.0
# homeassistant.components.holiday
# homeassistant.components.workday
-holidays==0.60
+holidays==0.58
# homeassistant.components.frontend
-home-assistant-frontend==20241106.2
+home-assistant-frontend==20241002.2
# homeassistant.components.conversation
-home-assistant-intents==2024.11.13
+home-assistant-intents==2024.10.2
# homeassistant.components.home_connect
homeconnect==0.8.0
@@ -971,10 +961,10 @@ homematicip==1.1.2
httplib2==0.20.4
# homeassistant.components.huawei_lte
-huawei-lte-api==1.10.0
+huawei-lte-api==1.7.3
# homeassistant.components.huum
-huum==0.7.12
+huum==0.7.10
# homeassistant.components.hyperion
hyperion-py==0.7.5
@@ -1058,16 +1048,16 @@ knx-frontend==2024.9.10.221729
konnected==1.2.0
# homeassistant.components.kraken
-krakenex==2.2.2
+krakenex==2.1.0
# homeassistant.components.lacrosse_view
-lacrosse-view==1.0.3
+lacrosse-view==1.0.2
# homeassistant.components.laundrify
laundrify-aio==1.2.2
# homeassistant.components.lcn
-lcn-frontend==0.2.2
+lcn-frontend==0.1.6
# homeassistant.components.ld2410_ble
ld2410-ble==0.1.1
@@ -1079,7 +1069,7 @@ leaone-ble==0.1.0
led-ble==1.0.2
# homeassistant.components.lektrico
-lektricowifi==0.0.43
+lektricowifi==0.0.42
# homeassistant.components.foscam
libpyfoscam==1.2.2
@@ -1093,6 +1083,9 @@ libsoundtouch==0.8
# homeassistant.components.linear_garage_door
linear-garage-door==0.2.9
+# homeassistant.components.lamarzocco
+lmcloud==1.2.3
+
# homeassistant.components.london_underground
london-tube-status==0.5
@@ -1145,7 +1138,7 @@ microBeesPy==0.3.2
mill-local==0.3.0
# homeassistant.components.mill
-millheater==0.12.2
+millheater==0.11.8
# homeassistant.components.minio
minio==7.1.12
@@ -1157,7 +1150,7 @@ moat-ble==0.1.1
moehlenhoff-alpha2==1.3.1
# homeassistant.components.monzo
-monzopy==1.4.2
+monzopy==1.3.2
# homeassistant.components.mopeka
mopeka-iot-ble==0.8.0
@@ -1172,14 +1165,11 @@ motionblindsble==0.1.2
motioneye-client==0.3.14
# homeassistant.components.bang_olufsen
-mozart-api==4.1.1.116.0
+mozart-api==3.4.1.8.8
# homeassistant.components.mullvad
mullvad-api==1.0.0
-# homeassistant.components.music_assistant
-music-assistant-client==1.0.5
-
# homeassistant.components.tts
mutagen==1.47.0
@@ -1220,7 +1210,7 @@ nextdns==3.3.0
nibe==2.11.0
# homeassistant.components.nice_go
-nice-go==0.3.10
+nice-go==0.3.9
# homeassistant.components.nfandroidtv
notifications-android-tv==0.1.5
@@ -1242,10 +1232,10 @@ numato-gpio==0.13.0
# homeassistant.components.stream
# homeassistant.components.tensorflow
# homeassistant.components.trend
-numpy==2.1.3
+numpy==1.26.4
# homeassistant.components.nyt_games
-nyt_games==0.4.4
+nyt_games==0.4.3
# homeassistant.components.google
oauth2client==4.1.3
@@ -1287,7 +1277,7 @@ openhomedevice==2.2.0
openwebifpy==4.2.7
# homeassistant.components.opower
-opower==0.8.6
+opower==0.8.2
# homeassistant.components.oralb
oralb-ble==0.17.6
@@ -1329,7 +1319,7 @@ plexauth==0.0.6
plexwebsocket==0.0.14
# homeassistant.components.plugwise
-plugwise==1.5.0
+plugwise==1.4.0
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@@ -1352,7 +1342,7 @@ prometheus-client==0.21.0
psutil-home-assistant==0.0.1
# homeassistant.components.systemmonitor
-psutil==6.1.0
+psutil==6.0.0
# homeassistant.components.androidtv
pure-python-adb[async]==0.3.0.dev0
@@ -1409,7 +1399,7 @@ pyCEC==0.5.2
pyControl4==1.2.0
# homeassistant.components.duotecno
-pyDuotecno==2024.10.1
+pyDuotecno==2024.10.0
# homeassistant.components.electrasmart
pyElectra==1.2.4
@@ -1418,7 +1408,7 @@ pyElectra==1.2.4
pyRFXtrx==0.31.1
# homeassistant.components.tibber
-pyTibber==0.30.8
+pyTibber==0.30.2
# homeassistant.components.dlink
pyW215==0.7.0
@@ -1461,7 +1451,7 @@ pybalboa==1.0.2
pyblackbird==0.6
# homeassistant.components.bluesound
-pyblu==1.0.4
+pyblu==1.0.3
# homeassistant.components.neato
pybotvac==0.0.25
@@ -1479,7 +1469,7 @@ pycomfoconnect==0.5.1
pycoolmasternet-async==0.2.2
# homeassistant.components.radio_browser
-pycountry==24.6.1
+pycountry==23.12.11
# homeassistant.components.microsoft
pycsspeechtts==1.0.8
@@ -1488,10 +1478,10 @@ pycsspeechtts==1.0.8
pydaikin==2.13.7
# homeassistant.components.deako
-pydeako==0.5.4
+pydeako==0.4.0
# homeassistant.components.deconz
-pydeconz==118
+pydeconz==116
# homeassistant.components.dexcom
pydexcom==0.2.3
@@ -1520,11 +1510,8 @@ pyefergy==22.5.0
# homeassistant.components.energenie_power_sockets
pyegps==0.2.5
-# homeassistant.components.onkyo
-pyeiscp==0.0.7
-
# homeassistant.components.emoncms
-pyemoncms==0.1.1
+pyemoncms==0.0.7
# homeassistant.components.enphase_envoy
pyenphase==1.22.0
@@ -1539,7 +1526,7 @@ pyevilgenius==2.0.0
pyezviz==0.2.1.2
# homeassistant.components.fibaro
-pyfibaro==0.8.0
+pyfibaro==0.7.8
# homeassistant.components.fido
pyfido==2.1.2
@@ -1593,7 +1580,7 @@ pyinsteon==1.6.3
pyipma==3.0.7
# homeassistant.components.ipp
-pyipp==0.17.0
+pyipp==0.16.0
# homeassistant.components.iqvia
pyiqvia==2022.04.0
@@ -1631,9 +1618,6 @@ pykrakenapi==0.1.8
# homeassistant.components.kulersky
pykulersky==0.5.2
-# homeassistant.components.lamarzocco
-pylamarzocco==1.2.3
-
# homeassistant.components.lastfm
pylast==5.1.0
@@ -1656,7 +1640,7 @@ pylitterbot==2023.5.0
pylutron-caseta==0.21.1
# homeassistant.components.lutron
-pylutron==0.2.16
+pylutron==0.2.15
# homeassistant.components.mailgun
pymailgunner==1.4
@@ -1686,7 +1670,7 @@ pymonoprice==0.4
pymysensors==0.24.0
# homeassistant.components.iron_os
-pynecil==0.2.1
+pynecil==0.2.0
# homeassistant.components.netgear
pynetgear==0.10.10
@@ -1694,9 +1678,6 @@ pynetgear==0.10.10
# homeassistant.components.nobo_hub
pynobo==1.8.1
-# homeassistant.components.nordpool
-pynordpool==0.2.2
-
# homeassistant.components.nuki
pynuki==1.6.3
@@ -1719,7 +1700,7 @@ pyoctoprintapi==0.1.12
pyopenuv==2023.02.0
# homeassistant.components.openweathermap
-pyopenweathermap==0.2.1
+pyopenweathermap==0.1.1
# homeassistant.components.opnsense
pyopnsense==0.4.0
@@ -1728,7 +1709,7 @@ pyopnsense==0.4.0
pyosoenergyapi==1.1.4
# homeassistant.components.opentherm_gw
-pyotgw==2.2.2
+pyotgw==2.2.1
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
@@ -1741,9 +1722,6 @@ pyoverkiz==1.14.1
# homeassistant.components.onewire
pyownet==0.10.0.post1
-# homeassistant.components.palazzetti
-pypalazzetti==0.1.11
-
# homeassistant.components.lcn
pypck==0.7.24
@@ -1822,9 +1800,6 @@ pysmartapp==0.3.5
# homeassistant.components.smartthings
pysmartthings==0.7.8
-# homeassistant.components.smarty
-pysmarty2==0.10.1
-
# homeassistant.components.edl21
pysml==0.0.12
@@ -1847,10 +1822,10 @@ pyspcwebgw==0.7.0
pyspeex-noise==1.0.2
# homeassistant.components.squeezebox
-pysqueezebox==0.10.0
+pysqueezebox==0.9.3
# homeassistant.components.suez_water
-pysuezV2==1.3.1
+pysuez==0.2.0
# homeassistant.components.switchbee
pyswitchbee==1.8.3
@@ -1858,6 +1833,9 @@ pyswitchbee==1.8.3
# homeassistant.components.tautulli
pytautulli==23.1.1
+# homeassistant.components.tedee
+pytedee-async==0.2.20
+
# homeassistant.components.motionmount
python-MotionMount==2.2.0
@@ -1865,7 +1843,7 @@ python-MotionMount==2.2.0
python-awair==0.2.4
# homeassistant.components.bsblan
-python-bsblan==1.2.1
+python-bsblan==0.6.2
# homeassistant.components.ecobee
python-ecobee-api==0.2.20
@@ -1889,10 +1867,10 @@ python-izone==1.2.9
python-juicenet==1.1.0
# homeassistant.components.tplink
-python-kasa[speedups]==0.7.7
+python-kasa[speedups]==0.7.5
# homeassistant.components.linkplay
-python-linkplay==0.0.20
+python-linkplay==0.0.15
# homeassistant.components.matter
python-matter-server==6.6.0
@@ -1923,7 +1901,7 @@ python-picnic-api==1.1.0
python-rabbitair==0.0.8
# homeassistant.components.roborock
-python-roborock==2.7.2
+python-roborock==2.6.0
# homeassistant.components.smarttub
python-smarttub==0.0.36
@@ -1932,7 +1910,7 @@ python-smarttub==0.0.36
python-songpal==0.16.2
# homeassistant.components.tado
-python-tado==0.17.7
+python-tado==0.17.6
# homeassistant.components.technove
python-technove==1.3.1
@@ -2026,7 +2004,7 @@ qingping-ble==0.10.0
qnapstats==0.4.0
# homeassistant.components.radio_browser
-radios==0.3.2
+radios==0.3.1
# homeassistant.components.radiotherm
radiotherm==2.1.0
@@ -2047,13 +2025,13 @@ renault-api==0.2.7
renson-endura-delta==1.7.1
# homeassistant.components.reolink
-reolink-aio==0.11.1
+reolink-aio==0.9.11
# homeassistant.components.rflink
rflink==0.0.66
# homeassistant.components.ring
-ring-doorbell==0.9.12
+ring-doorbell==0.9.7
# homeassistant.components.roku
rokuecp==0.19.3
@@ -2099,7 +2077,7 @@ securetar==2024.2.1
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
-sense-energy==0.13.3
+sense-energy==0.12.4
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.1
@@ -2108,7 +2086,7 @@ sensirion-ble==0.1.1
sensorpro-ble==0.5.3
# homeassistant.components.sensorpush
-sensorpush-ble==1.7.1
+sensorpush-ble==1.6.2
# homeassistant.components.sensoterra
sensoterra==2.0.1
@@ -2134,9 +2112,6 @@ simplepush==2.2.3
# homeassistant.components.simplisafe
simplisafe-python==2024.01.0
-# homeassistant.components.sky_remote
-skyboxremote==0.0.6
-
# homeassistant.components.slack
slackclient==2.5.0
@@ -2150,10 +2125,10 @@ smhi-pkg==1.0.18
snapcast==2.3.6
# homeassistant.components.sonos
-soco==0.30.6
+soco==0.30.4
# homeassistant.components.solarlog
-solarlog_cli==0.3.2
+solarlog_cli==0.3.1
# homeassistant.components.solax
solax==3.1.1
@@ -2171,7 +2146,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3
# homeassistant.components.spotify
-spotifyaio==0.8.8
+spotipy==2.23.0
# homeassistant.components.sql
sqlparse==0.5.0
@@ -2238,7 +2213,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry
# homeassistant.components.tessie
-tesla-fleet-api==0.8.4
+tesla-fleet-api==0.7.8
# homeassistant.components.powerwall
tesla-powerwall==0.5.2
@@ -2246,9 +2221,6 @@ tesla-powerwall==0.5.2
# homeassistant.components.tesla_wall_connector
tesla-wall-connector==1.0.2
-# homeassistant.components.teslemetry
-teslemetry-stream==0.4.2
-
# homeassistant.components.tessie
tessie-api==0.1.1
@@ -2258,9 +2230,6 @@ thermobeacon-ble==0.7.0
# homeassistant.components.thermopro
thermopro-ble==0.10.0
-# homeassistant.components.lg_thinq
-thinqconnect==1.0.0
-
# homeassistant.components.tilt_ble
tilt-ble==0.2.3
@@ -2277,7 +2246,7 @@ toonapi==0.3.0
total-connect-client==2024.5
# homeassistant.components.tplink_omada
-tplink-omada-client==1.4.3
+tplink-omada-client==1.4.2
# homeassistant.components.transmission
transmission-rpc==7.0.3
@@ -2292,7 +2261,7 @@ ttls==1.8.3
ttn_client==1.2.0
# homeassistant.components.tuya
-tuya-device-sharing-sdk==0.2.1
+tuya-device-sharing-sdk==0.1.9
# homeassistant.components.twentemilieu
twentemilieu==2.0.1
@@ -2310,7 +2279,7 @@ typedmonarchmoney==0.3.1
uasiren==0.0.1
# homeassistant.components.unifiprotect
-uiprotect==6.4.0
+uiprotect==6.3.1
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7
@@ -2319,13 +2288,13 @@ ultraheat-api==0.5.7
unifi-discovery==1.2.0
# homeassistant.components.zha
-universal-silabs-flasher==0.0.24
+universal-silabs-flasher==0.0.22
# homeassistant.components.upb
upb-lib==0.5.8
# homeassistant.components.upcloud
-upcloud-api==2.6.0
+upcloud-api==2.5.1
# homeassistant.components.huawei_lte
# homeassistant.components.syncthru
@@ -2345,7 +2314,7 @@ vallox-websocket-api==5.3.0
vehicle==2.2.2
# homeassistant.components.velbus
-velbus-aio==2024.10.0
+velbus-aio==2024.7.6
# homeassistant.components.venstar
venstarcolortouch==0.19
@@ -2381,14 +2350,11 @@ watchdog==2.3.1
# homeassistant.components.weatherflow_cloud
weatherflow4py==1.0.6
-# homeassistant.components.nasweb
-webio-api==0.1.8
-
# homeassistant.components.webmin
webmin-xmlrpc==0.0.2
# homeassistant.components.weheat
-weheat==2024.11.02
+weheat==2024.09.23
# homeassistant.components.whirlpool
whirlpool-sixth-sense==0.18.8
@@ -2412,13 +2378,13 @@ wyoming==1.5.4
xbox-webapi==2.0.11
# homeassistant.components.xiaomi_ble
-xiaomi-ble==0.33.0
+xiaomi-ble==0.32.0
# homeassistant.components.knx
-xknx==3.3.0
+xknx==3.2.0
# homeassistant.components.knx
-xknxproject==3.8.1
+xknxproject==3.8.0
# homeassistant.components.fritz
# homeassistant.components.rest
@@ -2452,22 +2418,22 @@ youless-api==2.1.2
youtubeaio==1.1.5
# homeassistant.components.media_extractor
-yt-dlp[default]==2024.11.04
+yt-dlp==2024.09.27
# homeassistant.components.zamg
zamg==0.3.6
# homeassistant.components.zeroconf
-zeroconf==0.136.0
+zeroconf==0.135.0
# homeassistant.components.zeversolar
-zeversolar==0.3.2
+zeversolar==0.3.1
# homeassistant.components.zha
-zha==0.0.37
+zha==0.0.34
# homeassistant.components.zwave_js
-zwave-js-server-python==0.59.1
+zwave-js-server-python==0.58.1
# homeassistant.components.zwave_me
zwave-me-ws==0.4.3
diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt
index 23f584dd0de..addc8fa0e85 100644
--- a/requirements_test_pre_commit.txt
+++ b/requirements_test_pre_commit.txt
@@ -1,5 +1,5 @@
# Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit
codespell==2.3.0
-ruff==0.7.3
+ruff==0.6.9
yamllint==1.35.1
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index 7d53741c661..7787578902c 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -58,16 +58,8 @@ INCLUDED_REQUIREMENTS_WHEELS = {
# will be included in requirements_all_{action}.txt
OVERRIDDEN_REQUIREMENTS_ACTIONS = {
- "pytest": {
- "exclude": set(),
- "include": {"python-gammu"},
- "markers": {},
- },
- "wheels_aarch64": {
- "exclude": set(),
- "include": INCLUDED_REQUIREMENTS_WHEELS,
- "markers": {},
- },
+ "pytest": {"exclude": set(), "include": {"python-gammu"}},
+ "wheels_aarch64": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS},
# Pandas has issues building on armhf, it is expected they
# will drop the platform in the near future (they consider it
# "flimsy" on 386). The following packages depend on pandas,
@@ -75,23 +67,10 @@ OVERRIDDEN_REQUIREMENTS_ACTIONS = {
"wheels_armhf": {
"exclude": {"env-canada", "noaa-coops", "pyezviz", "pykrakenapi"},
"include": INCLUDED_REQUIREMENTS_WHEELS,
- "markers": {},
- },
- "wheels_armv7": {
- "exclude": set(),
- "include": INCLUDED_REQUIREMENTS_WHEELS,
- "markers": {},
- },
- "wheels_amd64": {
- "exclude": set(),
- "include": INCLUDED_REQUIREMENTS_WHEELS,
- "markers": {},
- },
- "wheels_i386": {
- "exclude": set(),
- "include": INCLUDED_REQUIREMENTS_WHEELS,
- "markers": {},
},
+ "wheels_armv7": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS},
+ "wheels_amd64": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS},
+ "wheels_i386": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS},
}
IGNORE_PIN = ("colorlog>2.1,<3", "urllib3")
@@ -117,9 +96,9 @@ httplib2>=0.19.0
# gRPC is an implicit dependency that we want to make explicit so we manage
# upgrades intentionally. It is a large package to build from source and we
# want to ensure we have wheels built.
-grpcio==1.67.1
-grpcio-status==1.67.1
-grpcio-reflection==1.67.1
+grpcio==1.66.2
+grpcio-status==1.66.2
+grpcio-reflection==1.66.2
# This is a old unmaintained library and is replaced with pycryptodome
pycrypto==1000000000.0.0
@@ -139,7 +118,7 @@ uuid==1000000000.0.0
# these requirements are quite loose. As the entire stack has some outstanding issues, and
# even newer versions seem to introduce new issues, it's useful for us to pin all these
# requirements so we can directly link HA versions to these library versions.
-anyio==4.6.2.post1
+anyio==4.6.0
h11==0.14.0
httpcore==1.0.5
@@ -148,8 +127,7 @@ httpcore==1.0.5
hyperframe>=5.2.0
# Ensure we run compatible with musllinux build env
-numpy==2.1.3
-pandas~=2.2.3
+numpy==1.26.4
# Constrain multidict to avoid typing issues
# https://github.com/home-assistant/core/pull/67046
@@ -160,10 +138,7 @@ backoff>=2.0
# Required to avoid breaking (#101042).
# v2 has breaking changes (#99218).
-pydantic==1.10.19
-
-# Required for Python 3.12.4 compatibility (#119223).
-mashumaro>=3.13.1
+pydantic==1.10.18
# Breaks asyncio
# https://github.com/pubnub/python/issues/130
@@ -179,7 +154,7 @@ pyOpenSSL>=24.0.0
# protobuf must be in package constraints for the wheel
# builder to build binary wheels
-protobuf==5.28.3
+protobuf==5.28.2
# faust-cchardet: Ensure we have a version we can build wheels
# 2.1.18 is the first version that works with our wheel builder
@@ -201,12 +176,15 @@ get-mac==1000000000.0.0
# We want to skip the binary wheels for the 'charset-normalizer' packages.
# They are build with mypyc, but causes issues with our wheel builder.
# In order to do so, we need to constrain the version.
-charset-normalizer==3.4.0
+charset-normalizer==3.2.0
# dacite: Ensure we have a version that is able to handle type unions for
-# NAM, Brother, and GIOS.
+# Roborock, NAM, Brother, and GIOS.
dacite>=1.7.0
+# Musle wheels for pandas 2.2.0 cannot be build for any architecture.
+pandas==2.1.4
+
# chacha20poly1305-reuseable==0.12.x is incompatible with cryptography==43.0.x
chacha20poly1305-reuseable>=0.13.0
@@ -214,8 +192,8 @@ chacha20poly1305-reuseable>=0.13.0
# https://github.com/pycountry/pycountry/blob/ea69bab36f00df58624a0e490fdad4ccdc14268b/HISTORY.txt#L39
pycountry>=23.12.11
-# scapy==2.6.0 causes CI failures due to a race condition
-scapy>=2.6.1
+# scapy<2.5.0 will not work with python3.12
+scapy>=2.5.0
# tuf isn't updated to deal with breaking changes in securesystemslib==1.0.
# Only tuf>=4 includes a constraint to <1.0.
@@ -224,10 +202,6 @@ tuf>=4.0.0
# https://github.com/jd/tenacity/issues/471
tenacity!=8.4.0
-
-# 5.0.0 breaks Timeout as a context manager
-# TypeError: 'Timeout' object does not support the context manager protocol
-async-timeout==4.0.3
"""
GENERATED_MESSAGE = (
@@ -332,10 +306,6 @@ def process_action_requirement(req: str, action: str) -> str:
return req
if normalized_package_name in EXCLUDED_REQUIREMENTS_ALL:
return f"# {req}"
- if markers := OVERRIDDEN_REQUIREMENTS_ACTIONS[action]["markers"].get(
- normalized_package_name, None
- ):
- return f"{req};{markers}"
return req
diff --git a/script/hassfest/config_schema.py b/script/hassfest/config_schema.py
index 6b863ab9ecd..06ef2065127 100644
--- a/script/hassfest/config_schema.py
+++ b/script/hassfest/config_schema.py
@@ -10,7 +10,7 @@ from .model import Config, Integration
CONFIG_SCHEMA_IGNORE = {
# Configuration under the homeassistant key is a special case, it's handled by
- # core_config.async_process_ha_core_config already during bootstrapping, not by
+ # conf_util.async_process_ha_core_config already during bootstrapping, not by
# a schema in the homeassistant integration.
HOMEASSISTANT_DOMAIN,
}
diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py
index 0c7f4f11a8c..66796d4dd0d 100644
--- a/script/hassfest/dependencies.py
+++ b/script/hassfest/dependencies.py
@@ -44,15 +44,6 @@ class ImportCollector(ast.NodeVisitor):
assert self._cur_fil_dir
self.referenced[self._cur_fil_dir].add(reference_domain)
- def visit_If(self, node: ast.If) -> None:
- """Visit If node."""
- if isinstance(node.test, ast.Name) and node.test.id == "TYPE_CHECKING":
- # Ignore TYPE_CHECKING block
- return
-
- # Have it visit other kids
- self.generic_visit(node)
-
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
"""Visit ImportFrom node."""
if node.module is None:
@@ -121,10 +112,10 @@ ALLOWED_USED_COMPONENTS = {
"alert",
"automation",
"conversation",
- "default_config",
"device_automation",
"frontend",
"group",
+ "hassio",
"homeassistant",
"input_boolean",
"input_button",
diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py
index 57d86bc4def..213f21a7a3e 100644
--- a/script/hassfest/docker.py
+++ b/script/hassfest/docker.py
@@ -4,7 +4,6 @@ from dataclasses import dataclass
from pathlib import Path
from homeassistant import core
-from homeassistant.components.go2rtc.const import RECOMMENDED_VERSION as GO2RTC_VERSION
from homeassistant.const import Platform
from homeassistant.util import executor, thread
from script.gen_requirements_all import gather_recursive_requirements
@@ -21,8 +20,7 @@ FROM ${{BUILD_FROM}}
# Synchronize with homeassistant/core.py:async_stop
ENV \
S6_SERVICES_GRACETIME={timeout} \
- UV_SYSTEM_PYTHON=true \
- UV_NO_CACHE=true
+ UV_SYSTEM_PYTHON=true
ARG QEMU_CPU
@@ -80,7 +78,7 @@ WORKDIR /config
_HASSFEST_TEMPLATE = r"""# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
-FROM python:3.13-alpine
+FROM python:alpine
ENV \
UV_SYSTEM_PYTHON=true \
@@ -113,6 +111,8 @@ LABEL "com.github.actions.icon"="terminal"
LABEL "com.github.actions.color"="gray-dark"
"""
+_GO2RTC_VERSION = "1.9.4"
+
def _get_package_versions(file: Path, packages: set[str]) -> dict[str, str]:
package_versions: dict[str, str] = {}
@@ -161,8 +161,6 @@ def _generate_hassfest_dockerimage(
packages.update(
gather_recursive_requirements(platform.value, already_checked_domains)
)
- # Add go2rtc requirements as this file needs the go2rtc integration
- packages.update(gather_recursive_requirements("go2rtc", already_checked_domains))
return File(
_HASSFEST_TEMPLATE.format(
@@ -198,7 +196,7 @@ def _generate_files(config: Config) -> list[File]:
DOCKERFILE_TEMPLATE.format(
timeout=timeout,
**package_versions,
- go2rtc=GO2RTC_VERSION,
+ go2rtc=_GO2RTC_VERSION,
),
config.root / "Dockerfile",
),
diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile
index 0fa0a1a89fa..f1194e37e2f 100644
--- a/script/hassfest/docker/Dockerfile
+++ b/script/hassfest/docker/Dockerfile
@@ -1,7 +1,7 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
-FROM python:3.13-alpine
+FROM python:alpine
ENV \
UV_SYSTEM_PYTHON=true \
@@ -14,7 +14,7 @@ WORKDIR "/github/workspace"
COPY . /usr/src/homeassistant
# Uv is only needed during build
-RUN --mount=from=ghcr.io/astral-sh/uv:0.5.0,source=/uv,target=/bin/uv \
+RUN --mount=from=ghcr.io/astral-sh/uv:0.4.17,source=/uv,target=/bin/uv \
# Required for PyTurboJPEG
apk add --no-cache libturbojpeg \
&& uv pip install \
@@ -22,8 +22,8 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.5.0,source=/uv,target=/bin/uv \
--no-cache \
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
-r /usr/src/homeassistant/requirements.txt \
- stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.3 \
- PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==2.0.1 home-assistant-intents==2024.11.13 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
+ stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.6.9 \
+ PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.0 hassil==1.7.4 home-assistant-intents==2024.10.2 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
LABEL "name"="hassfest"
LABEL "maintainer"="Home Assistant "
diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py
index 4013c8a6c19..8643e34725f 100644
--- a/script/hassfest/manifest.py
+++ b/script/hassfest/manifest.py
@@ -88,10 +88,12 @@ NO_IOT_CLASS = [
"logbook",
"logger",
"lovelace",
+ "map",
"media_source",
"my",
"onboarding",
"panel_custom",
+ "panel_iframe",
"plant",
"profiler",
"proxy",
@@ -268,6 +270,7 @@ INTEGRATION_MANIFEST_SCHEMA = vol.Schema(
)
],
vol.Required("documentation"): vol.All(vol.Url(), documentation_url),
+ vol.Optional("issue_tracker"): vol.Url(),
vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES),
vol.Optional("requirements"): [str],
vol.Optional("dependencies"): [str],
@@ -303,7 +306,6 @@ def manifest_schema(value: dict[str, Any]) -> vol.Schema:
CUSTOM_INTEGRATION_MANIFEST_SCHEMA = INTEGRATION_MANIFEST_SCHEMA.extend(
{
vol.Optional("version"): vol.All(str, verify_version),
- vol.Optional("issue_tracker"): vol.Url(),
vol.Optional("import_executor"): bool,
}
)
diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py
index 25fe875e437..de42c964ddf 100644
--- a/script/hassfest/mypy_config.py
+++ b/script/hassfest/mypy_config.py
@@ -43,7 +43,6 @@ GENERAL_SETTINGS: Final[dict[str, str]] = {
"local_partial_types": "true",
"strict_equality": "true",
"no_implicit_optional": "true",
- "report_deprecated_as_error": "true",
"warn_incomplete_stub": "true",
"warn_redundant_casts": "true",
"warn_unused_configs": "true",
diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py
index 998593d20ec..3df25f3284a 100644
--- a/script/hassfest/requirements.py
+++ b/script/hassfest/requirements.py
@@ -28,6 +28,12 @@ PACKAGE_REGEX = re.compile(
PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)")
PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$")
+IGNORE_STANDARD_LIBRARY_VIOLATIONS = {
+ # Integrations which have standard library requirements.
+ "slide",
+ "suez_water",
+}
+
def validate(integrations: dict[str, Integration], config: Config) -> None:
"""Handle requirements for integrations."""
@@ -138,7 +144,10 @@ def validate_requirements(integration: Integration) -> None:
if req in sys.stdlib_module_names:
standard_library_violations.add(req)
- if standard_library_violations:
+ if (
+ standard_library_violations
+ and integration.domain not in IGNORE_STANDARD_LIBRARY_VIOLATIONS
+ ):
integration.add_error(
"requirements",
(
@@ -146,6 +155,18 @@ def validate_requirements(integration: Integration) -> None:
"are not compatible with the Python standard library"
),
)
+ elif (
+ not standard_library_violations
+ and integration.domain in IGNORE_STANDARD_LIBRARY_VIOLATIONS
+ ):
+ integration.add_error(
+ "requirements",
+ (
+ f"Integration {integration.domain} no longer has requirements which are"
+ " incompatible with the Python standard library, remove it from "
+ "IGNORE_STANDARD_LIBRARY_VIOLATIONS"
+ ),
+ )
@cache
diff --git a/script/hassfest/services.py b/script/hassfest/services.py
index 8c9ab5c0c0b..92fca14d373 100644
--- a/script/hassfest/services.py
+++ b/script/hassfest/services.py
@@ -75,14 +75,6 @@ CUSTOM_INTEGRATION_FIELD_SCHEMA = CORE_INTEGRATION_FIELD_SCHEMA.extend(
}
)
-CUSTOM_INTEGRATION_SECTION_SCHEMA = vol.Schema(
- {
- vol.Optional("collapsed"): bool,
- vol.Required("fields"): vol.Schema({str: CUSTOM_INTEGRATION_FIELD_SCHEMA}),
- }
-)
-
-
CORE_INTEGRATION_SERVICE_SCHEMA = vol.Any(
vol.Schema(
{
@@ -113,17 +105,7 @@ CUSTOM_INTEGRATION_SERVICE_SCHEMA = vol.Any(
vol.Optional("target"): vol.Any(
selector.TargetSelector.CONFIG_SCHEMA, None
),
- vol.Optional("fields"): vol.All(
- vol.Schema(
- {
- str: vol.Any(
- CUSTOM_INTEGRATION_FIELD_SCHEMA,
- CUSTOM_INTEGRATION_SECTION_SCHEMA,
- )
- }
- ),
- unique_field_validator,
- ),
+ vol.Optional("fields"): vol.Schema({str: CUSTOM_INTEGRATION_FIELD_SCHEMA}),
}
),
None,
diff --git a/script/json_schemas/manifest_schema.json b/script/json_schemas/manifest_schema.json
deleted file mode 100644
index 40f08fd2c85..00000000000
--- a/script/json_schemas/manifest_schema.json
+++ /dev/null
@@ -1,391 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "title": "Home Assistant integration manifest",
- "description": "The manifest for a Home Assistant integration",
- "type": "object",
- "if": {
- "properties": { "integration_type": { "const": "virtual" } },
- "required": ["integration_type"]
- },
- "then": {
- "oneOf": [
- {
- "properties": {
- "domain": {
- "description": "The domain identifier of the integration.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#domain",
- "examples": ["mobile_app"],
- "type": "string",
- "pattern": "[0-9a-z_]+"
- },
- "name": {
- "description": "The friendly name of the integration.",
- "type": "string"
- },
- "integration_type": {
- "description": "The integration type.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#integration-type",
- "const": "virtual"
- },
- "iot_standards": {
- "description": "The IoT standards which supports devices or services of this virtual integration.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#iot-standards",
- "type": "array",
- "minItems": 1,
- "items": {
- "type": "string",
- "enum": ["homekit", "zigbee", "zwave"]
- }
- }
- },
- "additionalProperties": false,
- "required": ["domain", "name", "integration_type", "iot_standards"]
- },
- {
- "properties": {
- "domain": {
- "description": "The domain identifier of the integration.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#domain",
- "examples": ["mobile_app"],
- "type": "string",
- "pattern": "[0-9a-z_]+"
- },
- "name": {
- "description": "The friendly name of the integration.",
- "type": "string"
- },
- "integration_type": {
- "description": "The integration type.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#integration-type",
- "const": "virtual"
- },
- "supported_by": {
- "description": "The integration which supports devices or services of this virtual integration.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#supported-by",
- "type": "string"
- }
- },
- "additionalProperties": false,
- "required": ["domain", "name", "integration_type", "supported_by"]
- }
- ]
- },
- "else": {
- "properties": {
- "domain": {
- "description": "The domain identifier of the integration.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#domain",
- "examples": ["mobile_app"],
- "type": "string",
- "pattern": "[0-9a-z_]+"
- },
- "name": {
- "description": "The friendly name of the integration.",
- "type": "string"
- },
- "integration_type": {
- "description": "The integration type.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#integration-type",
- "type": "string",
- "default": "hub",
- "enum": [
- "device",
- "entity",
- "hardware",
- "helper",
- "hub",
- "service",
- "system"
- ]
- },
- "config_flow": {
- "description": "Whether the integration is configurable from the UI.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#config-flow",
- "type": "boolean"
- },
- "mqtt": {
- "description": "A list of topics to subscribe for the discovery of devices via MQTT.\nThis requires to specify \"mqtt\" in either the \"dependencies\" or \"after_dependencies\".\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#mqtt",
- "type": "array",
- "items": {
- "type": "string"
- },
- "uniqueItems": true
- },
- "zeroconf": {
- "description": "A list containing service domains to search for devices to discover via Zeroconf. Items can either be strings, which discovers all devices in the specific service domain, and/or objects which include filters. (useful for generic service domains like _http._tcp.local.)\nA device is discovered if it matches one of the items, but inside the individual item all properties have to be matched.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#zeroconf",
- "type": "array",
- "minItems": 1,
- "items": {
- "anyOf": [
- {
- "type": "string",
- "pattern": "^.*\\.local\\.$",
- "description": "Service domain to search for devices."
- },
- {
- "type": "object",
- "properties": {
- "type": {
- "description": "The service domain to search for devices.",
- "examples": ["_http._tcp.local."],
- "type": "string",
- "pattern": "^.*\\.local\\.$"
- },
- "name": {
- "description": "The name or name pattern of the devices to filter.",
- "type": "string"
- },
- "properties": {
- "description": "The properties of the Zeroconf advertisement to filter.",
- "type": "object",
- "additionalProperties": { "type": "string" }
- }
- },
- "required": ["type"],
- "additionalProperties": false
- }
- ]
- },
- "uniqueItems": true
- },
- "ssdp": {
- "description": "A list of matchers to find devices discoverable via SSDP/UPnP. In order to be discovered, the device has to match all properties of any of the matchers.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#ssdp",
- "type": "array",
- "minItems": 1,
- "items": {
- "description": "A matcher for the SSDP discovery.",
- "type": "object",
- "properties": {
- "st": {
- "type": "string"
- },
- "deviceType": {
- "type": "string"
- },
- "manufacturer": {
- "type": "string"
- },
- "modelDescription": {
- "type": "string"
- }
- },
- "additionalProperties": { "type": "string" }
- }
- },
- "bluetooth": {
- "description": "A list of matchers to find devices discoverable via Bluetooth. In order to be discovered, the device has to match all properties of any of the matchers.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#bluetooth",
- "type": "array",
- "minItems": 1,
- "items": {
- "description": "A matcher for the bluetooth discovery",
- "type": "object",
- "properties": {
- "connectable": {
- "description": "Whether the device needs to be connected to or it works with just advertisement data.",
- "type": "boolean"
- },
- "local_name": {
- "description": "The name or a name pattern of the device to match.",
- "type": "string",
- "pattern": "^([^*]+|[^*]{3,}[*].*)$"
- },
- "service_uuid": {
- "description": "The 128-bit service data UUID to match.",
- "type": "string",
- "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
- },
- "service_data_uuid": {
- "description": "The 16-bit service data UUID to match, converted into the corresponding 128-bit UUID by replacing the 3rd and 4th byte of `00000000-0000-1000-8000-00805f9b34fb` with the 16-bit UUID.",
- "examples": ["0000fd3d-0000-1000-8000-00805f9b34fb"],
- "type": "string",
- "pattern": "0000[0-9a-f]{4}-0000-1000-8000-00805f9b34fb"
- },
- "manufacturer_id": {
- "description": "The Manufacturer ID to match.",
- "type": "integer"
- },
- "manufacturer_data_start": {
- "description": "The start bytes of the manufacturer data to match.",
- "type": "array",
- "minItems": 1,
- "items": {
- "type": "integer",
- "minimum": 0,
- "maximum": 255
- }
- }
- },
- "additionalProperties": false
- },
- "uniqueItems": true
- },
- "homekit": {
- "description": "A list of model names to find devices which are discoverable via HomeKit. A device is discovered if the model name of the device starts with any of the specified model names.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#homekit",
- "type": "object",
- "properties": {
- "models": {
- "description": "The model names to search for.",
- "type": "array",
- "items": {
- "type": "string"
- },
- "uniqueItems": true
- }
- },
- "required": ["models"],
- "additionalProperties": false
- },
- "dhcp": {
- "description": "A list of matchers to find devices discoverable via DHCP. In order to be discovered, the device has to match all properties of any of the matchers.\nYou can specify an item with \"registered_devices\" set to true to check for devices with MAC addresses specified in the device registry.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#dhcp",
- "type": "array",
- "items": {
- "anyOf": [
- {
- "type": "object",
- "properties": {
- "registered_devices": {
- "description": "Whether the MAC addresses of devices in the device registry should be used for discovery, useful if the discovery is used to update the IP address of already registered devices.",
- "const": true
- }
- },
- "additionalProperties": false
- },
- {
- "type": "object",
- "properties": {
- "hostname": {
- "description": "The hostname or hostname pattern to match.",
- "type": "string"
- },
- "macaddress": {
- "description": "The MAC address or MAC address pattern to match.",
- "type": "string",
- "maxLength": 12
- }
- },
- "additionalProperties": false
- }
- ]
- },
- "uniqueItems": true
- },
- "usb": {
- "description": "A list of matchers to find devices discoverable via USB. In order to be discovered, the device has to match all properties of any of the matchers.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#usb",
- "type": "array",
- "uniqueItems": true,
- "items": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "vid": {
- "description": "The vendor ID to match.",
- "type": "string",
- "pattern": "[0-9A-F]{4}"
- },
- "pid": {
- "description": "The product ID to match.",
- "type": "string",
- "pattern": "[0-9A-F]{4}"
- },
- "description": {
- "description": "The USB device description to match.",
- "type": "string"
- },
- "manufacturer": {
- "description": "The manufacturer to match.",
- "type": "string"
- },
- "serial_number": {
- "description": "The serial number to match.",
- "type": "string"
- },
- "known_devices": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- }
- }
- },
- "documentation": {
- "description": "The website containing the documentation for the integration. It has to be in the format \"https://www.home-assistant.io/integrations/[domain]\"\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#documentation",
- "type": "string",
- "pattern": "^https://www.home-assistant.io/integrations/[0-9a-z_]+$",
- "format": "uri"
- },
- "quality_scale": {
- "description": "The quality scale of the integration.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#integration-quality-scale",
- "type": "string",
- "enum": ["internal", "silver", "gold", "platinum"]
- },
- "requirements": {
- "description": "The PyPI package requirements for the integration. The package has to be pinned to a specific version.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#requirements",
- "type": "array",
- "items": {
- "type": "string",
- "pattern": ".+==.+"
- },
- "uniqueItems": true
- },
- "dependencies": {
- "description": "A list of integrations which need to be loaded before this integration can be set up.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#dependencies",
- "type": "array",
- "items": {
- "type": "string"
- },
- "minItems": 1,
- "uniqueItems": true
- },
- "after_dependencies": {
- "description": "A list of integrations which need to be loaded before this integration is set up when it is configured. The integration will still be set up when the \"after_dependencies\" are not configured.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#after-dependencies",
- "type": "array",
- "items": {
- "type": "string"
- },
- "minItems": 1,
- "uniqueItems": true
- },
- "codeowners": {
- "description": "A list of GitHub usernames or GitHub team names of the integration owners.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#code-owners",
- "type": "array",
- "minItems": 0,
- "items": {
- "type": "string",
- "pattern": "^@.+$"
- },
- "uniqueItems": true
- },
- "loggers": {
- "description": "A list of logger names used by the requirements.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#loggers",
- "type": "array",
- "minItems": 1,
- "items": {
- "type": "string"
- },
- "uniqueItems": true
- },
- "disabled": {
- "description": "The reason for the integration being disabled.",
- "type": "string"
- },
- "iot_class": {
- "description": "The IoT class of the integration, describing how the integration connects to the device or service.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#iot-class",
- "type": "string",
- "enum": [
- "assumed_state",
- "cloud_polling",
- "cloud_push",
- "local_polling",
- "local_push",
- "calculated"
- ]
- },
- "single_config_entry": {
- "description": "Whether the integration only supports a single config entry.\nhttps://developers.home-assistant.io/docs/creating_integration_manifest/#single-config-entry-only",
- "const": true
- }
- },
- "additionalProperties": false,
- "required": ["domain", "name", "codeowners", "documentation"],
- "dependencies": {
- "mqtt": {
- "anyOf": [
- { "required": ["dependencies"] },
- { "required": ["after_dependencies"] }
- ]
- }
- }
- }
-}
diff --git a/script/licenses.py b/script/licenses.py
index 464a2fc456b..7a2ddc814de 100644
--- a/script/licenses.py
+++ b/script/licenses.py
@@ -2,36 +2,14 @@
from __future__ import annotations
-from argparse import ArgumentParser, Namespace
+from argparse import ArgumentParser
from collections.abc import Sequence
from dataclasses import dataclass
-from importlib import metadata
import json
from pathlib import Path
import sys
-from typing import TypedDict, cast
from awesomeversion import AwesomeVersion
-from license_expression import (
- AND,
- OR,
- ExpressionError,
- LicenseExpression,
- LicenseSymbol,
- get_spdx_licensing,
-)
-
-licensing = get_spdx_licensing()
-
-
-class PackageMetadata(TypedDict):
- """Package metadata."""
-
- name: str
- version: str
- license_expression: str | None
- license_metadata: str | None
- license_classifier: list[str]
@dataclass
@@ -39,60 +17,19 @@ class PackageDefinition:
"""Package definition."""
license: str
- license_expression: str | None
- license_metadata: str | None
- license_classifier: list[str]
name: str
version: AwesomeVersion
@classmethod
- def from_dict(cls, data: PackageMetadata) -> PackageDefinition:
- """Create a package definition from PackageMetadata."""
- if not (license_str := "; ".join(data["license_classifier"])):
- license_str = data["license_metadata"] or "UNKNOWN"
+ def from_dict(cls, data: dict[str, str]) -> PackageDefinition:
+ """Create a package definition from a dictionary."""
return cls(
- license=license_str,
- license_expression=data["license_expression"],
- license_metadata=data["license_metadata"],
- license_classifier=data["license_classifier"],
- name=data["name"],
- version=AwesomeVersion(data["version"]),
+ license=data["License"],
+ name=data["Name"],
+ version=AwesomeVersion(data["Version"]),
)
-# Incomplete list of OSI approved SPDX identifiers
-# Add more as needed, see https://spdx.org/licenses/
-OSI_APPROVED_LICENSES_SPDX = {
- "0BSD",
- "AFL-2.1",
- "AGPL-3.0-only",
- "AGPL-3.0-or-later",
- "Apache-2.0",
- "BSD-1-Clause",
- "BSD-2-Clause",
- "BSD-3-Clause",
- "EPL-1.0",
- "EPL-2.0",
- "GPL-2.0-only",
- "GPL-2.0-or-later",
- "GPL-3.0-only",
- "GPL-3.0-or-later",
- "HPND",
- "ISC",
- "LGPL-2.1-only",
- "LGPL-2.1-or-later",
- "LGPL-3.0-only",
- "LGPL-3.0-or-later",
- "MIT",
- "MIT-CMU",
- "MPL-1.1",
- "MPL-2.0",
- "PSF-2.0",
- "Unlicense",
- "Zlib",
- "ZPL-2.1",
-}
-
OSI_APPROVED_LICENSES = {
"Academic Free License (AFL)",
"Apache Software License",
@@ -161,10 +98,13 @@ OSI_APPROVED_LICENSES = {
"Zero-Clause BSD (0BSD)",
"Zope Public License",
"zlib/libpng License",
- # End license classifier
"Apache License",
"MIT",
+ "apache-2.0",
+ "GPL-3.0",
+ "GPLv3+",
"MPL2",
+ "MPL-2.0",
"Apache 2",
"LGPL v3",
"BSD",
@@ -172,16 +112,29 @@ OSI_APPROVED_LICENSES = {
"GPLv3",
"Eclipse Public License v2.0",
"ISC",
+ "GPL-2.0-only",
+ "mit",
"GNU General Public License v3",
+ "Unlicense",
+ "Apache-2",
"GPLv2",
+ "Python-2.0.1",
}
EXCEPTIONS = {
"PyMicroBot", # https://github.com/spycle/pyMicroBot/pull/3
"PySwitchmate", # https://github.com/Danielhiversen/pySwitchmate/pull/16
"PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201
+ "aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138
"aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180
+ "aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94
+ "aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8
+ "aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6
+ "aiovodafone", # https://github.com/chemelli74/aiovodafone/pull/131
+ "apple_weatherkit", # https://github.com/tjhorner/python-weatherkit/pull/3
+ "asyncio", # PSF License
"chacha20poly1305", # LGPL
+ "chacha20poly1305-reuseable", # Apache 2.0 or BSD 3-Clause
"commentjson", # https://github.com/vaidik/commentjson/pull/55
"crownstone-cloud", # https://github.com/crownstone/crownstone-lib-python-cloud/pull/5
"crownstone-core", # https://github.com/crownstone/crownstone-lib-python-core/pull/6
@@ -189,9 +142,13 @@ EXCEPTIONS = {
"crownstone-uart", # https://github.com/crownstone/crownstone-lib-python-uart/pull/12
"eliqonline", # https://github.com/molobrakos/eliqonline/pull/17
"enocean", # https://github.com/kipe/enocean/pull/142
+ "gardena-bluetooth", # https://github.com/elupus/gardena-bluetooth/pull/11
+ "heatmiserV3", # https://github.com/andylockran/heatmiserV3/pull/94
+ "huum", # https://github.com/frwickst/pyhuum/pull/8
"imutils", # https://github.com/PyImageSearch/imutils/pull/292
"iso4217", # Public domain
"kiwiki_client", # https://github.com/c7h/kiwiki_client/pull/6
+ "krakenex", # https://github.com/veox/python3-krakenex/pull/145
"ld2410-ble", # https://github.com/930913/ld2410-ble/pull/7
"maxcube-api", # https://github.com/uebelack/python-maxcube-api/pull/48
"neurio", # https://github.com/jordanh/neurio-python/pull/13
@@ -202,9 +159,14 @@ EXCEPTIONS = {
"pyeconet", # https://github.com/w1ll1am23/pyeconet/pull/41
"pysabnzbd", # https://github.com/jeradM/pysabnzbd/pull/6
"pyvera", # https://github.com/maximvelichko/pyvera/pull/164
+ "pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11
"repoze.lru",
+ "ruuvitag-ble", # https://github.com/Bluetooth-Devices/ruuvitag-ble/pull/10
+ "sensirion-ble", # https://github.com/akx/sensirion-ble/pull/9
"sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14
"tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5
+ "vincenty", # Public domain
+ "zeversolar", # https://github.com/kvanzuijlen/zeversolar/pull/46
}
TODO = {
@@ -213,181 +175,13 @@ TODO = {
), # https://github.com/aio-libs/aiocache/blob/master/LICENSE all rights reserved?
}
-EXCEPTIONS_AND_TODOS = EXCEPTIONS.union(TODO)
-
-
-def check_licenses(args: CheckArgs) -> int:
- """Check licenses are OSI approved."""
- exit_code = 0
- raw_licenses = json.loads(Path(args.path).read_text())
- license_status = {
- pkg.name: (pkg, check_license_status(pkg))
- for data in raw_licenses
- if (pkg := PackageDefinition.from_dict(data))
- }
-
- for name, version in TODO.items():
- pkg, status = license_status.get(name, (None, None))
- if pkg is None or not (version < pkg.version):
- continue
- assert status is not None
-
- if status is True:
- print(
- "Approved license detected for "
- f"{pkg.name}@{pkg.version}: {get_license_str(pkg)}\n"
- "Please remove the package from the TODO list.\n"
- )
- else:
- print(
- "We could not detect an OSI-approved license for "
- f"{pkg.name}@{pkg.version}: {get_license_str(pkg)}\n"
- "Please update the package version on the TODO list.\n"
- )
- exit_code = 1
-
- for pkg, status in license_status.values():
- if status is False and pkg.name not in EXCEPTIONS_AND_TODOS:
- print(
- "We could not detect an OSI-approved license for "
- f"{pkg.name}@{pkg.version}: {get_license_str(pkg)}\n"
- )
- exit_code = 1
- if status is True and pkg.name in EXCEPTIONS:
- print(
- "Approved license detected for "
- f"{pkg.name}@{pkg.version}: {get_license_str(pkg)}\n"
- "Please remove the package from the EXCEPTIONS list.\n"
- )
- exit_code = 1
-
- for name in EXCEPTIONS_AND_TODOS.difference(license_status):
- print(
- f"Package {name} is tracked, but not used. "
- "Please remove it from the licenses.py file.\n"
- )
- exit_code = 1
-
- return exit_code
-
-
-def check_license_status(package: PackageDefinition) -> bool:
- """Check if package licenses is OSI approved."""
- if package.license_expression:
- # Prefer 'License-Expression' if it exists
- return check_license_expression(package.license_expression) or False
-
- if (
- package.license_metadata
- and (check := check_license_expression(package.license_metadata)) is not None
- ):
- # Check license metadata if it's a valid SPDX license expression
- return check
-
- for approved_license in OSI_APPROVED_LICENSES:
- if approved_license in package.license:
- return True
- return False
-
-
-def check_license_expression(license_str: str) -> bool | None:
- """Check if license expression is a valid and approved SPDX license string."""
- if license_str == "UNKNOWN" or "\n" in license_str:
- # Ignore common errors for license metadata values
- return None
-
- try:
- expr = licensing.parse(license_str, validate=True)
- except ExpressionError:
- return None
- return check_spdx_license(expr)
-
-
-def check_spdx_license(expr: LicenseExpression) -> bool:
- """Check a SPDX license expression."""
- if isinstance(expr, LicenseSymbol):
- return expr.key in OSI_APPROVED_LICENSES_SPDX
- if isinstance(expr, OR):
- return any(check_spdx_license(arg) for arg in expr.args)
- if isinstance(expr, AND):
- return all(check_spdx_license(arg) for arg in expr.args)
- return False
-
-
-def get_license_str(package: PackageDefinition) -> str:
- """Return license string."""
- return (
- f"{package.license_expression} -- {package.license_metadata} "
- f"-- {package.license_classifier}"
- )
-
-
-def extract_licenses(args: ExtractArgs) -> int:
- """Extract license data for installed packages."""
- licenses = sorted(
- [get_package_metadata(dist) for dist in list(metadata.distributions())],
- key=lambda dist: dist["name"],
- )
- Path(args.output_file).write_text(json.dumps(licenses, indent=2))
- return 0
-
-
-def get_package_metadata(dist: metadata.Distribution) -> PackageMetadata:
- """Get package metadata for distribution."""
- return {
- "name": dist.name,
- "version": dist.version,
- "license_expression": dist.metadata.get("License-Expression"),
- "license_metadata": dist.metadata.get("License"),
- "license_classifier": extract_license_classifier(
- dist.metadata.get_all("Classifier")
- ),
- }
-
-
-def extract_license_classifier(classifiers: list[str] | None) -> list[str]:
- """Extract license from list of classifiers.
-
- E.g. 'License :: OSI Approved :: MIT License' -> 'MIT License'.
- Filter out bare 'License :: OSI Approved'.
- """
- return [
- license_classifier
- for classifier in classifiers or ()
- if classifier.startswith("License")
- and (license_classifier := classifier.rpartition(" :: ")[2])
- and license_classifier != "OSI Approved"
- ]
-
-
-class ExtractArgs(Namespace):
- """Extract arguments."""
-
- output_file: str
-
-
-class CheckArgs(Namespace):
- """Check arguments."""
-
- path: str
-
def main(argv: Sequence[str] | None = None) -> int:
"""Run the main script."""
+ exit_code = 0
+
parser = ArgumentParser()
- subparsers = parser.add_subparsers(title="Subcommands", required=True)
-
- parser_extract = subparsers.add_parser("extract")
- parser_extract.set_defaults(action="extract")
- parser_extract.add_argument(
- "--output-file",
- default="licenses.json",
- help="Path to store the licenses file",
- )
-
- parser_check = subparsers.add_parser("check")
- parser_check.set_defaults(action="check")
- parser_check.add_argument(
+ parser.add_argument(
"path",
nargs="?",
metavar="PATH",
@@ -398,16 +192,60 @@ def main(argv: Sequence[str] | None = None) -> int:
argv = argv or sys.argv[1:]
args = parser.parse_args(argv)
- if args.action == "extract":
- args = cast(ExtractArgs, args)
- return extract_licenses(args)
- if args.action == "check":
- args = cast(CheckArgs, args)
- if (exit_code := check_licenses(args)) == 0:
- print("All licenses are approved!")
- return exit_code
- return 0
+ raw_licenses = json.loads(Path(args.path).read_text())
+ package_definitions = [PackageDefinition.from_dict(data) for data in raw_licenses]
+ for package in package_definitions:
+ previous_unapproved_version = TODO.get(package.name)
+ approved = False
+ for approved_license in OSI_APPROVED_LICENSES:
+ if approved_license in package.license:
+ approved = True
+ break
+ if previous_unapproved_version is not None:
+ if previous_unapproved_version < package.version:
+ if approved:
+ print(
+ "Approved license detected for "
+ f"{package.name}@{package.version}: {package.license}"
+ )
+ print("Please remove the package from the TODO list.")
+ print()
+ else:
+ print(
+ "We could not detect an OSI-approved license for "
+ f"{package.name}@{package.version}: {package.license}"
+ )
+ print()
+ exit_code = 1
+ elif not approved and package.name not in EXCEPTIONS:
+ print(
+ "We could not detect an OSI-approved license for "
+ f"{package.name}@{package.version}: {package.license}"
+ )
+ print()
+ exit_code = 1
+ elif approved and package.name in EXCEPTIONS:
+ print(
+ "Approved license detected for "
+ f"{package.name}@{package.version}: {package.license}"
+ )
+ print(f"Please remove the package from the EXCEPTIONS list: {package.name}")
+ print()
+ exit_code = 1
+ current_packages = {package.name for package in package_definitions}
+ for package in [*TODO.keys(), *EXCEPTIONS]:
+ if package not in current_packages:
+ print(
+ f"Package {package} is tracked, but not used. Please remove from the licenses.py"
+ "file."
+ )
+ print()
+ exit_code = 1
+ return exit_code
if __name__ == "__main__":
- sys.exit(main())
+ exit_code = main()
+ if exit_code == 0:
+ print("All licenses are approved!")
+ sys.exit(exit_code)
diff --git a/script/scaffold/templates/config_flow_oauth2/integration/api.py b/script/scaffold/templates/config_flow_oauth2/integration/api.py
index 9516dd99122..3f4aa3cfb82 100644
--- a/script/scaffold/templates/config_flow_oauth2/integration/api.py
+++ b/script/scaffold/templates/config_flow_oauth2/integration/api.py
@@ -49,6 +49,7 @@ class AsyncConfigEntryAuth(my_pypi_package.AbstractAuth):
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
- await self._oauth_session.async_ensure_token_valid()
+ if not self._oauth_session.valid_token:
+ await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token["access_token"]
diff --git a/script/split_tests.py b/script/split_tests.py
index c64de46a068..e124f722552 100755
--- a/script/split_tests.py
+++ b/script/split_tests.py
@@ -49,27 +49,16 @@ class BucketHolder:
test_folder.get_all_flatten(), reverse=True, key=lambda x: x.total_tests
)
for tests in sorted_tests:
+ print(f"{tests.total_tests:>{digits}} tests in {tests.path}")
if tests.added_to_bucket:
# Already added to bucket
continue
- print(f"{tests.total_tests:>{digits}} tests in {tests.path}")
smallest_bucket = min(self._buckets, key=lambda x: x.total_tests)
- is_file = isinstance(tests, TestFile)
if (
smallest_bucket.total_tests + tests.total_tests < self._tests_per_bucket
- ) or is_file:
+ ) or isinstance(tests, TestFile):
smallest_bucket.add(tests)
- # Ensure all files from the same folder are in the same bucket
- # to ensure that syrupy correctly identifies unused snapshots
- if is_file:
- for other_test in tests.parent.children.values():
- if other_test is tests or isinstance(other_test, TestFolder):
- continue
- print(
- f"{other_test.total_tests:>{digits}} tests in {other_test.path} (same bucket)"
- )
- smallest_bucket.add(other_test)
# verify that all tests are added to a bucket
if not test_folder.added_to_bucket:
@@ -90,7 +79,6 @@ class TestFile:
total_tests: int
path: Path
added_to_bucket: bool = field(default=False, init=False)
- parent: TestFolder | None = field(default=None, init=False)
def add_to_bucket(self) -> None:
"""Add test file to bucket."""
@@ -137,7 +125,6 @@ class TestFolder:
def add_test_file(self, file: TestFile) -> None:
"""Add test file to folder."""
path = file.path
- file.parent = self
relative_path = path.relative_to(self.path)
if not relative_path.parts:
raise ValueError("Path is not a child of this folder")
diff --git a/tests/common.py b/tests/common.py
index 8bd45e4d7f8..ad14481e385 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -1064,8 +1064,6 @@ class MockConfigEntry(config_entries.ConfigEntry):
data: dict[str, Any] | None = None,
) -> ConfigFlowResult:
"""Start a reauthentication flow."""
- if self.entry_id not in hass.config_entries._entries:
- raise ValueError("Config entry must be added to hass to start reauth flow")
return await start_reauth_flow(hass, self, context, data)
async def start_reconfigure_flow(
@@ -1075,10 +1073,6 @@ class MockConfigEntry(config_entries.ConfigEntry):
show_advanced_options: bool = False,
) -> ConfigFlowResult:
"""Start a reconfiguration flow."""
- if self.entry_id not in hass.config_entries._entries:
- raise ValueError(
- "Config entry must be added to hass to start reconfiguration flow"
- )
return await hass.config_entries.flow.async_init(
self.domain,
context={
diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py
index 025afa74b80..51e0ee46838 100644
--- a/tests/components/abode/test_alarm_control_panel.py
+++ b/tests/components/abode/test_alarm_control_panel.py
@@ -3,10 +3,7 @@
from unittest.mock import PropertyMock, patch
from homeassistant.components.abode import ATTR_DEVICE_ID
-from homeassistant.components.alarm_control_panel import (
- DOMAIN as ALARM_DOMAIN,
- AlarmControlPanelState,
-)
+from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
@@ -14,6 +11,9 @@ from homeassistant.const import (
SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_HOME,
SERVICE_ALARM_DISARM,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_DISARMED,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -39,7 +39,7 @@ async def test_attributes(hass: HomeAssistant) -> None:
await setup_platform(hass, ALARM_DOMAIN)
state = hass.states.get(DEVICE_ID)
- assert state.state == AlarmControlPanelState.DISARMED
+ assert state.state == STATE_ALARM_DISARMED
assert state.attributes.get(ATTR_DEVICE_ID) == "area_1"
assert not state.attributes.get("battery_backup")
assert not state.attributes.get("cellular_backup")
@@ -75,7 +75,7 @@ async def test_set_alarm_away(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
state = hass.states.get(DEVICE_ID)
- assert state.state == AlarmControlPanelState.ARMED_AWAY
+ assert state.state == STATE_ALARM_ARMED_AWAY
async def test_set_alarm_home(hass: HomeAssistant) -> None:
@@ -105,7 +105,7 @@ async def test_set_alarm_home(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
state = hass.states.get(DEVICE_ID)
- assert state.state == AlarmControlPanelState.ARMED_HOME
+ assert state.state == STATE_ALARM_ARMED_HOME
async def test_set_alarm_standby(hass: HomeAssistant) -> None:
@@ -134,7 +134,7 @@ async def test_set_alarm_standby(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
state = hass.states.get(DEVICE_ID)
- assert state.state == AlarmControlPanelState.DISARMED
+ assert state.state == STATE_ALARM_DISARMED
async def test_state_unknown(hass: HomeAssistant) -> None:
diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py
index 2abed387566..a37fb8cbe33 100644
--- a/tests/components/abode/test_config_flow.py
+++ b/tests/components/abode/test_config_flow.py
@@ -10,6 +10,7 @@ from jaraco.abode.helpers.errors import MFA_CODE_REQUIRED
import pytest
from requests.exceptions import ConnectTimeout
+from homeassistant.components.abode import config_flow
from homeassistant.components.abode.const import CONF_POLLING, DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
@@ -21,110 +22,114 @@ from tests.common import MockConfigEntry
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
+async def test_show_form(hass: HomeAssistant) -> None:
+ """Test that the form is served with no input."""
+ flow = config_flow.AbodeFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_user(user_input=None)
+
+ assert result["type"] is FlowResultType.FORM
+ assert result["step_id"] == "user"
+
+
async def test_one_config_allowed(hass: HomeAssistant) -> None:
"""Test that only one Abode configuration is allowed."""
+ flow = config_flow.AbodeFlowHandler()
+ flow.hass = hass
+
MockConfigEntry(
domain=DOMAIN,
data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"},
).add_to_hass(hass)
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}
- )
+ step_user_result = await flow.async_step_user()
- assert result["type"] is FlowResultType.ABORT
- assert result["reason"] == "single_instance_allowed"
+ assert step_user_result["type"] is FlowResultType.ABORT
+ assert step_user_result["reason"] == "single_instance_allowed"
-async def test_user_flow(hass: HomeAssistant) -> None:
- """Test user flow, with various errors."""
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}
- )
- assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "user"
+async def test_invalid_credentials(hass: HomeAssistant) -> None:
+ """Test that invalid credentials throws an error."""
+ conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
+
+ flow = config_flow.AbodeFlowHandler()
+ flow.hass = hass
- # Test that invalid credentials throws an error.
with patch(
"homeassistant.components.abode.config_flow.Abode",
side_effect=AbodeAuthenticationException(
(HTTPStatus.BAD_REQUEST, "auth error")
),
):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"},
- )
- assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "user"
- assert result["errors"] == {"base": "invalid_auth"}
+ result = await flow.async_step_user(user_input=conf)
+ assert result["errors"] == {"base": "invalid_auth"}
+
+
+async def test_connection_auth_error(hass: HomeAssistant) -> None:
+ """Test other than invalid credentials throws an error."""
+ conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
+
+ flow = config_flow.AbodeFlowHandler()
+ flow.hass = hass
- # Test other than invalid credentials throws an error.
with patch(
"homeassistant.components.abode.config_flow.Abode",
side_effect=AbodeAuthenticationException(
(HTTPStatus.INTERNAL_SERVER_ERROR, "connection error")
),
):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"},
- )
- assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "user"
- assert result["errors"] == {"base": "cannot_connect"}
+ result = await flow.async_step_user(user_input=conf)
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_connection_error(hass: HomeAssistant) -> None:
+ """Test login throws an error if connection times out."""
+ conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
+
+ flow = config_flow.AbodeFlowHandler()
+ flow.hass = hass
- # Test login throws an error if connection times out.
with patch(
"homeassistant.components.abode.config_flow.Abode",
side_effect=ConnectTimeout,
):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"},
- )
- assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "user"
- assert result["errors"] == {"base": "cannot_connect"}
+ result = await flow.async_step_user(user_input=conf)
+ assert result["errors"] == {"base": "cannot_connect"}
- # Test success
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}
- )
+
+async def test_step_user(hass: HomeAssistant) -> None:
+ """Test that the user step works."""
+ conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
with patch("homeassistant.components.abode.config_flow.Abode"):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"},
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=conf
)
- assert result["type"] is FlowResultType.CREATE_ENTRY
- assert result["title"] == "user@email.com"
- assert result["data"] == {
- CONF_USERNAME: "user@email.com",
- CONF_PASSWORD: "password",
- CONF_POLLING: False,
- }
+ assert result["type"] is FlowResultType.CREATE_ENTRY
+ assert result["title"] == "user@email.com"
+ assert result["data"] == {
+ CONF_USERNAME: "user@email.com",
+ CONF_PASSWORD: "password",
+ CONF_POLLING: False,
+ }
async def test_step_mfa(hass: HomeAssistant) -> None:
"""Test that the MFA step works."""
-
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}
- )
+ conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
with patch(
"homeassistant.components.abode.config_flow.Abode",
side_effect=AbodeAuthenticationException(MFA_CODE_REQUIRED),
):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"},
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=conf
)
- assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "mfa"
+ assert result["type"] is FlowResultType.FORM
+ assert result["step_id"] == "mfa"
with patch(
"homeassistant.components.abode.config_flow.Abode",
@@ -136,51 +141,46 @@ async def test_step_mfa(hass: HomeAssistant) -> None:
result["flow_id"], user_input={"mfa_code": "123456"}
)
- assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "mfa"
- assert result["errors"] == {"base": "invalid_mfa_code"}
+ assert result["errors"] == {"base": "invalid_mfa_code"}
with patch("homeassistant.components.abode.config_flow.Abode"):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"mfa_code": "123456"}
)
- assert result["type"] is FlowResultType.CREATE_ENTRY
- assert result["title"] == "user@email.com"
- assert result["data"] == {
- CONF_USERNAME: "user@email.com",
- CONF_PASSWORD: "password",
- CONF_POLLING: False,
- }
+ assert result["type"] is FlowResultType.CREATE_ENTRY
+ assert result["title"] == "user@email.com"
+ assert result["data"] == {
+ CONF_USERNAME: "user@email.com",
+ CONF_PASSWORD: "password",
+ CONF_POLLING: False,
+ }
async def test_step_reauth(hass: HomeAssistant) -> None:
"""Test the reauth flow."""
+ conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
+
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="user@email.com",
- data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"},
+ data=conf,
)
entry.add_to_hass(hass)
- result = await entry.start_reauth_flow(hass)
+ with patch("homeassistant.components.abode.config_flow.Abode"):
+ result = await entry.start_reauth_flow(hass)
- assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reauth_confirm"
+ assert result["type"] is FlowResultType.FORM
+ assert result["step_id"] == "reauth_confirm"
- with (
- patch("homeassistant.components.abode.config_flow.Abode"),
- ):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={
- CONF_USERNAME: "user@email.com",
- CONF_PASSWORD: "new_password",
- },
- )
+ with patch("homeassistant.config_entries.ConfigEntries.async_reload"):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input=conf,
+ )
- assert result["type"] is FlowResultType.ABORT
- assert result["reason"] == "reauth_successful"
+ assert result["type"] is FlowResultType.ABORT
+ assert result["reason"] == "reauth_successful"
- assert len(hass.config_entries.async_entries()) == 1
- assert entry.data[CONF_PASSWORD] == "new_password"
+ assert len(hass.config_entries.async_entries()) == 1
diff --git a/tests/components/acaia/__init__.py b/tests/components/acaia/__init__.py
deleted file mode 100644
index f4eaa39e615..00000000000
--- a/tests/components/acaia/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""Common test tools for the acaia integration."""
-
-from homeassistant.core import HomeAssistant
-
-from tests.common import MockConfigEntry
-
-
-async def setup_integration(
- hass: HomeAssistant, mock_config_entry: MockConfigEntry
-) -> None:
- """Set up the acaia integration for testing."""
- mock_config_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(mock_config_entry.entry_id)
- await hass.async_block_till_done()
diff --git a/tests/components/acaia/conftest.py b/tests/components/acaia/conftest.py
deleted file mode 100644
index 1dc6ff31051..00000000000
--- a/tests/components/acaia/conftest.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""Common fixtures for the acaia tests."""
-
-from collections.abc import Generator
-from unittest.mock import AsyncMock, MagicMock, patch
-
-from aioacaia.acaiascale import AcaiaDeviceState
-from aioacaia.const import UnitMass as AcaiaUnitOfMass
-import pytest
-
-from homeassistant.components.acaia.const import CONF_IS_NEW_STYLE_SCALE, DOMAIN
-from homeassistant.const import CONF_ADDRESS
-from homeassistant.core import HomeAssistant
-
-from . import setup_integration
-
-from tests.common import MockConfigEntry
-
-
-@pytest.fixture
-def mock_setup_entry() -> Generator[AsyncMock]:
- """Override async_setup_entry."""
- with patch(
- "homeassistant.components.acaia.async_setup_entry", return_value=True
- ) as mock_setup_entry:
- yield mock_setup_entry
-
-
-@pytest.fixture
-def mock_verify() -> Generator[AsyncMock]:
- """Override is_new_scale check."""
- with patch(
- "homeassistant.components.acaia.config_flow.is_new_scale", return_value=True
- ) as mock_verify:
- yield mock_verify
-
-
-@pytest.fixture
-def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
- """Return the default mocked config entry."""
- return MockConfigEntry(
- title="LUNAR-DDEEFF",
- domain=DOMAIN,
- version=1,
- data={
- CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
- CONF_IS_NEW_STYLE_SCALE: True,
- },
- unique_id="aa:bb:cc:dd:ee:ff",
- )
-
-
-@pytest.fixture
-async def init_integration(
- hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_scale: MagicMock
-) -> None:
- """Set up the acaia integration for testing."""
- await setup_integration(hass, mock_config_entry)
-
-
-@pytest.fixture
-def mock_scale() -> Generator[MagicMock]:
- """Return a mocked acaia scale client."""
- with (
- patch(
- "homeassistant.components.acaia.coordinator.AcaiaScale",
- autospec=True,
- ) as scale_mock,
- ):
- scale = scale_mock.return_value
- scale.connected = True
- scale.mac = "aa:bb:cc:dd:ee:ff"
- scale.model = "Lunar"
- scale.timer_running = True
- scale.heartbeat_task = None
- scale.process_queue_task = None
- scale.device_state = AcaiaDeviceState(
- battery_level=42, units=AcaiaUnitOfMass.GRAMS
- )
- scale.weight = 123.45
- yield scale
diff --git a/tests/components/acaia/snapshots/test_button.ambr b/tests/components/acaia/snapshots/test_button.ambr
deleted file mode 100644
index cd91ca1a17a..00000000000
--- a/tests/components/acaia/snapshots/test_button.ambr
+++ /dev/null
@@ -1,139 +0,0 @@
-# serializer version: 1
-# name: test_buttons[button.lunar_ddeeff_reset_timer-entry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': None,
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'button',
- 'entity_category': None,
- 'entity_id': 'button.lunar_ddeeff_reset_timer',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'labels': set({
- }),
- 'name': None,
- 'options': dict({
- }),
- 'original_device_class': None,
- 'original_icon': None,
- 'original_name': 'Reset timer',
- 'platform': 'acaia',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': 'reset_timer',
- 'unique_id': 'aa:bb:cc:dd:ee:ff_reset_timer',
- 'unit_of_measurement': None,
- })
-# ---
-# name: test_buttons[button.lunar_ddeeff_reset_timer-state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'friendly_name': 'LUNAR-DDEEFF Reset timer',
- }),
- 'context': ,
- 'entity_id': 'button.lunar_ddeeff_reset_timer',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'unknown',
- })
-# ---
-# name: test_buttons[button.lunar_ddeeff_start_stop_timer-entry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': None,
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'button',
- 'entity_category': None,
- 'entity_id': 'button.lunar_ddeeff_start_stop_timer',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'labels': set({
- }),
- 'name': None,
- 'options': dict({
- }),
- 'original_device_class': None,
- 'original_icon': None,
- 'original_name': 'Start/stop timer',
- 'platform': 'acaia',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': 'start_stop',
- 'unique_id': 'aa:bb:cc:dd:ee:ff_start_stop',
- 'unit_of_measurement': None,
- })
-# ---
-# name: test_buttons[button.lunar_ddeeff_start_stop_timer-state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'friendly_name': 'LUNAR-DDEEFF Start/stop timer',
- }),
- 'context': ,
- 'entity_id': 'button.lunar_ddeeff_start_stop_timer',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'unknown',
- })
-# ---
-# name: test_buttons[button.lunar_ddeeff_tare-entry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': None,
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'button',
- 'entity_category': None,
- 'entity_id': 'button.lunar_ddeeff_tare',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'labels': set({
- }),
- 'name': None,
- 'options': dict({
- }),
- 'original_device_class': None,
- 'original_icon': None,
- 'original_name': 'Tare',
- 'platform': 'acaia',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': 'tare',
- 'unique_id': 'aa:bb:cc:dd:ee:ff_tare',
- 'unit_of_measurement': None,
- })
-# ---
-# name: test_buttons[button.lunar_ddeeff_tare-state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'friendly_name': 'LUNAR-DDEEFF Tare',
- }),
- 'context': ,
- 'entity_id': 'button.lunar_ddeeff_tare',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'unknown',
- })
-# ---
diff --git a/tests/components/acaia/snapshots/test_init.ambr b/tests/components/acaia/snapshots/test_init.ambr
deleted file mode 100644
index 1cc3d8dbbc0..00000000000
--- a/tests/components/acaia/snapshots/test_init.ambr
+++ /dev/null
@@ -1,33 +0,0 @@
-# serializer version: 1
-# name: test_device
- DeviceRegistryEntrySnapshot({
- 'area_id': 'kitchen',
- 'config_entries': ,
- 'configuration_url': None,
- 'connections': set({
- }),
- 'disabled_by': None,
- 'entry_type': None,
- 'hw_version': None,
- 'id': ,
- 'identifiers': set({
- tuple(
- 'acaia',
- 'aa:bb:cc:dd:ee:ff',
- ),
- }),
- 'is_new': False,
- 'labels': set({
- }),
- 'manufacturer': 'Acaia',
- 'model': 'Lunar',
- 'model_id': None,
- 'name': 'LUNAR-DDEEFF',
- 'name_by_user': None,
- 'primary_config_entry': ,
- 'serial_number': None,
- 'suggested_area': 'Kitchen',
- 'sw_version': None,
- 'via_device_id': None,
- })
-# ---
diff --git a/tests/components/acaia/test_button.py b/tests/components/acaia/test_button.py
deleted file mode 100644
index f68f85e253d..00000000000
--- a/tests/components/acaia/test_button.py
+++ /dev/null
@@ -1,90 +0,0 @@
-"""Tests for the acaia buttons."""
-
-from datetime import timedelta
-from unittest.mock import MagicMock, patch
-
-from freezegun.api import FrozenDateTimeFactory
-from syrupy import SnapshotAssertion
-
-from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
-from homeassistant.const import (
- ATTR_ENTITY_ID,
- STATE_UNAVAILABLE,
- STATE_UNKNOWN,
- Platform,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers import entity_registry as er
-
-from . import setup_integration
-
-from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
-
-BUTTONS = (
- "tare",
- "reset_timer",
- "start_stop_timer",
-)
-
-
-async def test_buttons(
- hass: HomeAssistant,
- entity_registry: er.EntityRegistry,
- snapshot: SnapshotAssertion,
- mock_scale: MagicMock,
- mock_config_entry: MockConfigEntry,
-) -> None:
- """Test the acaia buttons."""
-
- with patch("homeassistant.components.acaia.PLATFORMS", [Platform.BUTTON]):
- await setup_integration(hass, mock_config_entry)
- await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
-
-
-async def test_button_presses(
- hass: HomeAssistant,
- mock_scale: MagicMock,
- mock_config_entry: MockConfigEntry,
-) -> None:
- """Test the acaia button presses."""
-
- await setup_integration(hass, mock_config_entry)
-
- for button in BUTTONS:
- await hass.services.async_call(
- BUTTON_DOMAIN,
- SERVICE_PRESS,
- {
- ATTR_ENTITY_ID: f"button.lunar_ddeeff_{button}",
- },
- blocking=True,
- )
-
- function = getattr(mock_scale, button)
- function.assert_called_once()
-
-
-async def test_buttons_unavailable_on_disconnected_scale(
- hass: HomeAssistant,
- mock_scale: MagicMock,
- mock_config_entry: MockConfigEntry,
- freezer: FrozenDateTimeFactory,
-) -> None:
- """Test the acaia buttons are unavailable when the scale is disconnected."""
-
- await setup_integration(hass, mock_config_entry)
-
- for button in BUTTONS:
- state = hass.states.get(f"button.lunar_ddeeff_{button}")
- assert state
- assert state.state == STATE_UNKNOWN
-
- mock_scale.connected = False
- freezer.tick(timedelta(minutes=10))
- async_fire_time_changed(hass)
- await hass.async_block_till_done()
-
- for button in BUTTONS:
- state = hass.states.get(f"button.lunar_ddeeff_{button}")
- assert state
- assert state.state == STATE_UNAVAILABLE
diff --git a/tests/components/acaia/test_config_flow.py b/tests/components/acaia/test_config_flow.py
deleted file mode 100644
index 2bf4b1dbe8a..00000000000
--- a/tests/components/acaia/test_config_flow.py
+++ /dev/null
@@ -1,242 +0,0 @@
-"""Test the acaia config flow."""
-
-from collections.abc import Generator
-from unittest.mock import AsyncMock, patch
-
-from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError, AcaiaUnknownDevice
-import pytest
-
-from homeassistant.components.acaia.const import CONF_IS_NEW_STYLE_SCALE, DOMAIN
-from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER
-from homeassistant.const import CONF_ADDRESS
-from homeassistant.core import HomeAssistant
-from homeassistant.data_entry_flow import FlowResultType
-from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
-
-from tests.common import MockConfigEntry
-
-service_info = BluetoothServiceInfo(
- name="LUNAR-DDEEFF",
- address="aa:bb:cc:dd:ee:ff",
- rssi=-63,
- manufacturer_data={},
- service_data={},
- service_uuids=[],
- source="local",
-)
-
-
-@pytest.fixture
-def mock_discovered_service_info() -> Generator[AsyncMock]:
- """Override getting Bluetooth service info."""
- with patch(
- "homeassistant.components.acaia.config_flow.async_discovered_service_info",
- return_value=[service_info],
- ) as mock_discovered_service_info:
- yield mock_discovered_service_info
-
-
-async def test_form(
- hass: HomeAssistant,
- mock_setup_entry: AsyncMock,
- mock_verify: AsyncMock,
- mock_discovered_service_info: AsyncMock,
-) -> None:
- """Test we get the form."""
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}
- )
- assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "user"
-
- user_input = {
- CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
- }
- result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input=user_input,
- )
-
- assert result2["type"] is FlowResultType.CREATE_ENTRY
- assert result2["title"] == "LUNAR-DDEEFF"
- assert result2["data"] == {
- **user_input,
- CONF_IS_NEW_STYLE_SCALE: True,
- }
-
-
-async def test_bluetooth_discovery(
- hass: HomeAssistant,
- mock_setup_entry: AsyncMock,
- mock_verify: AsyncMock,
-) -> None:
- """Test we can discover a device."""
-
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info
- )
-
- assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "bluetooth_confirm"
-
- result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={},
- )
-
- assert result2["type"] is FlowResultType.CREATE_ENTRY
- assert result2["title"] == service_info.name
- assert result2["data"] == {
- CONF_ADDRESS: service_info.address,
- CONF_IS_NEW_STYLE_SCALE: True,
- }
-
-
-@pytest.mark.parametrize(
- ("exception", "error"),
- [
- (AcaiaDeviceNotFound("Error"), "device_not_found"),
- (AcaiaError, "unknown"),
- (AcaiaUnknownDevice, "unsupported_device"),
- ],
-)
-async def test_bluetooth_discovery_errors(
- hass: HomeAssistant,
- mock_verify: AsyncMock,
- exception: Exception,
- error: str,
-) -> None:
- """Test abortions of Bluetooth discovery."""
- mock_verify.side_effect = exception
-
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info
- )
-
- assert result["type"] is FlowResultType.ABORT
- assert result["reason"] == error
-
-
-async def test_already_configured(
- hass: HomeAssistant,
- mock_config_entry: MockConfigEntry,
- mock_verify: AsyncMock,
- mock_discovered_service_info: AsyncMock,
-) -> None:
- """Ensure we can't add the same device twice."""
-
- mock_config_entry.add_to_hass(hass)
-
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}
- )
- assert result["type"] is FlowResultType.FORM
-
- result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {
- CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
- },
- )
- await hass.async_block_till_done()
-
- assert result2["type"] is FlowResultType.ABORT
- assert result2["reason"] == "already_configured"
-
-
-async def test_already_configured_bluetooth_discovery(
- hass: HomeAssistant,
- mock_config_entry: MockConfigEntry,
-) -> None:
- """Ensure configure device is not discovered again."""
-
- mock_config_entry.add_to_hass(hass)
-
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info
- )
-
- assert result["type"] is FlowResultType.ABORT
- assert result["reason"] == "already_configured"
-
-
-@pytest.mark.parametrize(
- ("exception", "error"),
- [
- (AcaiaDeviceNotFound("Error"), "device_not_found"),
- (AcaiaError, "unknown"),
- ],
-)
-async def test_recoverable_config_flow_errors(
- hass: HomeAssistant,
- mock_setup_entry: AsyncMock,
- mock_verify: AsyncMock,
- mock_discovered_service_info: AsyncMock,
- exception: Exception,
- error: str,
-) -> None:
- """Test recoverable errors."""
- mock_verify.side_effect = exception
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}
- )
- assert result["type"] is FlowResultType.FORM
-
- result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {
- CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
- },
- )
-
- assert result2["type"] is FlowResultType.FORM
- assert result2["errors"] == {"base": error}
-
- # recover
- mock_verify.side_effect = None
- result3 = await hass.config_entries.flow.async_configure(
- result2["flow_id"],
- {
- CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
- },
- )
- assert result3["type"] is FlowResultType.CREATE_ENTRY
-
-
-async def test_unsupported_device(
- hass: HomeAssistant,
- mock_setup_entry: AsyncMock,
- mock_verify: AsyncMock,
- mock_discovered_service_info: AsyncMock,
-) -> None:
- """Test flow aborts on unsupported device."""
- mock_verify.side_effect = AcaiaUnknownDevice
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}
- )
- assert result["type"] is FlowResultType.FORM
-
- result2 = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {
- CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
- },
- )
-
- assert result2["type"] is FlowResultType.ABORT
- assert result2["reason"] == "unsupported_device"
-
-
-async def test_no_bluetooth_devices(
- hass: HomeAssistant,
- mock_setup_entry: AsyncMock,
- mock_discovered_service_info: AsyncMock,
-) -> None:
- """Test flow aborts on unsupported device."""
- mock_discovered_service_info.return_value = []
-
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}
- )
- assert result["type"] is FlowResultType.ABORT
- assert result["reason"] == "no_devices_found"
diff --git a/tests/components/acaia/test_init.py b/tests/components/acaia/test_init.py
deleted file mode 100644
index 8ad988d3b9b..00000000000
--- a/tests/components/acaia/test_init.py
+++ /dev/null
@@ -1,65 +0,0 @@
-"""Test init of acaia integration."""
-
-from datetime import timedelta
-from unittest.mock import MagicMock
-
-from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
-from freezegun.api import FrozenDateTimeFactory
-import pytest
-from syrupy import SnapshotAssertion
-
-from homeassistant.components.acaia.const import DOMAIN
-from homeassistant.config_entries import ConfigEntryState
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers import device_registry as dr
-
-from tests.common import MockConfigEntry, async_fire_time_changed
-
-pytestmark = pytest.mark.usefixtures("init_integration")
-
-
-async def test_load_unload_config_entry(
- hass: HomeAssistant,
- mock_config_entry: MockConfigEntry,
-) -> None:
- """Test loading and unloading the integration."""
-
- assert mock_config_entry.state is ConfigEntryState.LOADED
-
- await hass.config_entries.async_unload(mock_config_entry.entry_id)
- await hass.async_block_till_done()
-
- assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
-
-
-@pytest.mark.parametrize(
- "exception", [AcaiaError, AcaiaDeviceNotFound("Boom"), TimeoutError]
-)
-async def test_update_exception_leads_to_active_disconnect(
- hass: HomeAssistant,
- mock_scale: MagicMock,
- freezer: FrozenDateTimeFactory,
- exception: Exception,
-) -> None:
- """Test scale gets disconnected on exception."""
-
- mock_scale.connect.side_effect = exception
- mock_scale.connected = False
-
- freezer.tick(timedelta(minutes=10))
- async_fire_time_changed(hass)
- await hass.async_block_till_done()
-
- mock_scale.device_disconnected_handler.assert_called_once()
-
-
-async def test_device(
- mock_scale: MagicMock,
- device_registry: dr.DeviceRegistry,
- snapshot: SnapshotAssertion,
-) -> None:
- """Snapshot the device from registry."""
-
- device = device_registry.async_get_device({(DOMAIN, mock_scale.mac)})
- assert device
- assert device == snapshot
diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py
index 6644a4ca20f..d493962611f 100644
--- a/tests/components/adguard/test_config_flow.py
+++ b/tests/components/adguard/test_config_flow.py
@@ -4,6 +4,7 @@ import aiohttp
from homeassistant import config_entries
from homeassistant.components.adguard.const import DOMAIN
+from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import (
CONF_HOST,
@@ -16,7 +17,6 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
-from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py
index d0088d96ba5..13bbadb38f9 100644
--- a/tests/components/advantage_air/test_binary_sensor.py
+++ b/tests/components/advantage_air/test_binary_sensor.py
@@ -1,8 +1,10 @@
"""Test the Advantage Air Binary Sensor Platform."""
from datetime import timedelta
-from unittest.mock import AsyncMock, patch
+from unittest.mock import AsyncMock
+from homeassistant.components.advantage_air import ADVANTAGE_AIR_SYNC_INTERVAL
+from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -68,14 +70,22 @@ async def test_binary_sensor_async_setup_entry(
assert not hass.states.get(entity_id)
mock_get.reset_mock()
+ entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
+ await hass.async_block_till_done()
- with patch("homeassistant.config_entries.RELOAD_AFTER_UPDATE_DELAY", 1):
- entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
- await hass.async_block_till_done()
+ async_fire_time_changed(
+ hass,
+ dt_util.utcnow() + timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL + 1),
+ )
+ await hass.async_block_till_done(wait_background_tasks=True)
+ assert len(mock_get.mock_calls) == 1
- async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2))
- await hass.async_block_till_done(wait_background_tasks=True)
- assert len(mock_get.mock_calls) == 1
+ async_fire_time_changed(
+ hass,
+ dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
+ )
+ await hass.async_block_till_done(wait_background_tasks=True)
+ assert len(mock_get.mock_calls) == 2
state = hass.states.get(entity_id)
assert state
@@ -91,14 +101,22 @@ async def test_binary_sensor_async_setup_entry(
assert not hass.states.get(entity_id)
mock_get.reset_mock()
+ entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
+ await hass.async_block_till_done()
- with patch("homeassistant.config_entries.RELOAD_AFTER_UPDATE_DELAY", 1):
- entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
- await hass.async_block_till_done()
+ async_fire_time_changed(
+ hass,
+ dt_util.utcnow() + timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL + 1),
+ )
+ await hass.async_block_till_done(wait_background_tasks=True)
+ assert len(mock_get.mock_calls) == 1
- async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2))
- await hass.async_block_till_done(wait_background_tasks=True)
- assert len(mock_get.mock_calls) == 1
+ async_fire_time_changed(
+ hass,
+ dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
+ )
+ await hass.async_block_till_done(wait_background_tasks=True)
+ assert len(mock_get.mock_calls) == 2
state = hass.states.get(entity_id)
assert state
diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py
index 3ea368a59fb..06243921a64 100644
--- a/tests/components/advantage_air/test_sensor.py
+++ b/tests/components/advantage_air/test_sensor.py
@@ -1,13 +1,15 @@
"""Test the Advantage Air Sensor Platform."""
from datetime import timedelta
-from unittest.mock import AsyncMock, patch
+from unittest.mock import AsyncMock
+from homeassistant.components.advantage_air import ADVANTAGE_AIR_SYNC_INTERVAL
from homeassistant.components.advantage_air.const import DOMAIN as ADVANTAGE_AIR_DOMAIN
from homeassistant.components.advantage_air.sensor import (
ADVANTAGE_AIR_SERVICE_SET_TIME_TO,
ADVANTAGE_AIR_SET_COUNTDOWN_VALUE,
)
+from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -122,15 +124,23 @@ async def test_sensor_platform_disabled_entity(
assert not hass.states.get(entity_id)
+ entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
+ await hass.async_block_till_done(wait_background_tasks=True)
mock_get.reset_mock()
- with patch("homeassistant.config_entries.RELOAD_AFTER_UPDATE_DELAY", 1):
- entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
- await hass.async_block_till_done(wait_background_tasks=True)
+ async_fire_time_changed(
+ hass,
+ dt_util.utcnow() + timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL + 1),
+ )
+ await hass.async_block_till_done(wait_background_tasks=True)
+ assert len(mock_get.mock_calls) == 1
- async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2))
- await hass.async_block_till_done(wait_background_tasks=True)
- assert len(mock_get.mock_calls) == 1
+ async_fire_time_changed(
+ hass,
+ dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
+ )
+ await hass.async_block_till_done(wait_background_tasks=True)
+ assert len(mock_get.mock_calls) == 2
state = hass.states.get(entity_id)
assert state
diff --git a/tests/components/airgradient/conftest.py b/tests/components/airgradient/conftest.py
index 395c5cd96a4..1899e12c8ae 100644
--- a/tests/components/airgradient/conftest.py
+++ b/tests/components/airgradient/conftest.py
@@ -1,7 +1,7 @@
"""AirGradient tests configuration."""
from collections.abc import Generator
-from unittest.mock import AsyncMock, patch
+from unittest.mock import patch
from airgradient import Config, Measures
import pytest
@@ -10,6 +10,7 @@ from homeassistant.components.airgradient.const import DOMAIN
from homeassistant.const import CONF_HOST
from tests.common import MockConfigEntry, load_fixture
+from tests.components.smhi.common import AsyncMock
@pytest.fixture
diff --git a/tests/components/airgradient/snapshots/test_update.ambr b/tests/components/airgradient/snapshots/test_update.ambr
index 1f944bb528b..c639a97d5dd 100644
--- a/tests/components/airgradient/snapshots/test_update.ambr
+++ b/tests/components/airgradient/snapshots/test_update.ambr
@@ -37,7 +37,6 @@
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
- 'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/airgradient/icon.png',
'friendly_name': 'Airgradient Firmware',
'in_progress': False,
@@ -48,7 +47,6 @@
'skipped_version': None,
'supported_features': ,
'title': None,
- 'update_percentage': None,
}),
'context': ,
'entity_id': 'update.airgradient_firmware',
diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py
index add21b1067f..a736fa979e9 100644
--- a/tests/components/airthings_ble/__init__.py
+++ b/tests/components/airthings_ble/__init__.py
@@ -49,7 +49,7 @@ def patch_airthings_ble(return_value=AirthingsDevice, side_effect=None):
def patch_airthings_device_update():
"""Patch airthings-ble device."""
return patch(
- "homeassistant.components.airthings_ble.coordinator.AirthingsBluetoothDeviceData.update_device",
+ "homeassistant.components.airthings_ble.AirthingsBluetoothDeviceData.update_device",
return_value=WAVE_DEVICE_INFO,
)
diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py
index 632bdb72eb4..e38fc64587e 100644
--- a/tests/components/airvisual/test_config_flow.py
+++ b/tests/components/airvisual/test_config_flow.py
@@ -155,6 +155,10 @@ async def test_step_reauth(
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+ assert result["type"] is FlowResultType.FORM
+ assert result["step_id"] == "reauth_confirm"
+
new_api_key = "defgh67890"
result = await hass.config_entries.flow.async_configure(
diff --git a/tests/components/airzone/snapshots/test_diagnostics.ambr b/tests/components/airzone/snapshots/test_diagnostics.ambr
index fb4f6530b1e..693550a3e1c 100644
--- a/tests/components/airzone/snapshots/test_diagnostics.ambr
+++ b/tests/components/airzone/snapshots/test_diagnostics.ambr
@@ -220,45 +220,6 @@
}),
]),
}),
- dict({
- 'data': list([
- dict({
- 'air_demand': 0,
- 'coldStage': 0,
- 'coldStages': 0,
- 'coolmaxtemp': 30,
- 'coolmintemp': 15,
- 'coolsetpoint': 20,
- 'errors': list([
- ]),
- 'floor_demand': 0,
- 'heatStage': 0,
- 'heatStages': 0,
- 'heatmaxtemp': 30,
- 'heatmintemp': 15,
- 'heatsetpoint': 20,
- 'humidity': 0,
- 'maxTemp': 30,
- 'minTemp': 15,
- 'mode': 6,
- 'modes': list([
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- ]),
- 'name': 'Aux Heat',
- 'on': 1,
- 'roomTemp': 22,
- 'setpoint': 20,
- 'systemID': 4,
- 'units': 0,
- 'zoneID': 1,
- }),
- ]),
- }),
]),
}),
'version': dict({
@@ -308,8 +269,8 @@
'temp-set': 45,
'temp-unit': 0,
}),
- 'num-systems': 4,
- 'num-zones': 8,
+ 'num-systems': 3,
+ 'num-zones': 7,
'systems': dict({
'1': dict({
'available': True,
@@ -359,23 +320,6 @@
]),
'problems': False,
}),
- '4': dict({
- 'available': True,
- 'full-name': 'Airzone [4] System',
- 'id': 4,
- 'master-system-zone': '4:1',
- 'master-zone': 1,
- 'mode': 6,
- 'modes': list([
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- ]),
- 'problems': False,
- }),
}),
'version': '1.62',
'webserver': dict({
@@ -739,46 +683,6 @@
'temp-step': 1.0,
'temp-unit': 1,
}),
- '4:1': dict({
- 'absolute-temp-max': 30.0,
- 'absolute-temp-min': 15.0,
- 'action': 5,
- 'air-demand': False,
- 'available': True,
- 'cold-stage': 0,
- 'cool-temp-max': 30.0,
- 'cool-temp-min': 15.0,
- 'cool-temp-set': 20.0,
- 'demand': False,
- 'double-set-point': False,
- 'floor-demand': False,
- 'full-name': 'Airzone [4:1] Aux Heat',
- 'heat-stage': 0,
- 'heat-temp-max': 30.0,
- 'heat-temp-min': 15.0,
- 'heat-temp-set': 20.0,
- 'id': 1,
- 'master': True,
- 'mode': 6,
- 'modes': list([
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- ]),
- 'name': 'Aux Heat',
- 'on': True,
- 'problems': False,
- 'system': 4,
- 'temp': 22.0,
- 'temp-max': 30.0,
- 'temp-min': 15.0,
- 'temp-set': 20.0,
- 'temp-step': 0.5,
- 'temp-unit': 0,
- }),
}),
}),
})
diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py
index 12a73a6a268..0f23c151e0e 100644
--- a/tests/components/airzone/test_climate.py
+++ b/tests/components/airzone/test_climate.py
@@ -225,23 +225,6 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None:
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 22.8
- state = hass.states.get("climate.aux_heat")
- assert state.state == HVACMode.HEAT
- assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None
- assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 22
- assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.IDLE
- assert state.attributes.get(ATTR_HVAC_MODES) == [
- HVACMode.OFF,
- HVACMode.COOL,
- HVACMode.HEAT,
- HVACMode.FAN_ONLY,
- HVACMode.DRY,
- ]
- assert state.attributes.get(ATTR_MAX_TEMP) == 30
- assert state.attributes.get(ATTR_MIN_TEMP) == 15
- assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP
- assert state.attributes.get(ATTR_TEMPERATURE) == 20.0
-
HVAC_MOCK_CHANGED = copy.deepcopy(HVAC_MOCK)
HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MAX_TEMP] = 25
HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MIN_TEMP] = 10
diff --git a/tests/components/airzone/test_switch.py b/tests/components/airzone/test_switch.py
deleted file mode 100644
index f761b53ed4c..00000000000
--- a/tests/components/airzone/test_switch.py
+++ /dev/null
@@ -1,102 +0,0 @@
-"""The switch tests for the Airzone platform."""
-
-from unittest.mock import patch
-
-from aioairzone.const import API_DATA, API_ON, API_SYSTEM_ID, API_ZONE_ID
-
-from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
-from homeassistant.const import (
- ATTR_ENTITY_ID,
- SERVICE_TURN_OFF,
- SERVICE_TURN_ON,
- STATE_OFF,
- STATE_ON,
-)
-from homeassistant.core import HomeAssistant
-
-from .util import async_init_integration
-
-
-async def test_airzone_create_switches(hass: HomeAssistant) -> None:
- """Test creation of switches."""
-
- await async_init_integration(hass)
-
- state = hass.states.get("switch.despacho")
- assert state.state == STATE_OFF
-
- state = hass.states.get("switch.dorm_1")
- assert state.state == STATE_ON
-
- state = hass.states.get("switch.dorm_2")
- assert state.state == STATE_OFF
-
- state = hass.states.get("switch.dorm_ppal")
- assert state.state == STATE_ON
-
- state = hass.states.get("switch.salon")
- assert state.state == STATE_OFF
-
-
-async def test_airzone_switch_off(hass: HomeAssistant) -> None:
- """Test switch off."""
-
- await async_init_integration(hass)
-
- put_hvac_off = {
- API_DATA: [
- {
- API_SYSTEM_ID: 1,
- API_ZONE_ID: 3,
- API_ON: False,
- }
- ]
- }
-
- with patch(
- "homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
- return_value=put_hvac_off,
- ):
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_OFF,
- {
- ATTR_ENTITY_ID: "switch.dorm_1",
- },
- blocking=True,
- )
-
- state = hass.states.get("switch.dorm_1")
- assert state.state == STATE_OFF
-
-
-async def test_airzone_switch_on(hass: HomeAssistant) -> None:
- """Test switch on."""
-
- await async_init_integration(hass)
-
- put_hvac_on = {
- API_DATA: [
- {
- API_SYSTEM_ID: 1,
- API_ZONE_ID: 5,
- API_ON: True,
- }
- ]
- }
-
- with patch(
- "homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
- return_value=put_hvac_on,
- ):
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_ENTITY_ID: "switch.dorm_2",
- },
- blocking=True,
- )
-
- state = hass.states.get("switch.dorm_2")
- assert state.state == STATE_ON
diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py
index 278663b7a97..2cdb7a9c6f9 100644
--- a/tests/components/airzone/util.py
+++ b/tests/components/airzone/util.py
@@ -272,37 +272,6 @@ HVAC_MOCK = {
},
]
},
- {
- API_DATA: [
- {
- API_SYSTEM_ID: 4,
- API_ZONE_ID: 1,
- API_NAME: "Aux Heat",
- API_ON: 1,
- API_COOL_SET_POINT: 20,
- API_COOL_MAX_TEMP: 30,
- API_COOL_MIN_TEMP: 15,
- API_HEAT_SET_POINT: 20,
- API_HEAT_MAX_TEMP: 30,
- API_HEAT_MIN_TEMP: 15,
- API_MAX_TEMP: 30,
- API_MIN_TEMP: 15,
- API_SET_POINT: 20,
- API_ROOM_TEMP: 22,
- API_MODES: [1, 2, 3, 4, 5, 6],
- API_MODE: 6,
- API_COLD_STAGES: 0,
- API_COLD_STAGE: 0,
- API_HEAT_STAGES: 0,
- API_HEAT_STAGE: 0,
- API_HUMIDITY: 0,
- API_UNITS: 0,
- API_ERRORS: [],
- API_AIR_DEMAND: 0,
- API_FLOOR_DEMAND: 0,
- },
- ]
- },
]
}
diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr
index c6ad36916bf..86b5c75b290 100644
--- a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr
+++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr
@@ -136,7 +136,6 @@
}),
'temperature': 21.0,
'temperature-setpoint': 22.0,
- 'temperature-setpoint-auto-air': 22.0,
'temperature-setpoint-cool-air': 22.0,
'temperature-setpoint-hot-air': 22.0,
'temperature-setpoint-max': 30.0,
@@ -192,7 +191,6 @@
}),
'temperature': 20.0,
'temperature-setpoint': 22.0,
- 'temperature-setpoint-auto-air': 22.0,
'temperature-setpoint-cool-air': 22.0,
'temperature-setpoint-hot-air': 18.0,
'temperature-setpoint-max': 30.0,
@@ -299,7 +297,6 @@
'dhw1': dict({
'active': False,
'available': True,
- 'double-set-point': False,
'id': 'dhw1',
'installation': 'installation1',
'is-connected': True,
@@ -382,7 +379,6 @@
'aq-present': True,
'aq-status': 'good',
'available': True,
- 'double-set-point': False,
'errors': list([
dict({
'_id': 'error-id',
diff --git a/tests/components/airzone_cloud/test_climate.py b/tests/components/airzone_cloud/test_climate.py
index 2b587680a57..37c5ff8e1af 100644
--- a/tests/components/airzone_cloud/test_climate.py
+++ b/tests/components/airzone_cloud/test_climate.py
@@ -97,7 +97,8 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None:
assert state.attributes[ATTR_MAX_TEMP] == 30
assert state.attributes[ATTR_MIN_TEMP] == 15
assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_DEFAULT_TEMP_STEP
- assert state.attributes.get(ATTR_TEMPERATURE) == 22.0
+ assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 22.0
+ assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 18.0
# Groups
state = hass.states.get("climate.group")
@@ -588,7 +589,6 @@ async def test_airzone_climate_set_temp(hass: HomeAssistant) -> None:
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: "climate.bron_pro",
- ATTR_HVAC_MODE: HVACMode.HEAT_COOL,
ATTR_TARGET_TEMP_HIGH: 25.0,
ATTR_TARGET_TEMP_LOW: 20.0,
},
@@ -596,7 +596,7 @@ async def test_airzone_climate_set_temp(hass: HomeAssistant) -> None:
)
state = hass.states.get("climate.bron_pro")
- assert state.state == HVACMode.HEAT_COOL
+ assert state.state == HVACMode.HEAT
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 20.0
diff --git a/tests/components/airzone_cloud/test_init.py b/tests/components/airzone_cloud/test_init.py
index 6cab0be6e7c..b5b4bcebaa8 100644
--- a/tests/components/airzone_cloud/test_init.py
+++ b/tests/components/airzone_cloud/test_init.py
@@ -2,8 +2,6 @@
from unittest.mock import patch
-from aioairzone_cloud.exceptions import AirzoneTimeout
-
from homeassistant.components.airzone_cloud.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
@@ -52,20 +50,3 @@ async def test_unload_entry(hass: HomeAssistant) -> None:
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.NOT_LOADED
-
-
-async def test_init_api_timeout(hass: HomeAssistant) -> None:
- """Test API timeouts when loading the Airzone Cloud integration."""
-
- with patch(
- "homeassistant.components.airzone_cloud.AirzoneCloudApi.login",
- side_effect=AirzoneTimeout,
- ):
- config_entry = MockConfigEntry(
- data=CONFIG,
- domain=DOMAIN,
- unique_id="airzone_cloud_unique_id",
- )
- config_entry.add_to_hass(hass)
-
- assert await hass.config_entries.async_setup(config_entry.entry_id) is False
diff --git a/tests/components/airzone_cloud/test_select.py b/tests/components/airzone_cloud/test_select.py
index d0993365083..5a6b6104468 100644
--- a/tests/components/airzone_cloud/test_select.py
+++ b/tests/components/airzone_cloud/test_select.py
@@ -4,7 +4,7 @@ from unittest.mock import patch
import pytest
-from homeassistant.components.select import ATTR_OPTIONS, DOMAIN as SELECT_DOMAIN
+from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_SELECT_OPTION
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
@@ -22,21 +22,9 @@ async def test_airzone_create_selects(hass: HomeAssistant) -> None:
state = hass.states.get("select.dormitorio_air_quality_mode")
assert state.state == "auto"
- state = hass.states.get("select.dormitorio_mode")
- assert state is None
-
state = hass.states.get("select.salon_air_quality_mode")
assert state.state == "auto"
- state = hass.states.get("select.salon_mode")
- assert state.state == "cool"
- assert state.attributes.get(ATTR_OPTIONS) == [
- "cool",
- "dry",
- "fan",
- "heat",
- ]
-
async def test_airzone_select_air_quality_mode(hass: HomeAssistant) -> None:
"""Test select Air Quality mode."""
@@ -70,37 +58,3 @@ async def test_airzone_select_air_quality_mode(hass: HomeAssistant) -> None:
state = hass.states.get("select.dormitorio_air_quality_mode")
assert state.state == "off"
-
-
-async def test_airzone_select_mode(hass: HomeAssistant) -> None:
- """Test select HVAC mode."""
-
- await async_init_integration(hass)
-
- with pytest.raises(ServiceValidationError):
- await hass.services.async_call(
- SELECT_DOMAIN,
- SERVICE_SELECT_OPTION,
- {
- ATTR_ENTITY_ID: "select.salon_mode",
- ATTR_OPTION: "Invalid",
- },
- blocking=True,
- )
-
- with patch(
- "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device",
- return_value=None,
- ):
- await hass.services.async_call(
- SELECT_DOMAIN,
- SERVICE_SELECT_OPTION,
- {
- ATTR_ENTITY_ID: "select.salon_mode",
- ATTR_OPTION: "heat",
- },
- blocking=True,
- )
-
- state = hass.states.get("select.salon_mode")
- assert state.state == "heat"
diff --git a/tests/components/airzone_cloud/test_switch.py b/tests/components/airzone_cloud/test_switch.py
deleted file mode 100644
index 5ee65f11fa8..00000000000
--- a/tests/components/airzone_cloud/test_switch.py
+++ /dev/null
@@ -1,71 +0,0 @@
-"""The switch tests for the Airzone Cloud platform."""
-
-from unittest.mock import patch
-
-from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
-from homeassistant.const import (
- ATTR_ENTITY_ID,
- SERVICE_TURN_OFF,
- SERVICE_TURN_ON,
- STATE_OFF,
- STATE_ON,
-)
-from homeassistant.core import HomeAssistant
-
-from .util import async_init_integration
-
-
-async def test_airzone_create_switches(hass: HomeAssistant) -> None:
- """Test creation of switches."""
-
- await async_init_integration(hass)
-
- state = hass.states.get("switch.dormitorio")
- assert state.state == STATE_OFF
-
- state = hass.states.get("switch.salon")
- assert state.state == STATE_ON
-
-
-async def test_airzone_switch_off(hass: HomeAssistant) -> None:
- """Test switch off."""
-
- await async_init_integration(hass)
-
- with patch(
- "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device",
- return_value=None,
- ):
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_OFF,
- {
- ATTR_ENTITY_ID: "switch.salon",
- },
- blocking=True,
- )
-
- state = hass.states.get("switch.salon")
- assert state.state == STATE_OFF
-
-
-async def test_airzone_switch_on(hass: HomeAssistant) -> None:
- """Test switch on."""
-
- await async_init_integration(hass)
-
- with patch(
- "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device",
- return_value=None,
- ):
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_ENTITY_ID: "switch.dormitorio",
- },
- blocking=True,
- )
-
- state = hass.states.get("switch.dormitorio")
- assert state.state == STATE_ON
diff --git a/tests/components/alarm_control_panel/common.py b/tests/components/alarm_control_panel/common.py
index 8a631eeff36..36e9918f54c 100644
--- a/tests/components/alarm_control_panel/common.py
+++ b/tests/components/alarm_control_panel/common.py
@@ -8,7 +8,6 @@ from homeassistant.components.alarm_control_panel import (
DOMAIN,
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.const import (
ATTR_CODE,
@@ -21,6 +20,12 @@ from homeassistant.const import (
SERVICE_ALARM_ARM_VACATION,
SERVICE_ALARM_DISARM,
SERVICE_ALARM_TRIGGER,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
)
from homeassistant.core import HomeAssistant
@@ -140,31 +145,31 @@ class MockAlarm(MockEntity, AlarmControlPanelEntity):
def alarm_arm_away(self, code=None):
"""Send arm away command."""
- self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
+ self._attr_state = STATE_ALARM_ARMED_AWAY
self.schedule_update_ha_state()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
- self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
+ self._attr_state = STATE_ALARM_ARMED_HOME
self.schedule_update_ha_state()
def alarm_arm_night(self, code=None):
"""Send arm night command."""
- self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
+ self._attr_state = STATE_ALARM_ARMED_NIGHT
self.schedule_update_ha_state()
def alarm_arm_vacation(self, code=None):
"""Send arm night command."""
- self._attr_alarm_state = AlarmControlPanelState.ARMED_VACATION
+ self._attr_state = STATE_ALARM_ARMED_VACATION
self.schedule_update_ha_state()
def alarm_disarm(self, code=None):
"""Send disarm command."""
if code == "1234":
- self._attr_alarm_state = AlarmControlPanelState.DISARMED
+ self._attr_state = STATE_ALARM_DISARMED
self.schedule_update_ha_state()
def alarm_trigger(self, code=None):
"""Send alarm trigger command."""
- self._attr_alarm_state = AlarmControlPanelState.TRIGGERED
+ self._attr_state = STATE_ALARM_TRIGGERED
self.schedule_update_ha_state()
diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py
index a7335017691..9c5aaffd733 100644
--- a/tests/components/alarm_control_panel/test_device_action.py
+++ b/tests/components/alarm_control_panel/test_device_action.py
@@ -7,10 +7,19 @@ from homeassistant.components import automation
from homeassistant.components.alarm_control_panel import (
DOMAIN,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.components.device_automation import DeviceAutomationType
-from homeassistant.const import CONF_PLATFORM, STATE_UNKNOWN, EntityCategory
+from homeassistant.const import (
+ CONF_PLATFORM,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+ STATE_UNKNOWN,
+ EntityCategory,
+)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
@@ -532,44 +541,27 @@ async def test_action(
hass.bus.async_fire("test_event_arm_away")
await hass.async_block_till_done()
- assert (
- hass.states.get(entity_entry.entity_id).state
- == AlarmControlPanelState.ARMED_AWAY
- )
+ assert hass.states.get(entity_entry.entity_id).state == STATE_ALARM_ARMED_AWAY
hass.bus.async_fire("test_event_arm_home")
await hass.async_block_till_done()
- assert (
- hass.states.get(entity_entry.entity_id).state
- == AlarmControlPanelState.ARMED_HOME
- )
+ assert hass.states.get(entity_entry.entity_id).state == STATE_ALARM_ARMED_HOME
hass.bus.async_fire("test_event_arm_vacation")
await hass.async_block_till_done()
- assert (
- hass.states.get(entity_entry.entity_id).state
- == AlarmControlPanelState.ARMED_VACATION
- )
+ assert hass.states.get(entity_entry.entity_id).state == STATE_ALARM_ARMED_VACATION
hass.bus.async_fire("test_event_arm_night")
await hass.async_block_till_done()
- assert (
- hass.states.get(entity_entry.entity_id).state
- == AlarmControlPanelState.ARMED_NIGHT
- )
+ assert hass.states.get(entity_entry.entity_id).state == STATE_ALARM_ARMED_NIGHT
hass.bus.async_fire("test_event_disarm")
await hass.async_block_till_done()
- assert (
- hass.states.get(entity_entry.entity_id).state == AlarmControlPanelState.DISARMED
- )
+ assert hass.states.get(entity_entry.entity_id).state == STATE_ALARM_DISARMED
hass.bus.async_fire("test_event_trigger")
await hass.async_block_till_done()
- assert (
- hass.states.get(entity_entry.entity_id).state
- == AlarmControlPanelState.TRIGGERED
- )
+ assert hass.states.get(entity_entry.entity_id).state == STATE_ALARM_TRIGGERED
async def test_action_legacy(
@@ -623,7 +615,4 @@ async def test_action_legacy(
hass.bus.async_fire("test_event_arm_away")
await hass.async_block_till_done()
- assert (
- hass.states.get(entity_entry.entity_id).state
- == AlarmControlPanelState.ARMED_AWAY
- )
+ assert hass.states.get(entity_entry.entity_id).state == STATE_ALARM_ARMED_AWAY
diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py
index 37cbc466e6d..da1d77f50a3 100644
--- a/tests/components/alarm_control_panel/test_device_condition.py
+++ b/tests/components/alarm_control_panel/test_device_condition.py
@@ -7,10 +7,18 @@ from homeassistant.components import automation
from homeassistant.components.alarm_control_panel import (
DOMAIN,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.components.device_automation import DeviceAutomationType
-from homeassistant.const import EntityCategory
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
+ EntityCategory,
+)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
@@ -346,7 +354,7 @@ async def test_if_state(
]
},
)
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.TRIGGERED)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_TRIGGERED)
hass.bus.async_fire("test_event1")
hass.bus.async_fire("test_event2")
hass.bus.async_fire("test_event3")
@@ -358,7 +366,7 @@ async def test_if_state(
assert len(service_calls) == 1
assert service_calls[0].data["some"] == "is_triggered - event - test_event1"
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.DISARMED)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_DISARMED)
hass.bus.async_fire("test_event1")
hass.bus.async_fire("test_event2")
hass.bus.async_fire("test_event3")
@@ -370,7 +378,7 @@ async def test_if_state(
assert len(service_calls) == 2
assert service_calls[1].data["some"] == "is_disarmed - event - test_event2"
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.ARMED_HOME)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_HOME)
hass.bus.async_fire("test_event1")
hass.bus.async_fire("test_event2")
hass.bus.async_fire("test_event3")
@@ -382,7 +390,7 @@ async def test_if_state(
assert len(service_calls) == 3
assert service_calls[2].data["some"] == "is_armed_home - event - test_event3"
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.ARMED_AWAY)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_AWAY)
hass.bus.async_fire("test_event1")
hass.bus.async_fire("test_event2")
hass.bus.async_fire("test_event3")
@@ -394,7 +402,7 @@ async def test_if_state(
assert len(service_calls) == 4
assert service_calls[3].data["some"] == "is_armed_away - event - test_event4"
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.ARMED_NIGHT)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_NIGHT)
hass.bus.async_fire("test_event1")
hass.bus.async_fire("test_event2")
hass.bus.async_fire("test_event3")
@@ -406,7 +414,7 @@ async def test_if_state(
assert len(service_calls) == 5
assert service_calls[4].data["some"] == "is_armed_night - event - test_event5"
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.ARMED_VACATION)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_VACATION)
hass.bus.async_fire("test_event1")
hass.bus.async_fire("test_event2")
hass.bus.async_fire("test_event3")
@@ -418,7 +426,7 @@ async def test_if_state(
assert len(service_calls) == 6
assert service_calls[5].data["some"] == "is_armed_vacation - event - test_event6"
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.ARMED_CUSTOM_BYPASS)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_CUSTOM_BYPASS)
hass.bus.async_fire("test_event1")
hass.bus.async_fire("test_event2")
hass.bus.async_fire("test_event3")
@@ -480,7 +488,7 @@ async def test_if_state_legacy(
]
},
)
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.TRIGGERED)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_TRIGGERED)
hass.bus.async_fire("test_event1")
await hass.async_block_till_done()
assert len(service_calls) == 1
diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py
index 17a301ccdf1..46eba314dc1 100644
--- a/tests/components/alarm_control_panel/test_device_trigger.py
+++ b/tests/components/alarm_control_panel/test_device_trigger.py
@@ -9,10 +9,18 @@ from homeassistant.components import automation
from homeassistant.components.alarm_control_panel import (
DOMAIN,
AlarmControlPanelEntityFeature,
- AlarmControlPanelState,
)
from homeassistant.components.device_automation import DeviceAutomationType
-from homeassistant.const import EntityCategory
+from homeassistant.const import (
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_PENDING,
+ STATE_ALARM_TRIGGERED,
+ EntityCategory,
+)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
@@ -248,7 +256,7 @@ async def test_if_fires_on_state_change(
DOMAIN, "test", "5678", device_id=device_entry.id
)
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.PENDING)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_PENDING)
assert await async_setup_component(
hass,
@@ -392,7 +400,7 @@ async def test_if_fires_on_state_change(
)
# Fake that the entity is triggered.
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.TRIGGERED)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_TRIGGERED)
await hass.async_block_till_done()
assert len(service_calls) == 1
assert (
@@ -401,7 +409,7 @@ async def test_if_fires_on_state_change(
)
# Fake that the entity is disarmed.
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.DISARMED)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_DISARMED)
await hass.async_block_till_done()
assert len(service_calls) == 2
assert (
@@ -410,7 +418,7 @@ async def test_if_fires_on_state_change(
)
# Fake that the entity is armed home.
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.ARMED_HOME)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_HOME)
await hass.async_block_till_done()
assert len(service_calls) == 3
assert (
@@ -419,7 +427,7 @@ async def test_if_fires_on_state_change(
)
# Fake that the entity is armed away.
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.ARMED_AWAY)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_AWAY)
await hass.async_block_till_done()
assert len(service_calls) == 4
assert (
@@ -428,7 +436,7 @@ async def test_if_fires_on_state_change(
)
# Fake that the entity is armed night.
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.ARMED_NIGHT)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_NIGHT)
await hass.async_block_till_done()
assert len(service_calls) == 5
assert (
@@ -437,7 +445,7 @@ async def test_if_fires_on_state_change(
)
# Fake that the entity is armed vacation.
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.ARMED_VACATION)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_VACATION)
await hass.async_block_till_done()
assert len(service_calls) == 6
assert (
@@ -463,7 +471,7 @@ async def test_if_fires_on_state_change_with_for(
DOMAIN, "test", "5678", device_id=device_entry.id
)
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.DISARMED)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_DISARMED)
assert await async_setup_component(
hass,
@@ -498,7 +506,7 @@ async def test_if_fires_on_state_change_with_for(
await hass.async_block_till_done()
assert len(service_calls) == 0
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.TRIGGERED)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_TRIGGERED)
await hass.async_block_till_done()
assert len(service_calls) == 0
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
@@ -528,7 +536,7 @@ async def test_if_fires_on_state_change_legacy(
DOMAIN, "test", "5678", device_id=device_entry.id
)
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.DISARMED)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_DISARMED)
assert await async_setup_component(
hass,
@@ -562,7 +570,7 @@ async def test_if_fires_on_state_change_legacy(
await hass.async_block_till_done()
assert len(service_calls) == 0
- hass.states.async_set(entry.entity_id, AlarmControlPanelState.TRIGGERED)
+ hass.states.async_set(entry.entity_id, STATE_ALARM_TRIGGERED)
await hass.async_block_till_done()
assert len(service_calls) == 1
assert (
diff --git a/tests/components/alarm_control_panel/test_init.py b/tests/components/alarm_control_panel/test_init.py
index 89a2a2a2b1a..06724978ce3 100644
--- a/tests/components/alarm_control_panel/test_init.py
+++ b/tests/components/alarm_control_panel/test_init.py
@@ -2,17 +2,14 @@
from types import ModuleType
from typing import Any
-from unittest.mock import patch
import pytest
from homeassistant.components import alarm_control_panel
-from homeassistant.components.alarm_control_panel import (
- DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
+from homeassistant.components.alarm_control_panel.const import (
AlarmControlPanelEntityFeature,
CodeFormat,
)
-from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CODE,
SERVICE_ALARM_ARM_AWAY,
@@ -26,20 +23,11 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
-from .conftest import TEST_DOMAIN, MockAlarmControlPanel
+from .conftest import MockAlarmControlPanel
-from tests.common import (
- MockConfigEntry,
- MockModule,
- MockPlatform,
- help_test_all,
- import_and_test_deprecated_constant_enum,
- mock_integration,
- mock_platform,
-)
+from tests.common import help_test_all, import_and_test_deprecated_constant_enum
async def help_test_async_alarm_control_panel_service(
@@ -295,290 +283,3 @@ async def test_alarm_control_panel_with_default_code(
hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_DISARM
)
mock_alarm_control_panel_entity.calls_disarm.assert_called_with("1234")
-
-
-async def test_alarm_control_panel_not_log_deprecated_state_warning(
- hass: HomeAssistant,
- mock_alarm_control_panel_entity: MockAlarmControlPanel,
- caplog: pytest.LogCaptureFixture,
-) -> None:
- """Test correctly using alarm_state doesn't log issue or raise repair."""
- state = hass.states.get(mock_alarm_control_panel_entity.entity_id)
- assert state is not None
- assert "Entities should implement the 'alarm_state' property and" not in caplog.text
-
-
-async def test_alarm_control_panel_log_deprecated_state_warning_using_state_prop(
- hass: HomeAssistant,
- code_format: CodeFormat | None,
- supported_features: AlarmControlPanelEntityFeature,
- code_arm_required: bool,
- caplog: pytest.LogCaptureFixture,
-) -> None:
- """Test incorrectly using state property does log issue and raise repair."""
-
- async def async_setup_entry_init(
- hass: HomeAssistant, config_entry: ConfigEntry
- ) -> bool:
- """Set up test config entry."""
- await hass.config_entries.async_forward_entry_setups(
- config_entry, [ALARM_CONTROL_PANEL_DOMAIN]
- )
- return True
-
- mock_integration(
- hass,
- MockModule(
- TEST_DOMAIN,
- async_setup_entry=async_setup_entry_init,
- ),
- )
-
- class MockLegacyAlarmControlPanel(MockAlarmControlPanel):
- """Mocked alarm control entity."""
-
- def __init__(
- self,
- supported_features: AlarmControlPanelEntityFeature = AlarmControlPanelEntityFeature(
- 0
- ),
- code_format: CodeFormat | None = None,
- code_arm_required: bool = True,
- ) -> None:
- """Initialize the alarm control."""
- super().__init__(supported_features, code_format, code_arm_required)
-
- @property
- def state(self) -> str:
- """Return the state of the entity."""
- return "disarmed"
-
- entity = MockLegacyAlarmControlPanel(
- supported_features=supported_features,
- code_format=code_format,
- code_arm_required=code_arm_required,
- )
-
- async def async_setup_entry_platform(
- hass: HomeAssistant,
- config_entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
- ) -> None:
- """Set up test alarm control panel platform via config entry."""
- async_add_entities([entity])
-
- mock_platform(
- hass,
- f"{TEST_DOMAIN}.{ALARM_CONTROL_PANEL_DOMAIN}",
- MockPlatform(async_setup_entry=async_setup_entry_platform),
- )
-
- with patch.object(
- MockLegacyAlarmControlPanel,
- "__module__",
- "tests.custom_components.test.alarm_control_panel",
- ):
- config_entry = MockConfigEntry(domain=TEST_DOMAIN)
- config_entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(config_entry.entry_id)
- await hass.async_block_till_done()
-
- state = hass.states.get(entity.entity_id)
- assert state is not None
-
- assert "Entities should implement the 'alarm_state' property and" in caplog.text
-
-
-async def test_alarm_control_panel_log_deprecated_state_warning_using_attr_state_attr(
- hass: HomeAssistant,
- code_format: CodeFormat | None,
- supported_features: AlarmControlPanelEntityFeature,
- code_arm_required: bool,
- caplog: pytest.LogCaptureFixture,
-) -> None:
- """Test incorrectly using _attr_state attribute does log issue and raise repair."""
-
- async def async_setup_entry_init(
- hass: HomeAssistant, config_entry: ConfigEntry
- ) -> bool:
- """Set up test config entry."""
- await hass.config_entries.async_forward_entry_setups(
- config_entry, [ALARM_CONTROL_PANEL_DOMAIN]
- )
- return True
-
- mock_integration(
- hass,
- MockModule(
- TEST_DOMAIN,
- async_setup_entry=async_setup_entry_init,
- ),
- )
-
- class MockLegacyAlarmControlPanel(MockAlarmControlPanel):
- """Mocked alarm control entity."""
-
- def __init__(
- self,
- supported_features: AlarmControlPanelEntityFeature = AlarmControlPanelEntityFeature(
- 0
- ),
- code_format: CodeFormat | None = None,
- code_arm_required: bool = True,
- ) -> None:
- """Initialize the alarm control."""
- super().__init__(supported_features, code_format, code_arm_required)
-
- def alarm_disarm(self, code: str | None = None) -> None:
- """Mock alarm disarm calls."""
- self._attr_state = "disarmed"
-
- entity = MockLegacyAlarmControlPanel(
- supported_features=supported_features,
- code_format=code_format,
- code_arm_required=code_arm_required,
- )
-
- async def async_setup_entry_platform(
- hass: HomeAssistant,
- config_entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
- ) -> None:
- """Set up test alarm control panel platform via config entry."""
- async_add_entities([entity])
-
- mock_platform(
- hass,
- f"{TEST_DOMAIN}.{ALARM_CONTROL_PANEL_DOMAIN}",
- MockPlatform(async_setup_entry=async_setup_entry_platform),
- )
-
- with patch.object(
- MockLegacyAlarmControlPanel,
- "__module__",
- "tests.custom_components.test.alarm_control_panel",
- ):
- config_entry = MockConfigEntry(domain=TEST_DOMAIN)
- config_entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(config_entry.entry_id)
- await hass.async_block_till_done()
-
- state = hass.states.get(entity.entity_id)
- assert state is not None
-
- assert "Entities should implement the 'alarm_state' property and" not in caplog.text
-
- with patch.object(
- MockLegacyAlarmControlPanel,
- "__module__",
- "tests.custom_components.test.alarm_control_panel",
- ):
- await help_test_async_alarm_control_panel_service(
- hass, entity.entity_id, SERVICE_ALARM_DISARM
- )
-
- assert "Entities should implement the 'alarm_state' property and" in caplog.text
- caplog.clear()
- with patch.object(
- MockLegacyAlarmControlPanel,
- "__module__",
- "tests.custom_components.test.alarm_control_panel",
- ):
- await help_test_async_alarm_control_panel_service(
- hass, entity.entity_id, SERVICE_ALARM_DISARM
- )
- # Test we only log once
- assert "Entities should implement the 'alarm_state' property and" not in caplog.text
-
-
-async def test_alarm_control_panel_deprecated_state_does_not_break_state(
- hass: HomeAssistant,
- code_format: CodeFormat | None,
- supported_features: AlarmControlPanelEntityFeature,
- code_arm_required: bool,
- caplog: pytest.LogCaptureFixture,
-) -> None:
- """Test using _attr_state attribute does not break state."""
-
- async def async_setup_entry_init(
- hass: HomeAssistant, config_entry: ConfigEntry
- ) -> bool:
- """Set up test config entry."""
- await hass.config_entries.async_forward_entry_setups(
- config_entry, [ALARM_CONTROL_PANEL_DOMAIN]
- )
- return True
-
- mock_integration(
- hass,
- MockModule(
- TEST_DOMAIN,
- async_setup_entry=async_setup_entry_init,
- ),
- )
-
- class MockLegacyAlarmControlPanel(MockAlarmControlPanel):
- """Mocked alarm control entity."""
-
- def __init__(
- self,
- supported_features: AlarmControlPanelEntityFeature = AlarmControlPanelEntityFeature(
- 0
- ),
- code_format: CodeFormat | None = None,
- code_arm_required: bool = True,
- ) -> None:
- """Initialize the alarm control."""
- self._attr_state = "armed_away"
- super().__init__(supported_features, code_format, code_arm_required)
-
- def alarm_disarm(self, code: str | None = None) -> None:
- """Mock alarm disarm calls."""
- self._attr_state = "disarmed"
-
- entity = MockLegacyAlarmControlPanel(
- supported_features=supported_features,
- code_format=code_format,
- code_arm_required=code_arm_required,
- )
-
- async def async_setup_entry_platform(
- hass: HomeAssistant,
- config_entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
- ) -> None:
- """Set up test alarm control panel platform via config entry."""
- async_add_entities([entity])
-
- mock_platform(
- hass,
- f"{TEST_DOMAIN}.{ALARM_CONTROL_PANEL_DOMAIN}",
- MockPlatform(async_setup_entry=async_setup_entry_platform),
- )
-
- with patch.object(
- MockLegacyAlarmControlPanel,
- "__module__",
- "tests.custom_components.test.alarm_control_panel",
- ):
- config_entry = MockConfigEntry(domain=TEST_DOMAIN)
- config_entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(config_entry.entry_id)
- await hass.async_block_till_done()
-
- state = hass.states.get(entity.entity_id)
- assert state is not None
- assert state.state == "armed_away"
-
- with patch.object(
- MockLegacyAlarmControlPanel,
- "__module__",
- "tests.custom_components.test.alarm_control_panel",
- ):
- await help_test_async_alarm_control_panel_service(
- hass, entity.entity_id, SERVICE_ALARM_DISARM
- )
-
- state = hass.states.get(entity.entity_id)
- assert state is not None
- assert state.state == "disarmed"
diff --git a/tests/components/alarm_control_panel/test_reproduce_state.py b/tests/components/alarm_control_panel/test_reproduce_state.py
index fcb4fdee36e..c7984b0793e 100644
--- a/tests/components/alarm_control_panel/test_reproduce_state.py
+++ b/tests/components/alarm_control_panel/test_reproduce_state.py
@@ -2,7 +2,6 @@
import pytest
-from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.const import (
SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
@@ -11,6 +10,13 @@ from homeassistant.const import (
SERVICE_ALARM_ARM_VACATION,
SERVICE_ALARM_DISARM,
SERVICE_ALARM_TRIGGER,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_ARMED_VACATION,
+ STATE_ALARM_DISARMED,
+ STATE_ALARM_TRIGGERED,
)
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.state import async_reproduce_state
@@ -23,37 +29,27 @@ async def test_reproducing_states(
) -> None:
"""Test reproducing Alarm control panel states."""
hass.states.async_set(
- "alarm_control_panel.entity_armed_away",
- AlarmControlPanelState.ARMED_AWAY,
- {},
+ "alarm_control_panel.entity_armed_away", STATE_ALARM_ARMED_AWAY, {}
)
hass.states.async_set(
"alarm_control_panel.entity_armed_custom_bypass",
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
{},
)
hass.states.async_set(
- "alarm_control_panel.entity_armed_home",
- AlarmControlPanelState.ARMED_HOME,
- {},
+ "alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_HOME, {}
)
hass.states.async_set(
- "alarm_control_panel.entity_armed_night",
- AlarmControlPanelState.ARMED_NIGHT,
- {},
+ "alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_NIGHT, {}
)
hass.states.async_set(
- "alarm_control_panel.entity_armed_vacation",
- AlarmControlPanelState.ARMED_VACATION,
- {},
+ "alarm_control_panel.entity_armed_vacation", STATE_ALARM_ARMED_VACATION, {}
)
hass.states.async_set(
- "alarm_control_panel.entity_disarmed", AlarmControlPanelState.DISARMED, {}
+ "alarm_control_panel.entity_disarmed", STATE_ALARM_DISARMED, {}
)
hass.states.async_set(
- "alarm_control_panel.entity_triggered",
- AlarmControlPanelState.TRIGGERED,
- {},
+ "alarm_control_panel.entity_triggered", STATE_ALARM_TRIGGERED, {}
)
arm_away_calls = async_mock_service(
@@ -80,34 +76,18 @@ async def test_reproducing_states(
await async_reproduce_state(
hass,
[
- State(
- "alarm_control_panel.entity_armed_away",
- AlarmControlPanelState.ARMED_AWAY,
- ),
+ State("alarm_control_panel.entity_armed_away", STATE_ALARM_ARMED_AWAY),
State(
"alarm_control_panel.entity_armed_custom_bypass",
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
),
+ State("alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_HOME),
+ State("alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_NIGHT),
State(
- "alarm_control_panel.entity_armed_home",
- AlarmControlPanelState.ARMED_HOME,
- ),
- State(
- "alarm_control_panel.entity_armed_night",
- AlarmControlPanelState.ARMED_NIGHT,
- ),
- State(
- "alarm_control_panel.entity_armed_vacation",
- AlarmControlPanelState.ARMED_VACATION,
- ),
- State(
- "alarm_control_panel.entity_disarmed",
- AlarmControlPanelState.DISARMED,
- ),
- State(
- "alarm_control_panel.entity_triggered",
- AlarmControlPanelState.TRIGGERED,
+ "alarm_control_panel.entity_armed_vacation", STATE_ALARM_ARMED_VACATION
),
+ State("alarm_control_panel.entity_disarmed", STATE_ALARM_DISARMED),
+ State("alarm_control_panel.entity_triggered", STATE_ALARM_TRIGGERED),
],
)
@@ -137,34 +117,17 @@ async def test_reproducing_states(
await async_reproduce_state(
hass,
[
+ State("alarm_control_panel.entity_armed_away", STATE_ALARM_TRIGGERED),
State(
- "alarm_control_panel.entity_armed_away",
- AlarmControlPanelState.TRIGGERED,
+ "alarm_control_panel.entity_armed_custom_bypass", STATE_ALARM_ARMED_AWAY
),
State(
- "alarm_control_panel.entity_armed_custom_bypass",
- AlarmControlPanelState.ARMED_AWAY,
- ),
- State(
- "alarm_control_panel.entity_armed_home",
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
- ),
- State(
- "alarm_control_panel.entity_armed_night",
- AlarmControlPanelState.ARMED_HOME,
- ),
- State(
- "alarm_control_panel.entity_armed_vacation",
- AlarmControlPanelState.ARMED_NIGHT,
- ),
- State(
- "alarm_control_panel.entity_disarmed",
- AlarmControlPanelState.ARMED_VACATION,
- ),
- State(
- "alarm_control_panel.entity_triggered",
- AlarmControlPanelState.DISARMED,
+ "alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_CUSTOM_BYPASS
),
+ State("alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_HOME),
+ State("alarm_control_panel.entity_armed_vacation", STATE_ALARM_ARMED_NIGHT),
+ State("alarm_control_panel.entity_disarmed", STATE_ALARM_ARMED_VACATION),
+ State("alarm_control_panel.entity_triggered", STATE_ALARM_DISARMED),
# Should not raise
State("alarm_control_panel.non_existing", "on"),
],
diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py
index a41c2f47b2d..5acdbdb271a 100644
--- a/tests/components/alexa/test_capabilities.py
+++ b/tests/components/alexa/test_capabilities.py
@@ -5,7 +5,6 @@ from unittest.mock import patch
import pytest
-from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.components.alexa import smart_home
from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE,
@@ -24,6 +23,11 @@ from homeassistant.components.water_heater import (
)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS,
+ STATE_ALARM_ARMED_HOME,
+ STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED,
STATE_OFF,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
@@ -1347,23 +1351,15 @@ async def test_temperature_sensor_water_heater(hass: HomeAssistant) -> None:
async def test_report_alarm_control_panel_state(hass: HomeAssistant) -> None:
"""Test SecurityPanelController implements armState property."""
+ hass.states.async_set("alarm_control_panel.armed_away", STATE_ALARM_ARMED_AWAY, {})
hass.states.async_set(
- "alarm_control_panel.armed_away", AlarmControlPanelState.ARMED_AWAY, {}
+ "alarm_control_panel.armed_custom_bypass", STATE_ALARM_ARMED_CUSTOM_BYPASS, {}
)
+ hass.states.async_set("alarm_control_panel.armed_home", STATE_ALARM_ARMED_HOME, {})
hass.states.async_set(
- "alarm_control_panel.armed_custom_bypass",
- AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
- {},
- )
- hass.states.async_set(
- "alarm_control_panel.armed_home", AlarmControlPanelState.ARMED_HOME, {}
- )
- hass.states.async_set(
- "alarm_control_panel.armed_night", AlarmControlPanelState.ARMED_NIGHT, {}
- )
- hass.states.async_set(
- "alarm_control_panel.disarmed", AlarmControlPanelState.DISARMED, {}
+ "alarm_control_panel.armed_night", STATE_ALARM_ARMED_NIGHT, {}
)
+ hass.states.async_set("alarm_control_panel.disarmed", STATE_ALARM_DISARMED, {})
properties = await reported_properties(hass, "alarm_control_panel.armed_away")
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py
index 68010a6a711..6ccf265dcdc 100644
--- a/tests/components/alexa/test_smart_home.py
+++ b/tests/components/alexa/test_smart_home.py
@@ -12,6 +12,7 @@ from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature
from homeassistant.components.media_player import MediaPlayerEntityFeature
from homeassistant.components.vacuum import VacuumEntityFeature
from homeassistant.components.valve import SERVICE_STOP_VALVE, ValveEntityFeature
+from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
@@ -19,7 +20,6 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import Context, Event, HomeAssistant
-from homeassistant.core_config import async_process_ha_core_config
from homeassistant.helpers import entityfilter
from homeassistant.setup import async_setup_component
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
@@ -3999,108 +3999,6 @@ async def test_alarm_control_panel_code_arm_required(hass: HomeAssistant) -> Non
await discovery_test(device, hass, expected_endpoints=0)
-async def test_alarm_control_panel_disarm_required(hass: HomeAssistant) -> None:
- """Test alarm_control_panel disarm required."""
- device = (
- "alarm_control_panel.test_4",
- "armed_away",
- {
- "friendly_name": "Test Alarm Control Panel 4",
- "code_arm_required": False,
- "code_format": "FORMAT_NUMBER",
- "code": "1234",
- "supported_features": 3,
- },
- )
- appliance = await discovery_test(device, hass)
-
- assert appliance["endpointId"] == "alarm_control_panel#test_4"
- assert appliance["displayCategories"][0] == "SECURITY_PANEL"
- assert appliance["friendlyName"] == "Test Alarm Control Panel 4"
- assert_endpoint_capabilities(
- appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth", "Alexa"
- )
-
- properties = await reported_properties(hass, "alarm_control_panel#test_4")
- properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
-
- msg = await assert_request_fails(
- "Alexa.SecurityPanelController",
- "Arm",
- "alarm_control_panel#test_4",
- "alarm_control_panel.alarm_arm_home",
- hass,
- payload={"armState": "ARMED_STAY"},
- )
- assert msg["event"]["payload"]["type"] == "AUTHORIZATION_REQUIRED"
- assert (
- msg["event"]["payload"]["message"]
- == "You must disarm the system before you can set the requested arm state."
- )
-
- _, msg = await assert_request_calls_service(
- "Alexa.SecurityPanelController",
- "Arm",
- "alarm_control_panel#test_4",
- "alarm_control_panel.alarm_arm_away",
- hass,
- response_type="Arm.Response",
- payload={"armState": "ARMED_AWAY"},
- )
- properties = ReportedProperties(msg["context"]["properties"])
- properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
-
-
-async def test_alarm_control_panel_change_arm_type(hass: HomeAssistant) -> None:
- """Test alarm_control_panel change arm type."""
- device = (
- "alarm_control_panel.test_5",
- "armed_home",
- {
- "friendly_name": "Test Alarm Control Panel 5",
- "code_arm_required": False,
- "code_format": "FORMAT_NUMBER",
- "code": "1234",
- "supported_features": 3,
- },
- )
- appliance = await discovery_test(device, hass)
-
- assert appliance["endpointId"] == "alarm_control_panel#test_5"
- assert appliance["displayCategories"][0] == "SECURITY_PANEL"
- assert appliance["friendlyName"] == "Test Alarm Control Panel 5"
- assert_endpoint_capabilities(
- appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth", "Alexa"
- )
-
- properties = await reported_properties(hass, "alarm_control_panel#test_5")
- properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY")
-
- _, msg = await assert_request_calls_service(
- "Alexa.SecurityPanelController",
- "Arm",
- "alarm_control_panel#test_5",
- "alarm_control_panel.alarm_arm_home",
- hass,
- response_type="Arm.Response",
- payload={"armState": "ARMED_STAY"},
- )
- properties = ReportedProperties(msg["context"]["properties"])
- properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY")
-
- _, msg = await assert_request_calls_service(
- "Alexa.SecurityPanelController",
- "Arm",
- "alarm_control_panel#test_5",
- "alarm_control_panel.alarm_arm_away",
- hass,
- response_type="Arm.Response",
- payload={"armState": "ARMED_AWAY"},
- )
- properties = ReportedProperties(msg["context"]["properties"])
- properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
-
-
async def test_range_unsupported_domain(hass: HomeAssistant) -> None:
"""Test rangeController with unsupported domain."""
device = ("switch.test", "on", {"friendly_name": "Test switch"})
diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py
index ba7e46bdde7..5542aab4b30 100644
--- a/tests/components/analytics/test_analytics.py
+++ b/tests/components/analytics/test_analytics.py
@@ -76,7 +76,7 @@ async def test_no_send(
"""Test send when no preferences are defined."""
analytics = Analytics(hass)
with patch(
- "homeassistant.components.analytics.analytics.is_hassio",
+ "homeassistant.components.hassio.is_hassio",
side_effect=Mock(return_value=False),
):
assert not analytics.preferences[ATTR_BASE]
@@ -97,7 +97,7 @@ async def test_load_with_supervisor_diagnostics(hass: HomeAssistant) -> None:
side_effect=Mock(return_value={"diagnostics": True}),
),
patch(
- "homeassistant.components.analytics.analytics.is_hassio",
+ "homeassistant.components.hassio.is_hassio",
side_effect=Mock(return_value=True),
),
):
@@ -118,7 +118,7 @@ async def test_load_with_supervisor_without_diagnostics(hass: HomeAssistant) ->
side_effect=Mock(return_value={"diagnostics": False}),
),
patch(
- "homeassistant.components.analytics.analytics.is_hassio",
+ "homeassistant.components.hassio.is_hassio",
side_effect=Mock(return_value=True),
),
):
@@ -219,12 +219,8 @@ async def test_send_base_with_supervisor(
side_effect=Mock(return_value={}),
),
patch(
- "homeassistant.components.analytics.analytics.is_hassio",
+ "homeassistant.components.hassio.is_hassio",
side_effect=Mock(return_value=True),
- ) as is_hassio_mock,
- patch(
- "homeassistant.helpers.system_info.is_hassio",
- new=is_hassio_mock,
),
):
await analytics.load()
@@ -318,12 +314,8 @@ async def test_send_usage_with_supervisor(
side_effect=Mock(return_value={}),
),
patch(
- "homeassistant.components.analytics.analytics.is_hassio",
+ "homeassistant.components.hassio.is_hassio",
side_effect=Mock(return_value=True),
- ) as is_hassio_mock,
- patch(
- "homeassistant.helpers.system_info.is_hassio",
- new=is_hassio_mock,
),
):
await analytics.send_analytics()
@@ -537,12 +529,8 @@ async def test_send_statistics_with_supervisor(
side_effect=Mock(return_value={}),
),
patch(
- "homeassistant.components.analytics.analytics.is_hassio",
+ "homeassistant.components.hassio.is_hassio",
side_effect=Mock(return_value=True),
- ) as is_hassio_mock,
- patch(
- "homeassistant.helpers.system_info.is_hassio",
- new=is_hassio_mock,
),
):
await analytics.send_analytics()
diff --git a/tests/components/analytics_insights/conftest.py b/tests/components/analytics_insights/conftest.py
index a9c152b8ab9..fcdda95e9bd 100644
--- a/tests/components/analytics_insights/conftest.py
+++ b/tests/components/analytics_insights/conftest.py
@@ -5,10 +5,9 @@ from unittest.mock import AsyncMock, patch
import pytest
from python_homeassistant_analytics import CurrentAnalytics
-from python_homeassistant_analytics.models import Addon, CustomIntegration, Integration
+from python_homeassistant_analytics.models import CustomIntegration, Integration
from homeassistant.components.analytics_insights.const import (
- CONF_TRACKED_ADDONS,
CONF_TRACKED_CUSTOM_INTEGRATIONS,
CONF_TRACKED_INTEGRATIONS,
DOMAIN,
@@ -44,10 +43,6 @@ def mock_analytics_client() -> Generator[AsyncMock]:
client.get_current_analytics.return_value = CurrentAnalytics.from_json(
load_fixture("analytics_insights/current_data.json")
)
- addons = load_json_object_fixture("analytics_insights/addons.json")
- client.get_addons.return_value = {
- key: Addon.from_dict(value) for key, value in addons.items()
- }
integrations = load_json_object_fixture("analytics_insights/integrations.json")
client.get_integrations.return_value = {
key: Integration.from_dict(value) for key, value in integrations.items()
@@ -70,7 +65,6 @@ def mock_config_entry() -> MockConfigEntry:
title="Homeassistant Analytics",
data={},
options={
- CONF_TRACKED_ADDONS: ["core_samba"],
CONF_TRACKED_INTEGRATIONS: ["youtube", "spotify", "myq"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
diff --git a/tests/components/analytics_insights/fixtures/addons.json b/tests/components/analytics_insights/fixtures/addons.json
deleted file mode 100644
index cb7ae42c86b..00000000000
--- a/tests/components/analytics_insights/fixtures/addons.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "core_samba": {
- "total": 76357,
- "versions": {
- "12.3.2": 65875,
- "12.2.0": 1313,
- "12.3.1": 5018,
- "12.1.0": 211,
- "10.0.0": 1139,
- "9.4.0": 4,
- "12.3.0": 704,
- "9.3.1": 36,
- "10.0.2": 1290,
- "9.5.1": 379,
- "9.6.1": 66,
- "10.0.1": 200,
- "9.3.0": 20,
- "9.2.0": 9,
- "9.5.0": 13,
- "12.0.0": 39,
- "9.7.0": 20,
- "11.0.0": 13,
- "3.0": 1,
- "9.6.0": 2,
- "8.1": 2,
- "9.0": 3
- },
- "protected": 76345,
- "auto_update": 32732
- }
-}
diff --git a/tests/components/analytics_insights/snapshots/test_sensor.ambr b/tests/components/analytics_insights/snapshots/test_sensor.ambr
index 6e11b344b0b..1a8f4cec078 100644
--- a/tests/components/analytics_insights/snapshots/test_sensor.ambr
+++ b/tests/components/analytics_insights/snapshots/test_sensor.ambr
@@ -1,54 +1,4 @@
# serializer version: 1
-# name: test_all_entities[sensor.homeassistant_analytics_core_samba-entry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': dict({
- 'state_class': ,
- }),
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'sensor',
- 'entity_category': ,
- 'entity_id': 'sensor.homeassistant_analytics_core_samba',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'labels': set({
- }),
- 'name': None,
- 'options': dict({
- }),
- 'original_device_class': None,
- 'original_icon': None,
- 'original_name': 'core_samba',
- 'platform': 'analytics_insights',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': 'addons',
- 'unique_id': 'addon_core_samba_active_installations',
- 'unit_of_measurement': 'active installations',
- })
-# ---
-# name: test_all_entities[sensor.homeassistant_analytics_core_samba-state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'friendly_name': 'Homeassistant Analytics core_samba',
- 'state_class': ,
- 'unit_of_measurement': 'active installations',
- }),
- 'context': ,
- 'entity_id': 'sensor.homeassistant_analytics_core_samba',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': '76357',
- })
-# ---
# name: test_all_entities[sensor.homeassistant_analytics_hacs_custom-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -349,3 +299,103 @@
'state': '339',
})
# ---
+# name: test_all_entities[sensor.total_active_installations-entry]
+ EntityRegistryEntrySnapshot({
+ 'aliases': set({
+ }),
+ 'area_id': None,
+ 'capabilities': dict({
+ 'state_class': ,
+ }),
+ 'config_entry_id': ,
+ 'device_class': None,
+ 'device_id': ,
+ 'disabled_by': None,
+ 'domain': 'sensor',
+ 'entity_category': ,
+ 'entity_id': 'sensor.total_active_installations',
+ 'has_entity_name': True,
+ 'hidden_by': None,
+ 'icon': None,
+ 'id': ,
+ 'labels': set({
+ }),
+ 'name': None,
+ 'options': dict({
+ }),
+ 'original_device_class': None,
+ 'original_icon': None,
+ 'original_name': 'Total active installations',
+ 'platform': 'analytics_insights',
+ 'previous_unique_id': None,
+ 'supported_features': 0,
+ 'translation_key': 'total_active_installations',
+ 'unique_id': 'total_active_installations',
+ 'unit_of_measurement': 'active installations',
+ })
+# ---
+# name: test_all_entities[sensor.total_active_installations-state]
+ StateSnapshot({
+ 'attributes': ReadOnlyDict({
+ 'friendly_name': 'Homeassistant Analytics Total active installations',
+ 'state_class': ,
+ 'unit_of_measurement': 'active installations',
+ }),
+ 'context': ,
+ 'entity_id': 'sensor.total_active_installations',
+ 'last_changed': ,
+ 'last_reported': ,
+ 'last_updated': ,
+ 'state': '310400',
+ })
+# ---
+# name: test_all_entities[sensor.total_reports_integrations-entry]
+ EntityRegistryEntrySnapshot({
+ 'aliases': set({
+ }),
+ 'area_id': None,
+ 'capabilities': dict({
+ 'state_class': ,
+ }),
+ 'config_entry_id': ,
+ 'device_class': None,
+ 'device_id': ,
+ 'disabled_by': None,
+ 'domain': 'sensor',
+ 'entity_category': ,
+ 'entity_id': 'sensor.total_reports_integrations',
+ 'has_entity_name': True,
+ 'hidden_by': None,
+ 'icon': None,
+ 'id': ,
+ 'labels': set({
+ }),
+ 'name': None,
+ 'options': dict({
+ }),
+ 'original_device_class': None,
+ 'original_icon': None,
+ 'original_name': 'Total reported integrations',
+ 'platform': 'analytics_insights',
+ 'previous_unique_id': None,
+ 'supported_features': 0,
+ 'translation_key': 'total_reports_integrations',
+ 'unique_id': 'total_reports_integrations',
+ 'unit_of_measurement': 'active installations',
+ })
+# ---
+# name: test_all_entities[sensor.total_reports_integrations-state]
+ StateSnapshot({
+ 'attributes': ReadOnlyDict({
+ 'friendly_name': 'Homeassistant Analytics Total reported integrations',
+ 'state_class': ,
+ 'unit_of_measurement': 'active installations',
+ }),
+ 'context': ,
+ 'entity_id': 'sensor.total_reports_integrations',
+ 'last_changed': ,
+ 'last_reported': ,
+ 'last_updated': ,
+ 'state': '249256',
+ })
+# ---
diff --git a/tests/components/analytics_insights/test_config_flow.py b/tests/components/analytics_insights/test_config_flow.py
index 747f24930a4..0c9d4c074f8 100644
--- a/tests/components/analytics_insights/test_config_flow.py
+++ b/tests/components/analytics_insights/test_config_flow.py
@@ -7,7 +7,6 @@ import pytest
from python_homeassistant_analytics import HomeassistantAnalyticsConnectionError
from homeassistant.components.analytics_insights.const import (
- CONF_TRACKED_ADDONS,
CONF_TRACKED_CUSTOM_INTEGRATIONS,
CONF_TRACKED_INTEGRATIONS,
DOMAIN,
@@ -26,12 +25,10 @@ from tests.common import MockConfigEntry
[
(
{
- CONF_TRACKED_ADDONS: ["core_samba"],
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
{
- CONF_TRACKED_ADDONS: ["core_samba"],
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
@@ -41,7 +38,6 @@ from tests.common import MockConfigEntry
CONF_TRACKED_INTEGRATIONS: ["youtube"],
},
{
- CONF_TRACKED_ADDONS: [],
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: [],
},
@@ -51,7 +47,6 @@ from tests.common import MockConfigEntry
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
{
- CONF_TRACKED_ADDONS: [],
CONF_TRACKED_INTEGRATIONS: [],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
@@ -88,7 +83,6 @@ async def test_form(
"user_input",
[
{
- CONF_TRACKED_ADDONS: [],
CONF_TRACKED_INTEGRATIONS: [],
CONF_TRACKED_CUSTOM_INTEGRATIONS: [],
},
@@ -119,7 +113,6 @@ async def test_submitting_empty_form(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
- CONF_TRACKED_ADDONS: ["core_samba"],
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
@@ -130,7 +123,6 @@ async def test_submitting_empty_form(
assert result["title"] == "Home Assistant Analytics Insights"
assert result["data"] == {}
assert result["options"] == {
- CONF_TRACKED_ADDONS: ["core_samba"],
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
}
@@ -169,7 +161,6 @@ async def test_form_already_configured(
domain=DOMAIN,
data={},
options={
- CONF_TRACKED_ADDONS: [],
CONF_TRACKED_INTEGRATIONS: ["youtube", "spotify"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: [],
},
@@ -188,32 +179,19 @@ async def test_form_already_configured(
[
(
{
- CONF_TRACKED_ADDONS: ["core_samba"],
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
{
- CONF_TRACKED_ADDONS: ["core_samba"],
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
),
- (
- {
- CONF_TRACKED_ADDONS: ["core_samba"],
- },
- {
- CONF_TRACKED_ADDONS: ["core_samba"],
- CONF_TRACKED_INTEGRATIONS: [],
- CONF_TRACKED_CUSTOM_INTEGRATIONS: [],
- },
- ),
(
{
CONF_TRACKED_INTEGRATIONS: ["youtube"],
},
{
- CONF_TRACKED_ADDONS: [],
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: [],
},
@@ -223,7 +201,6 @@ async def test_form_already_configured(
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
{
- CONF_TRACKED_ADDONS: [],
CONF_TRACKED_INTEGRATIONS: [],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
@@ -260,7 +237,6 @@ async def test_options_flow(
"user_input",
[
{
- CONF_TRACKED_ADDONS: [],
CONF_TRACKED_INTEGRATIONS: [],
CONF_TRACKED_CUSTOM_INTEGRATIONS: [],
},
@@ -291,7 +267,6 @@ async def test_submitting_empty_options_flow(
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{
- CONF_TRACKED_ADDONS: ["core_samba"],
CONF_TRACKED_INTEGRATIONS: ["youtube", "hue"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
@@ -300,7 +275,6 @@ async def test_submitting_empty_options_flow(
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
- CONF_TRACKED_ADDONS: ["core_samba"],
CONF_TRACKED_INTEGRATIONS: ["youtube", "hue"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
}
diff --git a/tests/components/androidtv/common.py b/tests/components/androidtv/common.py
index 133f6b1470b..23e048e4d52 100644
--- a/tests/components/androidtv/common.py
+++ b/tests/components/androidtv/common.py
@@ -100,12 +100,7 @@ CONFIG_FIRETV_DEFAULT = CONFIG_FIRETV_PYTHON_ADB
def setup_mock_entry(
- config: dict[str, Any],
- entity_domain: str,
- *,
- options=None,
- version=1,
- minor_version=2,
+ config: dict[str, Any], entity_domain: str
) -> tuple[str, str, MockConfigEntry]:
"""Prepare mock entry for entities tests."""
patch_key = config[ADB_PATCH_KEY]
@@ -114,9 +109,6 @@ def setup_mock_entry(
domain=DOMAIN,
data=config[DOMAIN],
unique_id="a1:b1:c1:d1:e1:f1",
- options=options,
- version=version,
- minor_version=minor_version,
)
return patch_key, entity_id, config_entry
diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py
index cb1015e4198..b73fee9fb10 100644
--- a/tests/components/androidtv/test_config_flow.py
+++ b/tests/components/androidtv/test_config_flow.py
@@ -22,7 +22,7 @@ from homeassistant.components.androidtv.const import (
CONF_APPS,
CONF_EXCLUDE_UNNAMED_APPS,
CONF_GET_SOURCES,
- CONF_SCREENCAP_INTERVAL,
+ CONF_SCREENCAP,
CONF_STATE_DETECTION_RULES,
CONF_TURN_OFF_COMMAND,
CONF_TURN_ON_COMMAND,
@@ -501,7 +501,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
user_input={
CONF_GET_SOURCES: True,
CONF_EXCLUDE_UNNAMED_APPS: True,
- CONF_SCREENCAP_INTERVAL: 1,
+ CONF_SCREENCAP: True,
CONF_TURN_OFF_COMMAND: "off",
CONF_TURN_ON_COMMAND: "on",
},
@@ -515,6 +515,6 @@ async def test_options_flow(hass: HomeAssistant) -> None:
assert config_entry.options[CONF_GET_SOURCES] is True
assert config_entry.options[CONF_EXCLUDE_UNNAMED_APPS] is True
- assert config_entry.options[CONF_SCREENCAP_INTERVAL] == 1
+ assert config_entry.options[CONF_SCREENCAP] is True
assert config_entry.options[CONF_TURN_OFF_COMMAND] == "off"
assert config_entry.options[CONF_TURN_ON_COMMAND] == "on"
diff --git a/tests/components/androidtv/test_init.py b/tests/components/androidtv/test_init.py
deleted file mode 100644
index 8ff7df1668b..00000000000
--- a/tests/components/androidtv/test_init.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""Tests for AndroidTV integration initialization."""
-
-from homeassistant.components.androidtv.const import (
- CONF_SCREENCAP,
- CONF_SCREENCAP_INTERVAL,
-)
-from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
-from homeassistant.core import HomeAssistant
-
-from . import patchers
-from .common import CONFIG_ANDROID_DEFAULT, SHELL_RESPONSE_OFF, setup_mock_entry
-
-
-async def test_migrate_version(
- hass: HomeAssistant,
-) -> None:
- """Test migration to new version."""
- patch_key, _, mock_config_entry = setup_mock_entry(
- CONFIG_ANDROID_DEFAULT,
- MP_DOMAIN,
- options={CONF_SCREENCAP: False},
- minor_version=1,
- )
- mock_config_entry.add_to_hass(hass)
-
- with (
- patchers.patch_connect(True)[patch_key],
- patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key],
- ):
- assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
- await hass.async_block_till_done()
-
- assert mock_config_entry.options[CONF_SCREENCAP_INTERVAL] == 0
- assert mock_config_entry.minor_version == 2
diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py
index 5a8d88dd9f6..ef0d0c63b06 100644
--- a/tests/components/androidtv/test_media_player.py
+++ b/tests/components/androidtv/test_media_player.py
@@ -13,7 +13,7 @@ import pytest
from homeassistant.components.androidtv.const import (
CONF_APPS,
CONF_EXCLUDE_UNNAMED_APPS,
- CONF_SCREENCAP_INTERVAL,
+ CONF_SCREENCAP,
CONF_STATE_DETECTION_RULES,
CONF_TURN_OFF_COMMAND,
CONF_TURN_ON_COMMAND,
@@ -801,9 +801,6 @@ async def test_get_image_http(
"""
patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_DEFAULT)
config_entry.add_to_hass(hass)
- hass.config_entries.async_update_entry(
- config_entry, options={CONF_SCREENCAP_INTERVAL: 2}
- )
with (
patchers.patch_connect(True)[patch_key],
@@ -831,27 +828,21 @@ async def test_get_image_http(
content = await resp.read()
assert content == b"image"
- next_update = utcnow() + timedelta(minutes=1)
+ next_update = utcnow() + timedelta(seconds=30)
with (
patchers.patch_shell("11")[patch_key],
patchers.PATCH_SCREENCAP as patch_screen_cap,
- patch(
- "homeassistant.components.androidtv.media_player.utcnow",
- return_value=next_update,
- ),
+ patch("homeassistant.util.utcnow", return_value=next_update),
):
async_fire_time_changed(hass, next_update, True)
await hass.async_block_till_done()
patch_screen_cap.assert_not_called()
- next_update = utcnow() + timedelta(minutes=2)
+ next_update = utcnow() + timedelta(seconds=60)
with (
patchers.patch_shell("11")[patch_key],
patchers.PATCH_SCREENCAP as patch_screen_cap,
- patch(
- "homeassistant.components.androidtv.media_player.utcnow",
- return_value=next_update,
- ),
+ patch("homeassistant.util.utcnow", return_value=next_update),
):
async_fire_time_changed(hass, next_update, True)
await hass.async_block_till_done()
@@ -863,9 +854,6 @@ async def test_get_image_http_fail(hass: HomeAssistant) -> None:
patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_DEFAULT)
config_entry.add_to_hass(hass)
- hass.config_entries.async_update_entry(
- config_entry, options={CONF_SCREENCAP_INTERVAL: 2}
- )
with (
patchers.patch_connect(True)[patch_key],
@@ -897,7 +885,7 @@ async def test_get_image_disabled(hass: HomeAssistant) -> None:
patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_DEFAULT)
config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
- config_entry, options={CONF_SCREENCAP_INTERVAL: 0}
+ config_entry, options={CONF_SCREENCAP: False}
)
with (
@@ -1145,7 +1133,7 @@ async def test_options_reload(hass: HomeAssistant) -> None:
with patchers.PATCH_SETUP_ENTRY as setup_entry_call:
# change an option that not require integration reload
hass.config_entries.async_update_entry(
- config_entry, options={CONF_EXCLUDE_UNNAMED_APPS: True}
+ config_entry, options={CONF_SCREENCAP: False}
)
await hass.async_block_till_done()
diff --git a/tests/components/androidtv_remote/test_config_flow.py b/tests/components/androidtv_remote/test_config_flow.py
index 02e15bca415..93c9067d1c8 100644
--- a/tests/components/androidtv_remote/test_config_flow.py
+++ b/tests/components/androidtv_remote/test_config_flow.py
@@ -757,59 +757,6 @@ async def test_zeroconf_flow_abort_if_mac_is_missing(
assert result["reason"] == "cannot_connect"
-async def test_zeroconf_flow_already_configured_zeroconf_has_multiple_invalid_ip_addresses(
- hass: HomeAssistant,
- mock_setup_entry: AsyncMock,
- mock_unload_entry: AsyncMock,
- mock_api: MagicMock,
-) -> None:
- """Test we abort the zeroconf flow if already configured and zeroconf has invalid ip addresses."""
- host = "1.2.3.4"
- name = "My Android TV"
- mac = "1A:2B:3C:4D:5E:6F"
- unique_id = "1a:2b:3c:4d:5e:6f"
- name_existing = name
- host_existing = host
-
- mock_config_entry = MockConfigEntry(
- title=name,
- domain=DOMAIN,
- data={
- "host": host_existing,
- "name": name_existing,
- "mac": mac,
- },
- unique_id=unique_id,
- state=ConfigEntryState.LOADED,
- )
- mock_config_entry.add_to_hass(hass)
-
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": config_entries.SOURCE_ZEROCONF},
- data=zeroconf.ZeroconfServiceInfo(
- ip_address=ip_address("1.2.3.5"),
- ip_addresses=[ip_address("1.2.3.5"), ip_address(host)],
- port=6466,
- hostname=host,
- type="mock_type",
- name=name + "._androidtvremote2._tcp.local.",
- properties={"bt": mac},
- ),
- )
- assert result["type"] is FlowResultType.ABORT
- assert result["reason"] == "already_configured"
-
- await hass.async_block_till_done()
- assert hass.config_entries.async_entries(DOMAIN)[0].data == {
- "host": host,
- "name": name,
- "mac": mac,
- }
- assert len(mock_unload_entry.mock_calls) == 0
- assert len(mock_setup_entry.mock_calls) == 0
-
-
async def test_reauth_flow_success(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
diff --git a/tests/components/anova/__init__.py b/tests/components/anova/__init__.py
index 903a1180980..887f5b3b05b 100644
--- a/tests/components/anova/__init__.py
+++ b/tests/components/anova/__init__.py
@@ -36,7 +36,6 @@ def create_entry(hass: HomeAssistant, device_id: str = DEVICE_UNIQUE_ID) -> Conf
},
unique_id="sample@gmail.com",
version=1,
- minor_version=2,
)
entry.add_to_hass(hass)
return entry
diff --git a/tests/components/anova/test_config_flow.py b/tests/components/anova/test_config_flow.py
index 3b2afaa49c0..0f93b869296 100644
--- a/tests/components/anova/test_config_flow.py
+++ b/tests/components/anova/test_config_flow.py
@@ -6,7 +6,7 @@ from anova_wifi import AnovaApi, InvalidLogin
from homeassistant import config_entries
from homeassistant.components.anova.const import DOMAIN
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
@@ -27,6 +27,7 @@ async def test_flow_user(hass: HomeAssistant, anova_api: AnovaApi) -> None:
assert result["data"] == {
CONF_USERNAME: "sample@gmail.com",
CONF_PASSWORD: "sample",
+ CONF_DEVICES: [],
}
diff --git a/tests/components/anova/test_init.py b/tests/components/anova/test_init.py
index 2e3e2920abc..66ea11fdaef 100644
--- a/tests/components/anova/test_init.py
+++ b/tests/components/anova/test_init.py
@@ -1,18 +1,13 @@
"""Test init for Anova."""
-from unittest.mock import patch
-
from anova_wifi import AnovaApi
from homeassistant.components.anova.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
-from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from . import async_init_integration, create_entry
-from tests.common import MockConfigEntry
-
async def test_async_setup_entry(hass: HomeAssistant, anova_api: AnovaApi) -> None:
"""Test a successful setup entry."""
@@ -60,34 +55,3 @@ async def test_websocket_failure(
"""Test that we successfully handle a websocket failure on setup."""
entry = await async_init_integration(hass)
assert entry.state is ConfigEntryState.SETUP_RETRY
-
-
-async def test_migration_removing_devices_in_config_entry(
- hass: HomeAssistant, anova_api: AnovaApi
-) -> None:
- """Test a successful setup entry."""
- entry = MockConfigEntry(
- domain=DOMAIN,
- title="Anova",
- data={
- CONF_USERNAME: "sample@gmail.com",
- CONF_PASSWORD: "sample",
- CONF_DEVICES: [],
- },
- unique_id="sample@gmail.com",
- version=1,
- minor_version=1,
- )
- entry.add_to_hass(hass)
-
- with patch("homeassistant.components.anova.AnovaApi.authenticate"):
- await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
- state = hass.states.get("sensor.anova_precision_cooker_mode")
- assert state is not None
- assert state.state == "idle"
-
- assert entry.version == 1
- assert entry.minor_version == 2
- assert CONF_DEVICES not in entry.data
diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py
index 4567bd32582..f37042a6f50 100644
--- a/tests/components/apple_tv/test_config_flow.py
+++ b/tests/components/apple_tv/test_config_flow.py
@@ -16,7 +16,6 @@ from homeassistant.components.apple_tv.const import (
CONF_START_OFF,
DOMAIN,
)
-from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
@@ -1190,17 +1189,18 @@ async def test_reconfigure_update_credentials(hass: HomeAssistant) -> None:
)
config_entry.add_to_hass(hass)
- result = await config_entry.start_reauth_flow(hass, data={"name": "apple tv"})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "reauth"},
+ data={"identifier": "mrpid", "name": "apple tv"},
+ )
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
assert result2["type"] is FlowResultType.FORM
- assert result2["description_placeholders"] == {
- CONF_NAME: "Mock Title",
- "protocol": "MRP",
- }
+ assert result2["description_placeholders"] == {"protocol": "MRP"}
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"], {"pin": 1111}
diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py
index b72d9653c2d..d90084fa7c9 100644
--- a/tests/components/application_credentials/test_init.py
+++ b/tests/components/application_credentials/test_init.py
@@ -423,10 +423,6 @@ async def test_import_named_credential(
]
-@pytest.mark.parametrize(
- "ignore_translations",
- ["component.fake_integration.config.abort.missing_credentials"],
-)
async def test_config_flow_no_credentials(hass: HomeAssistant) -> None:
"""Test config flow base case with no credentials registered."""
result = await hass.config_entries.flow.async_init(
@@ -436,10 +432,6 @@ async def test_config_flow_no_credentials(hass: HomeAssistant) -> None:
assert result.get("reason") == "missing_credentials"
-@pytest.mark.parametrize(
- "ignore_translations",
- ["component.fake_integration.config.abort.missing_credentials"],
-)
async def test_config_flow_other_domain(
hass: HomeAssistant,
ws_client: ClientFixture,
@@ -567,10 +559,6 @@ async def test_config_flow_multiple_entries(
)
-@pytest.mark.parametrize(
- "ignore_translations",
- ["component.fake_integration.config.abort.missing_credentials"],
-)
async def test_config_flow_create_delete_credential(
hass: HomeAssistant,
ws_client: ClientFixture,
@@ -616,10 +604,6 @@ async def test_config_flow_with_config_credential(
assert result["data"].get("auth_implementation") == TEST_DOMAIN
-@pytest.mark.parametrize(
- "ignore_translations",
- ["component.fake_integration.config.abort.missing_configuration"],
-)
@pytest.mark.parametrize("mock_application_credentials_integration", [None])
async def test_import_without_setup(hass: HomeAssistant, config_credential) -> None:
"""Test import of credentials without setting up the integration."""
@@ -635,10 +619,6 @@ async def test_import_without_setup(hass: HomeAssistant, config_credential) -> N
assert result.get("reason") == "missing_configuration"
-@pytest.mark.parametrize(
- "ignore_translations",
- ["component.fake_integration.config.abort.missing_configuration"],
-)
@pytest.mark.parametrize("mock_application_credentials_integration", [None])
async def test_websocket_without_platform(
hass: HomeAssistant, ws_client: ClientFixture
diff --git a/tests/components/aseko_pool_live/test_config_flow.py b/tests/components/aseko_pool_live/test_config_flow.py
index b307f00abbe..eb40decf213 100644
--- a/tests/components/aseko_pool_live/test_config_flow.py
+++ b/tests/components/aseko_pool_live/test_config_flow.py
@@ -128,9 +128,8 @@ async def test_async_step_reauth_success(hass: HomeAssistant, user: User) -> Non
mock_entry = MockConfigEntry(
domain=DOMAIN,
- unique_id="a_user_id",
- data={CONF_EMAIL: "aseko@example.com", CONF_PASSWORD: "passw0rd"},
- version=2,
+ unique_id="UID",
+ data={CONF_EMAIL: "aseko@example.com"},
)
mock_entry.add_to_hass(hass)
@@ -152,61 +151,13 @@ async def test_async_step_reauth_success(hass: HomeAssistant, user: User) -> Non
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- {CONF_EMAIL: "aseko@example.com", CONF_PASSWORD: "new_password"},
+ {CONF_EMAIL: "aseko@example.com", CONF_PASSWORD: "passw0rd"},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert len(mock_setup_entry.mock_calls) == 1
- assert mock_entry.unique_id == "a_user_id"
- assert dict(mock_entry.data) == {
- CONF_EMAIL: "aseko@example.com",
- CONF_PASSWORD: "new_password",
- }
-
-
-async def test_async_step_reauth_mismatch(hass: HomeAssistant, user: User) -> None:
- """Test mismatch reauthentication."""
-
- mock_entry = MockConfigEntry(
- domain=DOMAIN,
- unique_id="UID",
- data={CONF_EMAIL: "aseko@example.com", CONF_PASSWORD: "passw0rd"},
- version=2,
- )
- mock_entry.add_to_hass(hass)
-
- result = await mock_entry.start_reauth_flow(hass)
-
- assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reauth_confirm"
- assert result["errors"] == {}
-
- with (
- patch(
- "homeassistant.components.aseko_pool_live.config_flow.Aseko.login",
- return_value=user,
- ),
- patch(
- "homeassistant.components.aseko_pool_live.async_setup_entry",
- return_value=True,
- ) as mock_setup_entry,
- ):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- {CONF_EMAIL: "aseko@example.com", CONF_PASSWORD: "new_password"},
- )
- await hass.async_block_till_done()
-
- assert result["type"] is FlowResultType.ABORT
- assert result["reason"] == "unique_id_mismatch"
- assert len(mock_setup_entry.mock_calls) == 0
- assert mock_entry.unique_id == "UID"
- assert dict(mock_entry.data) == {
- CONF_EMAIL: "aseko@example.com",
- CONF_PASSWORD: "passw0rd",
- }
@pytest.mark.parametrize(
diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr
index b806c6faf23..131444c17ac 100644
--- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr
+++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr
@@ -697,7 +697,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
- 'speech': 'Sorry, I am not aware of any area called Are',
+ 'speech': 'Sorry, I am not aware of any area called are',
}),
}),
}),
@@ -741,7 +741,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
- 'speech': 'Sorry, I am not aware of any area called Are',
+ 'speech': 'Sorry, I am not aware of any area called are',
}),
}),
}),
diff --git a/tests/components/assist_pipeline/test_vad.py b/tests/components/assist_pipeline/test_vad.py
index bd07601cd5d..fda26d2fb94 100644
--- a/tests/components/assist_pipeline/test_vad.py
+++ b/tests/components/assist_pipeline/test_vad.py
@@ -16,7 +16,7 @@ def test_silence() -> None:
segmenter = VoiceCommandSegmenter()
# True return value indicates voice command has not finished
- assert segmenter.process(_ONE_SECOND * 3, 0.0)
+ assert segmenter.process(_ONE_SECOND * 3, False)
assert not segmenter.in_command
@@ -26,15 +26,15 @@ def test_speech() -> None:
segmenter = VoiceCommandSegmenter()
# silence
- assert segmenter.process(_ONE_SECOND, 0.0)
+ assert segmenter.process(_ONE_SECOND, False)
# "speech"
- assert segmenter.process(_ONE_SECOND, 1.0)
+ assert segmenter.process(_ONE_SECOND, True)
assert segmenter.in_command
# silence
# False return value indicates voice command is finished
- assert not segmenter.process(_ONE_SECOND, 0.0)
+ assert not segmenter.process(_ONE_SECOND, False)
assert not segmenter.in_command
@@ -112,19 +112,19 @@ def test_silence_seconds() -> None:
segmenter = VoiceCommandSegmenter(silence_seconds=1.0)
# silence
- assert segmenter.process(_ONE_SECOND, 0.0)
+ assert segmenter.process(_ONE_SECOND, False)
assert not segmenter.in_command
# "speech"
- assert segmenter.process(_ONE_SECOND, 1.0)
+ assert segmenter.process(_ONE_SECOND, True)
assert segmenter.in_command
# not enough silence to end
- assert segmenter.process(_ONE_SECOND * 0.5, 0.0)
+ assert segmenter.process(_ONE_SECOND * 0.5, False)
assert segmenter.in_command
# exactly enough silence now
- assert not segmenter.process(_ONE_SECOND * 0.5, 0.0)
+ assert not segmenter.process(_ONE_SECOND * 0.5, False)
assert not segmenter.in_command
@@ -134,27 +134,27 @@ def test_silence_reset() -> None:
segmenter = VoiceCommandSegmenter(silence_seconds=1.0, reset_seconds=0.5)
# silence
- assert segmenter.process(_ONE_SECOND, 0.0)
+ assert segmenter.process(_ONE_SECOND, False)
assert not segmenter.in_command
# "speech"
- assert segmenter.process(_ONE_SECOND, 1.0)
+ assert segmenter.process(_ONE_SECOND, True)
assert segmenter.in_command
# not enough silence to end
- assert segmenter.process(_ONE_SECOND * 0.5, 0.0)
+ assert segmenter.process(_ONE_SECOND * 0.5, False)
assert segmenter.in_command
# speech should reset silence detection
- assert segmenter.process(_ONE_SECOND * 0.5, 1.0)
+ assert segmenter.process(_ONE_SECOND * 0.5, True)
assert segmenter.in_command
# not enough silence to end
- assert segmenter.process(_ONE_SECOND * 0.5, 0.0)
+ assert segmenter.process(_ONE_SECOND * 0.5, False)
assert segmenter.in_command
# exactly enough silence now
- assert not segmenter.process(_ONE_SECOND * 0.5, 0.0)
+ assert not segmenter.process(_ONE_SECOND * 0.5, False)
assert not segmenter.in_command
@@ -166,23 +166,23 @@ def test_speech_reset() -> None:
)
# silence
- assert segmenter.process(_ONE_SECOND, 0.0)
+ assert segmenter.process(_ONE_SECOND, False)
assert not segmenter.in_command
# not enough speech to start voice command
- assert segmenter.process(_ONE_SECOND * 0.5, 1.0)
+ assert segmenter.process(_ONE_SECOND * 0.5, True)
assert not segmenter.in_command
# silence should reset speech detection
- assert segmenter.process(_ONE_SECOND, 0.0)
+ assert segmenter.process(_ONE_SECOND, False)
assert not segmenter.in_command
# not enough speech to start voice command
- assert segmenter.process(_ONE_SECOND * 0.5, 1.0)
+ assert segmenter.process(_ONE_SECOND * 0.5, True)
assert not segmenter.in_command
# exactly enough speech now
- assert segmenter.process(_ONE_SECOND * 0.5, 1.0)
+ assert segmenter.process(_ONE_SECOND * 0.5, True)
assert segmenter.in_command
@@ -193,18 +193,18 @@ def test_timeout() -> None:
# not enough to time out
assert not segmenter.timed_out
- assert segmenter.process(_ONE_SECOND * 0.5, 0.0)
+ assert segmenter.process(_ONE_SECOND * 0.5, False)
assert not segmenter.timed_out
# enough to time out
- assert not segmenter.process(_ONE_SECOND * 0.5, 1.0)
+ assert not segmenter.process(_ONE_SECOND * 0.5, True)
assert segmenter.timed_out
# flag resets with more audio
- assert segmenter.process(_ONE_SECOND * 0.5, 1.0)
+ assert segmenter.process(_ONE_SECOND * 0.5, True)
assert not segmenter.timed_out
- assert not segmenter.process(_ONE_SECOND * 0.5, 0.0)
+ assert not segmenter.process(_ONE_SECOND * 0.5, False)
assert segmenter.timed_out
@@ -215,38 +215,14 @@ def test_command_seconds() -> None:
command_seconds=3, speech_seconds=1, silence_seconds=1, reset_seconds=1
)
- assert segmenter.process(_ONE_SECOND, 1.0)
+ assert segmenter.process(_ONE_SECOND, True)
# Silence counts towards total command length
- assert segmenter.process(_ONE_SECOND * 0.5, 0.0)
+ assert segmenter.process(_ONE_SECOND * 0.5, False)
# Enough to finish command now
- assert segmenter.process(_ONE_SECOND, 1.0)
- assert segmenter.process(_ONE_SECOND * 0.5, 0.0)
+ assert segmenter.process(_ONE_SECOND, True)
+ assert segmenter.process(_ONE_SECOND * 0.5, False)
# Silence to finish
- assert not segmenter.process(_ONE_SECOND * 0.5, 0.0)
-
-
-def test_speech_thresholds() -> None:
- """Test before/in command speech thresholds."""
-
- segmenter = VoiceCommandSegmenter(
- before_command_speech_threshold=0.2,
- in_command_speech_threshold=0.5,
- command_seconds=2,
- speech_seconds=1,
- silence_seconds=1,
- )
-
- # Not high enough probability to trigger command
- assert segmenter.process(_ONE_SECOND, 0.1)
- assert not segmenter.in_command
-
- # Triggers command
- assert segmenter.process(_ONE_SECOND, 0.3)
- assert segmenter.in_command
-
- # Now that same probability is considered silence.
- # Finishes command.
- assert not segmenter.process(_ONE_SECOND, 0.3)
+ assert not segmenter.process(_ONE_SECOND * 0.5, False)
diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py
index b27f20e83f3..ac17cf41448 100644
--- a/tests/components/awair/test_config_flow.py
+++ b/tests/components/awair/test_config_flow.py
@@ -144,32 +144,27 @@ async def test_reauth(hass: HomeAssistant, user, cloud_devices) -> None:
with patch("python_awair.AwairClient.query", side_effect=AuthError()):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- user_input={CONF_ACCESS_TOKEN: "bad"},
+ user_input=CLOUD_CONFIG,
)
- assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reauth_confirm"
- assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"}
+ assert result["type"] is FlowResultType.FORM
+ assert result["step_id"] == "reauth_confirm"
+ assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"}
with (
patch(
"python_awair.AwairClient.query",
side_effect=[user, cloud_devices],
),
- patch(
- "homeassistant.components.awair.async_setup_entry", return_value=True
- ) as mock_setup_entry,
+ patch("homeassistant.components.awair.async_setup_entry", return_value=True),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- user_input={CONF_ACCESS_TOKEN: "good"},
+ user_input=CLOUD_CONFIG,
)
- await hass.async_block_till_done()
- assert result["type"] is FlowResultType.ABORT
- assert result["reason"] == "reauth_successful"
- mock_setup_entry.assert_called_once()
- assert dict(mock_config.data) == {CONF_ACCESS_TOKEN: "good"}
+ assert result["type"] is FlowResultType.ABORT
+ assert result["reason"] == "reauth_successful"
async def test_reauth_error(hass: HomeAssistant) -> None:
@@ -400,6 +395,10 @@ async def test_zeroconf_discovery_update_configuration(
return_value=True,
) as mock_setup_entry,
patch("python_awair.AwairClient.query", side_effect=[local_devices]),
+ patch(
+ "homeassistant.components.awair.async_setup_entry",
+ return_value=True,
+ ),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py
index 52dd9c2f8ad..c8ffc46ca3f 100644
--- a/tests/components/axis/test_config_flow.py
+++ b/tests/components/axis/test_config_flow.py
@@ -75,7 +75,7 @@ async def test_flow_manual_configuration(hass: HomeAssistant) -> None:
}
-async def test_manual_configuration_duplicate_fails(
+async def test_manual_configuration_update_configuration(
hass: HomeAssistant,
config_entry_setup: MockConfigEntry,
mock_requests: Callable[[str], None],
@@ -105,7 +105,7 @@ async def test_manual_configuration_duplicate_fails(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
- assert config_entry_setup.data[CONF_HOST] == "1.2.3.4"
+ assert config_entry_setup.data[CONF_HOST] == "2.3.4.5"
@pytest.mark.parametrize(
@@ -221,7 +221,7 @@ async def test_reauth_flow_update_configuration(
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT
- assert result["reason"] == "reauth_successful"
+ assert result["reason"] == "already_configured"
assert config_entry_setup.data[CONF_PROTOCOL] == "https"
assert config_entry_setup.data[CONF_HOST] == "2.3.4.5"
assert config_entry_setup.data[CONF_PORT] == 443
@@ -255,7 +255,7 @@ async def test_reconfiguration_flow_update_configuration(
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT
- assert result["reason"] == "reconfigure_successful"
+ assert result["reason"] == "already_configured"
assert config_entry_setup.data[CONF_PROTOCOL] == "http"
assert config_entry_setup.data[CONF_HOST] == "2.3.4.5"
assert config_entry_setup.data[CONF_PORT] == 80
diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py
index 64c771a7adc..9ebc9991939 100644
--- a/tests/components/azure_devops/test_config_flow.py
+++ b/tests/components/azure_devops/test_config_flow.py
@@ -57,13 +57,12 @@ async def test_reauth_authorization_error(
mock_devops_client: AsyncMock,
) -> None:
"""Test we show user form on Azure DevOps authorization error."""
- mock_config_entry.add_to_hass(hass)
mock_devops_client.authorize.return_value = False
mock_devops_client.authorized = False
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reauth_confirm"
+ assert result["step_id"] == "reauth"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -72,7 +71,7 @@ async def test_reauth_authorization_error(
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.FORM
- assert result2["step_id"] == "reauth_confirm"
+ assert result2["step_id"] == "reauth"
assert result2["errors"] == {"base": "invalid_auth"}
@@ -109,14 +108,13 @@ async def test_reauth_connection_error(
mock_devops_client: AsyncMock,
) -> None:
"""Test we show user form on Azure DevOps connection error."""
- mock_config_entry.add_to_hass(hass)
mock_devops_client.authorize.side_effect = aiohttp.ClientError
mock_devops_client.authorized = False
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reauth_confirm"
+ assert result["step_id"] == "reauth"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -125,7 +123,7 @@ async def test_reauth_connection_error(
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.FORM
- assert result2["step_id"] == "reauth_confirm"
+ assert result2["step_id"] == "reauth"
assert result2["errors"] == {"base": "cannot_connect"}
@@ -172,7 +170,7 @@ async def test_reauth_project_error(
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reauth_confirm"
+ assert result["step_id"] == "reauth"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -181,7 +179,7 @@ async def test_reauth_project_error(
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.FORM
- assert result2["step_id"] == "reauth_confirm"
+ assert result2["step_id"] == "reauth"
assert result2["errors"] == {"base": "project_error"}
@@ -199,7 +197,8 @@ async def test_reauth_flow(
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reauth_confirm"
+ assert result["step_id"] == "reauth"
+ assert result["errors"] == {"base": "invalid_auth"}
mock_devops_client.authorize.return_value = True
mock_devops_client.authorized = True
diff --git a/tests/components/backup/conftest.py b/tests/components/backup/conftest.py
deleted file mode 100644
index 631c774e63c..00000000000
--- a/tests/components/backup/conftest.py
+++ /dev/null
@@ -1,73 +0,0 @@
-"""Test fixtures for the Backup integration."""
-
-from __future__ import annotations
-
-from collections.abc import Generator
-from pathlib import Path
-from unittest.mock import MagicMock, Mock, patch
-
-import pytest
-
-from homeassistant.core import HomeAssistant
-
-
-@pytest.fixture(name="mocked_json_bytes")
-def mocked_json_bytes_fixture() -> Generator[Mock]:
- """Mock json_bytes."""
- with patch(
- "homeassistant.components.backup.manager.json_bytes",
- return_value=b"{}", # Empty JSON
- ) as mocked_json_bytes:
- yield mocked_json_bytes
-
-
-@pytest.fixture(name="mocked_tarfile")
-def mocked_tarfile_fixture() -> Generator[Mock]:
- """Mock tarfile."""
- with patch(
- "homeassistant.components.backup.manager.SecureTarFile"
- ) as mocked_tarfile:
- yield mocked_tarfile
-
-
-@pytest.fixture(name="mock_backup_generation")
-def mock_backup_generation_fixture(
- hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock
-) -> Generator[None]:
- """Mock backup generator."""
-
- def _mock_iterdir(path: Path) -> list[Path]:
- if not path.name.endswith("testing_config"):
- return []
- return [
- Path("test.txt"),
- Path(".DS_Store"),
- Path(".storage"),
- ]
-
- with (
- patch("pathlib.Path.iterdir", _mock_iterdir),
- patch("pathlib.Path.stat", MagicMock(st_size=123)),
- patch("pathlib.Path.is_file", lambda x: x.name != ".storage"),
- patch(
- "pathlib.Path.is_dir",
- lambda x: x.name == ".storage",
- ),
- patch(
- "pathlib.Path.exists",
- lambda x: x != Path(hass.config.path("backups")),
- ),
- patch(
- "pathlib.Path.is_symlink",
- lambda _: False,
- ),
- patch(
- "pathlib.Path.mkdir",
- MagicMock(),
- ),
- patch(
- "homeassistant.components.backup.manager.HAVERSION",
- "2025.1.0",
- ),
- ):
- yield
diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr
index 42eb524e529..a1d83f5cd75 100644
--- a/tests/components/backup/snapshots/test_websocket.ambr
+++ b/tests/components/backup/snapshots/test_websocket.ambr
@@ -147,54 +147,6 @@
'type': 'result',
})
# ---
-# name: test_details[with_hassio-with_backup_content]
- dict({
- 'error': dict({
- 'code': 'unknown_command',
- 'message': 'Unknown command.',
- }),
- 'id': 1,
- 'success': False,
- 'type': 'result',
- })
-# ---
-# name: test_details[with_hassio-without_backup_content]
- dict({
- 'error': dict({
- 'code': 'unknown_command',
- 'message': 'Unknown command.',
- }),
- 'id': 1,
- 'success': False,
- 'type': 'result',
- })
-# ---
-# name: test_details[without_hassio-with_backup_content]
- dict({
- 'id': 1,
- 'result': dict({
- 'backup': dict({
- 'date': '1970-01-01T00:00:00.000Z',
- 'name': 'Test',
- 'path': 'abc123.tar',
- 'size': 0.0,
- 'slug': 'abc123',
- }),
- }),
- 'success': True,
- 'type': 'result',
- })
-# ---
-# name: test_details[without_hassio-without_backup_content]
- dict({
- 'id': 1,
- 'result': dict({
- 'backup': None,
- }),
- 'success': True,
- 'type': 'result',
- })
-# ---
# name: test_generate[with_hassio]
dict({
'error': dict({
@@ -210,23 +162,16 @@
dict({
'id': 1,
'result': dict({
- 'slug': '27f5c632',
+ 'date': '1970-01-01T00:00:00.000Z',
+ 'name': 'Test',
+ 'path': 'abc123.tar',
+ 'size': 0.0,
+ 'slug': 'abc123',
}),
'success': True,
'type': 'result',
})
# ---
-# name: test_generate[without_hassio].1
- dict({
- 'event': dict({
- 'done': True,
- 'stage': None,
- 'success': True,
- }),
- 'id': 1,
- 'type': 'event',
- })
-# ---
# name: test_info[with_hassio]
dict({
'error': dict({
@@ -276,22 +221,3 @@
'type': 'result',
})
# ---
-# name: test_restore[with_hassio]
- dict({
- 'error': dict({
- 'code': 'unknown_command',
- 'message': 'Unknown command.',
- }),
- 'id': 1,
- 'success': False,
- 'type': 'result',
- })
-# ---
-# name: test_restore[without_hassio]
- dict({
- 'id': 1,
- 'result': None,
- 'success': True,
- 'type': 'result',
- })
-# ---
diff --git a/tests/components/backup/test_http.py b/tests/components/backup/test_http.py
index 76b1f76b55b..b4d9c52d055 100644
--- a/tests/components/backup/test_http.py
+++ b/tests/components/backup/test_http.py
@@ -1,11 +1,8 @@
"""Tests for the Backup integration."""
-import asyncio
-from io import StringIO
from unittest.mock import patch
from aiohttp import web
-import pytest
from homeassistant.core import HomeAssistant
@@ -26,7 +23,7 @@ async def test_downloading_backup(
with (
patch(
- "homeassistant.components.backup.manager.BackupManager.async_get_backup",
+ "homeassistant.components.backup.manager.BackupManager.get_backup",
return_value=TEST_BACKUP,
),
patch("pathlib.Path.exists", return_value=True),
@@ -52,12 +49,12 @@ async def test_downloading_backup_not_found(
assert resp.status == 404
-async def test_downloading_as_non_admin(
+async def test_non_admin(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
) -> None:
- """Test downloading a backup file when you are not an admin."""
+ """Test downloading a backup file that does not exist."""
hass_admin_user.groups = []
await setup_backup_integration(hass)
@@ -65,53 +62,3 @@ async def test_downloading_as_non_admin(
resp = await client.get("/api/backup/download/abc123")
assert resp.status == 401
-
-
-async def test_uploading_a_backup_file(
- hass: HomeAssistant,
- hass_client: ClientSessionGenerator,
-) -> None:
- """Test uploading a backup file."""
- await setup_backup_integration(hass)
-
- client = await hass_client()
-
- with patch(
- "homeassistant.components.backup.manager.BackupManager.async_receive_backup",
- ) as async_receive_backup_mock:
- resp = await client.post(
- "/api/backup/upload",
- data={"file": StringIO("test")},
- )
- assert resp.status == 201
- assert async_receive_backup_mock.called
-
-
-@pytest.mark.parametrize(
- ("error", "message"),
- [
- (OSError("Boom!"), "Can't write backup file Boom!"),
- (asyncio.CancelledError("Boom!"), ""),
- ],
-)
-async def test_error_handling_uploading_a_backup_file(
- hass: HomeAssistant,
- hass_client: ClientSessionGenerator,
- error: Exception,
- message: str,
-) -> None:
- """Test error handling when uploading a backup file."""
- await setup_backup_integration(hass)
-
- client = await hass_client()
-
- with patch(
- "homeassistant.components.backup.manager.BackupManager.async_receive_backup",
- side_effect=error,
- ):
- resp = await client.post(
- "/api/backup/upload",
- data={"file": StringIO("test")},
- )
- assert resp.status == 500
- assert await resp.text() == message
diff --git a/tests/components/backup/test_init.py b/tests/components/backup/test_init.py
index e064939d618..0472111e33e 100644
--- a/tests/components/backup/test_init.py
+++ b/tests/components/backup/test_init.py
@@ -33,7 +33,7 @@ async def test_create_service(
await setup_backup_integration(hass)
with patch(
- "homeassistant.components.backup.manager.BackupManager.async_create_backup",
+ "homeassistant.components.backup.manager.BackupManager.generate_backup",
) as generate_backup:
await hass.services.async_call(
DOMAIN,
diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py
index 9d24964aedf..41749298819 100644
--- a/tests/components/backup/test_manager.py
+++ b/tests/components/backup/test_manager.py
@@ -2,18 +2,13 @@
from __future__ import annotations
-import asyncio
-from unittest.mock import AsyncMock, MagicMock, Mock, mock_open, patch
+from pathlib import Path
+from unittest.mock import AsyncMock, MagicMock, Mock, patch
-import aiohttp
-from multidict import CIMultiDict, CIMultiDictProxy
import pytest
from homeassistant.components.backup import BackupManager
-from homeassistant.components.backup.manager import (
- BackupPlatformProtocol,
- BackupProgress,
-)
+from homeassistant.components.backup.manager import BackupPlatformProtocol
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
@@ -23,30 +18,59 @@ from .common import TEST_BACKUP
from tests.common import MockPlatform, mock_platform
-async def _mock_backup_generation(
- manager: BackupManager, mocked_json_bytes: Mock, mocked_tarfile: Mock
-) -> None:
+async def _mock_backup_generation(manager: BackupManager):
"""Mock backup generator."""
- progress: list[BackupProgress] = []
+ def _mock_iterdir(path: Path) -> list[Path]:
+ if not path.name.endswith("testing_config"):
+ return []
+ return [
+ Path("test.txt"),
+ Path(".DS_Store"),
+ Path(".storage"),
+ ]
- def on_progress(_progress: BackupProgress) -> None:
- """Mock progress callback."""
- progress.append(_progress)
+ with (
+ patch(
+ "homeassistant.components.backup.manager.SecureTarFile"
+ ) as mocked_tarfile,
+ patch("pathlib.Path.iterdir", _mock_iterdir),
+ patch("pathlib.Path.stat", MagicMock(st_size=123)),
+ patch("pathlib.Path.is_file", lambda x: x.name != ".storage"),
+ patch(
+ "pathlib.Path.is_dir",
+ lambda x: x.name == ".storage",
+ ),
+ patch(
+ "pathlib.Path.exists",
+ lambda x: x != manager.backup_dir,
+ ),
+ patch(
+ "pathlib.Path.is_symlink",
+ lambda _: False,
+ ),
+ patch(
+ "pathlib.Path.mkdir",
+ MagicMock(),
+ ),
+ patch(
+ "homeassistant.components.backup.manager.json_bytes",
+ return_value=b"{}", # Empty JSON
+ ) as mocked_json_bytes,
+ patch(
+ "homeassistant.components.backup.manager.HAVERSION",
+ "2025.1.0",
+ ),
+ ):
+ await manager.generate_backup()
- assert manager.backup_task is None
- await manager.async_create_backup(on_progress=on_progress)
- assert manager.backup_task is not None
- assert progress == []
-
- await manager.backup_task
- assert progress == [BackupProgress(done=True, stage=None, success=True)]
-
- assert mocked_json_bytes.call_count == 1
- backup_json_dict = mocked_json_bytes.call_args[0][0]
- assert isinstance(backup_json_dict, dict)
- assert backup_json_dict["homeassistant"] == {"version": "2025.1.0"}
- assert manager.backup_dir.as_posix() in str(mocked_tarfile.call_args_list[0][0][0])
+ assert mocked_json_bytes.call_count == 1
+ backup_json_dict = mocked_json_bytes.call_args[0][0]
+ assert isinstance(backup_json_dict, dict)
+ assert backup_json_dict["homeassistant"] == {"version": "2025.1.0"}
+ assert manager.backup_dir.as_posix() in str(
+ mocked_tarfile.call_args_list[0][0][0]
+ )
async def _setup_mock_domain(
@@ -84,7 +108,7 @@ async def test_load_backups(hass: HomeAssistant) -> None:
),
):
await manager.load_backups()
- backups = await manager.async_get_backups()
+ backups = await manager.get_backups()
assert backups == {TEST_BACKUP.slug: TEST_BACKUP}
@@ -99,7 +123,7 @@ async def test_load_backups_with_exception(
patch("tarfile.open", side_effect=OSError("Test exception")),
):
await manager.load_backups()
- backups = await manager.async_get_backups()
+ backups = await manager.get_backups()
assert f"Unable to read backup {TEST_BACKUP.path}: Test exception" in caplog.text
assert backups == {}
@@ -114,7 +138,7 @@ async def test_removing_backup(
manager.loaded_backups = True
with patch("pathlib.Path.exists", return_value=True):
- await manager.async_remove_backup(slug=TEST_BACKUP.slug)
+ await manager.remove_backup(TEST_BACKUP.slug)
assert "Removed backup located at" in caplog.text
@@ -125,7 +149,7 @@ async def test_removing_non_existing_backup(
"""Test removing not existing backup."""
manager = BackupManager(hass)
- await manager.async_remove_backup(slug="non_existing")
+ await manager.remove_backup("non_existing")
assert "Removed backup located at" not in caplog.text
@@ -139,7 +163,7 @@ async def test_getting_backup_that_does_not_exist(
manager.loaded_backups = True
with patch("pathlib.Path.exists", return_value=False):
- backup = await manager.async_get_backup(slug=TEST_BACKUP.slug)
+ backup = await manager.get_backup(TEST_BACKUP.slug)
assert backup is None
assert (
@@ -148,28 +172,23 @@ async def test_getting_backup_that_does_not_exist(
) in caplog.text
-async def test_async_create_backup_when_backing_up(hass: HomeAssistant) -> None:
+async def test_generate_backup_when_backing_up(hass: HomeAssistant) -> None:
"""Test generate backup."""
- event = asyncio.Event()
manager = BackupManager(hass)
- manager.backup_task = hass.async_create_task(event.wait())
+ manager.backing_up = True
with pytest.raises(HomeAssistantError, match="Backup already in progress"):
- await manager.async_create_backup(on_progress=None)
- event.set()
+ await manager.generate_backup()
-@pytest.mark.usefixtures("mock_backup_generation")
-async def test_async_create_backup(
+async def test_generate_backup(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
- mocked_json_bytes: Mock,
- mocked_tarfile: Mock,
) -> None:
"""Test generate backup."""
manager = BackupManager(hass)
manager.loaded_backups = True
- await _mock_backup_generation(manager, mocked_json_bytes, mocked_tarfile)
+ await _mock_backup_generation(manager)
assert "Generated new backup with slug " in caplog.text
assert "Creating backup directory" in caplog.text
@@ -226,9 +245,7 @@ async def test_not_loading_bad_platforms(
)
-async def test_exception_plaform_pre(
- hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock
-) -> None:
+async def test_exception_plaform_pre(hass: HomeAssistant) -> None:
"""Test exception in pre step."""
manager = BackupManager(hass)
manager.loaded_backups = True
@@ -245,12 +262,10 @@ async def test_exception_plaform_pre(
)
with pytest.raises(HomeAssistantError):
- await _mock_backup_generation(manager, mocked_json_bytes, mocked_tarfile)
+ await _mock_backup_generation(manager)
-async def test_exception_plaform_post(
- hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock
-) -> None:
+async def test_exception_plaform_post(hass: HomeAssistant) -> None:
"""Test exception in post step."""
manager = BackupManager(hass)
manager.loaded_backups = True
@@ -267,10 +282,10 @@ async def test_exception_plaform_post(
)
with pytest.raises(HomeAssistantError):
- await _mock_backup_generation(manager, mocked_json_bytes, mocked_tarfile)
+ await _mock_backup_generation(manager)
-async def test_loading_platforms_when_running_async_pre_backup_actions(
+async def test_loading_platforms_when_running_pre_backup_actions(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
@@ -287,7 +302,7 @@ async def test_loading_platforms_when_running_async_pre_backup_actions(
async_post_backup=AsyncMock(),
),
)
- await manager.async_pre_backup_actions()
+ await manager.pre_backup_actions()
assert manager.loaded_platforms
assert len(manager.platforms) == 1
@@ -295,7 +310,7 @@ async def test_loading_platforms_when_running_async_pre_backup_actions(
assert "Loaded 1 platforms" in caplog.text
-async def test_loading_platforms_when_running_async_post_backup_actions(
+async def test_loading_platforms_when_running_post_backup_actions(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
@@ -312,71 +327,9 @@ async def test_loading_platforms_when_running_async_post_backup_actions(
async_post_backup=AsyncMock(),
),
)
- await manager.async_post_backup_actions()
+ await manager.post_backup_actions()
assert manager.loaded_platforms
assert len(manager.platforms) == 1
assert "Loaded 1 platforms" in caplog.text
-
-
-async def test_async_receive_backup(
- hass: HomeAssistant,
- caplog: pytest.LogCaptureFixture,
-) -> None:
- """Test receiving a backup file."""
- manager = BackupManager(hass)
-
- size = 2 * 2**16
- protocol = Mock(_reading_paused=False)
- stream = aiohttp.StreamReader(protocol, 2**16)
- stream.feed_data(b"0" * size + b"\r\n--:--")
- stream.feed_eof()
-
- open_mock = mock_open()
-
- with patch("pathlib.Path.open", open_mock), patch("shutil.move") as mover_mock:
- await manager.async_receive_backup(
- contents=aiohttp.BodyPartReader(
- b"--:",
- CIMultiDictProxy(
- CIMultiDict(
- {
- aiohttp.hdrs.CONTENT_DISPOSITION: "attachment; filename=abc123.tar"
- }
- )
- ),
- stream,
- )
- )
- assert open_mock.call_count == 1
- assert mover_mock.call_count == 1
- assert mover_mock.mock_calls[0].args[1].name == "abc123.tar"
-
-
-async def test_async_trigger_restore(
- hass: HomeAssistant,
- caplog: pytest.LogCaptureFixture,
-) -> None:
- """Test trigger restore."""
- manager = BackupManager(hass)
- manager.loaded_backups = True
- manager.backups = {TEST_BACKUP.slug: TEST_BACKUP}
-
- with (
- patch("pathlib.Path.exists", return_value=True),
- patch("pathlib.Path.write_text") as mocked_write_text,
- patch("homeassistant.core.ServiceRegistry.async_call") as mocked_service_call,
- ):
- await manager.async_restore_backup(TEST_BACKUP.slug)
- assert mocked_write_text.call_args[0][0] == '{"path": "abc123.tar"}'
- assert mocked_service_call.called
-
-
-async def test_async_trigger_restore_missing_backup(hass: HomeAssistant) -> None:
- """Test trigger restore."""
- manager = BackupManager(hass)
- manager.loaded_backups = True
-
- with pytest.raises(HomeAssistantError, match="Backup abc123 not found"):
- await manager.async_restore_backup(TEST_BACKUP.slug)
diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py
index 3e031f172ae..388aba6bc04 100644
--- a/tests/components/backup/test_websocket.py
+++ b/tests/components/backup/test_websocket.py
@@ -2,11 +2,9 @@
from unittest.mock import patch
-from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
-from homeassistant.components.backup.manager import Backup
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -47,48 +45,13 @@ async def test_info(
await hass.async_block_till_done()
with patch(
- "homeassistant.components.backup.manager.BackupManager.async_get_backups",
+ "homeassistant.components.backup.manager.BackupManager.get_backups",
return_value={TEST_BACKUP.slug: TEST_BACKUP},
):
await client.send_json_auto_id({"type": "backup/info"})
assert snapshot == await client.receive_json()
-@pytest.mark.parametrize(
- "backup_content",
- [
- pytest.param(TEST_BACKUP, id="with_backup_content"),
- pytest.param(None, id="without_backup_content"),
- ],
-)
-@pytest.mark.parametrize(
- "with_hassio",
- [
- pytest.param(True, id="with_hassio"),
- pytest.param(False, id="without_hassio"),
- ],
-)
-async def test_details(
- hass: HomeAssistant,
- hass_ws_client: WebSocketGenerator,
- snapshot: SnapshotAssertion,
- with_hassio: bool,
- backup_content: Backup | None,
-) -> None:
- """Test getting backup info."""
- await setup_backup_integration(hass, with_hassio=with_hassio)
-
- client = await hass_ws_client(hass)
- await hass.async_block_till_done()
-
- with patch(
- "homeassistant.components.backup.manager.BackupManager.async_get_backup",
- return_value=backup_content,
- ):
- await client.send_json_auto_id({"type": "backup/details", "slug": "abc123"})
- assert await client.receive_json() == snapshot
-
-
@pytest.mark.parametrize(
"with_hassio",
[
@@ -109,40 +72,12 @@ async def test_remove(
await hass.async_block_till_done()
with patch(
- "homeassistant.components.backup.manager.BackupManager.async_remove_backup",
+ "homeassistant.components.backup.manager.BackupManager.remove_backup",
):
await client.send_json_auto_id({"type": "backup/remove", "slug": "abc123"})
assert snapshot == await client.receive_json()
-@pytest.mark.parametrize(
- ("with_hassio", "number_of_messages"),
- [
- pytest.param(True, 1, id="with_hassio"),
- pytest.param(False, 2, id="without_hassio"),
- ],
-)
-@pytest.mark.usefixtures("mock_backup_generation")
-async def test_generate(
- hass: HomeAssistant,
- hass_ws_client: WebSocketGenerator,
- freezer: FrozenDateTimeFactory,
- snapshot: SnapshotAssertion,
- with_hassio: bool,
- number_of_messages: int,
-) -> None:
- """Test generating a backup."""
- await setup_backup_integration(hass, with_hassio=with_hassio)
-
- client = await hass_ws_client(hass)
- freezer.move_to("2024-11-13 12:01:00+01:00")
- await hass.async_block_till_done()
-
- await client.send_json_auto_id({"type": "backup/generate"})
- for _ in range(number_of_messages):
- assert snapshot == await client.receive_json()
-
-
@pytest.mark.parametrize(
"with_hassio",
[
@@ -150,23 +85,24 @@ async def test_generate(
pytest.param(False, id="without_hassio"),
],
)
-async def test_restore(
+async def test_generate(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
with_hassio: bool,
) -> None:
- """Test calling the restore command."""
+ """Test generating a backup."""
await setup_backup_integration(hass, with_hassio=with_hassio)
client = await hass_ws_client(hass)
await hass.async_block_till_done()
with patch(
- "homeassistant.components.backup.manager.BackupManager.async_restore_backup",
+ "homeassistant.components.backup.manager.BackupManager.generate_backup",
+ return_value=TEST_BACKUP,
):
- await client.send_json_auto_id({"type": "backup/restore", "slug": "abc123"})
- assert await client.receive_json() == snapshot
+ await client.send_json_auto_id({"type": "backup/generate"})
+ assert snapshot == await client.receive_json()
@pytest.mark.parametrize(
@@ -196,7 +132,7 @@ async def test_backup_end(
await hass.async_block_till_done()
with patch(
- "homeassistant.components.backup.manager.BackupManager.async_post_backup_actions",
+ "homeassistant.components.backup.manager.BackupManager.post_backup_actions",
):
await client.send_json_auto_id({"type": "backup/end"})
assert snapshot == await client.receive_json()
@@ -229,7 +165,7 @@ async def test_backup_start(
await hass.async_block_till_done()
with patch(
- "homeassistant.components.backup.manager.BackupManager.async_pre_backup_actions",
+ "homeassistant.components.backup.manager.BackupManager.pre_backup_actions",
):
await client.send_json_auto_id({"type": "backup/start"})
assert snapshot == await client.receive_json()
@@ -257,7 +193,7 @@ async def test_backup_end_excepion(
await hass.async_block_till_done()
with patch(
- "homeassistant.components.backup.manager.BackupManager.async_post_backup_actions",
+ "homeassistant.components.backup.manager.BackupManager.post_backup_actions",
side_effect=exception,
):
await client.send_json_auto_id({"type": "backup/end"})
@@ -286,7 +222,7 @@ async def test_backup_start_excepion(
await hass.async_block_till_done()
with patch(
- "homeassistant.components.backup.manager.BackupManager.async_pre_backup_actions",
+ "homeassistant.components.backup.manager.BackupManager.pre_backup_actions",
side_effect=exception,
):
await client.send_json_auto_id({"type": "backup/start"})
diff --git a/tests/components/balboa/__init__.py b/tests/components/balboa/__init__.py
index 2cb100e3642..a27293e955f 100644
--- a/tests/components/balboa/__init__.py
+++ b/tests/components/balboa/__init__.py
@@ -4,7 +4,7 @@ from __future__ import annotations
from unittest.mock import MagicMock
-from homeassistant.components.balboa.const import CONF_SYNC_TIME, DOMAIN
+from homeassistant.components.balboa import CONF_SYNC_TIME, DOMAIN
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant, State
diff --git a/tests/components/bang_olufsen/conftest.py b/tests/components/bang_olufsen/conftest.py
index cbde856ff89..ff29592b137 100644
--- a/tests/components/bang_olufsen/conftest.py
+++ b/tests/components/bang_olufsen/conftest.py
@@ -6,7 +6,6 @@ from unittest.mock import AsyncMock, Mock, patch
from mozart_api.models import (
Action,
BeolinkPeer,
- BeolinkSelf,
ContentItem,
ListeningMode,
ListeningModeFeatures,
@@ -15,7 +14,6 @@ from mozart_api.models import (
PlaybackContentMetadata,
PlaybackProgress,
PlaybackState,
- PlayQueueSettings,
ProductState,
RemoteMenuItem,
RenderingState,
@@ -35,13 +33,11 @@ from .const import (
TEST_DATA_CREATE_ENTRY,
TEST_DATA_CREATE_ENTRY_2,
TEST_FRIENDLY_NAME,
+ TEST_FRIENDLY_NAME_2,
TEST_FRIENDLY_NAME_3,
- TEST_FRIENDLY_NAME_4,
- TEST_HOST_3,
- TEST_HOST_4,
TEST_JID_1,
+ TEST_JID_2,
TEST_JID_3,
- TEST_JID_4,
TEST_NAME,
TEST_NAME_2,
TEST_SERIAL_NUMBER,
@@ -104,7 +100,7 @@ def mock_mozart_client() -> Generator[AsyncMock]:
# REST API client methods
client.get_beolink_self = AsyncMock()
- client.get_beolink_self.return_value = BeolinkSelf(
+ client.get_beolink_self.return_value = BeolinkPeer(
friendly_name=TEST_FRIENDLY_NAME, jid=TEST_JID_1
)
client.get_softwareupdate_status = AsyncMock()
@@ -124,7 +120,7 @@ def mock_mozart_client() -> Generator[AsyncMock]:
client.get_available_sources = AsyncMock()
client.get_available_sources.return_value = SourceArray(
items=[
- # Is not playable, so should not be user selectable
+ # Is in the HIDDEN_SOURCE_IDS constant, so should not be user selectable
Source(
name="AirPlay",
id="airPlay",
@@ -137,16 +133,14 @@ def mock_mozart_client() -> Generator[AsyncMock]:
id="tidal",
is_enabled=True,
is_multiroom_available=True,
- is_playable=True,
),
Source(
name="Line-In",
id="lineIn",
is_enabled=True,
is_multiroom_available=False,
- is_playable=True,
),
- # Is disabled and not playable, so should not be user selectable
+ # Is disabled, so should not be user selectable
Source(
name="Powerlink",
id="pl",
@@ -267,29 +261,13 @@ def mock_mozart_client() -> Generator[AsyncMock]:
}
client.get_beolink_peers = AsyncMock()
client.get_beolink_peers.return_value = [
- BeolinkPeer(
- friendly_name=TEST_FRIENDLY_NAME_3,
- jid=TEST_JID_3,
- ip_address=TEST_HOST_3,
- ),
- BeolinkPeer(
- friendly_name=TEST_FRIENDLY_NAME_4,
- jid=TEST_JID_4,
- ip_address=TEST_HOST_4,
- ),
+ BeolinkPeer(friendly_name=TEST_FRIENDLY_NAME_2, jid=TEST_JID_2),
+ BeolinkPeer(friendly_name=TEST_FRIENDLY_NAME_3, jid=TEST_JID_3),
]
client.get_beolink_listeners = AsyncMock()
client.get_beolink_listeners.return_value = [
- BeolinkPeer(
- friendly_name=TEST_FRIENDLY_NAME_3,
- jid=TEST_JID_3,
- ip_address=TEST_HOST_3,
- ),
- BeolinkPeer(
- friendly_name=TEST_FRIENDLY_NAME_4,
- jid=TEST_JID_4,
- ip_address=TEST_HOST_4,
- ),
+ BeolinkPeer(friendly_name=TEST_FRIENDLY_NAME_2, jid=TEST_JID_2),
+ BeolinkPeer(friendly_name=TEST_FRIENDLY_NAME_3, jid=TEST_JID_3),
]
client.get_listening_mode_set = AsyncMock()
@@ -318,12 +296,6 @@ def mock_mozart_client() -> Generator[AsyncMock]:
href="",
id=123,
)
- client.get_settings_queue = AsyncMock()
- client.get_settings_queue.return_value = PlayQueueSettings(
- repeat="none",
- shuffle=False,
- )
-
client.post_standby = AsyncMock()
client.set_current_volume_level = AsyncMock()
client.set_volume_mute = AsyncMock()
@@ -345,7 +317,6 @@ def mock_mozart_client() -> Generator[AsyncMock]:
client.post_beolink_allstandby = AsyncMock()
client.join_latest_beolink_experience = AsyncMock()
client.activate_listening_mode = AsyncMock()
- client.set_settings_queue = AsyncMock()
# Non-REST API client methods
client.check_device_connection = AsyncMock()
diff --git a/tests/components/bang_olufsen/const.py b/tests/components/bang_olufsen/const.py
index 6602a898eb6..7cbe81dc06a 100644
--- a/tests/components/bang_olufsen/const.py
+++ b/tests/components/bang_olufsen/const.py
@@ -16,7 +16,6 @@ from mozart_api.models import (
PlayQueueItemType,
RenderingState,
SceneProperties,
- Source,
UserFlow,
VolumeLevel,
VolumeMute,
@@ -53,17 +52,14 @@ TEST_MEDIA_PLAYER_ENTITY_ID = "media_player.beosound_balance_11111111"
TEST_FRIENDLY_NAME_2 = "Laundry room Balance"
TEST_JID_2 = f"{TEST_TYPE_NUMBER}.{TEST_ITEM_NUMBER}.22222222@products.bang-olufsen.com"
TEST_MEDIA_PLAYER_ENTITY_ID_2 = "media_player.beosound_balance_22222222"
-TEST_HOST_2 = "192.168.0.2"
TEST_FRIENDLY_NAME_3 = "Lego room Balance"
TEST_JID_3 = f"{TEST_TYPE_NUMBER}.{TEST_ITEM_NUMBER}.33333333@products.bang-olufsen.com"
TEST_MEDIA_PLAYER_ENTITY_ID_3 = "media_player.beosound_balance_33333333"
-TEST_HOST_3 = "192.168.0.3"
TEST_FRIENDLY_NAME_4 = "Lounge room Balance"
TEST_JID_4 = f"{TEST_TYPE_NUMBER}.{TEST_ITEM_NUMBER}.44444444@products.bang-olufsen.com"
TEST_MEDIA_PLAYER_ENTITY_ID_4 = "media_player.beosound_balance_44444444"
-TEST_HOST_4 = "192.168.0.4"
TEST_HOSTNAME_ZEROCONF = TEST_NAME.replace(" ", "-") + ".local."
TEST_TYPE_ZEROCONF = "_bangolufsen._tcp.local."
@@ -126,15 +122,11 @@ TEST_DATA_ZEROCONF_IPV6 = ZeroconfServiceInfo(
},
)
-TEST_SOURCE = Source(
- name="Tidal", id="tidal", is_seekable=True, is_enabled=True, is_playable=True
-)
-TEST_AUDIO_SOURCES = [TEST_SOURCE.name, BangOlufsenSource.LINE_IN.name]
+TEST_AUDIO_SOURCES = [BangOlufsenSource.TIDAL.name, BangOlufsenSource.LINE_IN.name]
TEST_VIDEO_SOURCES = ["HDMI A"]
TEST_SOURCES = TEST_AUDIO_SOURCES + TEST_VIDEO_SOURCES
TEST_FALLBACK_SOURCES = [
"Audio Streamer",
- "Bluetooth",
"Spotify Connect",
"Line-In",
"Optical",
diff --git a/tests/components/bang_olufsen/snapshots/test_media_player.ambr b/tests/components/bang_olufsen/snapshots/test_media_player.ambr
deleted file mode 100644
index ea96e286821..00000000000
--- a/tests/components/bang_olufsen/snapshots/test_media_player.ambr
+++ /dev/null
@@ -1,874 +0,0 @@
-# serializer version: 1
-# name: test_async_beolink_allstandby
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_11111111',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'repeat': ,
- 'shuffle': False,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_beolink_expand[all_discovered-True-None-log_messages0-2]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_11111111',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'repeat': ,
- 'shuffle': False,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source': 'Tidal',
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_beolink_expand[all_discovered-True-expand_side_effect1-log_messages1-2]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_11111111',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'repeat': ,
- 'shuffle': False,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source': 'Tidal',
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_beolink_expand[beolink_jids-parameter_value2-None-log_messages2-1]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_11111111',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'repeat': ,
- 'shuffle': False,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source': 'Tidal',
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_beolink_expand[beolink_jids-parameter_value3-expand_side_effect3-log_messages3-1]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_11111111',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'repeat': ,
- 'shuffle': False,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source': 'Tidal',
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_beolink_join
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_11111111',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'repeat': ,
- 'shuffle': False,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_beolink_unexpand
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_11111111',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'repeat': ,
- 'shuffle': False,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_join_players[group_members0-1-0]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_11111111',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'repeat': ,
- 'shuffle': False,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source': 'Tidal',
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_join_players[group_members0-1-0].1
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_22222222',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_22222222',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_join_players[group_members1-0-1]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_11111111',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'repeat': ,
- 'shuffle': False,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source': 'Tidal',
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_join_players[group_members1-0-1].1
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_22222222',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_22222222',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_join_players_invalid[source0-group_members0-expected_result0-invalid_source]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_11111111',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'media_position': 0,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source': 'Line-In',
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_join_players_invalid[source0-group_members0-expected_result0-invalid_source].1
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_22222222',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_22222222',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_join_players_invalid[source1-group_members1-expected_result1-invalid_grouping_entity]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_11111111',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source': 'Tidal',
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_join_players_invalid[source1-group_members1-expected_result1-invalid_grouping_entity].1
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_22222222',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_22222222',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_unjoin_player
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_11111111',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'repeat': ,
- 'shuffle': False,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_update_beolink_listener
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'leader': dict({
- 'Laundry room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_22222222',
- 'media_player.beosound_balance_11111111',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_11111111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
-# name: test_async_update_beolink_listener.1
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'beolink': dict({
- 'listeners': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'peers': dict({
- 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
- 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
- }),
- 'self': dict({
- 'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
- }),
- }),
- 'device_class': 'speaker',
- 'entity_picture_local': None,
- 'friendly_name': 'Living room Balance',
- 'group_members': list([
- 'media_player.beosound_balance_22222222',
- 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
- 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
- ]),
- 'icon': 'mdi:speaker-wireless',
- 'media_content_type': ,
- 'sound_mode': 'Test Listening Mode (123)',
- 'sound_mode_list': list([
- 'Test Listening Mode (123)',
- 'Test Listening Mode (234)',
- 'Test Listening Mode 2 (345)',
- ]),
- 'source_list': list([
- 'Tidal',
- 'Line-In',
- 'HDMI A',
- ]),
- 'supported_features': ,
- }),
- 'context': ,
- 'entity_id': 'media_player.beosound_balance_22222222',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
diff --git a/tests/components/bang_olufsen/test_init.py b/tests/components/bang_olufsen/test_init.py
index c8e4c05f9ab..3eb98e956be 100644
--- a/tests/components/bang_olufsen/test_init.py
+++ b/tests/components/bang_olufsen/test_init.py
@@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceRegistry
-from .const import TEST_FRIENDLY_NAME, TEST_MODEL_BALANCE, TEST_SERIAL_NUMBER
+from .const import TEST_MODEL_BALANCE, TEST_NAME, TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry
@@ -35,8 +35,7 @@ async def test_setup_entry(
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
)
assert device is not None
- # Is usually TEST_NAME, but is updated to the device's friendly name by _update_name_and_beolink
- assert device.name == TEST_FRIENDLY_NAME
+ assert device.name == TEST_NAME
assert device.model == TEST_MODEL_BALANCE
# Ensure that the connection has been checked WebSocket connection has been initialized
@@ -86,7 +85,6 @@ async def test_unload_entry(
await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert mock_config_entry.state == ConfigEntryState.LOADED
- assert hasattr(mock_config_entry, "runtime_data")
# Unload entry
await hass.config_entries.async_unload(mock_config_entry.entry_id)
@@ -96,5 +94,5 @@ async def test_unload_entry(
assert mock_mozart_client.close_api_client.call_count == 1
# Ensure that the entry is not loaded and has been removed from hass
- assert not hasattr(mock_config_entry, "runtime_data")
+ assert mock_config_entry.entry_id not in hass.data[DOMAIN]
assert mock_config_entry.state == ConfigEntryState.NOT_LOADED
diff --git a/tests/components/bang_olufsen/test_media_player.py b/tests/components/bang_olufsen/test_media_player.py
index aa35b0265dc..ff42ae2a867 100644
--- a/tests/components/bang_olufsen/test_media_player.py
+++ b/tests/components/bang_olufsen/test_media_player.py
@@ -4,23 +4,16 @@ from contextlib import AbstractContextManager, nullcontext as does_not_raise
import logging
from unittest.mock import AsyncMock, patch
-from mozart_api.exceptions import NotFoundException
from mozart_api.models import (
BeolinkLeader,
- BeolinkSelf,
PlaybackContentMetadata,
- PlayQueueSettings,
RenderingState,
Source,
- SourceArray,
WebsocketNotificationTag,
)
import pytest
-from syrupy.assertion import SnapshotAssertion
-from syrupy.filters import props
from homeassistant.components.bang_olufsen.const import (
- BANG_OLUFSEN_REPEAT_FROM_HA,
BANG_OLUFSEN_STATES,
DOMAIN,
BangOlufsenSource,
@@ -39,9 +32,7 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_EXTRA,
ATTR_MEDIA_POSITION,
ATTR_MEDIA_POSITION_UPDATED_AT,
- ATTR_MEDIA_REPEAT,
ATTR_MEDIA_SEEK_POSITION,
- ATTR_MEDIA_SHUFFLE,
ATTR_MEDIA_TITLE,
ATTR_MEDIA_TRACK,
ATTR_MEDIA_VOLUME_LEVEL,
@@ -50,29 +41,23 @@ from homeassistant.components.media_player import (
ATTR_SOUND_MODE_LIST,
DOMAIN as MEDIA_PLAYER_DOMAIN,
SERVICE_CLEAR_PLAYLIST,
- SERVICE_JOIN,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PLAY_PAUSE,
SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_SEEK,
SERVICE_MEDIA_STOP,
SERVICE_PLAY_MEDIA,
- SERVICE_REPEAT_SET,
SERVICE_SELECT_SOUND_MODE,
SERVICE_SELECT_SOURCE,
- SERVICE_SHUFFLE_SET,
SERVICE_TURN_OFF,
- SERVICE_UNJOIN,
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
MediaPlayerState,
MediaType,
- RepeatMode,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
-from homeassistant.helpers.device_registry import DeviceRegistry
from homeassistant.setup import async_setup_component
from .const import (
@@ -85,10 +70,7 @@ from .const import (
TEST_DEEZER_TRACK,
TEST_FALLBACK_SOURCES,
TEST_FRIENDLY_NAME_2,
- TEST_JID_1,
TEST_JID_2,
- TEST_JID_3,
- TEST_JID_4,
TEST_LISTENING_MODE_REF,
TEST_MEDIA_PLAYER_ENTITY_ID,
TEST_MEDIA_PLAYER_ENTITY_ID_2,
@@ -105,7 +87,6 @@ from .const import (
TEST_SEEK_POSITION_HOME_ASSISTANT_FORMAT,
TEST_SOUND_MODE_2,
TEST_SOUND_MODES,
- TEST_SOURCE,
TEST_SOURCES,
TEST_VIDEO_SOURCES,
TEST_VOLUME,
@@ -149,9 +130,6 @@ async def test_initialization(
mock_mozart_client.get_remote_menu.assert_called_once()
mock_mozart_client.get_listening_mode_set.assert_called_once()
mock_mozart_client.get_active_listening_mode.assert_called_once()
- mock_mozart_client.get_beolink_self.assert_called_once()
- mock_mozart_client.get_beolink_peers.assert_called_once()
- mock_mozart_client.get_beolink_listeners.assert_called_once()
async def test_async_update_sources_audio_only(
@@ -212,37 +190,6 @@ async def test_async_update_sources_remote(
assert mock_mozart_client.get_remote_menu.call_count == 2
-async def test_async_update_sources_availability(
- hass: HomeAssistant,
- mock_mozart_client: AsyncMock,
- mock_config_entry: MockConfigEntry,
-) -> None:
- """Test that the playback_source WebSocket event updates available playback sources."""
- # Remove video sources to simplify test
- mock_mozart_client.get_remote_menu.return_value = {}
-
- mock_config_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(mock_config_entry.entry_id)
-
- playback_source_callback = (
- mock_mozart_client.get_playback_source_notifications.call_args[0][0]
- )
-
- assert mock_mozart_client.get_available_sources.call_count == 1
-
- # Add a source that is available and playable
- mock_mozart_client.get_available_sources.return_value = SourceArray(
- items=[TEST_SOURCE]
- )
-
- # Send playback_source. The source is not actually used, so its attributes don't matter
- playback_source_callback(Source())
-
- assert mock_mozart_client.get_available_sources.call_count == 2
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert states.attributes[ATTR_INPUT_SOURCE_LIST] == [TEST_SOURCE.name]
-
-
async def test_async_update_playback_metadata(
hass: HomeAssistant,
mock_mozart_client: AsyncMock,
@@ -358,17 +305,19 @@ async def test_async_update_playback_state(
@pytest.mark.parametrize(
- ("source", "content_type", "progress", "metadata"),
+ ("reported_source", "real_source", "content_type", "progress", "metadata"),
[
- # Normal source, music mediatype expected
+ # Normal source, music mediatype expected, no progress expected
(
- TEST_SOURCE,
+ BangOlufsenSource.TIDAL,
+ BangOlufsenSource.TIDAL,
MediaType.MUSIC,
TEST_PLAYBACK_PROGRESS.progress,
PlaybackContentMetadata(),
),
- # URI source, url media type expected
+ # URI source, url media type expected, no progress expected
(
+ BangOlufsenSource.URI_STREAMER,
BangOlufsenSource.URI_STREAMER,
MediaType.URL,
TEST_PLAYBACK_PROGRESS.progress,
@@ -377,17 +326,44 @@ async def test_async_update_playback_state(
# Line-In source,media type expected, progress 0 expected
(
BangOlufsenSource.LINE_IN,
+ BangOlufsenSource.CHROMECAST,
MediaType.MUSIC,
0,
PlaybackContentMetadata(),
),
+ # Chromecast as source, but metadata says Line-In.
+ # Progress is not set to 0 as the source is Chromecast first
+ (
+ BangOlufsenSource.CHROMECAST,
+ BangOlufsenSource.LINE_IN,
+ MediaType.MUSIC,
+ TEST_PLAYBACK_PROGRESS.progress,
+ PlaybackContentMetadata(title=BangOlufsenSource.LINE_IN.name),
+ ),
+ # Chromecast as source, but metadata says Bluetooth
+ (
+ BangOlufsenSource.CHROMECAST,
+ BangOlufsenSource.BLUETOOTH,
+ MediaType.MUSIC,
+ TEST_PLAYBACK_PROGRESS.progress,
+ PlaybackContentMetadata(title=BangOlufsenSource.BLUETOOTH.name),
+ ),
+ # Chromecast as source, but metadata says Bluetooth in another way
+ (
+ BangOlufsenSource.CHROMECAST,
+ BangOlufsenSource.BLUETOOTH,
+ MediaType.MUSIC,
+ TEST_PLAYBACK_PROGRESS.progress,
+ PlaybackContentMetadata(art=[]),
+ ),
],
)
async def test_async_update_source_change(
hass: HomeAssistant,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
- source: Source,
+ reported_source: Source,
+ real_source: Source,
content_type: MediaType,
progress: int,
metadata: PlaybackContentMetadata,
@@ -416,10 +392,10 @@ async def test_async_update_source_change(
# Simulate metadata
playback_metadata_callback(metadata)
- source_change_callback(source)
+ source_change_callback(reported_source)
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert states.attributes[ATTR_INPUT_SOURCE] == source.name
+ assert states.attributes[ATTR_INPUT_SOURCE] == real_source.name
assert states.attributes[ATTR_MEDIA_CONTENT_TYPE] == content_type
assert states.attributes[ATTR_MEDIA_POSITION] == progress
@@ -517,14 +493,11 @@ async def test_async_update_beolink_line_in(
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
assert states.attributes["group_members"] == []
- # Called once during _initialize and once during _async_update_beolink
- assert mock_mozart_client.get_beolink_listeners.call_count == 2
- assert mock_mozart_client.get_beolink_peers.call_count == 2
+ assert mock_mozart_client.get_beolink_listeners.call_count == 1
async def test_async_update_beolink_listener(
hass: HomeAssistant,
- snapshot: SnapshotAssertion,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_config_entry_2: MockConfigEntry,
@@ -557,56 +530,7 @@ async def test_async_update_beolink_listener(
TEST_MEDIA_PLAYER_ENTITY_ID,
]
- # Called once for each entity during _initialize
- assert mock_mozart_client.get_beolink_listeners.call_count == 2
- # Called once for each entity during _initialize and
- # once more during _async_update_beolink for the entity that has the callback associated with it.
- assert mock_mozart_client.get_beolink_peers.call_count == 3
-
- # Main entity
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert states == snapshot(exclude=props("media_position_updated_at"))
-
- # Secondary entity
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID_2))
- assert states == snapshot(exclude=props("media_position_updated_at"))
-
-
-async def test_async_update_name_and_beolink(
- hass: HomeAssistant,
- device_registry: DeviceRegistry,
- mock_mozart_client: AsyncMock,
- mock_config_entry: MockConfigEntry,
-) -> None:
- """Test _async_update_name_and_beolink."""
- # Change response to ensure device name is changed
- mock_mozart_client.get_beolink_self.return_value = BeolinkSelf(
- friendly_name=TEST_FRIENDLY_NAME_2, jid=TEST_JID_1
- )
-
- mock_config_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(mock_config_entry.entry_id)
-
- configuration_callback = (
- mock_mozart_client.get_notification_notifications.call_args[0][0]
- )
- # Trigger callback
- configuration_callback(WebsocketNotificationTag(value="configuration"))
-
- await hass.async_block_till_done()
-
- assert mock_mozart_client.get_beolink_self.call_count == 2
- assert mock_mozart_client.get_beolink_peers.call_count == 2
- assert mock_mozart_client.get_beolink_listeners.call_count == 2
-
- # Check that device name has been changed
- assert mock_config_entry.unique_id
- assert (
- device := device_registry.async_get_device(
- identifiers={(DOMAIN, mock_config_entry.unique_id)}
- )
- )
- assert device.name == TEST_FRIENDLY_NAME_2
+ assert mock_mozart_client.get_beolink_listeners.call_count == 0
async def test_async_mute_volume(
@@ -745,12 +669,10 @@ async def test_async_media_next_track(
@pytest.mark.parametrize(
("source", "expected_result", "seek_called_times"),
[
- # Seekable source, seek expected
- (TEST_SOURCE, does_not_raise(), 1),
- # Non seekable source, seek shouldn't work
- (BangOlufsenSource.LINE_IN, pytest.raises(HomeAssistantError), 0),
- # Malformed source, seek shouldn't work
- (Source(), pytest.raises(HomeAssistantError), 0),
+ # Deezer source, seek expected
+ (BangOlufsenSource.DEEZER, does_not_raise(), 1),
+ # Non deezer source, seek shouldn't work
+ (BangOlufsenSource.TIDAL, pytest.raises(HomeAssistantError), 0),
],
)
async def test_async_media_seek(
@@ -834,7 +756,7 @@ async def test_async_clear_playlist(
# Invalid source
("Test source", pytest.raises(ServiceValidationError), 0, 0),
# Valid audio source
- (TEST_SOURCE.name, does_not_raise(), 1, 0),
+ (BangOlufsenSource.TIDAL.name, does_not_raise(), 1, 0),
# Valid video source
(TEST_VIDEO_SOURCES[0], does_not_raise(), 0, 1),
],
@@ -1382,7 +1304,6 @@ async def test_async_browse_media(
)
async def test_async_join_players(
hass: HomeAssistant,
- snapshot: SnapshotAssertion,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_config_entry_2: MockConfigEntry,
@@ -1404,11 +1325,11 @@ async def test_async_join_players(
await hass.config_entries.async_setup(mock_config_entry_2.entry_id)
# Set the source to a beolink expandable source
- source_change_callback(TEST_SOURCE)
+ source_change_callback(BangOlufsenSource.TIDAL)
await hass.services.async_call(
- MEDIA_PLAYER_DOMAIN,
- SERVICE_JOIN,
+ "media_player",
+ "join",
{
ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
ATTR_GROUP_MEMBERS: group_members,
@@ -1419,14 +1340,6 @@ async def test_async_join_players(
assert mock_mozart_client.post_beolink_expand.call_count == expand_count
assert mock_mozart_client.join_latest_beolink_experience.call_count == join_count
- # Main entity
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert states == snapshot(exclude=props("media_position_updated_at"))
-
- # Secondary entity
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID_2))
- assert states == snapshot(exclude=props("media_position_updated_at"))
-
@pytest.mark.parametrize(
("source", "group_members", "expected_result", "error_type"),
@@ -1440,7 +1353,7 @@ async def test_async_join_players(
),
# Invalid media_player entity
(
- TEST_SOURCE,
+ BangOlufsenSource.TIDAL,
[TEST_MEDIA_PLAYER_ENTITY_ID_3],
pytest.raises(ServiceValidationError),
"invalid_grouping_entity",
@@ -1449,7 +1362,6 @@ async def test_async_join_players(
)
async def test_async_join_players_invalid(
hass: HomeAssistant,
- snapshot: SnapshotAssertion,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_config_entry_2: MockConfigEntry,
@@ -1474,8 +1386,8 @@ async def test_async_join_players_invalid(
with expected_result as exc_info:
await hass.services.async_call(
- MEDIA_PLAYER_DOMAIN,
- SERVICE_JOIN,
+ "media_player",
+ "join",
{
ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
ATTR_GROUP_MEMBERS: group_members,
@@ -1490,18 +1402,9 @@ async def test_async_join_players_invalid(
assert mock_mozart_client.post_beolink_expand.call_count == 0
assert mock_mozart_client.join_latest_beolink_experience.call_count == 0
- # Main entity
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert states == snapshot(exclude=props("media_position_updated_at"))
-
- # Secondary entity
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID_2))
- assert states == snapshot(exclude=props("media_position_updated_at"))
-
async def test_async_unjoin_player(
hass: HomeAssistant,
- snapshot: SnapshotAssertion,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
@@ -1511,270 +1414,10 @@ async def test_async_unjoin_player(
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.services.async_call(
- MEDIA_PLAYER_DOMAIN,
- SERVICE_UNJOIN,
+ "media_player",
+ "unjoin",
{ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID},
blocking=True,
)
mock_mozart_client.post_beolink_leave.assert_called_once()
-
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert states == snapshot(exclude=props("media_position_updated_at"))
-
-
-async def test_async_beolink_join(
- hass: HomeAssistant,
- snapshot: SnapshotAssertion,
- mock_mozart_client: AsyncMock,
- mock_config_entry: MockConfigEntry,
-) -> None:
- """Test async_beolink_join with defined JID."""
-
- mock_config_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(mock_config_entry.entry_id)
-
- await hass.services.async_call(
- DOMAIN,
- "beolink_join",
- {
- ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
- "beolink_jid": TEST_JID_2,
- },
- blocking=True,
- )
-
- mock_mozart_client.join_beolink_peer.assert_called_once_with(jid=TEST_JID_2)
-
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert states == snapshot(exclude=props("media_position_updated_at"))
-
-
-@pytest.mark.parametrize(
- (
- "parameter",
- "parameter_value",
- "expand_side_effect",
- "log_messages",
- "peers_call_count",
- ),
- [
- # All discovered
- # Valid peers
- ("all_discovered", True, None, [], 2),
- # Invalid peers
- (
- "all_discovered",
- True,
- NotFoundException(),
- [f"Unable to expand to {TEST_JID_3}", f"Unable to expand to {TEST_JID_4}"],
- 2,
- ),
- # Beolink JIDs
- # Valid peer
- ("beolink_jids", [TEST_JID_3, TEST_JID_4], None, [], 1),
- # Invalid peer
- (
- "beolink_jids",
- [TEST_JID_3, TEST_JID_4],
- NotFoundException(),
- [
- f"Unable to expand to {TEST_JID_3}. Is the device available on the network?",
- f"Unable to expand to {TEST_JID_4}. Is the device available on the network?",
- ],
- 1,
- ),
- ],
-)
-async def test_async_beolink_expand(
- hass: HomeAssistant,
- caplog: pytest.LogCaptureFixture,
- snapshot: SnapshotAssertion,
- mock_mozart_client: AsyncMock,
- mock_config_entry: MockConfigEntry,
- parameter: str,
- parameter_value: bool | list[str],
- expand_side_effect: NotFoundException | None,
- log_messages: list[str],
- peers_call_count: int,
-) -> None:
- """Test async_beolink_expand."""
- mock_mozart_client.post_beolink_expand.side_effect = expand_side_effect
-
- mock_config_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(mock_config_entry.entry_id)
-
- source_change_callback = (
- mock_mozart_client.get_source_change_notifications.call_args[0][0]
- )
-
- # Set the source to a beolink expandable source
- source_change_callback(TEST_SOURCE)
-
- await hass.services.async_call(
- DOMAIN,
- "beolink_expand",
- {
- ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
- parameter: parameter_value,
- },
- blocking=True,
- )
-
- # Check log messages
- for log_message in log_messages:
- assert log_message in caplog.text
-
- # Called once during _initialize and once during async_beolink_expand for all_discovered
- assert mock_mozart_client.get_beolink_peers.call_count == peers_call_count
-
- assert mock_mozart_client.post_beolink_expand.call_count == len(
- await mock_mozart_client.get_beolink_peers()
- )
-
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert states == snapshot(exclude=props("media_position_updated_at"))
-
-
-async def test_async_beolink_unexpand(
- hass: HomeAssistant,
- snapshot: SnapshotAssertion,
- mock_mozart_client: AsyncMock,
- mock_config_entry: MockConfigEntry,
-) -> None:
- """Test test_async_beolink_unexpand."""
-
- mock_config_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(mock_config_entry.entry_id)
-
- await hass.services.async_call(
- DOMAIN,
- "beolink_unexpand",
- {
- ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
- "beolink_jids": [TEST_JID_3, TEST_JID_4],
- },
- blocking=True,
- )
-
- assert mock_mozart_client.post_beolink_unexpand.call_count == 2
-
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert states == snapshot(exclude=props("media_position_updated_at"))
-
-
-async def test_async_beolink_allstandby(
- hass: HomeAssistant,
- snapshot: SnapshotAssertion,
- mock_mozart_client: AsyncMock,
- mock_config_entry: MockConfigEntry,
-) -> None:
- """Test async_beolink_allstandby."""
-
- mock_config_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(mock_config_entry.entry_id)
-
- await hass.services.async_call(
- DOMAIN,
- "beolink_allstandby",
- {ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID},
- blocking=True,
- )
-
- mock_mozart_client.post_beolink_allstandby.assert_called_once()
-
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert states == snapshot(exclude=props("media_position_updated_at"))
-
-
-@pytest.mark.parametrize(
- ("repeat"),
- [
- # Repeat all
- (RepeatMode.ALL),
- # Repeat track
- (RepeatMode.ONE),
- # Repeat none
- (RepeatMode.OFF),
- ],
-)
-async def test_async_set_repeat(
- hass: HomeAssistant,
- mock_mozart_client: AsyncMock,
- mock_config_entry: MockConfigEntry,
- repeat: RepeatMode,
-) -> None:
- """Test async_set_repeat."""
- mock_config_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(mock_config_entry.entry_id)
-
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert ATTR_MEDIA_REPEAT not in states.attributes
-
- # Set the return value of the repeat endpoint to match service call
- mock_mozart_client.get_settings_queue.return_value = PlayQueueSettings(
- repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
- )
-
- await hass.services.async_call(
- MEDIA_PLAYER_DOMAIN,
- SERVICE_REPEAT_SET,
- {
- ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
- ATTR_MEDIA_REPEAT: repeat,
- },
- blocking=True,
- )
- mock_mozart_client.set_settings_queue.assert_called_once_with(
- play_queue_settings=PlayQueueSettings(
- repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
- )
- )
-
- # Test the BANG_OLUFSEN_REPEAT_TO_HA dict by checking property value
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert states.attributes[ATTR_MEDIA_REPEAT] == repeat
-
-
-@pytest.mark.parametrize(
- ("shuffle"),
- [
- # Shuffle on
- (True),
- # Shuffle off
- (False),
- ],
-)
-async def test_async_set_shuffle(
- hass: HomeAssistant,
- mock_mozart_client: AsyncMock,
- mock_config_entry: MockConfigEntry,
- shuffle: bool,
-) -> None:
- """Test async_set_shuffle."""
- mock_config_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(mock_config_entry.entry_id)
-
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert ATTR_MEDIA_SHUFFLE not in states.attributes
-
- # Set the return value of the shuffle endpoint to match service call
- mock_mozart_client.get_settings_queue.return_value = PlayQueueSettings(
- shuffle=shuffle
- )
-
- await hass.services.async_call(
- MEDIA_PLAYER_DOMAIN,
- SERVICE_SHUFFLE_SET,
- {
- ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
- ATTR_MEDIA_SHUFFLE: shuffle,
- },
- blocking=True,
- )
- mock_mozart_client.set_settings_queue.assert_called_once_with(
- play_queue_settings=PlayQueueSettings(shuffle=shuffle)
- )
-
- assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
- assert states.attributes[ATTR_MEDIA_SHUFFLE] == shuffle
diff --git a/tests/components/blebox/test_init.py b/tests/components/blebox/test_init.py
index 0cb5139336c..f406df51bd4 100644
--- a/tests/components/blebox/test_init.py
+++ b/tests/components/blebox/test_init.py
@@ -5,6 +5,7 @@ import logging
import blebox_uniapi
import pytest
+from homeassistant.components.blebox.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
@@ -56,10 +57,10 @@ async def test_unload_config_entry(hass: HomeAssistant) -> None:
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
- assert hasattr(entry, "runtime_data")
+ assert hass.data[DOMAIN]
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
- assert not hasattr(entry, "runtime_data")
+ assert not hass.data.get(DOMAIN)
assert entry.state is ConfigEntryState.NOT_LOADED
diff --git a/tests/components/blink/test_init.py b/tests/components/blink/test_init.py
index 6d4a93e58ab..3cd2cd51ebd 100644
--- a/tests/components/blink/test_init.py
+++ b/tests/components/blink/test_init.py
@@ -66,17 +66,18 @@ async def test_setup_not_ready_authkey_required(
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
-async def test_unload_entry(
+async def test_unload_entry_multiple(
hass: HomeAssistant,
mock_blink_api: MagicMock,
mock_blink_auth_api: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
- """Test unload doesn't un-register services."""
+ """Test being able to unload one of 2 entries."""
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
+ hass.data[DOMAIN]["dummy"] = {1: 2}
assert mock_config_entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
diff --git a/tests/components/bluesound/conftest.py b/tests/components/bluesound/conftest.py
index b4ee61dee57..155d6b66e4e 100644
--- a/tests/components/bluesound/conftest.py
+++ b/tests/components/bluesound/conftest.py
@@ -1,124 +1,71 @@
"""Common fixtures for the Bluesound tests."""
-from collections.abc import AsyncGenerator, Generator
-from dataclasses import dataclass
-import ipaddress
-from typing import Any
+from collections.abc import Generator
from unittest.mock import AsyncMock, patch
-from pyblu import Input, Player, Preset, Status, SyncStatus
+from pyblu import Status, SyncStatus
import pytest
from homeassistant.components.bluesound.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
-from .utils import LongPollingMock
-
from tests.common import MockConfigEntry
-@dataclass
-class PlayerMockData:
- """Container for player mock data."""
-
- host: str
- player: AsyncMock
- status_long_polling_mock: LongPollingMock[Status]
- sync_status_long_polling_mock: LongPollingMock[SyncStatus]
-
- @staticmethod
- async def generate(host: str) -> "PlayerMockData":
- """Generate player mock data."""
- host_ip = ipaddress.ip_address(host)
- assert host_ip.version == 4
- mac_parts = [0xFF, 0xFF, *host_ip.packed]
- mac = ":".join(f"{x:02X}" for x in mac_parts)
-
- player_name = f"player-name{host.replace('.', '')}"
-
- player = await AsyncMock(spec=Player)()
- player.__aenter__.return_value = player
-
- status_long_polling_mock = LongPollingMock(
- Status(
- etag="etag",
- input_id=None,
- service=None,
- state="play",
- shuffle=False,
- album="album",
- artist="artist",
- name="song",
- image=None,
- volume=10,
- volume_db=22.3,
- mute=False,
- mute_volume=None,
- mute_volume_db=None,
- seconds=2,
- total_seconds=123.1,
- can_seek=False,
- sleep=0,
- group_name=None,
- group_volume=None,
- indexing=False,
- stream_url=None,
- )
- )
-
- sync_status_long_polling_mock = LongPollingMock(
- SyncStatus(
- etag="etag",
- id=f"{host}:11000",
- mac=mac,
- name=player_name,
- image="invalid_url",
- initialized=True,
- brand="brand",
- model="model",
- model_name="model-name",
- volume_db=0.5,
- volume=50,
- group=None,
- master=None,
- slaves=None,
- zone=None,
- zone_master=None,
- zone_slave=None,
- mute_volume_db=None,
- mute_volume=None,
- )
- )
-
- player.status.side_effect = status_long_polling_mock.side_effect()
- player.sync_status.side_effect = sync_status_long_polling_mock.side_effect()
-
- player.inputs = AsyncMock(
- return_value=[
- Input("1", "input1", "image1", "url1"),
- Input("2", "input2", "image2", "url2"),
- ]
- )
- player.presets = AsyncMock(
- return_value=[
- Preset("preset1", "1", "url1", "image1", None),
- Preset("preset2", "2", "url2", "image2", None),
- ]
- )
-
- return PlayerMockData(
- host, player, status_long_polling_mock, sync_status_long_polling_mock
- )
+@pytest.fixture
+def sync_status() -> SyncStatus:
+ """Return a sync status object."""
+ return SyncStatus(
+ etag="etag",
+ id="1.1.1.1:11000",
+ mac="00:11:22:33:44:55",
+ name="player-name",
+ image="invalid_url",
+ initialized=True,
+ brand="brand",
+ model="model",
+ model_name="model-name",
+ volume_db=0.5,
+ volume=50,
+ group=None,
+ master=None,
+ slaves=None,
+ zone=None,
+ zone_master=None,
+ zone_slave=None,
+ mute_volume_db=None,
+ mute_volume=None,
+ )
-@dataclass
-class PlayerMocks:
- """Container for mocks."""
-
- player_data: PlayerMockData
- player_data_secondary: PlayerMockData
- player_data_for_already_configured: PlayerMockData
+@pytest.fixture
+def status() -> Status:
+ """Return a status object."""
+ return Status(
+ etag="etag",
+ input_id=None,
+ service=None,
+ state="playing",
+ shuffle=False,
+ album=None,
+ artist=None,
+ name=None,
+ image=None,
+ volume=10,
+ volume_db=22.3,
+ mute=False,
+ mute_volume=None,
+ mute_volume_db=None,
+ seconds=2,
+ total_seconds=123.1,
+ can_seek=False,
+ sleep=0,
+ group_name=None,
+ group_volume=None,
+ indexing=False,
+ stream_url=None,
+ )
@pytest.fixture
@@ -131,76 +78,24 @@ def mock_setup_entry() -> Generator[AsyncMock]:
@pytest.fixture
-def config_entry() -> MockConfigEntry:
+def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
"""Return a mocked config entry."""
- return MockConfigEntry(
+ mock_entry = MockConfigEntry(
domain=DOMAIN,
data={
- CONF_HOST: "1.1.1.1",
+ CONF_HOST: "1.1.1.2",
CONF_PORT: 11000,
},
- unique_id="ff:ff:01:01:01:01-11000",
+ unique_id="00:11:22:33:44:55-11000",
)
+ mock_entry.add_to_hass(hass)
+
+ return mock_entry
@pytest.fixture
-def config_entry_secondary() -> MockConfigEntry:
- """Return a mocked config entry."""
- return MockConfigEntry(
- domain=DOMAIN,
- data={
- CONF_HOST: "2.2.2.2",
- CONF_PORT: 11000,
- },
- unique_id="ff:ff:02:02:02:02-11000",
- )
-
-
-@pytest.fixture
-async def setup_config_entry(
- hass: HomeAssistant, config_entry: MockConfigEntry, player_mocks: PlayerMocks
-) -> None:
- """Set up the platform."""
- config_entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(config_entry.entry_id)
- await hass.async_block_till_done()
-
-
-@pytest.fixture
-async def setup_config_entry_secondary(
- hass: HomeAssistant,
- config_entry_secondary: MockConfigEntry,
- player_mocks: PlayerMocks,
-) -> None:
- """Set up the platform."""
- config_entry_secondary.add_to_hass(hass)
- assert await hass.config_entries.async_setup(config_entry_secondary.entry_id)
- await hass.async_block_till_done()
-
-
-@pytest.fixture
-async def player_mocks() -> AsyncGenerator[PlayerMocks]:
+def mock_player(status: Status) -> Generator[AsyncMock]:
"""Mock the player."""
- player_mocks = PlayerMocks(
- player_data=await PlayerMockData.generate("1.1.1.1"),
- player_data_secondary=await PlayerMockData.generate("2.2.2.2"),
- player_data_for_already_configured=await PlayerMockData.generate("1.1.1.2"),
- )
-
- # to simulate a player that is already configured
- player_mocks.player_data_for_already_configured.sync_status_long_polling_mock.get().mac = player_mocks.player_data.sync_status_long_polling_mock.get().mac
-
- def select_player(*args: Any, **kwargs: Any) -> AsyncMock:
- match args[0]:
- case "1.1.1.1":
- return player_mocks.player_data.player
- case "2.2.2.2":
- return player_mocks.player_data_secondary.player
- case "1.1.1.2":
- return player_mocks.player_data_for_already_configured.player
- case _:
- raise ValueError("Invalid player")
-
with (
patch(
"homeassistant.components.bluesound.Player", autospec=True
@@ -210,6 +105,28 @@ async def player_mocks() -> AsyncGenerator[PlayerMocks]:
new=mock_player,
),
):
- mock_player.side_effect = select_player
-
- yield player_mocks
+ player = mock_player.return_value
+ player.__aenter__.return_value = player
+ player.status.return_value = status
+ player.sync_status.return_value = SyncStatus(
+ etag="etag",
+ id="1.1.1.1:11000",
+ mac="00:11:22:33:44:55",
+ name="player-name",
+ image="invalid_url",
+ initialized=True,
+ brand="brand",
+ model="model",
+ model_name="model-name",
+ volume_db=0.5,
+ volume=50,
+ group=None,
+ master=None,
+ slaves=None,
+ zone=None,
+ zone_master=None,
+ zone_slave=None,
+ mute_volume_db=None,
+ mute_volume=None,
+ )
+ yield player
diff --git a/tests/components/bluesound/snapshots/test_media_player.ambr b/tests/components/bluesound/snapshots/test_media_player.ambr
deleted file mode 100644
index 3e644d3038a..00000000000
--- a/tests/components/bluesound/snapshots/test_media_player.ambr
+++ /dev/null
@@ -1,31 +0,0 @@
-# serializer version: 1
-# name: test_attributes_set
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'friendly_name': 'player-name1111',
- 'is_volume_muted': False,
- 'master': False,
- 'media_album_name': 'album',
- 'media_artist': 'artist',
- 'media_content_type': ,
- 'media_duration': 123,
- 'media_position': 2,
- 'media_title': 'song',
- 'shuffle': False,
- 'source_list': list([
- 'input1',
- 'input2',
- 'preset1',
- 'preset2',
- ]),
- 'supported_features': ,
- 'volume_level': 0.1,
- }),
- 'context': ,
- 'entity_id': 'media_player.player_name1111',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'playing',
- })
-# ---
diff --git a/tests/components/bluesound/test_config_flow.py b/tests/components/bluesound/test_config_flow.py
index 63744cdf0ff..53cf40a8d46 100644
--- a/tests/components/bluesound/test_config_flow.py
+++ b/tests/components/bluesound/test_config_flow.py
@@ -11,13 +11,11 @@ from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
-from .conftest import PlayerMocks
-
from tests.common import MockConfigEntry
async def test_user_flow_success(
- hass: HomeAssistant, mock_setup_entry: AsyncMock, player_mocks: PlayerMocks
+ hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_player: AsyncMock
) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
@@ -35,17 +33,15 @@ async def test_user_flow_success(
)
assert result["type"] is FlowResultType.CREATE_ENTRY
- assert result["title"] == "player-name1111"
+ assert result["title"] == "player-name"
assert result["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 11000}
- assert result["result"].unique_id == "ff:ff:01:01:01:01-11000"
+ assert result["result"].unique_id == "00:11:22:33:44:55-11000"
mock_setup_entry.assert_called_once()
async def test_user_flow_cannot_connect(
- hass: HomeAssistant,
- player_mocks: PlayerMocks,
- mock_setup_entry: AsyncMock,
+ hass: HomeAssistant, mock_player: AsyncMock, mock_setup_entry: AsyncMock
) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
@@ -53,9 +49,7 @@ async def test_user_flow_cannot_connect(
context={"source": SOURCE_USER},
)
- player_mocks.player_data.sync_status_long_polling_mock.set_error(
- PlayerUnreachableError("Player not reachable")
- )
+ mock_player.sync_status.side_effect = PlayerUnreachableError("Player not reachable")
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@@ -67,7 +61,7 @@ async def test_user_flow_cannot_connect(
assert result["errors"] == {"base": "cannot_connect"}
assert result["step_id"] == "user"
- player_mocks.player_data.sync_status_long_polling_mock.set_error(None)
+ mock_player.sync_status.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@@ -76,7 +70,7 @@ async def test_user_flow_cannot_connect(
)
assert result["type"] is FlowResultType.CREATE_ENTRY
- assert result["title"] == "player-name1111"
+ assert result["title"] == "player-name"
assert result["data"] == {
CONF_HOST: "1.1.1.1",
CONF_PORT: 11000,
@@ -87,11 +81,10 @@ async def test_user_flow_cannot_connect(
async def test_user_flow_aleady_configured(
hass: HomeAssistant,
- player_mocks: PlayerMocks,
- config_entry: MockConfigEntry,
+ mock_player: AsyncMock,
+ mock_config_entry: MockConfigEntry,
) -> None:
"""Test we handle already configured."""
- config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
@@ -100,7 +93,7 @@ async def test_user_flow_aleady_configured(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
- CONF_HOST: "1.1.1.2",
+ CONF_HOST: "1.1.1.1",
CONF_PORT: 11000,
},
)
@@ -108,13 +101,13 @@ async def test_user_flow_aleady_configured(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
- assert config_entry.data[CONF_HOST] == "1.1.1.2"
+ assert mock_config_entry.data[CONF_HOST] == "1.1.1.1"
- player_mocks.player_data_for_already_configured.player.sync_status.assert_called_once()
+ mock_player.sync_status.assert_called_once()
async def test_import_flow_success(
- hass: HomeAssistant, mock_setup_entry: AsyncMock, player_mocks: PlayerMocks
+ hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_player: AsyncMock
) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
@@ -124,21 +117,19 @@ async def test_import_flow_success(
)
assert result["type"] is FlowResultType.CREATE_ENTRY
- assert result["title"] == "player-name1111"
+ assert result["title"] == "player-name"
assert result["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 11000}
- assert result["result"].unique_id == "ff:ff:01:01:01:01-11000"
+ assert result["result"].unique_id == "00:11:22:33:44:55-11000"
mock_setup_entry.assert_called_once()
- player_mocks.player_data.player.sync_status.assert_called_once()
+ mock_player.sync_status.assert_called_once()
async def test_import_flow_cannot_connect(
- hass: HomeAssistant, player_mocks: PlayerMocks
+ hass: HomeAssistant, mock_player: AsyncMock
) -> None:
"""Test we handle cannot connect error."""
- player_mocks.player_data.player.sync_status.side_effect = PlayerUnreachableError(
- "Player not reachable"
- )
+ mock_player.sync_status.side_effect = PlayerUnreachableError("Player not reachable")
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
@@ -148,30 +139,29 @@ async def test_import_flow_cannot_connect(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
- player_mocks.player_data.player.sync_status.assert_called_once()
+ mock_player.sync_status.assert_called_once()
async def test_import_flow_already_configured(
hass: HomeAssistant,
- player_mocks: PlayerMocks,
- config_entry: MockConfigEntry,
+ mock_player: AsyncMock,
+ mock_config_entry: MockConfigEntry,
) -> None:
"""Test we handle already configured."""
- config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
- data={CONF_HOST: "1.1.1.2", CONF_PORT: 11000},
+ data={CONF_HOST: "1.1.1.1", CONF_PORT: 11000},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
- player_mocks.player_data_for_already_configured.player.sync_status.assert_called_once()
+ mock_player.sync_status.assert_called_once()
async def test_zeroconf_flow_success(
- hass: HomeAssistant, mock_setup_entry: AsyncMock, player_mocks: PlayerMocks
+ hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_player: AsyncMock
) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
@@ -181,7 +171,7 @@ async def test_zeroconf_flow_success(
ip_address="1.1.1.1",
ip_addresses=["1.1.1.1"],
port=11000,
- hostname="player-name1111",
+ hostname="player-name",
type="_musc._tcp.local.",
name="player-name._musc._tcp.local.",
properties={},
@@ -192,27 +182,25 @@ async def test_zeroconf_flow_success(
assert result["step_id"] == "confirm"
mock_setup_entry.assert_not_called()
- player_mocks.player_data.player.sync_status.assert_called_once()
+ mock_player.sync_status.assert_called_once()
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
- assert result["title"] == "player-name1111"
+ assert result["title"] == "player-name"
assert result["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 11000}
- assert result["result"].unique_id == "ff:ff:01:01:01:01-11000"
+ assert result["result"].unique_id == "00:11:22:33:44:55-11000"
mock_setup_entry.assert_called_once()
async def test_zeroconf_flow_cannot_connect(
- hass: HomeAssistant, player_mocks: PlayerMocks
+ hass: HomeAssistant, mock_player: AsyncMock
) -> None:
"""Test we handle cannot connect error."""
- player_mocks.player_data.player.sync_status.side_effect = PlayerUnreachableError(
- "Player not reachable"
- )
+ mock_player.sync_status.side_effect = PlayerUnreachableError("Player not reachable")
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
@@ -220,7 +208,7 @@ async def test_zeroconf_flow_cannot_connect(
ip_address="1.1.1.1",
ip_addresses=["1.1.1.1"],
port=11000,
- hostname="player-name1111",
+ hostname="player-name",
type="_musc._tcp.local.",
name="player-name._musc._tcp.local.",
properties={},
@@ -230,24 +218,23 @@ async def test_zeroconf_flow_cannot_connect(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
- player_mocks.player_data.player.sync_status.assert_called_once()
+ mock_player.sync_status.assert_called_once()
async def test_zeroconf_flow_already_configured(
hass: HomeAssistant,
- player_mocks: PlayerMocks,
- config_entry: MockConfigEntry,
+ mock_player: AsyncMock,
+ mock_config_entry: MockConfigEntry,
) -> None:
"""Test we handle already configured and update the host."""
- config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
- ip_address="1.1.1.2",
- ip_addresses=["1.1.1.2"],
+ ip_address="1.1.1.1",
+ ip_addresses=["1.1.1.1"],
port=11000,
- hostname="player-name1112",
+ hostname="player-name",
type="_musc._tcp.local.",
name="player-name._musc._tcp.local.",
properties={},
@@ -257,6 +244,6 @@ async def test_zeroconf_flow_already_configured(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
- assert config_entry.data[CONF_HOST] == "1.1.1.2"
+ assert mock_config_entry.data[CONF_HOST] == "1.1.1.1"
- player_mocks.player_data_for_already_configured.player.sync_status.assert_called_once()
+ mock_player.sync_status.assert_called_once()
diff --git a/tests/components/bluesound/test_init.py b/tests/components/bluesound/test_init.py
deleted file mode 100644
index 4178c27acad..00000000000
--- a/tests/components/bluesound/test_init.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""Test bluesound integration."""
-
-from pyblu.errors import PlayerUnreachableError
-
-from homeassistant.config_entries import ConfigEntryState
-from homeassistant.core import HomeAssistant
-
-from .conftest import PlayerMocks
-
-from tests.common import MockConfigEntry
-
-
-async def test_setup_entry(
- hass: HomeAssistant, setup_config_entry: None, config_entry: MockConfigEntry
-) -> None:
- """Test a successful setup entry."""
- assert hass.states.get("media_player.player_name1111").state == "playing"
- assert config_entry.state is ConfigEntryState.LOADED
-
- assert await hass.config_entries.async_unload(config_entry.entry_id)
- await hass.async_block_till_done()
-
- assert hass.states.get("media_player.player_name1111").state == "unavailable"
- assert config_entry.state is ConfigEntryState.NOT_LOADED
-
-
-async def test_unload_entry_while_player_is_offline(
- hass: HomeAssistant,
- setup_config_entry: None,
- config_entry: MockConfigEntry,
- player_mocks: PlayerMocks,
-) -> None:
- """Test entries can be unloaded correctly while the player is offline."""
- player_mocks.player_data.player.status.side_effect = PlayerUnreachableError(
- "Player not reachable"
- )
- player_mocks.player_data.status_long_polling_mock.trigger()
-
- # give the long polling loop a chance to update the state; this could be any async call
- await hass.async_block_till_done()
-
- assert await hass.config_entries.async_unload(config_entry.entry_id)
- await hass.async_block_till_done()
-
- assert hass.states.get("media_player.player_name1111").state == "unavailable"
- assert config_entry.state is ConfigEntryState.NOT_LOADED
diff --git a/tests/components/bluesound/test_media_player.py b/tests/components/bluesound/test_media_player.py
deleted file mode 100644
index 0bf615de3da..00000000000
--- a/tests/components/bluesound/test_media_player.py
+++ /dev/null
@@ -1,375 +0,0 @@
-"""Tests for the Bluesound Media Player platform."""
-
-import dataclasses
-from unittest.mock import call
-
-from pyblu import PairedPlayer
-from pyblu.errors import PlayerUnreachableError
-import pytest
-from syrupy.assertion import SnapshotAssertion
-from syrupy.filters import props
-
-from homeassistant.components.bluesound import DOMAIN as BLUESOUND_DOMAIN
-from homeassistant.components.bluesound.const import ATTR_MASTER
-from homeassistant.components.bluesound.services import (
- SERVICE_CLEAR_TIMER,
- SERVICE_JOIN,
- SERVICE_SET_TIMER,
-)
-from homeassistant.components.media_player import (
- ATTR_MEDIA_VOLUME_LEVEL,
- DOMAIN as MEDIA_PLAYER_DOMAIN,
- SERVICE_MEDIA_NEXT_TRACK,
- SERVICE_MEDIA_PAUSE,
- SERVICE_MEDIA_PLAY,
- SERVICE_MEDIA_PREVIOUS_TRACK,
- SERVICE_VOLUME_DOWN,
- SERVICE_VOLUME_MUTE,
- SERVICE_VOLUME_SET,
- SERVICE_VOLUME_UP,
- MediaPlayerState,
-)
-from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ServiceValidationError
-
-from .conftest import PlayerMocks
-
-
-@pytest.mark.parametrize(
- ("service", "method"),
- [
- (SERVICE_MEDIA_PAUSE, "pause"),
- (SERVICE_MEDIA_PLAY, "play"),
- (SERVICE_MEDIA_NEXT_TRACK, "skip"),
- (SERVICE_MEDIA_PREVIOUS_TRACK, "back"),
- ],
-)
-async def test_simple_actions(
- hass: HomeAssistant,
- setup_config_entry: None,
- player_mocks: PlayerMocks,
- service: str,
- method: str,
-) -> None:
- """Test the media player simple actions."""
- await hass.services.async_call(
- MEDIA_PLAYER_DOMAIN,
- service,
- {ATTR_ENTITY_ID: "media_player.player_name1111"},
- blocking=True,
- )
-
- getattr(player_mocks.player_data.player, method).assert_called_once_with()
-
-
-async def test_volume_set(
- hass: HomeAssistant, setup_config_entry: None, player_mocks: PlayerMocks
-) -> None:
- """Test the media player volume set."""
- await hass.services.async_call(
- MEDIA_PLAYER_DOMAIN,
- SERVICE_VOLUME_SET,
- {ATTR_ENTITY_ID: "media_player.player_name1111", ATTR_MEDIA_VOLUME_LEVEL: 0.5},
- blocking=True,
- )
-
- player_mocks.player_data.player.volume.assert_called_once_with(level=50)
-
-
-async def test_volume_mute(
- hass: HomeAssistant, setup_config_entry: None, player_mocks: PlayerMocks
-) -> None:
- """Test the media player volume mute."""
- await hass.services.async_call(
- MEDIA_PLAYER_DOMAIN,
- SERVICE_VOLUME_MUTE,
- {ATTR_ENTITY_ID: "media_player.player_name1111", "is_volume_muted": True},
- blocking=True,
- )
-
- player_mocks.player_data.player.volume.assert_called_once_with(mute=True)
-
-
-async def test_volume_up(
- hass: HomeAssistant, setup_config_entry: None, player_mocks: PlayerMocks
-) -> None:
- """Test the media player volume up."""
- await hass.services.async_call(
- MEDIA_PLAYER_DOMAIN,
- SERVICE_VOLUME_UP,
- {ATTR_ENTITY_ID: "media_player.player_name1111"},
- blocking=True,
- )
-
- player_mocks.player_data.player.volume.assert_called_once_with(level=11)
-
-
-async def test_volume_down(
- hass: HomeAssistant, setup_config_entry: None, player_mocks: PlayerMocks
-) -> None:
- """Test the media player volume down."""
- await hass.services.async_call(
- MEDIA_PLAYER_DOMAIN,
- SERVICE_VOLUME_DOWN,
- {ATTR_ENTITY_ID: "media_player.player_name1111"},
- blocking=True,
- )
-
- player_mocks.player_data.player.volume.assert_called_once_with(level=9)
-
-
-async def test_attributes_set(
- hass: HomeAssistant,
- setup_config_entry: None,
- player_mocks: PlayerMocks,
- snapshot: SnapshotAssertion,
-) -> None:
- """Test the media player attributes set."""
- state = hass.states.get("media_player.player_name1111")
- assert state == snapshot(exclude=props("media_position_updated_at"))
-
-
-async def test_stop_maps_to_idle(
- hass: HomeAssistant,
- setup_config_entry: None,
- player_mocks: PlayerMocks,
-) -> None:
- """Test the media player stop maps to idle."""
- player_mocks.player_data.status_long_polling_mock.set(
- dataclasses.replace(
- player_mocks.player_data.status_long_polling_mock.get(), state="stop"
- )
- )
-
- # give the long polling loop a chance to update the state; this could be any async call
- await hass.async_block_till_done()
-
- assert (
- hass.states.get("media_player.player_name1111").state == MediaPlayerState.IDLE
- )
-
-
-async def test_status_updated(
- hass: HomeAssistant,
- setup_config_entry: None,
- player_mocks: PlayerMocks,
-) -> None:
- """Test the media player status updated."""
- pre_state = hass.states.get("media_player.player_name1111")
- assert pre_state.state == "playing"
- assert pre_state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.1
-
- status = player_mocks.player_data.status_long_polling_mock.get()
- status = dataclasses.replace(status, state="pause", volume=50, etag="changed")
- player_mocks.player_data.status_long_polling_mock.set(status)
-
- # give the long polling loop a chance to update the state; this could be any async call
- await hass.async_block_till_done()
-
- post_state = hass.states.get("media_player.player_name1111")
-
- assert post_state.state == MediaPlayerState.PAUSED
- assert post_state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.5
-
-
-async def test_unavailable_when_offline(
- hass: HomeAssistant,
- setup_config_entry: None,
- player_mocks: PlayerMocks,
-) -> None:
- """Test that the media player goes unavailable when the player is unreachable."""
- pre_state = hass.states.get("media_player.player_name1111")
- assert pre_state.state == "playing"
-
- player_mocks.player_data.status_long_polling_mock.set_error(
- PlayerUnreachableError("Player not reachable")
- )
- player_mocks.player_data.status_long_polling_mock.trigger()
-
- # give the long polling loop a chance to update the state; this could be any async call
- await hass.async_block_till_done()
-
- post_state = hass.states.get("media_player.player_name1111")
-
- assert post_state.state == STATE_UNAVAILABLE
-
-
-async def test_set_sleep_timer(
- hass: HomeAssistant, setup_config_entry: None, player_mocks: PlayerMocks
-) -> None:
- """Test the set sleep timer action."""
- await hass.services.async_call(
- BLUESOUND_DOMAIN,
- SERVICE_SET_TIMER,
- {ATTR_ENTITY_ID: "media_player.player_name1111"},
- blocking=True,
- )
-
- player_mocks.player_data.player.sleep_timer.assert_called_once()
-
-
-async def test_clear_sleep_timer(
- hass: HomeAssistant, setup_config_entry: None, player_mocks: PlayerMocks
-) -> None:
- """Test the clear sleep timer action."""
-
- player_mocks.player_data.player.sleep_timer.side_effect = [15, 30, 45, 60, 90, 0]
-
- await hass.services.async_call(
- BLUESOUND_DOMAIN,
- SERVICE_CLEAR_TIMER,
- {ATTR_ENTITY_ID: "media_player.player_name1111"},
- blocking=True,
- )
-
- player_mocks.player_data.player.sleep_timer.assert_has_calls([call()] * 6)
-
-
-async def test_join_cannot_join_to_self(
- hass: HomeAssistant, setup_config_entry: None, player_mocks: PlayerMocks
-) -> None:
- """Test that joining to self is not allowed."""
- with pytest.raises(ServiceValidationError, match="Cannot join player to itself"):
- await hass.services.async_call(
- BLUESOUND_DOMAIN,
- SERVICE_JOIN,
- {
- ATTR_ENTITY_ID: "media_player.player_name1111",
- ATTR_MASTER: "media_player.player_name1111",
- },
- blocking=True,
- )
-
-
-async def test_join(
- hass: HomeAssistant,
- setup_config_entry: None,
- setup_config_entry_secondary: None,
- player_mocks: PlayerMocks,
-) -> None:
- """Test the join action."""
- await hass.services.async_call(
- BLUESOUND_DOMAIN,
- SERVICE_JOIN,
- {
- ATTR_ENTITY_ID: "media_player.player_name1111",
- ATTR_MASTER: "media_player.player_name2222",
- },
- blocking=True,
- )
-
- player_mocks.player_data_secondary.player.add_slave.assert_called_once_with(
- "1.1.1.1", 11000
- )
-
-
-async def test_unjoin(
- hass: HomeAssistant,
- setup_config_entry: None,
- setup_config_entry_secondary: None,
- player_mocks: PlayerMocks,
-) -> None:
- """Test the unjoin action."""
- updated_sync_status = dataclasses.replace(
- player_mocks.player_data.sync_status_long_polling_mock.get(),
- master=PairedPlayer("2.2.2.2", 11000),
- )
- player_mocks.player_data.sync_status_long_polling_mock.set(updated_sync_status)
-
- # give the long polling loop a chance to update the state; this could be any async call
- await hass.async_block_till_done()
-
- await hass.services.async_call(
- BLUESOUND_DOMAIN,
- "unjoin",
- {ATTR_ENTITY_ID: "media_player.player_name1111"},
- blocking=True,
- )
-
- player_mocks.player_data_secondary.player.remove_slave.assert_called_once_with(
- "1.1.1.1", 11000
- )
-
-
-async def test_attr_master(
- hass: HomeAssistant,
- setup_config_entry: None,
- player_mocks: PlayerMocks,
-) -> None:
- """Test the media player master."""
- attr_master = hass.states.get("media_player.player_name1111").attributes[
- ATTR_MASTER
- ]
- assert attr_master is False
-
- updated_sync_status = dataclasses.replace(
- player_mocks.player_data.sync_status_long_polling_mock.get(),
- slaves=[PairedPlayer("2.2.2.2", 11000)],
- )
- player_mocks.player_data.sync_status_long_polling_mock.set(updated_sync_status)
-
- # give the long polling loop a chance to update the state; this could be any async call
- await hass.async_block_till_done()
-
- attr_master = hass.states.get("media_player.player_name1111").attributes[
- ATTR_MASTER
- ]
-
- assert attr_master is True
-
-
-async def test_attr_bluesound_group(
- hass: HomeAssistant,
- setup_config_entry: None,
- setup_config_entry_secondary: None,
- player_mocks: PlayerMocks,
-) -> None:
- """Test the media player grouping."""
- attr_bluesound_group = hass.states.get(
- "media_player.player_name1111"
- ).attributes.get("bluesound_group")
- assert attr_bluesound_group is None
-
- updated_status = dataclasses.replace(
- player_mocks.player_data.status_long_polling_mock.get(),
- group_name="player-name1111+player-name2222",
- )
- player_mocks.player_data.status_long_polling_mock.set(updated_status)
-
- # give the long polling loop a chance to update the state; this could be any async call
- await hass.async_block_till_done()
-
- attr_bluesound_group = hass.states.get(
- "media_player.player_name1111"
- ).attributes.get("bluesound_group")
-
- assert attr_bluesound_group == ["player-name1111", "player-name2222"]
-
-
-async def test_volume_up_from_6_to_7(
- hass: HomeAssistant,
- setup_config_entry: None,
- player_mocks: PlayerMocks,
-) -> None:
- """Test the media player volume up from 6 to 7.
-
- This fails if if rounding is not done correctly. See https://github.com/home-assistant/core/issues/129956 for more details.
- """
- player_mocks.player_data.status_long_polling_mock.set(
- dataclasses.replace(
- player_mocks.player_data.status_long_polling_mock.get(), volume=6
- )
- )
-
- # give the long polling loop a chance to update the state; this could be any async call
- await hass.async_block_till_done()
-
- await hass.services.async_call(
- MEDIA_PLAYER_DOMAIN,
- SERVICE_VOLUME_UP,
- {ATTR_ENTITY_ID: "media_player.player_name1111"},
- blocking=True,
- )
-
- player_mocks.player_data.player.volume.assert_called_once_with(level=7)
diff --git a/tests/components/bluesound/utils.py b/tests/components/bluesound/utils.py
deleted file mode 100644
index 112d077d7f5..00000000000
--- a/tests/components/bluesound/utils.py
+++ /dev/null
@@ -1,70 +0,0 @@
-"""Utils for bluesound tests."""
-
-import asyncio
-from typing import Protocol
-
-
-class Etag(Protocol):
- """Etag protocol."""
-
- etag: str
-
-
-class LongPollingMock[T: Etag]:
- """Mock long polling methods(status, sync_status)."""
-
- def __init__(self, value: T) -> None:
- """Store value and allows to wait for changes."""
- self._value = value
- self._error: Exception | None = None
- self._event = asyncio.Event()
- self._event.set()
-
- def trigger(self):
- """Trigger the event without changing the value."""
- self._event.set()
-
- def set(self, value: T):
- """Set the value and notify all waiting."""
- self._value = value
- self._event.set()
-
- def set_error(self, error: Exception | None):
- """Set the error and notify all waiting."""
- self._error = error
- self._event.set()
-
- def get(self) -> T:
- """Get the value without waiting."""
- return self._value
-
- async def wait(self) -> T:
- """Wait for the value or error to change."""
- await self._event.wait()
- self._event.clear()
-
- return self._value
-
- def side_effect(self):
- """Return the side_effect for mocking."""
- last_etag = None
-
- async def mock(*args, **kwargs) -> T:
- nonlocal last_etag
- if self._error is not None:
- raise self._error
-
- etag = kwargs.get("etag")
- if etag is None or etag != last_etag:
- last_etag = self.get().etag
- return self.get()
-
- value = await self.wait()
- last_etag = value.etag
-
- if self._error is not None:
- raise self._error
-
- return value
-
- return mock
diff --git a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr
index 624b2c6007f..2182ff2bb48 100644
--- a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr
+++ b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr
@@ -245,7 +245,7 @@
'suggested_display_precision': 0,
}),
}),
- 'original_device_class': None,
+ 'original_device_class': ,
'original_icon': None,
'original_name': 'Charging target',
'platform': 'bmw_connected_drive',
@@ -259,6 +259,7 @@
# name: test_entity_state_attrs[sensor.i3_rex_charging_target-state]
StateSnapshot({
'attributes': ReadOnlyDict({
+ 'device_class': 'battery',
'friendly_name': 'i3 (+ REX) Charging target',
'unit_of_measurement': '%',
}),
@@ -893,7 +894,7 @@
'suggested_display_precision': 0,
}),
}),
- 'original_device_class': None,
+ 'original_device_class': ,
'original_icon': None,
'original_name': 'Charging target',
'platform': 'bmw_connected_drive',
@@ -907,6 +908,7 @@
# name: test_entity_state_attrs[sensor.i4_edrive40_charging_target-state]
StateSnapshot({
'attributes': ReadOnlyDict({
+ 'device_class': 'battery',
'friendly_name': 'i4 eDrive40 Charging target',
'unit_of_measurement': '%',
}),
@@ -1898,7 +1900,7 @@
'suggested_display_precision': 0,
}),
}),
- 'original_device_class': None,
+ 'original_device_class': ,
'original_icon': None,
'original_name': 'Charging target',
'platform': 'bmw_connected_drive',
@@ -1912,6 +1914,7 @@
# name: test_entity_state_attrs[sensor.ix_xdrive50_charging_target-state]
StateSnapshot({
'attributes': ReadOnlyDict({
+ 'device_class': 'battery',
'friendly_name': 'iX xDrive50 Charging target',
'unit_of_measurement': '%',
}),
diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py
index f57f1a304ac..9d4d15703f2 100644
--- a/tests/components/bmw_connected_drive/test_config_flow.py
+++ b/tests/components/bmw_connected_drive/test_config_flow.py
@@ -4,13 +4,8 @@ from copy import deepcopy
from unittest.mock import patch
from bimmer_connected.api.authentication import MyBMWAuthentication
-from bimmer_connected.models import (
- MyBMWAPIError,
- MyBMWAuthError,
- MyBMWCaptchaMissingError,
-)
+from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError
from httpx import RequestError
-import pytest
from homeassistant import config_entries
from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN
@@ -316,31 +311,3 @@ async def test_reconfigure_unique_id_abort(hass: HomeAssistant) -> None:
assert result2["type"] is FlowResultType.ABORT
assert result2["reason"] == "account_mismatch"
assert config_entry.data == FIXTURE_COMPLETE_ENTRY
-
-
-@pytest.mark.usefixtures("bmw_fixture")
-async def test_captcha_flow_not_set(hass: HomeAssistant) -> None:
- """Test the external flow with captcha failing once and succeeding the second time."""
-
- TEST_REGION = "north_america"
-
- # Start flow and open form
- # Start flow and open form
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
- assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "user"
-
- # Add login data
- with patch(
- "bimmer_connected.api.authentication.MyBMWAuthentication._login_row_na",
- side_effect=MyBMWCaptchaMissingError(
- "Missing hCaptcha token for North America login"
- ),
- ):
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={**FIXTURE_USER_INPUT, CONF_REGION: TEST_REGION},
- )
- assert result["errors"]["base"] == "missing_captcha"
diff --git a/tests/components/bmw_connected_drive/test_coordinator.py b/tests/components/bmw_connected_drive/test_coordinator.py
index 774a85eb6da..b0f507bbfc2 100644
--- a/tests/components/bmw_connected_drive/test_coordinator.py
+++ b/tests/components/bmw_connected_drive/test_coordinator.py
@@ -1,19 +1,13 @@
"""Test BMW coordinator."""
-from copy import deepcopy
from datetime import timedelta
from unittest.mock import patch
-from bimmer_connected.models import (
- MyBMWAPIError,
- MyBMWAuthError,
- MyBMWCaptchaMissingError,
-)
+from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
-from homeassistant.const import CONF_REGION
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import issue_registry as ir
@@ -128,38 +122,3 @@ async def test_init_reauth(
f"config_entry_reauth_{BMW_DOMAIN}_{config_entry.entry_id}",
)
assert reauth_issue.active is True
-
-
-@pytest.mark.usefixtures("bmw_fixture")
-async def test_captcha_reauth(
- hass: HomeAssistant,
- freezer: FrozenDateTimeFactory,
-) -> None:
- """Test the reauth form."""
- TEST_REGION = "north_america"
-
- config_entry_fixure = deepcopy(FIXTURE_CONFIG_ENTRY)
- config_entry_fixure["data"][CONF_REGION] = TEST_REGION
- config_entry = MockConfigEntry(**config_entry_fixure)
- config_entry.add_to_hass(hass)
-
- await hass.config_entries.async_setup(config_entry.entry_id)
- await hass.async_block_till_done()
-
- coordinator = config_entry.runtime_data.coordinator
-
- assert coordinator.last_update_success is True
-
- freezer.tick(timedelta(minutes=10, seconds=1))
- with patch(
- "bimmer_connected.account.MyBMWAccount.get_vehicles",
- side_effect=MyBMWCaptchaMissingError(
- "Missing hCaptcha token for North America login"
- ),
- ):
- async_fire_time_changed(hass)
- await hass.async_block_till_done()
-
- assert coordinator.last_update_success is False
- assert isinstance(coordinator.last_exception, ConfigEntryAuthFailed) is True
- assert coordinator.last_exception.translation_key == "missing_captcha"
diff --git a/tests/components/bond/test_button.py b/tests/components/bond/test_button.py
index c14bba0d01f..8c8f38db72b 100644
--- a/tests/components/bond/test_button.py
+++ b/tests/components/bond/test_button.py
@@ -57,15 +57,6 @@ def light(name: str):
}
-def motorized_shade(name: str):
- """Create a motorized shade with a given name."""
- return {
- "name": name,
- "type": DeviceType.MOTORIZED_SHADES,
- "actions": [Action.OPEN, Action.OPEN_NEXT, Action.CLOSE, Action.CLOSE_NEXT],
- }
-
-
async def test_entity_registry(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
@@ -189,38 +180,3 @@ async def test_press_button(hass: HomeAssistant) -> None:
mock_action.assert_called_once_with(
"test-device-id", Action(Action.START_DECREASING_BRIGHTNESS)
)
-
-
-async def test_motorized_shade_actions(hass: HomeAssistant) -> None:
- """Tests motorized shade open next and close next actions."""
- await setup_platform(
- hass,
- BUTTON_DOMAIN,
- motorized_shade("name-1"),
- bond_device_id="test-device-id",
- )
-
- assert hass.states.get("button.name_1_open_next")
- assert hass.states.get("button.name_1_close_next")
-
- with patch_bond_action() as mock_action, patch_bond_device_state():
- await hass.services.async_call(
- BUTTON_DOMAIN,
- SERVICE_PRESS,
- {ATTR_ENTITY_ID: "button.name_1_open_next"},
- blocking=True,
- )
- await hass.async_block_till_done()
-
- mock_action.assert_called_once_with("test-device-id", Action(Action.OPEN_NEXT))
-
- with patch_bond_action() as mock_action, patch_bond_device_state():
- await hass.services.async_call(
- BUTTON_DOMAIN,
- SERVICE_PRESS,
- {ATTR_ENTITY_ID: "button.name_1_close_next"},
- blocking=True,
- )
- await hass.async_block_till_done()
-
- mock_action.assert_called_once_with("test-device-id", Action(Action.CLOSE_NEXT))
diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py
index 63f7169b026..eaabe112807 100644
--- a/tests/components/bosch_shc/test_config_flow.py
+++ b/tests/components/bosch_shc/test_config_flow.py
@@ -99,8 +99,8 @@ async def test_form_user(hass: HomeAssistant) -> None:
assert result3["title"] == "shc012345"
assert result3["data"] == {
"host": "1.1.1.1",
- "ssl_certificate": hass.config.path(DOMAIN, "test-mac", CONF_SHC_CERT),
- "ssl_key": hass.config.path(DOMAIN, "test-mac", CONF_SHC_KEY),
+ "ssl_certificate": hass.config.path(DOMAIN, CONF_SHC_CERT),
+ "ssl_key": hass.config.path(DOMAIN, CONF_SHC_KEY),
"token": "abc:123",
"hostname": "123",
}
@@ -549,8 +549,8 @@ async def test_zeroconf(hass: HomeAssistant) -> None:
assert result3["title"] == "shc012345"
assert result3["data"] == {
"host": "1.1.1.1",
- "ssl_certificate": hass.config.path(DOMAIN, "test-mac", CONF_SHC_CERT),
- "ssl_key": hass.config.path(DOMAIN, "test-mac", CONF_SHC_KEY),
+ "ssl_certificate": hass.config.path(DOMAIN, CONF_SHC_CERT),
+ "ssl_key": hass.config.path(DOMAIN, CONF_SHC_KEY),
"token": "abc:123",
"hostname": "123",
}
@@ -708,7 +708,6 @@ async def test_reauth(hass: HomeAssistant) -> None:
async def test_tls_assets_writer(hass: HomeAssistant) -> None:
"""Test we write tls assets to correct location."""
- unique_id = "test-mac"
assets = {
"token": "abc:123",
"cert": b"content_cert",
@@ -720,163 +719,14 @@ async def test_tls_assets_writer(hass: HomeAssistant) -> None:
"homeassistant.components.bosch_shc.config_flow.open", mock_open()
) as mocked_file,
):
- write_tls_asset(hass, unique_id, CONF_SHC_CERT, assets["cert"])
+ write_tls_asset(hass, CONF_SHC_CERT, assets["cert"])
mocked_file.assert_called_with(
- hass.config.path(DOMAIN, unique_id, CONF_SHC_CERT), "w", encoding="utf8"
+ hass.config.path(DOMAIN, CONF_SHC_CERT), "w", encoding="utf8"
)
mocked_file().write.assert_called_with("content_cert")
- write_tls_asset(hass, unique_id, CONF_SHC_KEY, assets["key"])
+ write_tls_asset(hass, CONF_SHC_KEY, assets["key"])
mocked_file.assert_called_with(
- hass.config.path(DOMAIN, unique_id, CONF_SHC_KEY), "w", encoding="utf8"
+ hass.config.path(DOMAIN, CONF_SHC_KEY), "w", encoding="utf8"
)
mocked_file().write.assert_called_with("content_key")
-
-
-@pytest.mark.usefixtures("mock_zeroconf")
-async def test_register_multiple_controllers(hass: HomeAssistant) -> None:
- """Test register multiple controllers.
-
- Each registered controller must get its own key/certificate pair,
- which must not get overwritten when a new controller is added.
- """
-
- controller_1 = {
- "hostname": "shc111111",
- "mac": "test-mac1",
- "host": "1.1.1.1",
- "register": {
- "token": "abc:shc111111",
- "cert": b"content_cert1",
- "key": b"content_key1",
- },
- }
- controller_2 = {
- "hostname": "shc222222",
- "mac": "test-mac2",
- "host": "2.2.2.2",
- "register": {
- "token": "abc:shc222222",
- "cert": b"content_cert2",
- "key": b"content_key2",
- },
- }
-
- # Set up controller 1
- ctrl_1_result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
-
- with (
- patch(
- "boschshcpy.session.SHCSession.mdns_info",
- return_value=SHCInformation,
- ),
- patch(
- "boschshcpy.information.SHCInformation.name",
- new_callable=PropertyMock,
- return_value=controller_1["hostname"],
- ),
- patch(
- "boschshcpy.information.SHCInformation.unique_id",
- new_callable=PropertyMock,
- return_value=controller_1["mac"],
- ),
- ):
- ctrl_1_result2 = await hass.config_entries.flow.async_configure(
- ctrl_1_result["flow_id"],
- {"host": controller_1["host"]},
- )
-
- with (
- patch(
- "boschshcpy.register_client.SHCRegisterClient.register",
- return_value=controller_1["register"],
- ),
- patch("os.mkdir"),
- patch("homeassistant.components.bosch_shc.config_flow.open"),
- patch("boschshcpy.session.SHCSession.authenticate"),
- patch(
- "homeassistant.components.bosch_shc.async_setup_entry",
- return_value=True,
- ),
- ):
- ctrl_1_result3 = await hass.config_entries.flow.async_configure(
- ctrl_1_result2["flow_id"],
- {"password": "test"},
- )
- await hass.async_block_till_done()
-
- assert ctrl_1_result3["type"] is FlowResultType.CREATE_ENTRY
- assert ctrl_1_result3["title"] == "shc111111"
- assert ctrl_1_result3["context"]["unique_id"] == controller_1["mac"]
- assert ctrl_1_result3["data"] == {
- "host": "1.1.1.1",
- "ssl_certificate": hass.config.path(DOMAIN, controller_1["mac"], CONF_SHC_CERT),
- "ssl_key": hass.config.path(DOMAIN, controller_1["mac"], CONF_SHC_KEY),
- "token": "abc:shc111111",
- "hostname": "shc111111",
- }
-
- # Set up controller 2
- ctrl_2_result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}
- )
-
- with (
- patch(
- "boschshcpy.session.SHCSession.mdns_info",
- return_value=SHCInformation,
- ),
- patch(
- "boschshcpy.information.SHCInformation.name",
- new_callable=PropertyMock,
- return_value=controller_2["hostname"],
- ),
- patch(
- "boschshcpy.information.SHCInformation.unique_id",
- new_callable=PropertyMock,
- return_value=controller_2["mac"],
- ),
- ):
- ctrl_2_result2 = await hass.config_entries.flow.async_configure(
- ctrl_2_result["flow_id"],
- {"host": controller_2["host"]},
- )
-
- with (
- patch(
- "boschshcpy.register_client.SHCRegisterClient.register",
- return_value=controller_2["register"],
- ),
- patch("os.mkdir"),
- patch("homeassistant.components.bosch_shc.config_flow.open"),
- patch("boschshcpy.session.SHCSession.authenticate"),
- patch(
- "homeassistant.components.bosch_shc.async_setup_entry",
- return_value=True,
- ),
- ):
- ctrl_2_result3 = await hass.config_entries.flow.async_configure(
- ctrl_2_result2["flow_id"],
- {"password": "test"},
- )
- await hass.async_block_till_done()
-
- assert ctrl_2_result3["type"] is FlowResultType.CREATE_ENTRY
- assert ctrl_2_result3["title"] == "shc222222"
- assert ctrl_2_result3["context"]["unique_id"] == controller_2["mac"]
- assert ctrl_2_result3["data"] == {
- "host": "2.2.2.2",
- "ssl_certificate": hass.config.path(DOMAIN, controller_2["mac"], CONF_SHC_CERT),
- "ssl_key": hass.config.path(DOMAIN, controller_2["mac"], CONF_SHC_KEY),
- "token": "abc:shc222222",
- "hostname": "shc222222",
- }
-
- # Check that each controller has its own key/certificate pair
- assert (
- ctrl_1_result3["data"]["ssl_certificate"]
- != ctrl_2_result3["data"]["ssl_certificate"]
- )
- assert ctrl_1_result3["data"]["ssl_key"] != ctrl_2_result3["data"]["ssl_key"]
diff --git a/tests/components/bring/fixtures/items_invitation.json b/tests/components/bring/fixtures/items_invitation.json
deleted file mode 100644
index 82ef623e439..00000000000
--- a/tests/components/bring/fixtures/items_invitation.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "uuid": "77a151f8-77c4-47a3-8295-c750a0e69d4f",
- "status": "INVITATION",
- "purchase": [
- {
- "uuid": "b5d0790b-5f32-4d5c-91da-e29066f167de",
- "itemId": "Paprika",
- "specification": "Rot",
- "attributes": [
- {
- "type": "PURCHASE_CONDITIONS",
- "content": {
- "urgent": true,
- "convenient": true,
- "discounted": true
- }
- }
- ]
- },
- {
- "uuid": "72d370ab-d8ca-4e41-b956-91df94795b4e",
- "itemId": "Pouletbrüstli",
- "specification": "Bio",
- "attributes": [
- {
- "type": "PURCHASE_CONDITIONS",
- "content": {
- "urgent": true,
- "convenient": true,
- "discounted": true
- }
- }
- ]
- }
- ],
- "recently": [
- {
- "uuid": "fc8db30a-647e-4e6c-9d71-3b85d6a2d954",
- "itemId": "Ananas",
- "specification": "",
- "attributes": []
- }
- ]
-}
diff --git a/tests/components/bring/fixtures/items_shared.json b/tests/components/bring/fixtures/items_shared.json
deleted file mode 100644
index 9ac999729d3..00000000000
--- a/tests/components/bring/fixtures/items_shared.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "uuid": "77a151f8-77c4-47a3-8295-c750a0e69d4f",
- "status": "SHARED",
- "purchase": [
- {
- "uuid": "b5d0790b-5f32-4d5c-91da-e29066f167de",
- "itemId": "Paprika",
- "specification": "Rot",
- "attributes": [
- {
- "type": "PURCHASE_CONDITIONS",
- "content": {
- "urgent": true,
- "convenient": true,
- "discounted": true
- }
- }
- ]
- },
- {
- "uuid": "72d370ab-d8ca-4e41-b956-91df94795b4e",
- "itemId": "Pouletbrüstli",
- "specification": "Bio",
- "attributes": [
- {
- "type": "PURCHASE_CONDITIONS",
- "content": {
- "urgent": true,
- "convenient": true,
- "discounted": true
- }
- }
- ]
- }
- ],
- "recently": [
- {
- "uuid": "fc8db30a-647e-4e6c-9d71-3b85d6a2d954",
- "itemId": "Ananas",
- "specification": "",
- "attributes": []
- }
- ]
-}
diff --git a/tests/components/bring/snapshots/test_sensor.ambr b/tests/components/bring/snapshots/test_sensor.ambr
index 97e1d1b4bd9..08e554632e9 100644
--- a/tests/components/bring/snapshots/test_sensor.ambr
+++ b/tests/components/bring/snapshots/test_sensor.ambr
@@ -46,64 +46,6 @@
'state': '2',
})
# ---
-# name: test_setup[sensor.baumarkt_list_access-entry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': dict({
- 'options': list([
- 'registered',
- 'shared',
- 'invitation',
- ]),
- }),
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'sensor',
- 'entity_category': ,
- 'entity_id': 'sensor.baumarkt_list_access',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'labels': set({
- }),
- 'name': None,
- 'options': dict({
- }),
- 'original_device_class': ,
- 'original_icon': None,
- 'original_name': 'List access',
- 'platform': 'bring',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': ,
- 'unique_id': '00000000-00000000-00000000-00000000_b4776778-7f6c-496e-951b-92a35d3db0dd_list_access',
- 'unit_of_measurement': None,
- })
-# ---
-# name: test_setup[sensor.baumarkt_list_access-state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'device_class': 'enum',
- 'friendly_name': 'Baumarkt List access',
- 'options': list([
- 'registered',
- 'shared',
- 'invitation',
- ]),
- }),
- 'context': ,
- 'entity_id': 'sensor.baumarkt_list_access',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'registered',
- })
-# ---
# name: test_setup[sensor.baumarkt_on_occasion-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -337,64 +279,6 @@
'state': '2',
})
# ---
-# name: test_setup[sensor.einkauf_list_access-entry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': dict({
- 'options': list([
- 'registered',
- 'shared',
- 'invitation',
- ]),
- }),
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'sensor',
- 'entity_category': ,
- 'entity_id': 'sensor.einkauf_list_access',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'labels': set({
- }),
- 'name': None,
- 'options': dict({
- }),
- 'original_device_class': ,
- 'original_icon': None,
- 'original_name': 'List access',
- 'platform': 'bring',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': ,
- 'unique_id': '00000000-00000000-00000000-00000000_e542eef6-dba7-4c31-a52c-29e6ab9d83a5_list_access',
- 'unit_of_measurement': None,
- })
-# ---
-# name: test_setup[sensor.einkauf_list_access-state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'device_class': 'enum',
- 'friendly_name': 'Einkauf List access',
- 'options': list([
- 'registered',
- 'shared',
- 'invitation',
- ]),
- }),
- 'context': ,
- 'entity_id': 'sensor.einkauf_list_access',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'registered',
- })
-# ---
# name: test_setup[sensor.einkauf_on_occasion-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
diff --git a/tests/components/bring/test_sensor.py b/tests/components/bring/test_sensor.py
index 974818ccedf..a36b0163165 100644
--- a/tests/components/bring/test_sensor.py
+++ b/tests/components/bring/test_sensor.py
@@ -1,18 +1,17 @@
"""Test for sensor platform of the Bring! integration."""
from collections.abc import Generator
-from unittest.mock import AsyncMock, patch
+from unittest.mock import patch
import pytest
from syrupy.assertion import SnapshotAssertion
-from homeassistant.components.bring.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
-from tests.common import MockConfigEntry, load_json_object_fixture, snapshot_platform
+from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture(autouse=True)
@@ -43,34 +42,3 @@ async def test_setup(
await snapshot_platform(
hass, entity_registry, snapshot, bring_config_entry.entry_id
)
-
-
-@pytest.mark.parametrize(
- ("fixture", "entity_state"),
- [
- ("items_invitation", "invitation"),
- ("items_shared", "shared"),
- ("items", "registered"),
- ],
-)
-async def test_list_access_states(
- hass: HomeAssistant,
- bring_config_entry: MockConfigEntry,
- mock_bring_client: AsyncMock,
- fixture: str,
- entity_state: str,
-) -> None:
- """Snapshot test states of list access sensor."""
-
- mock_bring_client.get_list.return_value = load_json_object_fixture(
- f"{fixture}.json", DOMAIN
- )
-
- bring_config_entry.add_to_hass(hass)
- await hass.config_entries.async_setup(bring_config_entry.entry_id)
- await hass.async_block_till_done()
-
- assert bring_config_entry.state is ConfigEntryState.LOADED
-
- assert (state := hass.states.get("sensor.einkauf_list_access"))
- assert state.state == entity_state
diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py
index 929e2f083e9..0dc179061b4 100644
--- a/tests/components/brother/test_config_flow.py
+++ b/tests/components/brother/test_config_flow.py
@@ -261,7 +261,7 @@ async def test_reconfigure_successful(
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reconfigure"
+ assert result["step_id"] == "reconfigure_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -297,7 +297,7 @@ async def test_reconfigure_not_successful(
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reconfigure"
+ assert result["step_id"] == "reconfigure_confirm"
mock_brother_client.async_update.side_effect = exc
@@ -307,7 +307,7 @@ async def test_reconfigure_not_successful(
)
assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reconfigure"
+ assert result["step_id"] == "reconfigure_confirm"
assert result["errors"] == {"base": base_error}
mock_brother_client.async_update.side_effect = None
@@ -336,7 +336,7 @@ async def test_reconfigure_invalid_hostname(
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reconfigure"
+ assert result["step_id"] == "reconfigure_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -344,7 +344,7 @@ async def test_reconfigure_invalid_hostname(
)
assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reconfigure"
+ assert result["step_id"] == "reconfigure_confirm"
assert result["errors"] == {CONF_HOST: "wrong_host"}
@@ -359,7 +359,7 @@ async def test_reconfigure_not_the_same_device(
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reconfigure"
+ assert result["step_id"] == "reconfigure_confirm"
mock_brother_client.serial = "9876543210"
@@ -369,5 +369,5 @@ async def test_reconfigure_not_the_same_device(
)
assert result["type"] is FlowResultType.FORM
- assert result["step_id"] == "reconfigure"
+ assert result["step_id"] == "reconfigure_confirm"
assert result["errors"] == {"base": "another_device"}
diff --git a/tests/components/bsblan/fixtures/state.json b/tests/components/bsblan/fixtures/state.json
index 8c458e173d4..51d4cf2e136 100644
--- a/tests/components/bsblan/fixtures/state.json
+++ b/tests/components/bsblan/fixtures/state.json
@@ -97,14 +97,5 @@
"dataType": 1,
"readonly": 1,
"unit": ""
- },
- "room1_temp_setpoint_boost": {
- "name": "Room 1 Temp Setpoint Boost",
- "error": 0,
- "value": "22.5",
- "desc": "Boost",
- "dataType": 1,
- "readonly": 1,
- "unit": "°C"
}
}
diff --git a/tests/components/bsblan/snapshots/test_diagnostics.ambr b/tests/components/bsblan/snapshots/test_diagnostics.ambr
index 9fabd373205..c1d152056ec 100644
--- a/tests/components/bsblan/snapshots/test_diagnostics.ambr
+++ b/tests/components/bsblan/snapshots/test_diagnostics.ambr
@@ -6,103 +6,60 @@
'current_temperature': dict({
'data_type': 0,
'desc': '',
- 'error': 0,
'name': 'Room temp 1 actual value',
- 'precision': None,
- 'readonly': 1,
- 'readwrite': 0,
'unit': '°C',
- 'value': 18.6,
+ 'value': '18.6',
}),
'outside_temperature': dict({
'data_type': 0,
'desc': '',
- 'error': 0,
'name': 'Outside temp sensor local',
- 'precision': None,
- 'readonly': 0,
- 'readwrite': 0,
'unit': '°C',
- 'value': 6.1,
+ 'value': '6.1',
}),
}),
'state': dict({
'current_temperature': dict({
'data_type': 0,
'desc': '',
- 'error': 0,
'name': 'Room temp 1 actual value',
- 'precision': None,
- 'readonly': 1,
- 'readwrite': 0,
'unit': '°C',
- 'value': 18.6,
+ 'value': '18.6',
}),
'hvac_action': dict({
'data_type': 1,
'desc': 'Raumtemp’begrenzung',
- 'error': 0,
'name': 'Status heating circuit 1',
- 'precision': None,
- 'readonly': 1,
- 'readwrite': 0,
'unit': '',
- 'value': 122,
+ 'value': '122',
}),
'hvac_mode': dict({
'data_type': 1,
'desc': 'Komfort',
- 'error': 0,
'name': 'Operating mode',
- 'precision': None,
- 'readonly': 0,
- 'readwrite': 0,
'unit': '',
'value': 'heat',
}),
'hvac_mode2': dict({
'data_type': 1,
'desc': 'Reduziert',
- 'error': 0,
'name': 'Operating mode',
- 'precision': None,
- 'readonly': 0,
- 'readwrite': 0,
'unit': '',
- 'value': 2,
- }),
- 'room1_temp_setpoint_boost': dict({
- 'data_type': 1,
- 'desc': 'Boost',
- 'error': 0,
- 'name': 'Room 1 Temp Setpoint Boost',
- 'precision': None,
- 'readonly': 1,
- 'readwrite': 0,
- 'unit': '°C',
- 'value': '22.5',
+ 'value': '2',
}),
'room1_thermostat_mode': dict({
'data_type': 1,
'desc': 'Kein Bedarf',
- 'error': 0,
'name': 'Raumthermostat 1',
- 'precision': None,
- 'readonly': 1,
- 'readwrite': 0,
'unit': '',
- 'value': 0,
+ 'value': '0',
}),
'target_temperature': dict({
'data_type': 0,
'desc': '',
- 'error': 0,
'name': 'Room temperature Comfort setpoint',
- 'precision': None,
- 'readonly': 0,
- 'readwrite': 0,
'unit': '°C',
- 'value': 18.5,
+ 'value': '18.5',
}),
}),
}),
@@ -116,33 +73,21 @@
'controller_family': dict({
'data_type': 0,
'desc': '',
- 'error': 0,
'name': 'Device family',
- 'precision': None,
- 'readonly': 0,
- 'readwrite': 0,
'unit': '',
- 'value': 211,
+ 'value': '211',
}),
'controller_variant': dict({
'data_type': 0,
'desc': '',
- 'error': 0,
'name': 'Device variant',
- 'precision': None,
- 'readonly': 0,
- 'readwrite': 0,
'unit': '',
- 'value': 127,
+ 'value': '127',
}),
'device_identification': dict({
'data_type': 7,
'desc': '',
- 'error': 0,
'name': 'Gerte-Identifikation',
- 'precision': None,
- 'readonly': 0,
- 'readwrite': 0,
'unit': '',
'value': 'RVS21.831F/127',
}),
@@ -151,24 +96,16 @@
'max_temp': dict({
'data_type': 0,
'desc': '',
- 'error': 0,
'name': 'Summer/winter changeover temp heat circuit 1',
- 'precision': None,
- 'readonly': 0,
- 'readwrite': 0,
'unit': '°C',
- 'value': 20.0,
+ 'value': '20.0',
}),
'min_temp': dict({
'data_type': 0,
'desc': '',
- 'error': 0,
'name': 'Room temp frost protection setpoint',
- 'precision': None,
- 'readonly': 0,
- 'readwrite': 0,
'unit': '°C',
- 'value': 8.0,
+ 'value': '8.0',
}),
}),
})
diff --git a/tests/components/cambridge_audio/conftest.py b/tests/components/cambridge_audio/conftest.py
index 33a9ded70e3..f17ff0cca3f 100644
--- a/tests/components/cambridge_audio/conftest.py
+++ b/tests/components/cambridge_audio/conftest.py
@@ -1,25 +1,16 @@
"""Cambridge Audio tests configuration."""
from collections.abc import Generator
-from unittest.mock import AsyncMock, Mock, patch
+from unittest.mock import Mock, patch
-from aiostreammagic.models import (
- AudioOutput,
- Display,
- Info,
- NowPlaying,
- PlayState,
- PresetList,
- Source,
- State,
- Update,
-)
+from aiostreammagic.models import Info, NowPlaying, PlayState, Source, State
import pytest
from homeassistant.components.cambridge_audio.const import DOMAIN
from homeassistant.const import CONF_HOST
from tests.common import MockConfigEntry, load_fixture, load_json_array_fixture
+from tests.components.smhi.common import AsyncMock
@pytest.fixture
@@ -59,17 +50,9 @@ def mock_stream_magic_client() -> Generator[AsyncMock]:
client.now_playing = NowPlaying.from_json(
load_fixture("get_now_playing.json", DOMAIN)
)
- client.display = Display.from_json(load_fixture("get_display.json", DOMAIN))
- client.update = Update.from_json(load_fixture("get_update.json", DOMAIN))
- client.preset_list = PresetList.from_json(
- load_fixture("get_presets_list.json", DOMAIN)
- )
- client.audio_output = AudioOutput.from_json(
- load_fixture("get_audio_output.json", DOMAIN)
- )
client.is_connected = Mock(return_value=True)
client.position_last_updated = client.play_state.position
- client.unregister_state_update_callbacks.return_value = True
+ client.unregister_state_update_callbacks = AsyncMock(return_value=True)
yield client
diff --git a/tests/components/cambridge_audio/fixtures/get_audio_output.json b/tests/components/cambridge_audio/fixtures/get_audio_output.json
deleted file mode 100644
index e38ae037307..00000000000
--- a/tests/components/cambridge_audio/fixtures/get_audio_output.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "outputs": [
- {
- "id": "speaker_a",
- "name": "Speaker A"
- },
- {
- "id": "speaker_b",
- "name": "Speaker B"
- },
- {
- "id": "headphones",
- "name": "Headphones"
- }
- ]
-}
diff --git a/tests/components/cambridge_audio/fixtures/get_display.json b/tests/components/cambridge_audio/fixtures/get_display.json
deleted file mode 100644
index 73cbf5a60b3..00000000000
--- a/tests/components/cambridge_audio/fixtures/get_display.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "brightness": "bright"
-}
diff --git a/tests/components/cambridge_audio/fixtures/get_presets_list.json b/tests/components/cambridge_audio/fixtures/get_presets_list.json
deleted file mode 100644
index 87d49e9fd30..00000000000
--- a/tests/components/cambridge_audio/fixtures/get_presets_list.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "start": 1,
- "end": 99,
- "max_presets": 99,
- "presettable": true,
- "presets": [
- {
- "id": 1,
- "name": "Chicago House Radio",
- "type": "Radio",
- "class": "stream.radio",
- "state": "OK",
- "is_playing": false,
- "art_url": "https://static.airable.io/43/68/432868.png",
- "airable_radio_id": 5317566146608442
- },
- {
- "id": 2,
- "name": "Spotify: Good & Evil",
- "type": "Spotify",
- "class": "stream.service.spotify",
- "state": "OK",
- "is_playing": true,
- "art_url": "https://i.scdn.co/image/ab67616d0000b27325a5a1ed28871e8e53e62d59"
- },
- {
- "id": 3,
- "name": "Unknown Preset Type",
- "type": "Unknown",
- "class": "stream.unknown",
- "state": "OK"
- }
- ]
-}
diff --git a/tests/components/cambridge_audio/fixtures/get_update.json b/tests/components/cambridge_audio/fixtures/get_update.json
deleted file mode 100644
index a6fec6265c0..00000000000
--- a/tests/components/cambridge_audio/fixtures/get_update.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "early_update": false,
- "update_available": false,
- "updating": false
-}
diff --git a/tests/components/cambridge_audio/snapshots/test_diagnostics.ambr b/tests/components/cambridge_audio/snapshots/test_diagnostics.ambr
index 1ba9c4093f6..c554785006e 100644
--- a/tests/components/cambridge_audio/snapshots/test_diagnostics.ambr
+++ b/tests/components/cambridge_audio/snapshots/test_diagnostics.ambr
@@ -1,196 +1,51 @@
# serializer version: 1
# name: test_entry_diagnostics
dict({
- 'display': dict({
- 'brightness': 'bright',
- }),
'info': dict({
- 'api_version': '1.8',
- 'locale': 'en_GB',
- 'model': 'CXNv2',
- 'name': 'Cambridge Audio CXNv2',
- 'timezone': 'America/Chicago',
- 'udn': '02680b5c-1320-4d54-9f7c-3cfe915ad4c3',
- 'unit_id': '0020c2d8',
- }),
- 'now_playing': dict({
- 'controls': list([
- 'play_pause',
- 'track_next',
- 'track_previous',
- ]),
- }),
- 'play_state': dict({
- 'metadata': dict({
- 'album': "Greatest Hits: God's Favorite Band",
- 'art_url': 'http://192.168.20.218:80/album-art-2d89?id=1:246',
- 'artist': 'Green Day',
- 'bitrate': None,
- 'class_name': 'md.track',
- 'codec': 'ALAC',
- 'duration': 232,
- 'encoding': None,
- 'lossless': True,
- 'mqa': 'none',
- 'name': 'AirPlay',
- 'radio_id': None,
- 'sample_format': None,
- 'sample_rate': 44100,
- 'signal': None,
- 'source': 'AIRPLAY',
- 'station': None,
- 'title': 'Holiday',
- }),
- 'mode_repeat': 'off',
- 'mode_shuffle': 'off',
- 'position': 179,
- 'presettable': False,
- 'state': 'play',
- }),
- 'presets_list': dict({
- 'end': 99,
- 'max_presets': 99,
- 'presets': list([
- dict({
- 'airable_radio_id': 5317566146608442,
- 'art_url': 'https://static.airable.io/43/68/432868.png',
- 'is_playing': False,
- 'name': 'Chicago House Radio',
- 'preset_class': 'stream.radio',
- 'preset_id': 1,
- 'state': 'OK',
- 'type': 'Radio',
- }),
- dict({
- 'airable_radio_id': None,
- 'art_url': 'https://i.scdn.co/image/ab67616d0000b27325a5a1ed28871e8e53e62d59',
- 'is_playing': True,
- 'name': 'Spotify: Good & Evil',
- 'preset_class': 'stream.service.spotify',
- 'preset_id': 2,
- 'state': 'OK',
- 'type': 'Spotify',
- }),
- dict({
- 'airable_radio_id': None,
- 'art_url': None,
- 'is_playing': False,
- 'name': 'Unknown Preset Type',
- 'preset_class': 'stream.unknown',
- 'preset_id': 3,
- 'state': 'OK',
- 'type': 'Unknown',
- }),
- ]),
- 'presettable': True,
- 'start': 1,
+ '__type': "",
+ 'repr': "Info(name='Cambridge Audio CXNv2', model='CXNv2', timezone='America/Chicago', locale='en_GB', udn='02680b5c-1320-4d54-9f7c-3cfe915ad4c3', unit_id='0020c2d8', api_version='1.8')",
}),
'sources': list([
dict({
- 'default_name': 'Internet Radio',
- 'description': 'Internet Radio',
- 'description_locale': 'Internet Radio',
- 'id': 'IR',
- 'name': 'Internet Radio',
- 'nameable': False,
- 'preferred_order': 9,
- 'ui_selectable': False,
+ '__type': "",
+ 'repr': "Source(id='IR', name='Internet Radio', default_name='Internet Radio', nameable=False, ui_selectable=False, description='Internet Radio', description_locale='Internet Radio', preferred_order=9)",
}),
dict({
- 'default_name': 'USB Audio',
- 'description': 'USB Audio',
- 'description_locale': 'USB Audio',
- 'id': 'USB_AUDIO',
- 'name': 'USB Audio',
- 'nameable': True,
- 'preferred_order': 1,
- 'ui_selectable': True,
+ '__type': "",
+ 'repr': "Source(id='USB_AUDIO', name='USB Audio', default_name='USB Audio', nameable=True, ui_selectable=True, description='USB Audio', description_locale='USB Audio', preferred_order=1)",
}),
dict({
- 'default_name': 'D2',
- 'description': 'Digital Co-axial',
- 'description_locale': 'Digital Co-axial',
- 'id': 'SPDIF_COAX',
- 'name': 'D2',
- 'nameable': True,
- 'preferred_order': 3,
- 'ui_selectable': False,
+ '__type': "",
+ 'repr': "Source(id='SPDIF_COAX', name='D2', default_name='D2', nameable=True, ui_selectable=False, description='Digital Co-axial', description_locale='Digital Co-axial', preferred_order=3)",
}),
dict({
- 'default_name': 'D1',
- 'description': 'Digital Optical',
- 'description_locale': 'Digital Optical',
- 'id': 'SPDIF_TOSLINK',
- 'name': 'D1',
- 'nameable': True,
- 'preferred_order': 2,
- 'ui_selectable': False,
+ '__type': "",
+ 'repr': "Source(id='SPDIF_TOSLINK', name='D1', default_name='D1', nameable=True, ui_selectable=False, description='Digital Optical', description_locale='Digital Optical', preferred_order=2)",
}),
dict({
- 'default_name': 'Media Library',
- 'description': 'Media Player',
- 'description_locale': 'Media Player',
- 'id': 'MEDIA_PLAYER',
- 'name': 'Media Library',
- 'nameable': False,
- 'preferred_order': 10,
- 'ui_selectable': True,
+ '__type': "",
+ 'repr': "Source(id='MEDIA_PLAYER', name='Media Library', default_name='Media Library', nameable=False, ui_selectable=True, description='Media Player', description_locale='Media Player', preferred_order=10)",
}),
dict({
- 'default_name': 'AirPlay',
- 'description': 'AirPlay',
- 'description_locale': 'AirPlay',
- 'id': 'AIRPLAY',
- 'name': 'AirPlay',
- 'nameable': False,
- 'preferred_order': 11,
- 'ui_selectable': True,
+ '__type': "",
+ 'repr': "Source(id='AIRPLAY', name='AirPlay', default_name='AirPlay', nameable=False, ui_selectable=True, description='AirPlay', description_locale='AirPlay', preferred_order=11)",
}),
dict({
- 'default_name': 'Spotify',
- 'description': 'Spotify',
- 'description_locale': 'Spotify',
- 'id': 'SPOTIFY',
- 'name': 'Spotify',
- 'nameable': False,
- 'preferred_order': 6,
- 'ui_selectable': True,
+ '__type': "",
+ 'repr': "Source(id='SPOTIFY', name='Spotify', default_name='Spotify', nameable=False, ui_selectable=True, description='Spotify', description_locale='Spotify', preferred_order=6)",
}),
dict({
- 'default_name': 'Chromecast built-in',
- 'description': 'Chromecast built-in',
- 'description_locale': 'Chromecast built-in',
- 'id': 'CAST',
- 'name': 'Chromecast built-in',
- 'nameable': False,
- 'preferred_order': 8,
- 'ui_selectable': True,
+ '__type': "",
+ 'repr': "Source(id='CAST', name='Chromecast built-in', default_name='Chromecast built-in', nameable=False, ui_selectable=True, description='Chromecast built-in', description_locale='Chromecast built-in', preferred_order=8)",
}),
dict({
- 'default_name': 'Roon Ready',
- 'description': 'Roon Ready',
- 'description_locale': 'Roon Ready',
- 'id': 'ROON',
- 'name': 'Roon Ready',
- 'nameable': False,
- 'preferred_order': 5,
- 'ui_selectable': False,
+ '__type': "",
+ 'repr': "Source(id='ROON', name='Roon Ready', default_name='Roon Ready', nameable=False, ui_selectable=False, description='Roon Ready', description_locale='Roon Ready', preferred_order=5)",
}),
dict({
- 'default_name': 'TIDAL Connect',
- 'description': 'TIDAL',
- 'description_locale': 'TIDAL',
- 'id': 'TIDAL',
- 'name': 'TIDAL Connect',
- 'nameable': False,
- 'preferred_order': 7,
- 'ui_selectable': False,
+ '__type': "",
+ 'repr': "Source(id='TIDAL', name='TIDAL Connect', default_name='TIDAL Connect', nameable=False, ui_selectable=False, description='TIDAL', description_locale='TIDAL', preferred_order=7)",
}),
]),
- 'update': dict({
- 'early_update': False,
- 'update_available': False,
- 'updating': False,
- }),
})
# ---
diff --git a/tests/components/cambridge_audio/snapshots/test_select.ambr b/tests/components/cambridge_audio/snapshots/test_select.ambr
deleted file mode 100644
index b40c8a8d5c4..00000000000
--- a/tests/components/cambridge_audio/snapshots/test_select.ambr
+++ /dev/null
@@ -1,115 +0,0 @@
-# serializer version: 1
-# name: test_all_entities[select.cambridge_audio_cxnv2_audio_output-entry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': dict({
- 'options': list([
- 'Speaker A',
- 'Speaker B',
- 'Headphones',
- ]),
- }),
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'select',
- 'entity_category': ,
- 'entity_id': 'select.cambridge_audio_cxnv2_audio_output',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'labels': set({
- }),
- 'name': None,
- 'options': dict({
- }),
- 'original_device_class': None,
- 'original_icon': None,
- 'original_name': 'Audio output',
- 'platform': 'cambridge_audio',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': 'audio_output',
- 'unique_id': '0020c2d8-audio_output',
- 'unit_of_measurement': None,
- })
-# ---
-# name: test_all_entities[select.cambridge_audio_cxnv2_audio_output-state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'friendly_name': 'Cambridge Audio CXNv2 Audio output',
- 'options': list([
- 'Speaker A',
- 'Speaker B',
- 'Headphones',
- ]),
- }),
- 'context': ,
- 'entity_id': 'select.cambridge_audio_cxnv2_audio_output',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'unknown',
- })
-# ---
-# name: test_all_entities[select.cambridge_audio_cxnv2_display_brightness-entry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': dict({
- 'options': list([
- 'bright',
- 'dim',
- 'off',
- ]),
- }),
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'select',
- 'entity_category': ,
- 'entity_id': 'select.cambridge_audio_cxnv2_display_brightness',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'labels': set({
- }),
- 'name': None,
- 'options': dict({
- }),
- 'original_device_class': None,
- 'original_icon': None,
- 'original_name': 'Display brightness',
- 'platform': 'cambridge_audio',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': 'display_brightness',
- 'unique_id': '0020c2d8-display_brightness',
- 'unit_of_measurement': None,
- })
-# ---
-# name: test_all_entities[select.cambridge_audio_cxnv2_display_brightness-state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'friendly_name': 'Cambridge Audio CXNv2 Display brightness',
- 'options': list([
- 'bright',
- 'dim',
- 'off',
- ]),
- }),
- 'context': ,
- 'entity_id': 'select.cambridge_audio_cxnv2_display_brightness',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'bright',
- })
-# ---
diff --git a/tests/components/cambridge_audio/snapshots/test_switch.ambr b/tests/components/cambridge_audio/snapshots/test_switch.ambr
deleted file mode 100644
index 9bfcd7c6da7..00000000000
--- a/tests/components/cambridge_audio/snapshots/test_switch.ambr
+++ /dev/null
@@ -1,93 +0,0 @@
-# serializer version: 1
-# name: test_all_entities[switch.cambridge_audio_cxnv2_early_update-entry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': None,
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'switch',
- 'entity_category': ,
- 'entity_id': 'switch.cambridge_audio_cxnv2_early_update',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'labels': set({
- }),
- 'name': None,
- 'options': dict({
- }),
- 'original_device_class': None,
- 'original_icon': None,
- 'original_name': 'Early update',
- 'platform': 'cambridge_audio',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': 'early_update',
- 'unique_id': '0020c2d8-early_update',
- 'unit_of_measurement': None,
- })
-# ---
-# name: test_all_entities[switch.cambridge_audio_cxnv2_early_update-state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'friendly_name': 'Cambridge Audio CXNv2 Early update',
- }),
- 'context': ,
- 'entity_id': 'switch.cambridge_audio_cxnv2_early_update',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'off',
- })
-# ---
-# name: test_all_entities[switch.cambridge_audio_cxnv2_pre_amp-entry]
- EntityRegistryEntrySnapshot({
- 'aliases': set({
- }),
- 'area_id': None,
- 'capabilities': None,
- 'config_entry_id': ,
- 'device_class': None,
- 'device_id': ,
- 'disabled_by': None,
- 'domain': 'switch',
- 'entity_category': ,
- 'entity_id': 'switch.cambridge_audio_cxnv2_pre_amp',
- 'has_entity_name': True,
- 'hidden_by': None,
- 'icon': None,
- 'id': ,
- 'labels': set({
- }),
- 'name': None,
- 'options': dict({
- }),
- 'original_device_class': None,
- 'original_icon': None,
- 'original_name': 'Pre-Amp',
- 'platform': 'cambridge_audio',
- 'previous_unique_id': None,
- 'supported_features': 0,
- 'translation_key': 'pre_amp',
- 'unique_id': '0020c2d8-pre_amp',
- 'unit_of_measurement': None,
- })
-# ---
-# name: test_all_entities[switch.cambridge_audio_cxnv2_pre_amp-state]
- StateSnapshot({
- 'attributes': ReadOnlyDict({
- 'friendly_name': 'Cambridge Audio CXNv2 Pre-Amp',
- }),
- 'context': ,
- 'entity_id': 'switch.cambridge_audio_cxnv2_pre_amp',
- 'last_changed': ,
- 'last_reported': ,
- 'last_updated': ,
- 'state': 'off',
- })
-# ---
diff --git a/tests/components/cambridge_audio/test_init.py b/tests/components/cambridge_audio/test_init.py
index 4a8c1b668e2..7dea193d9fd 100644
--- a/tests/components/cambridge_audio/test_init.py
+++ b/tests/components/cambridge_audio/test_init.py
@@ -2,11 +2,9 @@
from unittest.mock import AsyncMock
-from aiostreammagic import StreamMagicError
from syrupy import SnapshotAssertion
from homeassistant.components.cambridge_audio.const import DOMAIN
-from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
@@ -15,20 +13,6 @@ from . import setup_integration
from tests.common import MockConfigEntry
-async def test_config_entry_not_ready(
- hass: HomeAssistant,
- mock_config_entry: MockConfigEntry,
- mock_stream_magic_client: AsyncMock,
-) -> None:
- """Test the Cambridge Audio configuration entry not ready."""
- mock_stream_magic_client.connect = AsyncMock(side_effect=StreamMagicError())
- await setup_integration(hass, mock_config_entry)
-
- assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
-
- mock_stream_magic_client.connect = AsyncMock(return_value=True)
-
-
async def test_device_info(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
diff --git a/tests/components/cambridge_audio/test_media_player.py b/tests/components/cambridge_audio/test_media_player.py
index b857e61c235..391cdd868ec 100644
--- a/tests/components/cambridge_audio/test_media_player.py
+++ b/tests/components/cambridge_audio/test_media_player.py
@@ -11,14 +11,10 @@ from aiostreammagic.models import CallbackType
import pytest
from homeassistant.components.media_player import (
- ATTR_MEDIA_CONTENT_ID,
- ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_REPEAT,
ATTR_MEDIA_SEEK_POSITION,
ATTR_MEDIA_SHUFFLE,
- ATTR_MEDIA_VOLUME_LEVEL,
DOMAIN as MP_DOMAIN,
- SERVICE_PLAY_MEDIA,
MediaPlayerEntityFeature,
RepeatMode,
)
@@ -35,9 +31,6 @@ from homeassistant.const import (
SERVICE_SHUFFLE_SET,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
- SERVICE_VOLUME_DOWN,
- SERVICE_VOLUME_SET,
- SERVICE_VOLUME_UP,
STATE_BUFFERING,
STATE_IDLE,
STATE_OFF,
@@ -47,7 +40,6 @@ from homeassistant.const import (
STATE_STANDBY,
)
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from . import setup_integration
from .const import ENTITY_ID
@@ -57,8 +49,9 @@ from tests.common import MockConfigEntry
async def mock_state_update(client: AsyncMock) -> None:
"""Trigger a callback in the media player."""
- for callback in client.register_state_update_callbacks.call_args_list:
- await callback[0][0](client, CallbackType.STATE)
+ await client.register_state_update_callbacks.call_args[0][0](
+ client, CallbackType.STATE
+ )
async def test_entity_supported_features(
@@ -223,12 +216,12 @@ async def test_media_next_previous_track(
mock_stream_magic_client.previous_track.assert_called_once()
-async def test_shuffle_repeat_set(
+async def test_shuffle_repeat(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_stream_magic_client: AsyncMock,
) -> None:
- """Test shuffle and repeat set service."""
+ """Test shuffle and repeat service."""
await setup_integration(hass, mock_config_entry)
mock_stream_magic_client.now_playing.controls = [
@@ -271,36 +264,6 @@ async def test_shuffle_repeat_set(
mock_stream_magic_client.set_repeat.assert_called_with(CambridgeRepeatMode.ALL)
-async def test_shuffle_repeat_get(
- hass: HomeAssistant,
- mock_config_entry: MockConfigEntry,
- mock_stream_magic_client: AsyncMock,
-) -> None:
- """Test shuffle and repeat get service."""
- await setup_integration(hass, mock_config_entry)
-
- mock_stream_magic_client.play_state.mode_shuffle = None
-
- state = hass.states.get(ENTITY_ID)
- assert state.attributes[ATTR_MEDIA_SHUFFLE] is False
-
- mock_stream_magic_client.play_state.mode_shuffle = ShuffleMode.ALL
-
- await mock_state_update(mock_stream_magic_client)
- await hass.async_block_till_done()
-
- state = hass.states.get(ENTITY_ID)
- assert state.attributes[ATTR_MEDIA_SHUFFLE] is True
-
- mock_stream_magic_client.play_state.mode_repeat = CambridgeRepeatMode.ALL
-
- await mock_state_update(mock_stream_magic_client)
- await hass.async_block_till_done()
-
- state = hass.states.get(ENTITY_ID)
- assert state.attributes[ATTR_MEDIA_REPEAT] == RepeatMode.ALL
-
-
async def test_power_service(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
@@ -339,160 +302,3 @@ async def test_media_seek(
)
mock_stream_magic_client.media_seek.assert_called_once_with(100)
-
-
-async def test_media_volume(
- hass: HomeAssistant,
- mock_config_entry: MockConfigEntry,
- mock_stream_magic_client: AsyncMock,
-) -> None:
- """Test volume service."""
- await setup_integration(hass, mock_config_entry)
-
- mock_stream_magic_client.state.pre_amp_mode = True
-
- # Test volume up
- await hass.services.async_call(
- MP_DOMAIN,
- SERVICE_VOLUME_UP,
- {ATTR_ENTITY_ID: ENTITY_ID},
- )
-
- mock_stream_magic_client.volume_up.assert_called_once()
-
- # Test volume down
- await hass.services.async_call(
- MP_DOMAIN,
- SERVICE_VOLUME_DOWN,
- {ATTR_ENTITY_ID: ENTITY_ID},
- )
-
- mock_stream_magic_client.volume_down.assert_called_once()
-
- await hass.services.async_call(
- MP_DOMAIN,
- SERVICE_VOLUME_SET,
- {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.30},
- )
-
- mock_stream_magic_client.set_volume.assert_called_once_with(30)
-
-
-async def test_play_media_preset_item_id(
- hass: HomeAssistant,
- mock_config_entry: MockConfigEntry,
- mock_stream_magic_client: AsyncMock,
-) -> None:
- """Test playing media with a preset item id."""
- await setup_integration(hass, mock_config_entry)
-
- await hass.services.async_call(
- MP_DOMAIN,
- SERVICE_PLAY_MEDIA,
- {
- ATTR_ENTITY_ID: ENTITY_ID,
- ATTR_MEDIA_CONTENT_TYPE: "preset",
- ATTR_MEDIA_CONTENT_ID: "1",
- },
- blocking=True,
- )
- assert mock_stream_magic_client.recall_preset.call_count == 1
- assert mock_stream_magic_client.recall_preset.call_args_list[0].args[0] == 1
-
- with pytest.raises(ServiceValidationError, match="Missing preset for media_id: 10"):
- await hass.services.async_call(
- MP_DOMAIN,
- SERVICE_PLAY_MEDIA,
- {
- ATTR_ENTITY_ID: ENTITY_ID,
- ATTR_MEDIA_CONTENT_TYPE: "preset",
- ATTR_MEDIA_CONTENT_ID: "10",
- },
- blocking=True,
- )
-
- with pytest.raises(
- ServiceValidationError, match="Preset must be an integer, got: UNKNOWN_PRESET"
- ):
- await hass.services.async_call(
- MP_DOMAIN,
- SERVICE_PLAY_MEDIA,
- {
- ATTR_ENTITY_ID: ENTITY_ID,
- ATTR_MEDIA_CONTENT_TYPE: "preset",
- ATTR_MEDIA_CONTENT_ID: "UNKNOWN_PRESET",
- },
- blocking=True,
- )
-
-
-async def test_play_media_airable_radio_id(
- hass: HomeAssistant,
- mock_config_entry: MockConfigEntry,
- mock_stream_magic_client: AsyncMock,
-) -> None:
- """Test playing media with an airable radio id."""
- await setup_integration(hass, mock_config_entry)
-
- await hass.services.async_call(
- MP_DOMAIN,
- SERVICE_PLAY_MEDIA,
- {
- ATTR_ENTITY_ID: ENTITY_ID,
- ATTR_MEDIA_CONTENT_TYPE: "airable",
- ATTR_MEDIA_CONTENT_ID: "12345678",
- },
- blocking=True,
- )
- assert mock_stream_magic_client.play_radio_airable.call_count == 1
- call_args = mock_stream_magic_client.play_radio_airable.call_args_list[0].args
- assert call_args[0] == "Radio"
- assert call_args[1] == 12345678
-
-
-async def test_play_media_internet_radio(
- hass: HomeAssistant,
- mock_config_entry: MockConfigEntry,
- mock_stream_magic_client: AsyncMock,
-) -> None:
- """Test playing media with a url."""
- await setup_integration(hass, mock_config_entry)
-
- await hass.services.async_call(
- MP_DOMAIN,
- SERVICE_PLAY_MEDIA,
- {
- ATTR_ENTITY_ID: ENTITY_ID,
- ATTR_MEDIA_CONTENT_TYPE: "internet_radio",
- ATTR_MEDIA_CONTENT_ID: "https://example.com",
- },
- blocking=True,
- )
- assert mock_stream_magic_client.play_radio_url.call_count == 1
- call_args = mock_stream_magic_client.play_radio_url.call_args_list[0].args
- assert call_args[0] == "Radio"
- assert call_args[1] == "https://example.com"
-
-
-async def test_play_media_unknown_type(
- hass: HomeAssistant,
- mock_config_entry: MockConfigEntry,
- mock_stream_magic_client: AsyncMock,
-) -> None:
- """Test playing media with an unsupported content type."""
- await setup_integration(hass, mock_config_entry)
-
- with pytest.raises(
- HomeAssistantError,
- match="Unsupported media type for Cambridge Audio device: unsupported_content_type",
- ):
- await hass.services.async_call(
- MP_DOMAIN,
- SERVICE_PLAY_MEDIA,
- {
- ATTR_ENTITY_ID: ENTITY_ID,
- ATTR_MEDIA_CONTENT_TYPE: "unsupported_content_type",
- ATTR_MEDIA_CONTENT_ID: "1",
- },
- blocking=True,
- )
diff --git a/tests/components/cambridge_audio/test_select.py b/tests/components/cambridge_audio/test_select.py
deleted file mode 100644
index 473c4027163..00000000000
--- a/tests/components/cambridge_audio/test_select.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""Tests for the Cambridge Audio select platform."""
-
-from unittest.mock import AsyncMock, patch
-
-import pytest
-from syrupy import SnapshotAssertion
-
-from homeassistant.components.select import (
- DOMAIN as SELECT_DOMAIN,
- SERVICE_SELECT_OPTION,
-)
-from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers import entity_registry as er
-
-from . import setup_integration
-
-from tests.common import MockConfigEntry, snapshot_platform
-
-
-@pytest.mark.usefixtures("entity_registry_enabled_by_default")
-async def test_all_entities(
- hass: HomeAssistant,
- snapshot: SnapshotAssertion,
- mock_stream_magic_client: AsyncMock,
- mock_config_entry: MockConfigEntry,
- entity_registry: er.EntityRegistry,
-) -> None:
- """Test all entities."""
- with patch("homeassistant.components.cambridge_audio.PLATFORMS", [Platform.SELECT]):
- await setup_integration(hass, mock_config_entry)
-
- await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
-
-
-async def test_setting_value(
- hass: HomeAssistant,
- mock_stream_magic_client: AsyncMock,
- mock_config_entry: MockConfigEntry,
-) -> None:
- """Test setting value."""
- await setup_integration(hass, mock_config_entry)
-
- await hass.services.async_call(
- SELECT_DOMAIN,
- SERVICE_SELECT_OPTION,
- {
- ATTR_ENTITY_ID: "select.cambridge_audio_cxnv2_display_brightness",
- ATTR_OPTION: "dim",
- },
- blocking=True,
- )
- mock_stream_magic_client.set_display_brightness.assert_called_once_with("dim")
-
- await hass.services.async_call(
- SELECT_DOMAIN,
- SERVICE_SELECT_OPTION,
- {
- ATTR_ENTITY_ID: "select.cambridge_audio_cxnv2_audio_output",
- ATTR_OPTION: "Speaker A",
- },
- blocking=True,
- )
- mock_stream_magic_client.set_audio_output.assert_called_once_with("speaker_a")
diff --git a/tests/components/cambridge_audio/test_switch.py b/tests/components/cambridge_audio/test_switch.py
deleted file mode 100644
index 3192f198d1f..00000000000
--- a/tests/components/cambridge_audio/test_switch.py
+++ /dev/null
@@ -1,60 +0,0 @@
-"""Tests for the Cambridge Audio switch platform."""
-
-from unittest.mock import AsyncMock, patch
-
-import pytest
-from syrupy import SnapshotAssertion
-
-from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON
-from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, Platform
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers import entity_registry as er
-
-from . import setup_integration
-
-from tests.common import MockConfigEntry, snapshot_platform
-
-
-@pytest.mark.usefixtures("entity_registry_enabled_by_default")
-async def test_all_entities(
- hass: HomeAssistant,
- snapshot: SnapshotAssertion,
- mock_stream_magic_client: AsyncMock,
- mock_config_entry: MockConfigEntry,
- entity_registry: er.EntityRegistry,
-) -> None:
- """Test all entities."""
- with patch("homeassistant.components.cambridge_audio.PLATFORMS", [Platform.SWITCH]):
- await setup_integration(hass, mock_config_entry)
-
- await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
-
-
-async def test_setting_value(
- hass: HomeAssistant,
- mock_stream_magic_client: AsyncMock,
- mock_config_entry: MockConfigEntry,
-) -> None:
- """Test setting value."""
- await setup_integration(hass, mock_config_entry)
-
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_ON,
- {
- ATTR_ENTITY_ID: "switch.cambridge_audio_cxnv2_early_update",
- },
- blocking=True,
- )
- mock_stream_magic_client.set_early_update.assert_called_once_with(True)
- mock_stream_magic_client.set_early_update.reset_mock()
-
- await hass.services.async_call(
- SWITCH_DOMAIN,
- SERVICE_TURN_OFF,
- {
- ATTR_ENTITY_ID: "switch.cambridge_audio_cxnv2_early_update",
- },
- blocking=True,
- )
- mock_stream_magic_client.set_early_update.assert_called_once_with(False)
diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py
index 569756c2640..f7dcf46db01 100644
--- a/tests/components/camera/common.py
+++ b/tests/components/camera/common.py
@@ -6,16 +6,6 @@ components. Instead call the service directly.
from unittest.mock import Mock
-from webrtc_models import RTCIceCandidate
-
-from homeassistant.components.camera import (
- Camera,
- CameraWebRTCProvider,
- WebRTCAnswer,
- WebRTCSendMessage,
-)
-from homeassistant.core import callback
-
EMPTY_8_6_JPEG = b"empty_8_6"
WEBRTC_ANSWER = "a=sendonly"
STREAM_SOURCE = "rtsp://127.0.0.1/stream"
@@ -33,43 +23,3 @@ def mock_turbo_jpeg(
mocked_turbo_jpeg.scale_with_quality.return_value = EMPTY_8_6_JPEG
mocked_turbo_jpeg.encode.return_value = EMPTY_8_6_JPEG
return mocked_turbo_jpeg
-
-
-class SomeTestProvider(CameraWebRTCProvider):
- """Test provider."""
-
- def __init__(self) -> None:
- """Initialize the provider."""
- self._is_supported = True
-
- @property
- def domain(self) -> str:
- """Return the integration domain of the provider."""
- return "some_test"
-
- @callback
- def async_is_supported(self, stream_source: str) -> bool:
- """Determine if the provider supports the stream source."""
- return self._is_supported
-
- async def async_handle_async_webrtc_offer(
- self,
- camera: Camera,
- offer_sdp: str,
- session_id: str,
- send_message: WebRTCSendMessage,
- ) -> None:
- """Handle the WebRTC offer and return the answer via the provided callback.
-
- Return value determines if the offer was handled successfully.
- """
- send_message(WebRTCAnswer(answer="answer"))
-
- async def async_on_webrtc_candidate(
- self, session_id: str, candidate: RTCIceCandidate
- ) -> None:
- """Handle the WebRTC candidate."""
-
- @callback
- def async_close_session(self, session_id: str) -> None:
- """Close the session."""
diff --git a/tests/components/camera/conftest.py b/tests/components/camera/conftest.py
index f0c418711c7..5eda2f1eb55 100644
--- a/tests/components/camera/conftest.py
+++ b/tests/components/camera/conftest.py
@@ -1,30 +1,18 @@
"""Test helpers for camera."""
from collections.abc import AsyncGenerator, Generator
-from unittest.mock import AsyncMock, Mock, PropertyMock, patch
+from unittest.mock import AsyncMock, PropertyMock, patch
import pytest
-from webrtc_models import RTCIceCandidate
from homeassistant.components import camera
from homeassistant.components.camera.const import StreamType
-from homeassistant.components.camera.webrtc import WebRTCAnswer, WebRTCSendMessage
-from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.setup import async_setup_component
-from .common import STREAM_SOURCE, WEBRTC_ANSWER, SomeTestProvider
-
-from tests.common import (
- MockConfigEntry,
- MockModule,
- mock_config_flow,
- mock_integration,
- mock_platform,
- setup_test_component_platform,
-)
+from .common import STREAM_SOURCE, WEBRTC_ANSWER
@pytest.fixture(autouse=True)
@@ -68,37 +56,23 @@ def mock_camera_hls_fixture(mock_camera: None) -> Generator[None]:
yield
-@pytest.fixture
-async def mock_camera_webrtc_frontendtype_only(
- hass: HomeAssistant,
-) -> AsyncGenerator[None]:
+@pytest.fixture(name="mock_camera_web_rtc")
+async def mock_camera_web_rtc_fixture(hass: HomeAssistant) -> AsyncGenerator[None]:
"""Initialize a demo camera platform with WebRTC."""
assert await async_setup_component(
hass, "camera", {camera.DOMAIN: {"platform": "demo"}}
)
await hass.async_block_till_done()
- with patch(
- "homeassistant.components.camera.Camera.frontend_stream_type",
- new_callable=PropertyMock(return_value=StreamType.WEB_RTC),
- ):
- yield
-
-
-@pytest.fixture
-async def mock_camera_webrtc(
- mock_camera_webrtc_frontendtype_only: None,
-) -> AsyncGenerator[None]:
- """Initialize a demo camera platform with WebRTC."""
-
- async def async_handle_async_webrtc_offer(
- offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
- ) -> None:
- send_message(WebRTCAnswer(WEBRTC_ANSWER))
-
- with patch(
- "homeassistant.components.camera.Camera.async_handle_async_webrtc_offer",
- side_effect=async_handle_async_webrtc_offer,
+ with (
+ patch(
+ "homeassistant.components.camera.Camera.frontend_stream_type",
+ new_callable=PropertyMock(return_value=StreamType.WEB_RTC),
+ ),
+ patch(
+ "homeassistant.components.camera.Camera.async_handle_web_rtc_offer",
+ return_value=WEBRTC_ANSWER,
+ ),
):
yield
@@ -153,100 +127,3 @@ def mock_stream_source_fixture() -> Generator[AsyncMock]:
return_value=STREAM_SOURCE,
) as mock_stream_source:
yield mock_stream_source
-
-
-@pytest.fixture
-async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
- """Initialize test WebRTC cameras with native RTC support."""
-
- # Cannot use the fixture mock_camera_web_rtc as it's mocking Camera.async_handle_web_rtc_offer
- # and native support is checked by verify the function "async_handle_web_rtc_offer" was
- # overwritten(implemented) or not
- class BaseCamera(camera.Camera):
- """Base Camera."""
-
- _attr_supported_features: camera.CameraEntityFeature = (
- camera.CameraEntityFeature.STREAM
- )
- _attr_frontend_stream_type: camera.StreamType = camera.StreamType.WEB_RTC
-
- async def stream_source(self) -> str | None:
- return STREAM_SOURCE
-
- class SyncCamera(BaseCamera):
- """Mock Camera with native sync WebRTC support."""
-
- _attr_name = "Sync"
-
- async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
- return WEBRTC_ANSWER
-
- class AsyncCamera(BaseCamera):
- """Mock Camera with native async WebRTC support."""
-
- _attr_name = "Async"
-
- async def async_handle_async_webrtc_offer(
- self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
- ) -> None:
- send_message(WebRTCAnswer(WEBRTC_ANSWER))
-
- async def async_on_webrtc_candidate(
- self, session_id: str, candidate: RTCIceCandidate
- ) -> None:
- """Handle a WebRTC candidate."""
- # Do nothing
-
- domain = "test"
-
- entry = MockConfigEntry(domain=domain)
- entry.add_to_hass(hass)
-
- async def async_setup_entry_init(
- hass: HomeAssistant, config_entry: ConfigEntry
- ) -> bool:
- """Set up test config entry."""
- await hass.config_entries.async_forward_entry_setups(
- config_entry, [camera.DOMAIN]
- )
- return True
-
- async def async_unload_entry_init(
- hass: HomeAssistant, config_entry: ConfigEntry
- ) -> bool:
- """Unload test config entry."""
- await hass.config_entries.async_forward_entry_unload(
- config_entry, camera.DOMAIN
- )
- return True
-
- mock_integration(
- hass,
- MockModule(
- domain,
- async_setup_entry=async_setup_entry_init,
- async_unload_entry=async_unload_entry_init,
- ),
- )
- setup_test_component_platform(
- hass, camera.DOMAIN, [SyncCamera(), AsyncCamera()], from_config_entry=True
- )
- mock_platform(hass, f"{domain}.config_flow", Mock())
-
- with mock_config_flow(domain, ConfigFlow):
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
-
-@pytest.fixture
-async def register_test_provider(
- hass: HomeAssistant,
-) -> AsyncGenerator[SomeTestProvider]:
- """Add WebRTC test provider."""
- await async_setup_component(hass, "camera", {})
-
- provider = SomeTestProvider()
- unsub = camera.async_register_webrtc_provider(hass, provider)
- await hass.async_block_till_done()
- yield provider
- unsub()
diff --git a/tests/components/camera/snapshots/test_init.ambr b/tests/components/camera/snapshots/test_init.ambr
deleted file mode 100644
index eae1c481cc0..00000000000
--- a/tests/components/camera/snapshots/test_init.ambr
+++ /dev/null
@@ -1,127 +0,0 @@
-# serializer version: 1
-# name: test_record_service[/test/recording_{{ entity_id }}.mpg-/test/recording_.mpg-expected_issues1]
- IssueRegistryItemSnapshot({
- 'active': True,
- 'breaks_in_ha_version': '2025.6.0',
- 'created': ,
- 'data': None,
- 'dismissed_version': None,
- 'domain': 'camera',
- 'is_fixable': True,
- 'is_persistent': False,
- 'issue_domain': None,
- 'issue_id': 'deprecated_filename_template_camera.demo_camera_record',
- 'learn_more_url': None,
- 'severity': ,
- 'translation_key': 'deprecated_filename_template',
- 'translation_placeholders': dict({
- 'entity_id': 'camera.demo_camera',
- 'service': 'camera.record',
- }),
- })
-# ---
-# name: test_record_service[/test/recording_{{ entity_id.entity_id }}.mpg-/test/recording_camera.demo_camera.mpg-expected_issues3]
- IssueRegistryItemSnapshot({
- 'active': True,
- 'breaks_in_ha_version': '2025.6.0',
- 'created': ,
- 'data': None,
- 'dismissed_version': None,
- 'domain': 'camera',
- 'is_fixable': True,
- 'is_persistent': False,
- 'issue_domain': None,
- 'issue_id': 'deprecated_filename_template_camera.demo_camera_record',
- 'learn_more_url': None,
- 'severity': ,
- 'translation_key': 'deprecated_filename_template',
- 'translation_placeholders': dict({
- 'entity_id': 'camera.demo_camera',
- 'service': 'camera.record',
- }),
- })
-# ---
-# name: test_record_service[/test/recording_{{ entity_id.name }}.mpg-/test/recording_Demo camera.mpg-expected_issues2]
- IssueRegistryItemSnapshot({
- 'active': True,
- 'breaks_in_ha_version': '2025.6.0',
- 'created': ,
- 'data': None,
- 'dismissed_version': None,
- 'domain': 'camera',
- 'is_fixable': True,
- 'is_persistent': False,
- 'issue_domain': None,
- 'issue_id': 'deprecated_filename_template_camera.demo_camera_record',
- 'learn_more_url': None,
- 'severity': ,
- 'translation_key': 'deprecated_filename_template',
- 'translation_placeholders': dict({
- 'entity_id': 'camera.demo_camera',
- 'service': 'camera.record',
- }),
- })
-# ---
-# name: test_snapshot_service[/test/snapshot_{{ entity_id }}.jpg-/test/snapshot_