Compare commits

..

4 Commits

Author SHA1 Message Date
Abílio Costa 17c548c518 Merge branch 'dev' into radon_device_class 2026-06-30 10:44:16 +01:00
abmantis b39e5c5d7d Update knx snapshots 2026-06-29 20:00:26 +01:00
abmantis 0679af1685 Improve unit system tests 2026-06-29 19:57:58 +01:00
abmantis 0498542340 Add radon device class to number and sensor 2026-06-29 19:33:04 +01:00
130 changed files with 675 additions and 2477 deletions
@@ -1,125 +0,0 @@
name: Set up Python and restore or build the venv
description: >-
Sets up uv and the managed interpreter, then restores the full venv from the
cache. On a miss it rebuilds the venv from requirements instead of
hard-failing, so jobs survive a transient miss when the entry is evicted under
the repo cache size limit. Only the prepare job sets save to true and writes
the cache, so the validating jobs add no extra cache pressure.
inputs:
python-version:
description: The Python version uv should install and use.
required: true
uv-version:
description: The uv version setup-uv should install.
required: true
python-cache-key:
description: The shared python_cache_key from the info job.
required: true
uv-cache-dir:
description: The uv cache directory (env.UV_CACHE_DIR from the caller).
required: true
apt-cache-version:
description: The apt cache version (env.APT_CACHE_VERSION from the caller).
required: true
save:
description: Whether to save the rebuilt venv and uv cache. Only the prepare job should.
default: "false"
outputs:
python-version:
description: The Python version uv reports as installed.
value: ${{ steps.python.outputs.python-version }}
runs:
using: composite
steps:
- name: Set up uv and managed Python
id: python
uses: ./.github/actions/setup-uv-python
with:
uv-version: ${{ inputs.uv-version }}
python-version: ${{ inputs.python-version }}
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
inputs.python-cache-key }}
- name: Generate partial uv restore key
if: steps.cache-venv.outputs.cache-hit != 'true'
id: generate-uv-key
shell: bash
env:
RUNNER_OS: ${{ runner.os }}
RUNNER_ARCH: ${{ runner.arch }}
PYTHON_VERSION: ${{ steps.python.outputs.python-version }}
HASH_FILES: ${{ hashFiles('requirements.txt', 'requirements_all.txt', 'requirements_test.txt', 'homeassistant/package_constraints.txt') }}
run: |
partial_key="${RUNNER_OS}-${RUNNER_ARCH}-${PYTHON_VERSION}-uv-"
echo "partial_key=${partial_key}" >> $GITHUB_OUTPUT
echo "full_key=${partial_key}${HASH_FILES}" >> $GITHUB_OUTPUT
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ inputs.uv-cache-dir }}
key: ${{ steps.generate-uv-key.outputs.full_key }}
restore-keys: ${{ steps.generate-uv-key.outputs.partial_key }}
# Only runs on a cache miss when the venv is rebuilt. Composite steps cannot
# set timeout-minutes, so a stuck apt download is bounded by the job-level
# timeout instead of a per-step cap.
- name: Install additional OS dependencies
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: ./.github/actions/cache-apt-packages
with:
packages: >-
bluez
ffmpeg
libturbojpeg
libxml2-utils
libavcodec-dev
libavdevice-dev
libavfilter-dev
libavformat-dev
libavutil-dev
libswresample-dev
libswscale-dev
libudev-dev
version: ${{ inputs.apt-cache-version }}
execute_install_scripts: true
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
id: create-venv
shell: bash
env:
PYTHON_VERSION: ${{ steps.python.outputs.python-version }}
run: |
uv venv venv --python "${PYTHON_VERSION}"
. venv/bin/activate
python --version
uv pip install -r requirements.txt
uv pip install -r requirements_all.txt -r requirements_test.txt
uv pip install -e . --config-settings editable_mode=compat
- name: Prune uv cache
if: inputs.save == 'true' && steps.cache-venv.outputs.cache-hit != 'true'
shell: bash
run: |
. venv/bin/activate
uv cache prune --ci
- name: Save uv wheel cache
if: inputs.save == 'true' && steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ inputs.uv-cache-dir }}
key: ${{ steps.generate-uv-key.outputs.full_key }}
- name: Save full Python virtual environment
if: always() && inputs.save == 'true' && steps.create-venv.outcome == 'success'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
inputs.python-cache-key }}
@@ -1,46 +0,0 @@
name: Set up uv and managed Python
description: >-
Pins uv (avoids the raw.githubusercontent.com manifest fetch on cache miss)
and proactively installs the requested Python so cached venvs created with
`uv venv` resolve their interpreter symlinks in jobs that only restore the
venv. setup-uv alone only sets UV_PYTHON, it does not actually fetch the
interpreter until uv first uses it, so jobs that just activate the venv
blow up with broken symlinks on cache hit.
inputs:
python-version:
description: The Python version uv should install and use.
required: true
uv-version:
description: The uv version setup-uv should install.
required: true
outputs:
python-version:
description: The Python version uv reports as installed.
value: ${{ steps.uv.outputs.python-version }}
runs:
using: composite
steps:
- name: Set up uv
id: uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
version: ${{ inputs.uv-version }}
python-version: ${{ inputs.python-version }}
# Persist astral's managed Python across jobs so 'uv venv' below is
# fast on the second job onwards.
cache-python: true
# Lint-only and codegen jobs touch no Python deps, so the post-step
# cache save would otherwise abort the job.
ignore-nothing-to-cache: true
# setup-uv only sets UV_PYTHON; it does not fetch the interpreter until uv
# first uses it. Jobs that only restore and activate a cached venv never
# trigger that lazy install, so without this step they hit broken
# interpreter symlinks on a cache hit.
- name: Install Python interpreter
shell: bash
env:
PYTHON_VERSION: ${{ inputs.python-version }}
run: uv python install "${PYTHON_VERSION}"
+236 -93
View File
@@ -37,7 +37,7 @@ on:
type: boolean
env:
CACHE_VERSION: 4
CACHE_VERSION: 3
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.8"
ADDITIONAL_PYTHON_VERSIONS: "[]"
@@ -89,8 +89,6 @@ jobs:
mariadb_groups: ${{ steps.info.outputs.mariadb_groups }}
postgresql_groups: ${{ steps.info.outputs.postgresql_groups }}
python_versions: ${{ steps.info.outputs.python_versions }}
default_python: ${{ steps.info.outputs.default_python }}
uv_version: ${{ steps.info.outputs.uv_version }}
test_full_suite: ${{ steps.info.outputs.test_full_suite }}
test_group_count: ${{ steps.info.outputs.test_group_count }}
test_groups: ${{ steps.info.outputs.test_groups }}
@@ -237,11 +235,6 @@ jobs:
echo "postgresql_groups=${postgresql_groups}" >> $GITHUB_OUTPUT
echo "python_versions: ${all_python_versions}"
echo "python_versions=${all_python_versions}" >> $GITHUB_OUTPUT
echo "default_python: ${default_python}"
echo "default_python=${default_python}" >> $GITHUB_OUTPUT
uv_version=$(grep '^uv==' requirements.txt | cut -d'=' -f3)
echo "uv_version: ${uv_version}"
echo "uv_version=${uv_version}" >> $GITHUB_OUTPUT
echo "test_full_suite: ${test_full_suite}"
echo "test_full_suite=${test_full_suite}" >> $GITHUB_OUTPUT
echo "integrations_glob: ${integrations_glob}"
@@ -351,18 +344,82 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }} and build venv
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: ./.github/actions/restore-or-build-venv
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ matrix.python-version }}
python-cache-key: ${{ needs.info.outputs.python_cache_key }}
uv-cache-dir: ${{ env.UV_CACHE_DIR }}
apt-cache-version: ${{ env.APT_CACHE_VERSION }}
save: "true"
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Generate partial uv restore key
if: steps.cache-venv.outputs.cache-hit != 'true'
id: generate-uv-key
env:
RUNNER_OS: ${{ runner.os }}
RUNNER_ARCH: ${{ runner.arch }}
PYTHON_VERSION: ${{ steps.python.outputs.python-version }}
HASH_FILES: ${{ hashFiles('requirements.txt', 'requirements_all.txt', 'requirements_test.txt', 'homeassistant/package_constraints.txt') }}
run: |
partial_key="${RUNNER_OS}-${RUNNER_ARCH}-${PYTHON_VERSION}-uv-"
echo "partial_key=${partial_key}" >> $GITHUB_OUTPUT
echo "full_key=${partial_key}${HASH_FILES}" >> $GITHUB_OUTPUT
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.UV_CACHE_DIR }}
key: ${{ steps.generate-uv-key.outputs.full_key }}
restore-keys: ${{ steps.generate-uv-key.outputs.partial_key }}
- name: Install additional OS dependencies
if: steps.cache-venv.outputs.cache-hit != 'true'
timeout-minutes: 10
uses: ./.github/actions/cache-apt-packages
with:
packages: >-
bluez
ffmpeg
libturbojpeg
libxml2-utils
libavcodec-dev
libavdevice-dev
libavfilter-dev
libavformat-dev
libavutil-dev
libswresample-dev
libswscale-dev
libudev-dev
version: ${{ env.APT_CACHE_VERSION }}
execute_install_scripts: true
- name: Read uv version from requirements.txt
if: steps.cache-venv.outputs.cache-hit != 'true'
id: read-uv-version
run: |
echo "version=$(grep '^uv==' requirements.txt | cut -d'=' -f3)" >> "$GITHUB_OUTPUT"
- name: Set up uv
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
version: ${{ steps.read-uv-version.outputs.version }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
id: create-venv
run: |
python -m venv venv
. venv/bin/activate
python --version
uv pip install -r requirements.txt
uv pip install -r requirements_all.txt -r requirements_test.txt
uv pip install -e . --config-settings editable_mode=compat
- name: Dump pip freeze
run: |
python -m venv venv
. venv/bin/activate
python --version
uv pip freeze >> pip_freeze.txt
@@ -377,6 +434,26 @@ jobs:
- name: Check dirty
run: |
./script/check_dirty
- name: Prune uv cache
if: steps.cache-venv.outputs.cache-hit != 'true'
id: prune-uv-cache
run: |
. venv/bin/activate
uv cache prune --ci
- name: Save uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.UV_CACHE_DIR }}
key: ${{ steps.generate-uv-key.outputs.full_key }}
- name: Save base Python virtual environment
if: always() && steps.create-venv.outcome == 'success'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
hassfest:
name: Check hassfest
@@ -401,15 +478,21 @@ jobs:
with:
packages: libturbojpeg
version: ${{ env.APT_CACHE_VERSION }}
- name: Set up Python and restore venv
- name: Set up Python
id: python
uses: ./.github/actions/restore-or-build-venv
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ needs.info.outputs.default_python }}
python-cache-key: ${{ needs.info.outputs.python_cache_key }}
uv-cache-dir: ${{ env.UV_CACHE_DIR }}
apt-cache-version: ${{ env.APT_CACHE_VERSION }}
python-version-file: ".python-version"
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Run hassfest
run: |
. venv/bin/activate
@@ -432,15 +515,21 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python and restore venv
- name: Set up Python
id: python
uses: ./.github/actions/restore-or-build-venv
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ needs.info.outputs.default_python }}
python-cache-key: ${{ needs.info.outputs.python_cache_key }}
uv-cache-dir: ${{ env.UV_CACHE_DIR }}
apt-cache-version: ${{ env.APT_CACHE_VERSION }}
python-version-file: ".python-version"
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Run gen_requirements_all.py
run: |
. venv/bin/activate
@@ -464,13 +553,13 @@ jobs:
persist-credentials: false
- name: Set up Python
id: python
uses: ./.github/actions/setup-uv-python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ needs.info.outputs.default_python }}
python-version-file: ".python-version"
check-latest: true
- name: Run gen_copilot_instructions.py
run: |
uv run --no-project python -m script.gen_copilot_instructions validate
python -m script.gen_copilot_instructions validate
dependency-review:
name: Dependency review
@@ -517,15 +606,21 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }} and restore venv
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: ./.github/actions/restore-or-build-venv
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ matrix.python-version }}
python-cache-key: ${{ needs.info.outputs.python_cache_key }}
uv-cache-dir: ${{ env.UV_CACHE_DIR }}
apt-cache-version: ${{ env.APT_CACHE_VERSION }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Extract license data
env:
PYTHON_VERSION: ${{ matrix.python-version }}
@@ -562,15 +657,21 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python and restore venv
- name: Set up Python
id: python
uses: ./.github/actions/restore-or-build-venv
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ needs.info.outputs.default_python }}
python-cache-key: ${{ needs.info.outputs.python_cache_key }}
uv-cache-dir: ${{ env.UV_CACHE_DIR }}
apt-cache-version: ${{ env.APT_CACHE_VERSION }}
python-version-file: ".python-version"
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register pylint problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/pylint.json"
@@ -609,15 +710,21 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python and restore venv
- name: Set up Python
id: python
uses: ./.github/actions/restore-or-build-venv
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ needs.info.outputs.default_python }}
python-cache-key: ${{ needs.info.outputs.python_cache_key }}
uv-cache-dir: ${{ env.UV_CACHE_DIR }}
apt-cache-version: ${{ env.APT_CACHE_VERSION }}
python-version-file: ".python-version"
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register pylint problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/pylint.json"
@@ -654,23 +761,29 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version-file: ".python-version"
check-latest: true
- name: Generate partial mypy restore key
id: generate-mypy-key
run: |
mypy_version=$(cat requirements_test.txt | grep 'mypy.*=' | cut -d '=' -f 3)
echo "version=${mypy_version}" >> $GITHUB_OUTPUT
echo "key=mypy-${MYPY_CACHE_VERSION}-${mypy_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Set up Python and restore venv
id: python
uses: ./.github/actions/restore-or-build-venv
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ needs.info.outputs.default_python }}
python-cache-key: ${{ needs.info.outputs.python_cache_key }}
uv-cache-dir: ${{ env.UV_CACHE_DIR }}
apt-cache-version: ${{ env.APT_CACHE_VERSION }}
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .mypy_cache
key: >-
@@ -725,15 +838,21 @@ jobs:
libturbojpeg
version: ${{ env.APT_CACHE_VERSION }}
execute_install_scripts: true
- name: Set up Python and restore venv
- name: Set up Python
id: python
uses: ./.github/actions/restore-or-build-venv
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ needs.info.outputs.default_python }}
python-cache-key: ${{ needs.info.outputs.python_cache_key }}
uv-cache-dir: ${{ env.UV_CACHE_DIR }}
apt-cache-version: ${{ env.APT_CACHE_VERSION }}
python-version-file: ".python-version"
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Run split_tests.py
env:
TEST_GROUP_COUNT: ${{ needs.info.outputs.test_group_count }}
@@ -784,15 +903,21 @@ jobs:
libxml2-utils
version: ${{ env.APT_CACHE_VERSION }}
execute_install_scripts: true
- name: Set up Python ${{ matrix.python-version }} and restore venv
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: ./.github/actions/restore-or-build-venv
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ matrix.python-version }}
python-cache-key: ${{ needs.info.outputs.python_cache_key }}
uv-cache-dir: ${{ env.UV_CACHE_DIR }}
apt-cache-version: ${{ env.APT_CACHE_VERSION }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/python.json"
@@ -920,15 +1045,21 @@ jobs:
libxml2-utils
version: ${{ env.APT_CACHE_VERSION }}
execute_install_scripts: true
- name: Set up Python ${{ matrix.python-version }} and restore venv
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: ./.github/actions/restore-or-build-venv
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ matrix.python-version }}
python-cache-key: ${{ needs.info.outputs.python_cache_key }}
uv-cache-dir: ${{ env.UV_CACHE_DIR }}
apt-cache-version: ${{ env.APT_CACHE_VERSION }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/python.json"
@@ -1070,15 +1201,21 @@ jobs:
with:
packages: postgresql-server-dev-14
version: ${{ env.APT_CACHE_VERSION }}
- name: Set up Python ${{ matrix.python-version }} and restore venv
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: ./.github/actions/restore-or-build-venv
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ matrix.python-version }}
python-cache-key: ${{ needs.info.outputs.python_cache_key }}
uv-cache-dir: ${{ env.UV_CACHE_DIR }}
apt-cache-version: ${{ env.APT_CACHE_VERSION }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/python.json"
@@ -1232,15 +1369,21 @@ jobs:
libxml2-utils
version: ${{ env.APT_CACHE_VERSION }}
execute_install_scripts: true
- name: Set up Python ${{ matrix.python-version }} and restore venv
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: ./.github/actions/restore-or-build-venv
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ matrix.python-version }}
python-cache-key: ${{ needs.info.outputs.python_cache_key }}
uv-cache-dir: ${{ env.UV_CACHE_DIR }}
apt-cache-version: ${{ env.APT_CACHE_VERSION }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/python.json"
+1 -1
View File
@@ -108,7 +108,7 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity):
"""Initialize AdGuard Home sensor."""
super().__init__(data, entry)
self.entity_description = description
self._attr_unique_id = "_".join( # pylint: disable=home-assistant-entity-unique-id-redundant-domain,home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = "_".join( # pylint: disable=home-assistant-entity-unique-id-redundant-domain
[
DOMAIN,
self.adguard.host,
+1 -1
View File
@@ -103,7 +103,7 @@ class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity):
"""Initialize AdGuard Home switch."""
super().__init__(data, entry)
self.entity_description = description
self._attr_unique_id = "_".join( # pylint: disable=home-assistant-entity-unique-id-redundant-domain,home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = "_".join( # pylint: disable=home-assistant-entity-unique-id-redundant-domain
[
DOMAIN,
self.adguard.host,
+1 -1
View File
@@ -46,7 +46,7 @@ class AdGuardHomeUpdate(AdGuardHomeEntity, UpdateEntity):
"""Initialize AdGuard Home update."""
super().__init__(data, entry)
self._attr_unique_id = "_".join( # pylint: disable=home-assistant-entity-unique-id-redundant-domain,home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = "_".join( # pylint: disable=home-assistant-entity-unique-id-redundant-domain
[DOMAIN, self.adguard.host, str(self.adguard.port), "update"]
)
@@ -40,7 +40,7 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
def __init__(self, coordinator: AirGradientCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.serial_number}-update" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.serial_number}-update"
@cached_property
@override
+1 -1
View File
@@ -56,7 +56,7 @@ class AirOSUpdateEntity(AirOSEntity, UpdateEntity):
self.status = status
self.firmware = firmware
self._attr_unique_id = f"{status.data.derived.mac}_firmware_update" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{status.data.derived.mac}_firmware_update"
@property
@override
@@ -43,7 +43,7 @@ class IPWebcamCamera(MjpegCamera):
username=coordinator.config_entry.data.get(CONF_USERNAME),
password=coordinator.config_entry.data.get(CONF_PASSWORD, ""),
)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}-camera" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.config_entry.entry_id}-camera"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
name=coordinator.config_entry.data[CONF_HOST],
@@ -28,6 +28,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, llm
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.selector import (
NumberSelector,
NumberSelectorConfig,
@@ -65,7 +66,7 @@ from .const import (
TOOL_SEARCH_UNSUPPORTED_MODELS,
PromptCaching,
)
from .coordinator import async_create_client, model_alias
from .coordinator import model_alias
if TYPE_CHECKING:
from . import AnthropicConfigEntry
@@ -94,7 +95,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
client = await async_create_client(hass, data[CONF_API_KEY])
client = anthropic.AsyncAnthropic(
api_key=data[CONF_API_KEY], http_client=get_async_client(hass)
)
await client.models.list(timeout=10.0)
@@ -546,8 +549,9 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
location_data: dict[str, str] = {}
zone_home = self.hass.states.get(ENTITY_ID_HOME)
if zone_home is not None:
client = await async_create_client(
self.hass, self._get_entry().data[CONF_API_KEY]
client = anthropic.AsyncAnthropic(
api_key=self._get_entry().data[CONF_API_KEY],
http_client=get_async_client(self.hass),
)
location_schema = vol.Schema(
{
@@ -1,7 +1,6 @@
"""Coordinator for the Anthropic integration."""
import datetime
from functools import partial
from typing import override
import anthropic
@@ -21,19 +20,6 @@ UPDATE_INTERVAL_DISCONNECTED = datetime.timedelta(minutes=1)
type AnthropicConfigEntry = ConfigEntry[AnthropicCoordinator]
async def async_create_client(
hass: HomeAssistant, api_key: str
) -> anthropic.AsyncAnthropic:
"""Create an Anthropic client."""
return await hass.async_add_executor_job(
partial(
anthropic.AsyncAnthropic,
api_key=api_key,
http_client=get_async_client(hass),
)
)
@callback
def model_alias(model_id: str) -> str:
"""Resolve alias from versioned model name."""
@@ -47,8 +33,7 @@ def model_alias(model_id: str) -> str:
class AnthropicCoordinator(DataUpdateCoordinator[list[anthropic.types.ModelInfo]]):
"""Coordinator using different intervals after success and failure."""
config_entry: AnthropicConfigEntry
_client: anthropic.AsyncAnthropic
client: anthropic.AsyncAnthropic
def __init__(self, hass: HomeAssistant, config_entry: AnthropicConfigEntry) -> None:
"""Initialize the coordinator."""
@@ -61,17 +46,8 @@ class AnthropicCoordinator(DataUpdateCoordinator[list[anthropic.types.ModelInfo]
update_method=self.async_update_data,
always_update=False,
)
@property
def client(self) -> anthropic.AsyncAnthropic:
"""Return the Anthropic client."""
return self._client
@override
async def _async_setup(self) -> None:
"""Set up the coordinator."""
self._client = await async_create_client(
self.hass, self.config_entry.data[CONF_API_KEY]
self.client = anthropic.AsyncAnthropic(
api_key=config_entry.data[CONF_API_KEY], http_client=get_async_client(hass)
)
@callback
+1 -1
View File
@@ -34,7 +34,7 @@ class AutomaticBackupEvent(BackupManagerBaseEntity, EventEntity):
def __init__(self, coordinator: BackupDataUpdateCoordinator) -> None:
"""Initialize the automatic backup event."""
super().__init__(coordinator)
self._attr_unique_id = "automatic_backup_event" # pylint: disable=home-assistant-entity-unique-id-redundant-domain,home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = "automatic_backup_event" # pylint: disable=home-assistant-entity-unique-id-redundant-domain
self._attr_translation_key = "automatic_backup_event"
@callback
+1 -1
View File
@@ -56,7 +56,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
super().__init__(coordinator)
Camera.__init__(self)
self._camera = camera
self._attr_unique_id = f"{camera.serial}-camera" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{camera.serial}-camera"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, camera.serial)},
serial_number=camera.serial,
+1 -1
View File
@@ -187,7 +187,7 @@ async def async_process_advertisements(
)
stack.callback(unload)
if mode is BluetoothScanningMode.ACTIVE:
if mode == BluetoothScanningMode.ACTIVE:
task = hass.async_create_task(manager.async_request_active_scan(timeout))
stack.callback(task.cancel)
+1 -1
View File
@@ -36,7 +36,7 @@ class BroadlinkTime(BroadlinkEntity, TimeEntity):
"""Initialize the sensor."""
super().__init__(device)
self._attr_unique_id = f"{device.unique_id}-device_time" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{device.unique_id}-device_time"
@override
def _update_state(self, data: dict[str, Any]) -> None:
+2 -2
View File
@@ -93,9 +93,9 @@ class BSBLANClimate(BSBLanCircuitEntity, ClimateEntity):
# Backward compatible unique ID: circuit 1 keeps old format
if circuit == 1:
self._attr_unique_id = f"{mac}-climate" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{mac}-climate"
else:
self._attr_unique_id = f"{mac}-climate-{circuit}" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{mac}-climate-{circuit}"
# Set temperature range from per-circuit static data
if (static := data.static.get(circuit)) is not None:
+4 -21
View File
@@ -8,7 +8,6 @@ from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA,
EntityTriggerBase,
NotTriggeredReasonReporter,
Trigger,
)
@@ -31,11 +30,7 @@ class CounterBaseIntegerTrigger(EntityTriggerBase):
_schema = ENTITY_STATE_TRIGGER_SCHEMA
@override
def is_valid_state(
self,
state: State,
report_not_triggered: NotTriggeredReasonReporter,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check if the new state is valid."""
return _is_integer_state(state)
@@ -68,11 +63,7 @@ class CounterMaxReachedTrigger(CounterValueBaseTrigger):
"""Trigger for when a counter reaches its maximum value."""
@override
def is_valid_state(
self,
state: State,
report_not_triggered: NotTriggeredReasonReporter,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check if the new state matches the expected state(s)."""
if (max_value := state.attributes.get(CONF_MAXIMUM)) is None:
return False
@@ -83,11 +74,7 @@ class CounterMinReachedTrigger(CounterValueBaseTrigger):
"""Trigger for when a counter reaches its minimum value."""
@override
def is_valid_state(
self,
state: State,
report_not_triggered: NotTriggeredReasonReporter,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check if the new state matches the expected state(s)."""
if (min_value := state.attributes.get(CONF_MINIMUM)) is None:
return False
@@ -98,11 +85,7 @@ class CounterResetTrigger(CounterValueBaseTrigger):
"""Trigger for reset of counter entities."""
@override
def is_valid_state(
self,
state: State,
report_not_triggered: NotTriggeredReasonReporter,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check if the new state matches the expected state(s)."""
if (init_state := state.attributes.get(CONF_INITIAL)) is None:
return False
+2 -10
View File
@@ -5,11 +5,7 @@ from typing import override
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.trigger import (
EntityTriggerBase,
NotTriggeredReasonReporter,
Trigger,
)
from homeassistant.helpers.trigger import EntityTriggerBase, Trigger
from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
from .models import CoverDomainSpec
@@ -28,11 +24,7 @@ class CoverTriggerBase(EntityTriggerBase):
return state.state
@override
def is_valid_state(
self,
state: State,
report_not_triggered: NotTriggeredReasonReporter,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check if the state matches the target cover state."""
domain_spec = self._domain_specs[state.domain]
return self._get_value(state) == domain_spec.target_value
+1 -1
View File
@@ -149,7 +149,7 @@ class DemoWeather(WeatherEntity):
) -> None:
"""Initialize the Demo weather."""
self._attr_name = f"Demo Weather {name}"
self._attr_unique_id = f"demo-weather-{name.lower()}" # pylint: disable=home-assistant-entity-unique-id-redundant-domain,home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"demo-weather-{name.lower()}" # pylint: disable=home-assistant-entity-unique-id-redundant-domain
self._condition = condition
self._native_temperature = temperature
self._native_temperature_unit = temperature_unit
+2 -10
View File
@@ -10,11 +10,7 @@ from homeassistant.components.event import (
)
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
NotTriggeredReasonReporter,
StatelessEntityTriggerBase,
Trigger,
)
from homeassistant.helpers.trigger import StatelessEntityTriggerBase, Trigger
class DoorbellRangTrigger(StatelessEntityTriggerBase):
@@ -23,11 +19,7 @@ class DoorbellRangTrigger(StatelessEntityTriggerBase):
_domain_specs = {EVENT_DOMAIN: DomainSpec(device_class=EventDeviceClass.DOORBELL)}
@override
def is_valid_state(
self,
state: State,
report_not_triggered: NotTriggeredReasonReporter,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check if the event type is ring."""
return state.attributes.get(ATTR_EVENT_TYPE) == DoorbellEventType.RING
+3 -1
View File
@@ -83,7 +83,9 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
translation_key="time_state_end",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda node: (
dt_util.utc_from_timestamp(node.ventilation.time_state_end)
dt_util.utc_from_timestamp(node.ventilation.time_state_end).replace(
second=0, microsecond=0
)
if node.ventilation and node.ventilation.time_state_end != 0
else None
),
+1 -1
View File
@@ -31,7 +31,7 @@ class EcobeeNotifyEntity(EcobeeBaseEntity, NotifyEntity):
"""Initialize the thermostat."""
super().__init__(data, thermostat_index)
self._attr_unique_id = (
f"{self.thermostat['identifier']}_notify_{thermostat_index}" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
f"{self.thermostat['identifier']}_notify_{thermostat_index}"
)
@override
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["aioecowitt==2026.6.0"]
"requirements": ["aioecowitt==2025.9.2"]
}
+1 -6
View File
@@ -10,7 +10,6 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA,
NotTriggeredReasonReporter,
StatelessEntityTriggerBase,
Trigger,
TriggerConfig,
@@ -43,11 +42,7 @@ class EventReceivedTrigger(StatelessEntityTriggerBase):
self._event_types = set(self._options[CONF_EVENT_TYPE])
@override
def is_valid_state(
self,
state: State,
report_not_triggered: NotTriggeredReasonReporter,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check if the event type matches one of the configured types."""
return state.attributes.get(ATTR_EVENT_TYPE) in self._event_types
+1 -1
View File
@@ -40,7 +40,7 @@ class FibaroScene(Scene):
self._attr_name = f"{room_name} {fibaro_scene.name}"
self._attr_unique_id = (
f"{slugify(controller.hub_serial)}.scene.{fibaro_scene.fibaro_id}" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
f"{slugify(controller.hub_serial)}.scene.{fibaro_scene.fibaro_id}"
)
self._attr_extra_state_attributes = {"fibaro_id": fibaro_scene.fibaro_id}
# propagate hidden attribute set in fibaro home center to HA
+5 -14
View File
@@ -11,7 +11,7 @@ from typing import Any, TypedDict, cast, override
from xml.etree.ElementTree import ParseError
from fritzconnection import FritzConnection
from fritzconnection.core.exceptions import FritzActionError, FritzConnectionException
from fritzconnection.core.exceptions import FritzActionError
from fritzconnection.lib.fritzcall import FritzCall
from fritzconnection.lib.fritzhosts import FritzHosts
from fritzconnection.lib.fritzstatus import FritzStatus
@@ -267,7 +267,9 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
) = self._update_device_info()
if self.fritz_status.has_wan_support:
self.device_conn_type = self.fritz_status.connection_service
self.device_conn_type = (
self.fritz_status.get_default_connection_service().connection_service
)
self.device_is_router = self.fritz_status.has_wan_enabled
self.has_call_deflections = "X_AVM-DE_OnTel1" in self.connection.services
@@ -680,18 +682,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
async def async_trigger_reconnect(self) -> None:
"""Trigger device reconnect."""
try:
await self.hass.async_add_executor_job(
self.connection.call_action,
f"{self.device_conn_type}1",
"ForceTermination",
)
except FritzConnectionException as ex:
# ignore UPnPError:
# errorCode: 707
# errorDescription: DisconnectInProgress
if "disconnectinprogress" not in str(ex).lower():
raise
await self.hass.async_add_executor_job(self.connection.reconnect)
async def async_trigger_set_guest_password(
self, password: str | None, length: int
@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260624.2"]
"requirements": ["home-assistant-frontend==20260624.1"]
}
@@ -34,7 +34,7 @@ class FullyCameraEntity(FullyKioskEntity, Camera):
"""Initialize the camera."""
FullyKioskEntity.__init__(self, coordinator)
Camera.__init__(self)
self._attr_unique_id = f"{coordinator.data['deviceID']}-camera" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.data['deviceID']}-camera"
@override
async def async_camera_image(
@@ -88,7 +88,7 @@ class WaitingAddonManager(AddonManager):
info = None
# Do not try to uninstall an addon if it is already uninstalled
if info is not None and info.state is AddonState.NOT_INSTALLED:
if info is not None and info.state == AddonState.NOT_INSTALLED:
return
await self.async_uninstall_addon()
+1 -1
View File
@@ -36,7 +36,7 @@ class ImmichUpdateEntity(ImmichEntity, UpdateEntity):
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_update" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_update"
@property
@override
@@ -1 +0,0 @@
"""Virtual integration: IoTorero."""
@@ -1,6 +0,0 @@
{
"domain": "iotorero",
"name": "IoTorero",
"integration_type": "virtual",
"supported_by": "esphome"
}
@@ -108,7 +108,7 @@ class KaiterraAirQuality(AirQualityEntity):
@override
def unique_id(self):
"""Return the sensor's unique id."""
return f"{self._device_id}_air_quality" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
return f"{self._device_id}_air_quality"
@property
@override
+1 -1
View File
@@ -114,7 +114,7 @@ async def _get_coordinator(
for entry_id in device.config_entries:
entry = call.hass.config_entries.async_get_entry(entry_id)
if entry and entry.domain == DOMAIN:
if entry.state is not ConfigEntryState.LOADED:
if entry.state != ConfigEntryState.LOADED:
raise HomeAssistantError(f"{entry.title} is not loaded")
return entry.runtime_data
@@ -237,7 +237,7 @@ class DemoWeather(WeatherEntity):
) -> None:
"""Initialize the Demo weather."""
self._attr_name = f"Test Weather {name}"
self._attr_unique_id = f"test-weather-{name.lower()}" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"test-weather-{name.lower()}"
self._condition = condition
self._native_temperature = temperature
self._native_temperature_unit = temperature_unit
+1 -1
View File
@@ -33,7 +33,7 @@ class LaMetricUpdate(LaMetricEntity, UpdateEntity):
def __init__(self, coordinator: LaMetricDataUpdateCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.data.serial_number}-update" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.data.serial_number}-update"
@property
@override
+1 -1
View File
@@ -69,7 +69,7 @@ class LiebherrPresentationLight(LiebherrEntity, LightEntity):
) -> None:
"""Initialize the presentation light entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.device_id}_presentation_light" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.device_id}_presentation_light"
@property
def _light_control(self) -> PresentationLightControl | None:
@@ -42,7 +42,7 @@ class LutronCasetaScene(Scene):
identifiers={(DOMAIN, data.bridge_device["serial"])},
)
self._attr_name = scene["name"]
self._attr_unique_id = f"scene_{bridge_unique_id}_{self._scene_id}" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"scene_{bridge_unique_id}_{self._scene_id}"
@override
async def async_activate(self, **kwargs: Any) -> None:
@@ -9,7 +9,6 @@ from homeassistant.helpers.trigger import (
EntityNumericalStateCrossedThresholdTriggerBase,
EntityNumericalStateTriggerBase,
EntityTriggerBase,
NotTriggeredReasonReporter,
Trigger,
make_entity_transition_trigger,
)
@@ -61,11 +60,7 @@ class _MediaPlayerMutedStateTriggerBase(EntityTriggerBase):
return self.is_muted(from_state) != self.is_muted(to_state)
@override
def is_valid_state(
self,
state: State,
report_not_triggered: NotTriggeredReasonReporter,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check if the new state matches the expected state."""
if not self._has_volume_attributes(state):
return False
+1 -1
View File
@@ -179,7 +179,7 @@ class TemperatureSensor(BaseSensorEntity):
"""Initialize TemperatureSensor entity."""
super().__init__(coordinator)
self._temp_sensor = nasweb_temp_sensor
self._attr_unique_id = f"{DOMAIN}.{self._temp_sensor.webio_serial}.temp_sensor" # pylint: disable=home-assistant-entity-unique-id-redundant-domain,home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{DOMAIN}.{self._temp_sensor.webio_serial}.temp_sensor" # pylint: disable=home-assistant-entity-unique-id-redundant-domain
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._temp_sensor.webio_serial)}
)
+1 -1
View File
@@ -96,7 +96,7 @@ class RelaySwitch(SwitchEntity, BaseCoordinatorEntity):
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}" # pylint: disable=home-assistant-entity-unique-id-redundant-domain,home-assistant-entity-unique-id-redundant-platform
f"{DOMAIN}.{self._output.webio_serial}.relay_switch.{self._output.index}" # pylint: disable=home-assistant-entity-unique-id-redundant-domain
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._output.webio_serial)},
+1 -1
View File
@@ -151,7 +151,7 @@ class NestCameraBaseEntity(Camera, ABC):
self._attr_model = nest_device_info.device_model
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" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{self._device.name}-camera"
@override
async def async_added_to_hass(self) -> None:
+2 -2
View File
@@ -74,7 +74,7 @@ class NetatmoCameraLight(NetatmoModuleEntity, LightEntity):
def __init__(self, netatmo_device: NetatmoDevice) -> None:
"""Initialize a Netatmo Presence camera light."""
super().__init__(netatmo_device)
self._attr_unique_id = f"{self.device.entity_id}-light" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{self.device.entity_id}-light"
self._signal_name = f"{HOME}-{self.home.entity_id}"
self._publishers.extend(
@@ -154,7 +154,7 @@ class NetatmoLight(NetatmoModuleEntity, LightEntity):
def __init__(self, netatmo_device: NetatmoDevice) -> None:
"""Initialize a Netatmo light."""
super().__init__(netatmo_device)
self._attr_unique_id = f"{self.device.entity_id}-light" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{self.device.entity_id}-light"
if self.device.brightness is not None:
self._attr_color_mode = ColorMode.BRIGHTNESS
+1 -1
View File
@@ -69,7 +69,7 @@ class NetatmoScheduleSelect(NetatmoBaseEntity, SelectEntity):
configuration_url=CONF_URL_ENERGY,
)
self._attr_unique_id = f"{self.home.entity_id}-schedule-select" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{self.home.entity_id}-schedule-select"
schedule = self.home.get_selected_schedule()
assert schedule
+1 -1
View File
@@ -43,7 +43,7 @@ class NetgearUpdateEntity(
) -> None:
"""Initialize a Netgear device."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.router.serial_number}-update" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.router.serial_number}-update"
@property
@override
+12
View File
@@ -28,6 +28,7 @@ from homeassistant.const import (
UnitOfPower,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfRadiationConcentration,
UnitOfRatio,
UnitOfReactiveEnergy,
UnitOfReactivePower,
@@ -62,6 +63,7 @@ from homeassistant.util.unit_conversion import (
OzoneConcentrationConverter,
PowerConverter,
PressureConverter,
RadiationConcentrationConverter,
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
@@ -373,6 +375,14 @@ class NumberDeviceClass(StrEnum):
- `inH₂O`
"""
RADON = "radon"
"""Radon.
Unit of measurement: UnitOfRadiationConcentration
- SI / metric: `Bq/`
- USCS / imperial: `pCi/L`
"""
REACTIVE_ENERGY = "reactive_energy"
"""Reactive energy.
@@ -587,6 +597,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth),
NumberDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux),
NumberDeviceClass.PRESSURE: set(UnitOfPressure),
NumberDeviceClass.RADON: set(UnitOfRadiationConcentration),
NumberDeviceClass.REACTIVE_ENERGY: set(UnitOfReactiveEnergy),
NumberDeviceClass.REACTIVE_POWER: set(UnitOfReactivePower),
NumberDeviceClass.SIGNAL_STRENGTH: {
@@ -652,6 +663,7 @@ UNIT_CONVERTERS: dict[NumberDeviceClass, type[BaseUnitConverter]] = {
NumberDeviceClass.PRECIPITATION: DistanceConverter,
NumberDeviceClass.PRECIPITATION_INTENSITY: SpeedConverter,
NumberDeviceClass.PRESSURE: PressureConverter,
NumberDeviceClass.RADON: RadiationConcentrationConverter,
NumberDeviceClass.REACTIVE_ENERGY: ReactiveEnergyConverter,
NumberDeviceClass.REACTIVE_POWER: ReactivePowerConverter,
NumberDeviceClass.SULPHUR_DIOXIDE: SulphurDioxideConcentrationConverter,
@@ -114,6 +114,9 @@
"pressure": {
"default": "mdi:gauge"
},
"radon": {
"default": "mdi:radioactive"
},
"reactive_energy": {
"default": "mdi:lightning-bolt"
},
@@ -135,6 +135,9 @@
"pressure": {
"name": "[%key:component::sensor::entity_component::pressure::name%]"
},
"radon": {
"name": "[%key:component::sensor::entity_component::radon::name%]"
},
"reactive_energy": {
"name": "[%key:component::sensor::entity_component::reactive_energy::name%]"
},
+1 -1
View File
@@ -51,7 +51,7 @@ class OpenhomeUpdateEntity(UpdateEntity):
def __init__(self, device):
"""Initialize a Linn DS update entity."""
self._device = device
self._attr_unique_id = f"{device.uuid()}-update" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{device.uuid()}-update"
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, device.uuid()),
@@ -217,7 +217,7 @@ class ConversationFlowHandler(ConfigSubentryFlow):
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Manage conversation agent configuration."""
if self._get_entry().state is not ConfigEntryState.LOADED:
if self._get_entry().state != ConfigEntryState.LOADED:
return self.async_abort(reason="entry_not_loaded")
if user_input is not None:
+1 -1
View File
@@ -80,7 +80,7 @@ class PlexSensor(SensorEntity):
def __init__(self, hass, plex_server):
"""Initialize the sensor."""
self._attr_unique_id = f"sensor-{plex_server.machine_identifier}" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"sensor-{plex_server.machine_identifier}"
self._server = plex_server
self.async_refresh_sensor = Debouncer(
+1 -1
View File
@@ -102,7 +102,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
) -> None:
"""Set up the Plugwise API."""
super().__init__(coordinator, device_id)
self._attr_unique_id = f"{device_id}-climate" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{device_id}-climate"
self._api = coordinator.api
gateway_id: str = self._api.gateway_id
@@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: PooldoseConfigEntry) ->
translation_key="connect_failed",
) from err
if client_status is not RequestStatus.SUCCESS:
if client_status != RequestStatus.SUCCESS:
raise ConfigEntryNotReady(
translation_domain=entry.domain,
translation_key="client_init_failed",
@@ -62,7 +62,7 @@ class PooldoseCoordinator(DataUpdateCoordinator[StructuredValuesDict]):
translation_key="update_connect_failed",
) from err
if status is not RequestStatus.SUCCESS:
if status != RequestStatus.SUCCESS:
raise UpdateFailed(
translation_domain=self.config_entry.domain,
translation_key="api_status_error",
@@ -1132,7 +1132,7 @@ class PrometheusMetrics:
PERCENTAGE: "percent",
}
default = unit.replace("/", "_per_")
# Unit conversion for UnitOfDensity.MICROGRAMS_PER_CUBIC_METER "μg/m³"
# Unit conversion for CONCENTRATION_MICROGRAMS_PER_CUBIC_METER "μg/m³"
# "μ" == "\u03bc" but the API uses "\u00b5"
default = default.replace("\u03bc", "\u00b5")
default = default.lower()
+1 -1
View File
@@ -65,7 +65,7 @@ class RachioCalendarEntity(
self._attr_translation_placeholders = {
"base": coordinator.base_station[KEY_SERIAL_NUMBER]
}
self._attr_unique_id = f"{coordinator.base_station[KEY_ID]}-calendar" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.base_station[KEY_ID]}-calendar"
self._previous_event: dict[str, Any] | None = None
@property
@@ -64,6 +64,7 @@ from homeassistant.util.unit_conversion import (
OzoneConcentrationConverter,
PowerConverter,
PressureConverter,
RadiationConcentrationConverter,
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
@@ -219,6 +220,7 @@ _PRIMARY_UNIT_CONVERTERS: list[type[BaseUnitConverter]] = [
MassVolumeConcentrationConverter,
PowerConverter,
PressureConverter,
RadiationConcentrationConverter,
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
@@ -37,6 +37,7 @@ from homeassistant.util.unit_conversion import (
OzoneConcentrationConverter,
PowerConverter,
PressureConverter,
RadiationConcentrationConverter,
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
@@ -101,6 +102,9 @@ UNIT_SCHEMA = vol.Schema(
vol.Optional("ozone"): vol.In(OzoneConcentrationConverter.VALID_UNITS),
vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS),
vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS),
vol.Optional("radiation_concentration"): vol.In(
RadiationConcentrationConverter.VALID_UNITS
),
vol.Optional("reactive_energy"): vol.In(ReactiveEnergyConverter.VALID_UNITS),
vol.Optional("reactive_power"): vol.In(ReactivePowerConverter.VALID_UNITS),
vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS),
+1 -1
View File
@@ -40,7 +40,7 @@ class LeilSaunaLight(LeilSaunaEntity, LightEntity):
"""Initialize the light entity."""
super().__init__(coordinator)
# Override unique_id to differentiate from climate entity
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_light" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_light"
@property
@override
+14
View File
@@ -28,6 +28,7 @@ from homeassistant.const import (
UnitOfPower,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfRadiationConcentration,
UnitOfRatio,
UnitOfReactiveEnergy,
UnitOfReactivePower,
@@ -62,6 +63,7 @@ from homeassistant.util.unit_conversion import (
OzoneConcentrationConverter,
PowerConverter,
PressureConverter,
RadiationConcentrationConverter,
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
@@ -404,6 +406,14 @@ class SensorDeviceClass(StrEnum):
- `inH₂O`
"""
RADON = "radon"
"""Radon.
Unit of measurement: UnitOfRadiationConcentration
- SI / metric: `Bq/`
- USCS / imperial: `pCi/L`
"""
REACTIVE_ENERGY = "reactive_energy"
"""Reactive energy.
@@ -611,6 +621,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
SensorDeviceClass.PRECIPITATION: DistanceConverter,
SensorDeviceClass.PRECIPITATION_INTENSITY: SpeedConverter,
SensorDeviceClass.PRESSURE: PressureConverter,
SensorDeviceClass.RADON: RadiationConcentrationConverter,
SensorDeviceClass.REACTIVE_ENERGY: ReactiveEnergyConverter,
SensorDeviceClass.REACTIVE_POWER: ReactivePowerConverter,
SensorDeviceClass.SULPHUR_DIOXIDE: SulphurDioxideConcentrationConverter,
@@ -706,6 +717,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth),
SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux),
SensorDeviceClass.PRESSURE: set(UnitOfPressure),
SensorDeviceClass.RADON: set(UnitOfRadiationConcentration),
SensorDeviceClass.REACTIVE_ENERGY: set(UnitOfReactiveEnergy),
SensorDeviceClass.REACTIVE_POWER: set(UnitOfReactivePower),
SensorDeviceClass.SIGNAL_STRENGTH: {
@@ -780,6 +792,7 @@ UNITS_PRECISION = {
0,
),
SensorDeviceClass.PRESSURE: (UnitOfPressure.PA, 0),
SensorDeviceClass.RADON: (UnitOfRadiationConcentration.PICOCURIES_PER_LITER, 1),
SensorDeviceClass.REACTIVE_POWER: (UnitOfReactivePower.VOLT_AMPERE_REACTIVE, 0),
SensorDeviceClass.SOUND_PRESSURE: (UnitOfSoundPressure.DECIBEL, 0),
SensorDeviceClass.SPEED: (UnitOfSpeed.MILLIMETERS_PER_SECOND, 0),
@@ -839,6 +852,7 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = {
SensorDeviceClass.PRECIPITATION: set(SensorStateClass),
SensorDeviceClass.PRECIPITATION_INTENSITY: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.PRESSURE: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.RADON: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.REACTIVE_ENERGY: {
SensorStateClass.TOTAL,
SensorStateClass.TOTAL_INCREASING,
@@ -69,6 +69,7 @@ CONF_IS_POWER_FACTOR = "is_power_factor"
CONF_IS_PRECIPITATION = "is_precipitation"
CONF_IS_PRECIPITATION_INTENSITY = "is_precipitation_intensity"
CONF_IS_PRESSURE = "is_pressure"
CONF_IS_RADON = "is_radon"
CONF_IS_SPEED = "is_speed"
CONF_IS_REACTIVE_ENERGY = "is_reactive_energy"
CONF_IS_REACTIVE_POWER = "is_reactive_power"
@@ -132,6 +133,7 @@ ENTITY_CONDITIONS = {
{CONF_TYPE: CONF_IS_PRECIPITATION_INTENSITY}
],
SensorDeviceClass.PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}],
SensorDeviceClass.RADON: [{CONF_TYPE: CONF_IS_RADON}],
SensorDeviceClass.REACTIVE_ENERGY: [{CONF_TYPE: CONF_IS_REACTIVE_ENERGY}],
SensorDeviceClass.REACTIVE_POWER: [{CONF_TYPE: CONF_IS_REACTIVE_POWER}],
SensorDeviceClass.SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}],
@@ -201,6 +203,7 @@ CONDITION_SCHEMA = vol.All(
CONF_IS_PRECIPITATION,
CONF_IS_PRECIPITATION_INTENSITY,
CONF_IS_PRESSURE,
CONF_IS_RADON,
CONF_IS_REACTIVE_ENERGY,
CONF_IS_REACTIVE_POWER,
CONF_IS_SIGNAL_STRENGTH,
@@ -70,6 +70,7 @@ CONF_POWER_FACTOR = "power_factor"
CONF_PRECIPITATION = "precipitation"
CONF_PRECIPITATION_INTENSITY = "precipitation_intensity"
CONF_PRESSURE = "pressure"
CONF_RADON = "radon"
CONF_REACTIVE_ENERGY = "reactive_energy"
CONF_REACTIVE_POWER = "reactive_power"
CONF_SIGNAL_STRENGTH = "signal_strength"
@@ -133,6 +134,7 @@ ENTITY_TRIGGERS = {
{CONF_TYPE: CONF_PRECIPITATION_INTENSITY}
],
SensorDeviceClass.PRESSURE: [{CONF_TYPE: CONF_PRESSURE}],
SensorDeviceClass.RADON: [{CONF_TYPE: CONF_RADON}],
SensorDeviceClass.REACTIVE_ENERGY: [{CONF_TYPE: CONF_REACTIVE_ENERGY}],
SensorDeviceClass.REACTIVE_POWER: [{CONF_TYPE: CONF_REACTIVE_POWER}],
SensorDeviceClass.SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}],
@@ -203,6 +205,7 @@ TRIGGER_SCHEMA = vol.All(
CONF_PRECIPITATION,
CONF_PRECIPITATION_INTENSITY,
CONF_PRESSURE,
CONF_RADON,
CONF_REACTIVE_ENERGY,
CONF_REACTIVE_POWER,
CONF_SIGNAL_STRENGTH,
@@ -136,6 +136,9 @@
"pressure": {
"default": "mdi:gauge"
},
"radon": {
"default": "mdi:radioactive"
},
"reactive_energy": {
"default": "mdi:lightning-bolt"
},
@@ -39,6 +39,7 @@
"is_precipitation": "Current {entity_name} precipitation",
"is_precipitation_intensity": "Current {entity_name} precipitation intensity",
"is_pressure": "Current {entity_name} pressure",
"is_radon": "Current {entity_name} radon concentration level",
"is_reactive_energy": "Current {entity_name} reactive energy",
"is_reactive_power": "Current {entity_name} reactive power",
"is_signal_strength": "Current {entity_name} signal strength",
@@ -102,6 +103,7 @@
"precipitation": "{entity_name} precipitation changes",
"precipitation_intensity": "{entity_name} precipitation intensity changes",
"pressure": "{entity_name} pressure changes",
"radon": "{entity_name} radon concentration changes",
"reactive_energy": "{entity_name} reactive energy changes",
"reactive_power": "{entity_name} reactive power changes",
"signal_strength": "{entity_name} signal strength changes",
@@ -270,6 +272,9 @@
"pressure": {
"name": "Pressure"
},
"radon": {
"name": "Radon"
},
"reactive_energy": {
"name": "Reactive energy"
},
+1 -1
View File
@@ -45,7 +45,7 @@ class SleepIQLightEntity(SleepIQBedEntity[SleepIQDataUpdateCoordinator], LightEn
self.light = light
super().__init__(coordinator, bed)
self._attr_name = f"SleepNumber {bed.name} Light {light.outlet_id}"
self._attr_unique_id = f"{bed.id}-light-{light.outlet_id}" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{bed.id}-light-{light.outlet_id}"
@override
async def async_turn_on(self, **kwargs: Any) -> None:
+1 -1
View File
@@ -37,7 +37,7 @@ class SmInfraredEntity(SmEntity, InfraredEmitterEntity):
def __init__(self, coordinator: SmDataUpdateCoordinator) -> None:
"""Initialize the SLZB-Ultima infrared."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.unique_id}-infrared-emitter" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.unique_id}-infrared-emitter"
@override
async def async_send_command(self, command: InfraredCommand) -> None:
@@ -12,7 +12,7 @@
"documentation": "https://www.home-assistant.io/integrations/smlight",
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "platinum",
"quality_scale": "silver",
"requirements": ["pysmlight==0.5.2", "bleak-smlight==1.1.0"],
"zeroconf": [
{
@@ -54,11 +54,11 @@ rules:
discovery: done
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: |
@@ -1,7 +1,6 @@
"""Config flow for Steam integration."""
from collections.abc import Iterator, Mapping
import logging
from typing import Any, override
import steam.api
@@ -18,12 +17,9 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv, entity_registry as er
from .const import CONF_ACCOUNT, CONF_ACCOUNTS, DOMAIN, PLACEHOLDERS
from .const import CONF_ACCOUNT, CONF_ACCOUNTS, DOMAIN, LOGGER, PLACEHOLDERS
from .coordinator import SteamConfigEntry
_LOGGER = logging.getLogger(__name__)
# To avoid too long request URIs, the amount of ids to request is limited
MAX_IDS_TO_REQUEST = 275
@@ -79,8 +75,8 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
errors["base"] = (
"invalid_auth" if "403" in str(ex) else "cannot_connect"
)
except Exception:
_LOGGER.exception("Unknown exception")
except Exception: # noqa: BLE001
LOGGER.exception("Unknown exception")
errors["base"] = "unknown"
if not errors:
return self.async_create_entry(
@@ -133,8 +129,8 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
errors["base"] = (
"invalid_auth" if "403" in str(ex) else "cannot_connect"
)
except Exception:
_LOGGER.exception("Unknown exception")
except Exception: # noqa: BLE001
LOGGER.exception("Unknown exception")
errors["base"] = "unknown"
if not errors:
@@ -170,6 +166,7 @@ class SteamOptionsFlowHandler(OptionsFlowWithReload):
) -> ConfigFlowResult:
"""Manage Steam options."""
if user_input is not None:
await self.hass.config_entries.async_unload(self.config_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(
@@ -1,5 +1,6 @@
"""Steam constants."""
import logging
from typing import Final
CONF_ACCOUNT = "account"
@@ -9,6 +10,7 @@ DATA_KEY_COORDINATOR = "coordinator"
DEFAULT_NAME = "Steam"
DOMAIN: Final = "steam_online"
LOGGER = logging.getLogger(__package__)
PLACEHOLDERS = {
"api_key_url": "https://steamcommunity.com/dev/apikey",
@@ -1,11 +1,10 @@
"""Data update coordinator for the Steam integration."""
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import override
import steam.api
from steam.api import _interface_method as INTMethod
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
@@ -13,116 +12,65 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_ACCOUNTS, DOMAIN
from .const import CONF_ACCOUNTS, DOMAIN, LOGGER
type SteamConfigEntry = ConfigEntry[SteamDataUpdateCoordinator]
_LOGGER = logging.getLogger(__name__)
@dataclass(kw_only=True, frozen=True)
class PlayerData:
"""Steam player data."""
steamid: str
communityvisibilitystate: int
profilestate: int
personaname: str
commentpermission: int | None = None
profileurl: str
avatar: str
avatarmedium: str
avatarfull: str
avatarhash: str
lastlogoff: int
personastate: int
realname: str | None = None
primaryclanid: str | None = None
timecreated: int | None = None
personastateflags: int
loccountrycode: str | None = None
locstatecode: str | None = None
loccityid: int | None = None
gameextrainfo: str | None = None
gameid: str | None = None
level: int | None = None
class SteamDataUpdateCoordinator(DataUpdateCoordinator[dict[str, PlayerData]]):
class SteamDataUpdateCoordinator(
DataUpdateCoordinator[dict[str, dict[str, str | int]]]
):
"""Data update coordinator for the Steam integration."""
config_entry: SteamConfigEntry
user_interface: steam.api.interface
player_interface: steam.api.interface
def __init__(self, hass: HomeAssistant, config_entry: SteamConfigEntry) -> None:
"""Initialize the coordinator."""
super().__init__(
hass=hass,
logger=_LOGGER,
logger=LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=timedelta(seconds=30),
)
self.game_icons: dict[str, str] = {}
@override
async def _async_setup(self) -> None:
"""Set up the coordinator."""
self.game_icons: dict[int, str] = {}
self.player_interface: INTMethod = None
self.user_interface: INTMethod = None
steam.api.key.set(self.config_entry.data[CONF_API_KEY])
self.user_interface = steam.api.interface("ISteamUser")
self.player_interface = steam.api.interface("IPlayerService")
def _update(self) -> dict[str, PlayerData]:
def _update(self) -> dict[str, dict[str, str | int]]:
"""Fetch data from API endpoint."""
accounts = self.config_entry.options[CONF_ACCOUNTS]
_ids = list(accounts)
if not self.user_interface or not self.player_interface:
self.user_interface = steam.api.interface("ISteamUser")
self.player_interface = steam.api.interface("IPlayerService")
if not self.game_icons:
for _id in _ids:
res = self.player_interface.GetOwnedGames(
steamid=_id, include_appinfo=1
)["response"]
self.game_icons = self.game_icons | {
game["appid"]: game["img_icon_url"] for game in res.get("games", [])
}
response = self.user_interface.GetPlayerSummaries(steamids=_ids)
players = {
player["steamid"]: PlayerData(
**player,
level=self.player_interface.GetSteamLevel(steamid=player["steamid"])[
"response"
].get("player_level"),
)
player["steamid"]: player
for player in response["response"]["players"]["player"]
if player["steamid"] in _ids
}
for player in players.values():
if player.gameid and player.gameid not in self.game_icons:
games = self.player_interface.GetOwnedGames(
steamid=player.steamid,
include_appinfo=1,
include_played_free_games=True,
)["response"].get("games", [])
self.game_icons.update(
{str(game["appid"]): game["img_icon_url"] for game in games}
)
for value in players.values():
data = self.player_interface.GetSteamLevel(steamid=value["steamid"])
value["level"] = data["response"].get("player_level")
return players
@override
async def _async_update_data(self) -> dict[str, PlayerData]:
async def _async_update_data(self) -> dict[str, dict[str, str | int]]:
"""Send request to the executor."""
try:
return await self.hass.async_add_executor_job(self._update)
except steam.api.HTTPTimeoutError as ex:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="timeout_exception",
) from ex
except steam.api.HTTPError as ex:
_LOGGER.debug("Full exception:", exc_info=True)
if "401" in str(ex) or "403" in str(ex):
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_exception",
) from ex
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="request_exception",
) from ex
if "401" in str(ex):
raise ConfigEntryAuthFailed from ex
raise UpdateFailed(ex) from ex
+18 -16
View File
@@ -4,7 +4,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
from typing import Any, override
from typing import Any, cast, override
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.core import HomeAssistant
@@ -20,7 +20,7 @@ from .const import (
STEAM_MAIN_IMAGE_FILE,
STEAM_STATUSES,
)
from .coordinator import PlayerData, SteamConfigEntry, SteamDataUpdateCoordinator
from .coordinator import SteamConfigEntry, SteamDataUpdateCoordinator
from .entity import SteamEntity
PARALLEL_UPDATES = 1
@@ -36,18 +36,18 @@ class SteamSensor(StrEnum):
class SteamSensorEntityDescription(SensorEntityDescription):
"""Steam sensor description."""
value_fn: Callable[[PlayerData], StateType]
name_fn: Callable[[PlayerData], str]
entity_picture_fn: Callable[[PlayerData], str] | None = None
value_fn: Callable[[dict[str, Any]], StateType]
name_fn: Callable[[dict[str, Any]], str]
entity_picture_fn: Callable[[dict[str, Any]], str] | None = None
SENSOR_DESCRIPTIONS: tuple[SteamSensorEntityDescription, ...] = (
SteamSensorEntityDescription(
key=SteamSensor.ACCOUNT,
translation_key=SteamSensor.ACCOUNT,
value_fn=lambda x: STEAM_STATUSES[x.personastate],
name_fn=lambda x: x.personaname,
entity_picture_fn=lambda x: x.avatarfull,
value_fn=lambda x: STEAM_STATUSES[x["personastate"]],
name_fn=lambda x: x["personaname"],
entity_picture_fn=lambda x: x["avatarfull"],
),
)
@@ -106,27 +106,29 @@ class SteamSensorEntity(SteamEntity, SensorEntity):
player = self.coordinator.data[self._steamid]
attrs: dict[str, str | int | datetime] = {}
if game := player.gameextrainfo:
if game := player.get("gameextrainfo"):
attrs["game"] = game
if game_id := player.gameid:
if game_id := player.get("gameid"):
attrs["game_id"] = game_id
game_url = f"{STEAM_API_URL}{player.gameid}/"
game_url = f"{STEAM_API_URL}{player['gameid']}/"
attrs["game_image_header"] = f"{game_url}{STEAM_HEADER_IMAGE_FILE}"
attrs["game_image_main"] = f"{game_url}{STEAM_MAIN_IMAGE_FILE}"
if info := self._get_game_icon(player):
attrs["game_icon"] = f"{STEAM_ICON_URL}{game_id}/{info}.jpg"
if last_online := player.lastlogoff:
if last_online := cast(int | None, player.get("lastlogoff")):
attrs["last_online"] = dt_util.as_local(
dt_util.utc_from_timestamp(last_online)
)
if level := self.coordinator.data[self._steamid].level:
if level := self.coordinator.data[self._steamid]["level"]:
attrs["level"] = level
return attrs
def _get_game_icon(self, player: PlayerData) -> str | None:
def _get_game_icon(self, player: dict) -> str | None:
"""Get game icon identifier."""
if player.gameid is not None and player.gameid in self.coordinator.game_icons:
return self.coordinator.game_icons[player.gameid]
if player.get("gameid") in self.coordinator.game_icons:
return self.coordinator.game_icons[player["gameid"]]
# Reset game icons to have coordinator get id for new game
self.coordinator.game_icons = {}
return None
@property
@@ -70,17 +70,6 @@
}
}
},
"exceptions": {
"auth_exception": {
"message": "Failed to connect to Steam due to an authentication error"
},
"request_exception": {
"message": "Failed to connect to Steam due to a request error"
},
"timeout_exception": {
"message": "Failed to connect to Steam due to a request timeout"
}
},
"options": {
"error": {
"unauthorized": "Friends list restricted: Please refer to the documentation on how to see all other friends"
+1 -1
View File
@@ -58,7 +58,7 @@ class SwitchbotAirPurifierLightEntity(SwitchbotEntity, LightEntity, RestoreEntit
def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.base_unique_id}_light" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.base_unique_id}_light"
@override
async def async_added_to_hass(self) -> None:
@@ -71,6 +71,6 @@ rules:
stale-devices: todo
# Platinum
async-dependency: done
async-dependency: todo
inject-websession: done
strict-typing: done
+1 -1
View File
@@ -63,7 +63,7 @@ class SaunaClimate(ToloSaunaCoordinatorEntity, ClimateEntity):
"""Initialize TOLO Sauna Climate entity."""
super().__init__(coordinator, entry)
self._attr_unique_id = f"{entry.entry_id}_climate" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{entry.entry_id}_climate"
@property
@override
+1 -1
View File
@@ -32,7 +32,7 @@ class ToloFan(ToloSaunaCoordinatorEntity, FanEntity):
"""Initialize TOLO fan entity."""
super().__init__(coordinator, entry)
self._attr_unique_id = f"{entry.entry_id}_fan" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{entry.entry_id}_fan"
@property
@override
+1 -1
View File
@@ -33,7 +33,7 @@ class ToloLight(ToloSaunaCoordinatorEntity, LightEntity):
"""Initialize TOLO Sauna Light entity."""
super().__init__(coordinator, entry)
self._attr_unique_id = f"{entry.entry_id}_light" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{entry.entry_id}_light"
@property
@override
@@ -62,7 +62,7 @@ class ToonBinarySensor(ToonEntity, BinarySensorEntity):
self._attr_unique_id = (
# This unique ID is a bit ugly and contains unneeded information.
# It is here for legacy / backward compatible reasons.
f"{DOMAIN}_{coordinator.data.agreement.agreement_id}_binary_sensor_{description.key}" # pylint: disable=home-assistant-entity-unique-id-redundant-domain,home-assistant-entity-unique-id-redundant-platform
f"{DOMAIN}_{coordinator.data.agreement.agreement_id}_binary_sensor_{description.key}" # pylint: disable=home-assistant-entity-unique-id-redundant-domain
)
@property
+1 -1
View File
@@ -66,7 +66,7 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity):
PRESET_SLEEP,
]
self._attr_unique_id = (
f"{DOMAIN}_{coordinator.data.agreement.agreement_id}_climate" # pylint: disable=home-assistant-entity-unique-id-redundant-domain,home-assistant-entity-unique-id-redundant-platform
f"{DOMAIN}_{coordinator.data.agreement.agreement_id}_climate" # pylint: disable=home-assistant-entity-unique-id-redundant-domain
)
@property
+1 -1
View File
@@ -81,7 +81,7 @@ class ToonSensor(ToonEntity, SensorEntity):
self._attr_unique_id = (
# This unique ID is a bit ugly and contains unneeded information.
# It is here for legacy / backward compatible reasons.
f"{DOMAIN}_{coordinator.data.agreement.agreement_id}_sensor_{description.key}" # pylint: disable=home-assistant-entity-unique-id-redundant-domain,home-assistant-entity-unique-id-redundant-platform
f"{DOMAIN}_{coordinator.data.agreement.agreement_id}_sensor_{description.key}" # pylint: disable=home-assistant-entity-unique-id-redundant-domain
)
@property
+1 -1
View File
@@ -67,7 +67,7 @@ class TradfriLight(TradfriBaseEntity, LightEntity):
self._device_control = self._device.light_control
self._device_data = self._device_control.lights[0]
self._attr_unique_id = f"light-{gateway_id}-{self._device_id}" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"light-{gateway_id}-{self._device_id}"
self._hs_color = None
# Calculate supported color modes
+1 -1
View File
@@ -8,5 +8,5 @@
"integration_type": "entity",
"loggers": ["mutagen"],
"quality_scale": "internal",
"requirements": ["mutagen==1.48.0"]
"requirements": ["mutagen==1.47.0"]
}
@@ -67,7 +67,7 @@ class ProtectSiren(SirenEntity):
"""Initialise the siren entity."""
self.data = data
self._siren_id = siren.id
self._attr_unique_id = f"{siren.mac}_siren" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{siren.mac}_siren"
nvr = data.api.bootstrap.nvr
self._attr_device_info = DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, siren.mac)},
+1 -1
View File
@@ -26,7 +26,7 @@ class ValloxFilterChangeDateEntity(ValloxEntity, DateEntity):
"""Initialize the Vallox date."""
super().__init__(name, coordinator)
self._attr_unique_id = f"{self._device_uuid}-filter_change_date" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{self._device_uuid}-filter_change_date"
@property
@override
+1 -1
View File
@@ -42,7 +42,7 @@ class VelbusSelect(VelbusEntity, SelectEntity):
"""Initialize a select Velbus entity."""
super().__init__(channel)
self._attr_options = self._channel.get_options()
self._attr_unique_id = f"{self._attr_unique_id}-program_select" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{self._attr_unique_id}-program_select"
@api_call
@override
+1 -1
View File
@@ -42,7 +42,7 @@ class VeluxScene(Scene):
"""Init velux scene."""
self.scene = scene
# Renaming scenes in gateway keeps scene_id stable, we can use it as unique_id
self._attr_unique_id = f"{config_entry_id}_scene_{scene.scene_id}" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{config_entry_id}_scene_{scene.scene_id}"
self._attr_name = scene.name
# Associate scenes with the gateway device (where they are stored)
@@ -75,7 +75,7 @@ class VictronBaseEntity(Entity):
# 3. Dynamic units come from user-configured MQTT topics (e.g.
# SwitchableOutput Settings/Unit) and have no translation file
# entry, so we must set the unit programmatically.
or self._metric.metric_type is MetricType.DYNAMIC
or self._metric.metric_type == MetricType.DYNAMIC
):
return unit_of_measurement
+1 -1
View File
@@ -41,7 +41,7 @@ class VistapoolLight(VistapoolEntity, LightEntity):
def __init__(self, coordinator: VistapoolDataUpdateCoordinator) -> None:
"""Initialize the light entity."""
super().__init__(coordinator)
self._attr_unique_id = self.build_unique_id("pool_light") # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = self.build_unique_id("pool_light")
@property
@override
@@ -93,7 +93,7 @@ class WyomingConversationEntity(
)
self._supported_languages = list(model_languages)
self._attr_unique_id = f"{config_entry.entry_id}-conversation" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{config_entry.entry_id}-conversation"
@property
@override
+1 -1
View File
@@ -53,7 +53,7 @@ class WyomingSttProvider(stt.SpeechToTextEntity):
self._supported_languages = list(model_languages)
self._attr_name = asr_service.name
self._attr_unique_id = f"{config_entry.entry_id}-stt" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{config_entry.entry_id}-stt"
@property
@override
+1 -1
View File
@@ -86,7 +86,7 @@ class WyomingTtsProvider(tts.TextToSpeechEntity):
self._attr_default_language = self._attr_supported_languages[0]
self._attr_name = self._tts_service.name
self._attr_unique_id = f"{config_entry.entry_id}-tts" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{config_entry.entry_id}-tts"
@callback
@override
@@ -55,7 +55,7 @@ class WyomingWakeWordProvider(wake_word.WakeWordDetectionEntity):
for ww in wake_service.models
]
self._attr_name = wake_service.name
self._attr_unique_id = f"{config_entry.entry_id}-wake_word" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{config_entry.entry_id}-wake_word"
@override
async def get_supported_wake_words(self) -> list[wake_word.WakeWord]:
+1 -1
View File
@@ -67,7 +67,7 @@ class YoLinkClimateEntity(YoLinkEntity, ClimateEntity):
) -> None:
"""Init YoLink Thermostat."""
super().__init__(config_entry, coordinator)
self._attr_unique_id = f"{coordinator.device.device_id}_climate" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.device.device_id}_climate"
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_fan_modes = [FAN_ON, FAN_AUTO]
self._attr_min_temp = -10
+1 -1
View File
@@ -41,7 +41,7 @@ class YoLinkLockEntity(YoLinkEntity, LockEntity):
) -> None:
"""Init YoLink Lock."""
super().__init__(config_entry, coordinator)
self._attr_unique_id = f"{coordinator.device.device_id}_lock_state" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{coordinator.device.device_id}_lock_state"
@callback
@override
+4 -21
View File
@@ -36,7 +36,6 @@ from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR,
EntityTriggerBase,
NotTriggeredReasonReporter,
Trigger,
TriggerActionRunner,
TriggerConfig,
@@ -212,11 +211,7 @@ class EnteredZoneTrigger(ZoneTriggerBase):
return not self._in_target_zone(from_state)
@override
def is_valid_state(
self,
state: State,
report_not_triggered: NotTriggeredReasonReporter,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check that the entity is now in the selected zone."""
return self._in_target_zone(state)
@@ -230,11 +225,7 @@ class LeftZoneTrigger(ZoneTriggerBase):
return self._in_target_zone(from_state)
@override
def is_valid_state(
self,
state: State,
report_not_triggered: NotTriggeredReasonReporter,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check that the entity is no longer in the selected zone."""
return not self._in_target_zone(state)
@@ -288,11 +279,7 @@ class OccupancyDetectedTrigger(_ZoneOccupancyTriggerBase):
"""Trigger when a zone transitions to an occupied state."""
@override
def is_valid_state(
self,
state: State,
report_not_triggered: NotTriggeredReasonReporter,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check that the zone is occupied."""
return self._is_occupied(state)
@@ -306,11 +293,7 @@ class OccupancyClearedTrigger(_ZoneOccupancyTriggerBase):
"""Trigger when a zone transitions from occupied to unoccupied."""
@override
def is_valid_state(
self,
state: State,
report_not_triggered: NotTriggeredReasonReporter,
) -> bool:
def is_valid_state(self, state: State) -> bool:
"""Check that the zone is empty (count == 0)."""
return self._occupancy_count(state) == 0
+1 -1
View File
@@ -196,7 +196,7 @@ class ZWaveFirmwareUpdateEntity(ZWaveNodeBaseEntity, UpdateEntity):
# Entity class attributes
self._attr_name = "Firmware"
self._attr_unique_id = f"{self._base_unique_id}.firmware_update" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
self._attr_unique_id = f"{self._base_unique_id}.firmware_update"
self._attr_installed_version = node.firmware_version
@property
+17 -17
View File
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Final
from .generated.entity_platforms import EntityPlatforms
from .helpers.deprecation import (
DeprecatedConstant,
DeprecatedConstantEnum,
all_with_deprecated_constants,
check_if_deprecated_constant,
dir_with_deprecated_constants,
@@ -799,27 +798,21 @@ class UnitOfRatio(StrEnum):
# Concentration units
_DEPRECATED_CONCENTRATION_GRAMS_PER_CUBIC_METER = DeprecatedConstantEnum(
UnitOfDensity.GRAMS_PER_CUBIC_METER, "2027.8"
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = UnitOfDensity.GRAMS_PER_CUBIC_METER.value
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = (
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER.value
)
_DEPRECATED_CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER = DeprecatedConstantEnum(
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER, "2027.8"
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = (
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER.value
)
_DEPRECATED_CONCENTRATION_MICROGRAMS_PER_CUBIC_METER = DeprecatedConstantEnum(
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER, "2027.8"
)
_DEPRECATED_CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT = DeprecatedConstantEnum(
UnitOfDensity.MICROGRAMS_PER_CUBIC_FOOT, "2027.8"
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = (
UnitOfDensity.MICROGRAMS_PER_CUBIC_FOOT.value
)
_DEPRECATED_CONCENTRATION_PARTS_PER_CUBIC_METER = DeprecatedConstant(
"p/m³", "p/m³", "2027.8"
)
_DEPRECATED_CONCENTRATION_PARTS_PER_MILLION = DeprecatedConstantEnum(
UnitOfRatio.PARTS_PER_MILLION, "2027.8"
)
_DEPRECATED_CONCENTRATION_PARTS_PER_BILLION = DeprecatedConstantEnum(
UnitOfRatio.PARTS_PER_BILLION, "2027.8"
"p/m³", "p/m³", "2027.7"
)
CONCENTRATION_PARTS_PER_MILLION: Final = UnitOfRatio.PARTS_PER_MILLION.value
CONCENTRATION_PARTS_PER_BILLION: Final = UnitOfRatio.PARTS_PER_BILLION.value
PERCENTAGE: Final = UnitOfRatio.PERCENTAGE.value
@@ -830,6 +823,13 @@ class UnitOfBloodGlucoseConcentration(StrEnum):
MILLIMOLE_PER_LITER = "mmol/L"
class UnitOfRadiationConcentration(StrEnum):
"""Radiation concentration units."""
BECQUEREL_PER_CUBIC_METER = "Bq/m³"
PICOCURIES_PER_LITER = "pCi/L"
# Speed units
class UnitOfSpeed(StrEnum):
"""Speed units."""
+5 -5
View File
@@ -490,11 +490,11 @@ class FlowManager(abc.ABC, Generic[_FlowContextT, _FlowResultT, _HandlerT]):
)
if flow.flow_id not in self._progress:
# The flow was removed during the step, raise UnknownFlow unless
# the result is an abort. Compares against the string value
# because this runs before the legacy-string normalization
# below, and out-of-tree flow handlers may still return raw "abort".
if result["type"] != FlowResultType.ABORT.value:
# The flow was removed during the step, raise UnknownFlow
# unless the result is an abort. Uses `!=` (not `is not`) because
# this runs before the legacy-string normalization below, and
# out-of-tree flow handlers may still return raw "abort".
if result["type"] != FlowResultType.ABORT: # type: ignore[ha-enum-identity-compare,unused-ignore]
raise UnknownFlow
return result
@@ -3318,11 +3318,6 @@
"config_flow": true,
"iot_class": "local_polling"
},
"iotorero": {
"name": "IoTorero",
"integration_type": "virtual",
"supported_by": "esphome"
},
"iotty": {
"name": "iotty",
"integration_type": "device",

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