Compare commits

..

1 Commits

Author SHA1 Message Date
abmantis 6dea67e8c9 Add override decorator to components C to E 2026-05-24 19:42:28 +01:00
1313 changed files with 9474 additions and 61143 deletions
@@ -1,52 +0,0 @@
name: Cache and install APT packages
description: >-
Wraps awalsh128/cache-apt-pkgs-action with the workarounds Home Assistant CI
needs. Removes the conflicting Microsoft apt source before any apt run, and
points the dynamic linker at the host's multiarch lib subdirectories so
shared libraries that rely on update-alternatives or postinst-managed paths
(eg libblas, liblapack pulled in by ffmpeg) stay reachable since the upstream
action does not execute postinst scripts on cache restore.
inputs:
packages:
description: Space-delimited list of apt packages to install.
required: true
version:
description: Cache version. Bump to invalidate the cache.
required: false
default: "1"
execute_install_scripts:
description: >-
Pass-through to awalsh128/cache-apt-pkgs-action. Postinst scripts are not
actually cached by the upstream action, so this is largely a no-op today.
required: false
default: "false"
runs:
using: composite
steps:
- name: Remove conflicting Microsoft apt source
shell: bash
run: sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
- name: Install apt packages via cache
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
with:
packages: ${{ inputs.packages }}
version: ${{ inputs.version }}
execute_install_scripts: ${{ inputs.execute_install_scripts }}
- name: Refresh dynamic linker cache
shell: bash
run: |
# awalsh128/cache-apt-pkgs-action does not run postinst scripts on
# cache restore, so update-alternatives symlinks (eg the one libblas
# creates at /usr/lib/<multiarch>/libblas.so.3) are never produced.
# Add every /usr/lib/<multiarch> subdirectory that holds shared
# libraries to the ldconfig search path so the dynamic linker still
# finds them. Use dpkg-architecture to derive the host's multiarch
# tuple so this works on non-x86_64 runners too.
multiarch="$(dpkg-architecture -qDEB_HOST_MULTIARCH)"
find "/usr/lib/${multiarch}" -mindepth 2 -maxdepth 2 \
-name '*.so.*' -printf '%h\n' \
| sort -u \
| sudo tee /etc/ld.so.conf.d/zzz-cache-apt-extras.conf > /dev/null
sudo ldconfig
@@ -1,42 +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@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.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
- name: Install Python interpreter
shell: bash
env:
PYTHON_VERSION: ${{ inputs.python-version }}
run: uv python install "${PYTHON_VERSION}"
+245 -136
View File
@@ -37,7 +37,7 @@ on:
type: boolean
env:
CACHE_VERSION: 4
CACHE_VERSION: 3
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.6"
ADDITIONAL_PYTHON_VERSIONS: "[]"
@@ -60,7 +60,9 @@ env:
# - 15.2 is the latest (as of 9 Feb 2023)
POSTGRESQL_VERSIONS: "['postgres:12.14','postgres:15.2']"
UV_CACHE_DIR: /tmp/uv-cache
APT_CACHE_VERSION: 1
APT_CACHE_BASE: /home/runner/work/apt
APT_CACHE_DIR: /home/runner/work/apt/cache
APT_LIST_CACHE_DIR: /home/runner/work/apt/lists
SQLALCHEMY_WARN_20: 1
PYTHONASYNCIODEBUG: 1
HASS_CI: 1
@@ -84,13 +86,12 @@ jobs:
core: ${{ steps.core.outputs.changes }}
integrations_glob: ${{ steps.info.outputs.integrations_glob }}
integrations: ${{ steps.integrations.outputs.changes }}
apt_cache_key: ${{ steps.generate_apt_cache_key.outputs.key }}
python_cache_key: ${{ steps.generate_python_cache_key.outputs.key }}
requirements: ${{ steps.core.outputs.requirements }}
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 }}
@@ -115,6 +116,10 @@ jobs:
# Include HA_SHORT_VERSION to force the immediate creation
# of a new uv cache entry after a version bump.
echo "key=venv-${CACHE_VERSION}-${HA_SHORT_VERSION}-${HASH_REQUIREMENTS_TEST}-${HASH_REQUIREMENTS}-${HASH_REQUIREMENTS_ALL}-${HASH_PACKAGE_CONSTRAINTS}-${HASH_GEN_REQUIREMENTS}" >> $GITHUB_OUTPUT
- name: Generate partial apt restore key
id: generate_apt_cache_key
run: |
echo "key=$(lsb_release -rs)-apt-${CACHE_VERSION}-${HA_SHORT_VERSION}" >> $GITHUB_OUTPUT
- name: Filter for core changes
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: core
@@ -237,11 +242,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,12 +351,12 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up uv and Python ${{ matrix.python-version }}
- name: Set up Python ${{ matrix.python-version }}
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: ${{ matrix.python-version }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -384,40 +384,80 @@ jobs:
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
- name: Check if apt cache exists
id: cache-apt-check
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
packages: >-
bluez
ffmpeg
libturbojpeg
libxml2-utils
libavcodec-dev
libavdevice-dev
libavfilter-dev
libavformat-dev
libavutil-dev
libswresample-dev
libswscale-dev
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Install additional OS dependencies
if: |
steps.cache-venv.outputs.cache-hit != 'true'
|| steps.cache-apt-check.outputs.cache-hit != 'true'
id: install-os-deps
timeout-minutes: 10
env:
APT_CACHE_HIT: ${{ steps.cache-apt-check.outputs.cache-hit }}
run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
if [[ "${APT_CACHE_HIT}" != 'true' ]]; then
mkdir -p ${APT_CACHE_DIR}
mkdir -p ${APT_LIST_CACHE_DIR}
fi
sudo apt-get update \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
sudo apt-get -y install \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
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
if [[ "${APT_CACHE_HIT}" != 'true' ]]; then
sudo chmod -R 755 ${APT_CACHE_BASE}
fi
- name: Save apt cache
if: |
always()
&& steps.cache-apt-check.outputs.cache-hit != 'true'
&& steps.install-os-deps.outcome == 'success'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
id: create-venv
env:
PYTHON_VERSION: ${{ steps.python.outputs.python-version }}
run: |
uv venv venv --python "${PYTHON_VERSION}"
python -m venv venv
. venv/bin/activate
python --version
pip install "$(grep '^uv' < requirements.txt)"
uv pip install -U "pip>=25.2"
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
@@ -466,22 +506,36 @@ jobs:
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Restore apt cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Install additional OS dependencies
timeout-minutes: 10
run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
sudo apt-get -y install \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
libturbojpeg
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install additional OS dependencies
timeout-minutes: 10
uses: ./.github/actions/cache-apt-packages
with:
packages: libturbojpeg
version: ${{ env.APT_CACHE_VERSION }}
- 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: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -515,10 +569,10 @@ 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: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -551,10 +605,10 @@ 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: |
python -m script.gen_copilot_instructions validate
@@ -606,10 +660,10 @@ jobs:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
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: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -657,10 +711,10 @@ 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: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -710,10 +764,10 @@ 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: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -761,10 +815,10 @@ 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: Generate partial mypy restore key
id: generate-mypy-key
run: |
@@ -822,26 +876,38 @@ jobs:
- info
- base
steps:
- name: Restore apt cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Install additional OS dependencies
timeout-minutes: 10
run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
sudo apt-get -y install \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
bluez \
ffmpeg \
libturbojpeg
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install additional OS dependencies
timeout-minutes: 10
uses: ./.github/actions/cache-apt-packages
with:
packages: >-
bluez
ffmpeg
libturbojpeg
version: ${{ env.APT_CACHE_VERSION }}
execute_install_scripts: true
- 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: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -886,27 +952,39 @@ jobs:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Install additional OS dependencies
timeout-minutes: 10
run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
sudo apt-get -y install \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
bluez \
ffmpeg \
libturbojpeg \
libxml2-utils
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install additional OS dependencies
timeout-minutes: 10
uses: ./.github/actions/cache-apt-packages
with:
packages: >-
bluez
ffmpeg
libturbojpeg
libxml2-utils
version: ${{ env.APT_CACHE_VERSION }}
execute_install_scripts: true
- name: Set up Python ${{ matrix.python-version }}
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: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -1027,28 +1105,40 @@ jobs:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Install additional OS dependencies
timeout-minutes: 10
run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
sudo apt-get -y install \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
bluez \
ffmpeg \
libturbojpeg \
libmariadb-dev-compat \
libxml2-utils
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install additional OS dependencies
timeout-minutes: 10
uses: ./.github/actions/cache-apt-packages
with:
packages: >-
bluez
ffmpeg
libturbojpeg
libmariadb-dev-compat
libxml2-utils
version: ${{ env.APT_CACHE_VERSION }}
execute_install_scripts: true
- name: Set up Python ${{ matrix.python-version }}
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: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -1176,35 +1266,42 @@ jobs:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Install additional OS dependencies
timeout-minutes: 10
run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
sudo apt-get -y install \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
bluez \
ffmpeg \
libturbojpeg \
libxml2-utils
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
sudo apt-get -y install \
postgresql-server-dev-14
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install additional OS dependencies
timeout-minutes: 10
uses: ./.github/actions/cache-apt-packages
with:
packages: >-
bluez
ffmpeg
libturbojpeg
libxml2-utils
version: ${{ env.APT_CACHE_VERSION }}
execute_install_scripts: true
- name: Set up PostgreSQL apt repository
run: sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
- name: Cache PostgreSQL development headers
timeout-minutes: 10
uses: ./.github/actions/cache-apt-packages
with:
packages: postgresql-server-dev-14
version: ${{ env.APT_CACHE_VERSION }}
- name: Set up Python ${{ matrix.python-version }}
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: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -1324,7 +1421,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
fail_ci_if_error: true
flags: full-suite
@@ -1352,27 +1449,39 @@ jobs:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Install additional OS dependencies
timeout-minutes: 10
run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
sudo apt-get -y install \
-o Dir::Cache=${APT_CACHE_DIR} \
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
bluez \
ffmpeg \
libturbojpeg \
libxml2-utils
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install additional OS dependencies
timeout-minutes: 10
uses: ./.github/actions/cache-apt-packages
with:
packages: >-
bluez
ffmpeg
libturbojpeg
libxml2-utils
version: ${{ env.APT_CACHE_VERSION }}
execute_install_scripts: true
- name: Set up Python ${{ matrix.python-version }}
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: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -1483,7 +1592,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
@@ -1511,7 +1620,7 @@ jobs:
with:
pattern: test-results-*
- name: Upload test results to Codecov
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
report_type: test_results
fail_ci_if_error: true
+4 -4
View File
@@ -55,11 +55,11 @@ jobs:
- name: Generate app token
id: token
# Pinned to a specific version of the action for security reasons
# v3.2.0
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
# v1.7.0
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
with:
app-id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} # zizmor: ignore[secrets-outside-env]
private-key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }} # zizmor: ignore[secrets-outside-env]
app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} # zizmor: ignore[secrets-outside-env]
private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }} # zizmor: ignore[secrets-outside-env]
# The 90 day stale policy for issues
# Used for:
-4
View File
@@ -337,7 +337,6 @@ homeassistant.components.led_ble.*
homeassistant.components.lektrico.*
homeassistant.components.letpot.*
homeassistant.components.lg_infrared.*
homeassistant.components.lg_tv_rs232.*
homeassistant.components.libre_hardware_monitor.*
homeassistant.components.lidarr.*
homeassistant.components.liebherr.*
@@ -429,7 +428,6 @@ homeassistant.components.otp.*
homeassistant.components.ouman_eh_800.*
homeassistant.components.overkiz.*
homeassistant.components.overseerr.*
homeassistant.components.ovhcloud_ai_endpoints.*
homeassistant.components.p1_monitor.*
homeassistant.components.paj_gps.*
homeassistant.components.panel_custom.*
@@ -567,7 +565,6 @@ homeassistant.components.technove.*
homeassistant.components.tedee.*
homeassistant.components.telegram_bot.*
homeassistant.components.teleinfo.*
homeassistant.components.teltonika.*
homeassistant.components.teslemetry.*
homeassistant.components.text.*
homeassistant.components.thethingsnetwork.*
@@ -612,7 +609,6 @@ homeassistant.components.valve.*
homeassistant.components.velbus.*
homeassistant.components.velux.*
homeassistant.components.victron_gx.*
homeassistant.components.vistapool.*
homeassistant.components.vivotek.*
homeassistant.components.vlc_telnet.*
homeassistant.components.vodafone_station.*
Generated
-10
View File
@@ -987,8 +987,6 @@ CLAUDE.md @home-assistant/core
/tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
/tests/components/lg_thinq/ @LG-ThinQ-Integration
/homeassistant/components/lg_tv_rs232/ @balloob
/tests/components/lg_tv_rs232/ @balloob
/homeassistant/components/libre_hardware_monitor/ @Sab44
/tests/components/libre_hardware_monitor/ @Sab44
/homeassistant/components/lichess/ @aryanhasgithub
@@ -1292,8 +1290,6 @@ CLAUDE.md @home-assistant/core
/tests/components/openhome/ @bazwilliams
/homeassistant/components/openrgb/ @felipecrs
/tests/components/openrgb/ @felipecrs
/homeassistant/components/opensensemap/ @AlCalzone
/tests/components/opensensemap/ @AlCalzone
/homeassistant/components/opensky/ @joostlek
/tests/components/opensky/ @joostlek
/homeassistant/components/opentherm_gw/ @mvn23
@@ -1321,8 +1317,6 @@ CLAUDE.md @home-assistant/core
/tests/components/overkiz/ @imicknl
/homeassistant/components/overseerr/ @joostlek @AmGarera
/tests/components/overseerr/ @joostlek @AmGarera
/homeassistant/components/ovhcloud_ai_endpoints/ @Crocmagnon
/tests/components/ovhcloud_ai_endpoints/ @Crocmagnon
/homeassistant/components/ovo_energy/ @timmo001
/tests/components/ovo_energy/ @timmo001
/homeassistant/components/p1_monitor/ @klaasnicolaas
@@ -1936,8 +1930,6 @@ CLAUDE.md @home-assistant/core
/tests/components/victron_remote_monitoring/ @AndyTempel
/homeassistant/components/vilfo/ @ManneW
/tests/components/vilfo/ @ManneW
/homeassistant/components/vistapool/ @fdebrus
/tests/components/vistapool/ @fdebrus
/homeassistant/components/vivotek/ @HarlemSquirrel
/tests/components/vivotek/ @HarlemSquirrel
/homeassistant/components/vizio/ @raman325
@@ -2062,8 +2054,6 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/yi/ @bachya
/homeassistant/components/yolink/ @matrixd2
/tests/components/yolink/ @matrixd2
/homeassistant/components/yoto/ @cdnninja @piitaya
/tests/components/yoto/ @cdnninja @piitaya
/homeassistant/components/youless/ @gjong
/tests/components/youless/ @gjong
/homeassistant/components/youtube/ @joostlek
@@ -1,13 +1,9 @@
"""Alexa Devices integration."""
import asyncio
import contextlib
from homeassistant.const import CONF_COUNTRY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client, config_validation as cv, httpx_client
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.ssl import SSL_ALPN_HTTP11_HTTP2
from .const import _LOGGER, CONF_LOGIN_DATA, CONF_SITE, COUNTRY_DOMAINS, DOMAIN
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
@@ -16,8 +12,6 @@ from .services import async_setup_services
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.EVENT,
Platform.MEDIA_PLAYER,
Platform.NOTIFY,
Platform.SENSOR,
Platform.SWITCH,
@@ -40,28 +34,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
await coordinator.async_config_entry_first_refresh()
await coordinator.sync_history_state()
await coordinator.sync_media_state()
async def _on_http2_reauth_required() -> None:
entry.async_start_reauth(hass)
async def _cancel_http2() -> None:
http2_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await http2_task
alexa_httpx_client = httpx_client.get_async_client(
hass,
alpn_protocols=SSL_ALPN_HTTP11_HTTP2,
)
http2_task = await coordinator.api.start_http2_processing(
alexa_httpx_client, on_reauth_required=_on_http2_reauth_required
)
entry.async_on_unload(_cancel_http2)
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -8,18 +8,13 @@ from aioamazondevices.exceptions import (
CannotConnect,
CannotRetrieveData,
)
from aioamazondevices.structures import (
AmazonDevice,
AmazonMediaState,
AmazonVocalRecord,
AmazonVolumeState,
)
from aioamazondevices.structures import AmazonDevice
from aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -78,18 +73,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
if routine.domain == Platform.BUTTON
}
self._vocal_records: dict[str, AmazonVocalRecord] = {}
self.api.on_history_event.append(self.history_state_event_handler)
self.api.on_history_event.freeze()
self._volume_states: dict[str, AmazonVolumeState] = {}
self.api.on_volume_state_event.append(self.volume_state_event_handler)
self.api.on_volume_state_event.freeze()
self._media_states: dict[str, AmazonMediaState] = {}
self.api.on_media_state_event.append(self.media_state_event_handler)
self.api.on_media_state_event.freeze()
async def _async_update_data(self) -> dict[str, AmazonDevice]:
"""Update device data."""
try:
@@ -166,66 +149,3 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
)
if entity_id:
entity_registry.async_remove(entity_id)
async def sync_history_state(self) -> None:
"""Sync history state."""
try:
self._vocal_records = await self.api.sync_history_state()
except CannotAuthenticate as e:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(e)},
) from e
except CannotConnect as e:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect_with_error",
translation_placeholders={"error": repr(e)},
) from e
except BaseException as e:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(e)},
) from e
async def history_state_event_handler(
self, vocal_records: dict[str, AmazonVocalRecord]
) -> None:
"""Handle pushed vocal record events."""
self._vocal_records = {**self._vocal_records, **vocal_records}
self.async_update_listeners()
@property
def vocal_records(self) -> dict[str, AmazonVocalRecord]:
"""Vocal records of devices."""
return self._vocal_records
async def sync_media_state(self) -> None:
"""Sync media state."""
await self.api.sync_media_state()
async def media_state_event_handler(
self, media_state: dict[str, AmazonMediaState]
) -> None:
"""Handle pushed media state changed events."""
self._media_states = media_state
self.async_update_listeners()
@property
def media_states(self) -> dict[str, AmazonMediaState]:
"""Media state of devices."""
return self._media_states
async def volume_state_event_handler(
self, volume_states: dict[str, AmazonVolumeState]
) -> None:
"""Handle pushed volume change events."""
self._volume_states = volume_states
self.async_update_listeners()
@property
def volume_states(self) -> dict[str, AmazonVolumeState]:
"""Volumes of devices."""
return self._volume_states
@@ -1,86 +0,0 @@
"""Support for events."""
from typing import Final
from homeassistant.components.event import EventEntity, EventEntityDescription
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import _LOGGER
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .entity import AmazonEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
EVENTS: Final = {
EventEntityDescription(
key="voice_event",
translation_key="voice_event",
),
}
EVENT_TYPE = "triggered"
async def async_setup_entry(
hass: HomeAssistant,
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Alexa Devices events based on a config entry."""
coordinator = entry.runtime_data
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AlexaVoiceEvent(coordinator, serial_num, event_desc)
for event_desc in EVENTS
for serial_num in new_devices
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
class AlexaVoiceEvent(AmazonEntity, EventEntity):
"""Representation of an Alexa voice event."""
_attr_event_types = [EVENT_TYPE]
coordinator: AmazonDevicesCoordinator
_last_seen_timestamp: int | None = None
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if not (
vocal_record := self.coordinator.vocal_records.get(
self.device.serial_number
)
):
_LOGGER.debug(
"No vocal record found for device %s [%s]",
self.device.account_name,
self.device.serial_number,
)
return
if vocal_record.timestamp == self._last_seen_timestamp:
return
self._last_seen_timestamp = vocal_record.timestamp
self._trigger_event(
EVENT_TYPE,
{
"intent": vocal_record.intent,
"voice_command": vocal_record.title,
"voice_reply": vocal_record.sub_title,
},
)
self.async_write_ha_state()
@@ -1,10 +1,5 @@
{
"entity": {
"event": {
"voice_event": {
"default": "mdi:chat-processing"
}
},
"sensor": {
"voc_index": {
"default": "mdi:molecule"
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==13.8.0"]
"requirements": ["aioamazondevices==13.7.0"]
}
@@ -1,294 +0,0 @@
"""Media player platform for Alexa Devices."""
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Final
from aioamazondevices.structures import (
AmazonMediaControls,
AmazonMediaState,
AmazonVolumeState,
)
from homeassistant.components.media_player import (
MediaPlayerDeviceClass,
MediaPlayerEnqueue,
MediaPlayerEntity,
MediaPlayerEntityDescription,
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import _LOGGER
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .entity import AmazonEntity
from .utils import alexa_api_call
PARALLEL_UPDATES = 1
STANDARD_SUPPORTED_FEATURES = (
MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.PLAY_MEDIA
)
@dataclass(frozen=True, kw_only=True)
class AmazonDevicesMediaPlayerEntityDescription(MediaPlayerEntityDescription):
"""Describes an Alexa Devices media player entity."""
MEDIA_PLAYERS: Final = (
AmazonDevicesMediaPlayerEntityDescription(
key="media",
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Alexa Devices media player entities from a config entry."""
coordinator = entry.runtime_data
known_devices: set[str] = set()
def _check_device() -> None:
"""Add entities for newly discovered devices."""
new_entities: list[AlexaDevicesMediaPlayer] = []
for serial_num, device in coordinator.data.items():
if serial_num in known_devices or not device.media_player_supported:
continue
known_devices.add(serial_num)
new_entities.extend(
AlexaDevicesMediaPlayer(coordinator, serial_num, description)
for description in MEDIA_PLAYERS
)
if new_entities:
async_add_entities(new_entities)
remove_listener = coordinator.async_add_listener(_check_device)
entry.async_on_unload(remove_listener)
_check_device()
class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
"""Representation of an Alexa device media player."""
entity_description: AmazonDevicesMediaPlayerEntityDescription
_attr_name = None # Uses the device name
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
_attr_volume_step = 0.05
def __init__(
self,
coordinator: AmazonDevicesCoordinator,
serial_num: str,
description: AmazonDevicesMediaPlayerEntityDescription,
) -> None:
"""Initialize."""
self._prev_volume: int | None = None
super().__init__(coordinator, serial_num, description)
@property
def media_state(self) -> AmazonMediaState | None:
"""Return the media state relating to device."""
if not self.coordinator or not self.coordinator.media_states:
return None
return self.coordinator.media_states.get(self._serial_num)
@property
def volume_state(self) -> AmazonVolumeState | None:
"""Volume settings for device."""
if not self.coordinator or not self.coordinator.volume_states:
return None
return self.coordinator.volume_states.get(self._serial_num)
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Return dynamically supported features based on current media."""
features = STANDARD_SUPPORTED_FEATURES
if self.media_state is None:
return features
if self.media_state.pause_enabled:
features |= MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE
if self.media_state.next_enabled:
features |= MediaPlayerEntityFeature.NEXT_TRACK
if self.media_state.previous_enabled:
features |= MediaPlayerEntityFeature.PREVIOUS_TRACK
return features
@property
def state(self) -> MediaPlayerState | None:
"""Return the current state of the player."""
if not self.media_state:
return MediaPlayerState.IDLE
if self.media_state.player_state == "PLAYING":
return MediaPlayerState.PLAYING
if self.media_state.player_state == "PAUSED":
return MediaPlayerState.PAUSED
return MediaPlayerState.IDLE
@property
def volume_level(self) -> float | None:
"""Return the volume level (0.0 to 1.0)."""
if not self.volume_state or self.volume_state.volume is None:
return None
return self.volume_state.volume / 100
@property
def is_volume_muted(self) -> bool | None:
"""Return True if the volume is muted."""
if not self.volume_state:
return None
return self.volume_state.volume == 0
@property
def media_title(self) -> str | None:
"""Track title."""
if not self.media_state:
return None
return self.media_state.now_playing_title
@property
def media_artist(self) -> str | None:
"""Artist name."""
if not self.media_state:
return None
return self.media_state.now_playing_line1
@property
def media_album_name(self) -> str | None:
"""Album name."""
if not self.media_state:
return None
return self.media_state.now_playing_line2
@property
def media_image_url(self) -> str | None:
"""Album art URL."""
if not self.media_state:
return None
return self.media_state.now_playing_url
@property
def media_duration(self) -> int | None:
"""Duration in seconds."""
if not self.media_state:
return None
return self.media_state.media_length
@property
def media_position(self) -> int | None:
"""Current playback position in seconds."""
if not self.media_state:
return None
return self.media_state.media_position
@property
def media_position_updated_at(self) -> datetime | None:
"""When media_position was last updated — HA uses this to interpolate the progress bar."""
if not self.media_state:
return None
return self.media_state.media_position_updated_at
@property
def media_content_type(self) -> MediaType | None:
"""Content type — tells HA what kind of media is playing."""
if self.state in [MediaPlayerState.PLAYING, MediaPlayerState.PAUSED]:
return MediaType.MUSIC
return None
async def async_play_media(
self,
media_type: MediaType | str,
media_id: str,
enqueue: MediaPlayerEnqueue | None = None,
announce: bool | None = None,
**kwargs: Any,
) -> None:
"""Play a piece of media."""
await self.async_call_alexa_music(media_id, media_type)
@alexa_api_call
async def async_call_alexa_music(
self, search_phrase: str, provider_id: str
) -> None:
"""Call alexa music."""
await self.coordinator.api.call_alexa_music(
self.device, search_phrase, provider_id
)
@alexa_api_call
async def async_set_device_volume(self, volume: int) -> None:
"""Set the device volume."""
_LOGGER.debug(
"Setting volume for %s to %s%%",
self.device.serial_number,
volume,
)
await self.coordinator.api.set_device_volume(self.device, volume)
async def async_set_volume_level(self, volume: float) -> None:
"""Set the volume level (0.0 to 1.0)."""
device_volume = round(volume * 100)
await self.async_set_device_volume(device_volume)
async def async_mute_volume(self, mute: bool) -> None:
"""Mute or un-mute the volume."""
# Whilst you can mute a device by asking it there appears to be
# no way to do this programmatically so set volume to 0
if not self.volume_state or self.volume_state.volume is None:
return
if mute:
self._prev_volume = self.volume_state.volume
target_volume = 0
else:
if self._prev_volume is None:
return
target_volume = self._prev_volume
await self.async_set_volume_level(target_volume / 100)
@alexa_api_call
async def _send_media_command(self, command: AmazonMediaControls) -> None:
_LOGGER.debug(
"Sending media command '%s' to %s", command, self.device.serial_number
)
await self.coordinator.api.send_media_command(self.device, command)
async def async_media_stop(self) -> None:
"""Send stop command."""
await self._send_media_command(AmazonMediaControls.Stop)
async def async_media_pause(self) -> None:
"""Send pause command."""
await self._send_media_command(AmazonMediaControls.Pause)
async def async_media_play(self) -> None:
"""Send play command."""
await self._send_media_command(AmazonMediaControls.Play)
async def async_media_next_track(self) -> None:
"""Send next track command."""
await self._send_media_command(AmazonMediaControls.Next)
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
await self._send_media_command(AmazonMediaControls.Previous)
@@ -58,18 +58,6 @@
}
},
"entity": {
"event": {
"voice_event": {
"name": "Voice event",
"state_attributes": {
"event_type": {
"state": {
"triggered": "Triggered"
}
}
}
}
},
"notify": {
"announce": {
"name": "Announce"
@@ -7,11 +7,10 @@ from altruistclient import AltruistClient, AltruistDeviceModel, AltruistError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import DOMAIN
from .const import CONF_HOST, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -1,3 +1,6 @@
"""Constants for the Altruist integration."""
DOMAIN = "altruist"
# pylint: disable-next=home-assistant-duplicate-const
CONF_HOST = "host"
@@ -10,12 +10,13 @@ import logging
from altruistclient import AltruistClient, AltruistError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_HOST
_LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(seconds=15)
@@ -230,13 +230,13 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
if entry.version == 2 and entry.minor_version == 3:
# Remove Temperature parameter
temperature_key = "temperature"
CONF_TEMPERATURE = "temperature"
for subentry in entry.subentries.values():
data = subentry.data.copy()
if temperature_key not in data:
if CONF_TEMPERATURE not in data:
continue
data.pop(temperature_key, None)
data.pop(CONF_TEMPERATURE, None)
hass.config_entries.async_update_subentry(entry, subentry, data=data)
hass.config_entries.async_update_entry(entry, minor_version=4)
+24
View File
@@ -7,3 +7,27 @@ CONNECTION_TIMEOUT: int = 10
# Field name of last self test retrieved from apcupsd.
LAST_S_TEST: Final = "laststest"
# Mapping of deprecated sensor keys (as reported by apcupsd,
# lower-cased) to their deprecation
# repair issue translation keys.
DEPRECATED_SENSORS: Final = {
"apc": "apc_deprecated",
"end apc": "date_deprecated",
"date": "date_deprecated",
"apcmodel": "available_via_device_info",
"model": "available_via_device_info",
"firmware": "available_via_device_info",
"version": "available_via_device_info",
"upsname": "available_via_device_info",
"serialno": "available_via_device_info",
}
AVAILABLE_VIA_DEVICE_ATTR: Final = {
"apcmodel": "model",
"model": "model",
"firmware": "hw_version",
"version": "sw_version",
"upsname": "name",
"serialno": "serial_number",
}
+121 -19
View File
@@ -1,10 +1,11 @@
"""Support for APCUPSd sensors."""
import logging
from typing import Final
import dateutil
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
@@ -23,9 +24,11 @@ from homeassistant.const import (
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
import homeassistant.helpers.issue_registry as ir
from .const import LAST_S_TEST
from .const import AVAILABLE_VIA_DEVICE_ATTR, DEPRECATED_SENSORS, DOMAIN, LAST_S_TEST
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
from .entity import APCUPSdEntity
@@ -33,20 +36,6 @@ PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
# List of useless sensors to ignore, since they are either provided in device
# information, or not useful at all
IGNORED_SENSORS: Final = {
"apc",
"end apc",
"date",
"apcmodel",
"model",
"firmware",
"version",
"upsname",
"serialno",
}
SENSORS: dict[str, SensorEntityDescription] = {
"alarmdel": SensorEntityDescription(
key="alarmdel",
@@ -60,6 +49,18 @@ SENSORS: dict[str, SensorEntityDescription] = {
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
"apc": SensorEntityDescription(
key="apc",
translation_key="apc_status",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"apcmodel": SensorEntityDescription(
key="apcmodel",
translation_key="apc_model",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"badbatts": SensorEntityDescription(
key="badbatts",
translation_key="bad_batteries",
@@ -99,6 +100,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.DURATION,
),
"date": SensorEntityDescription(
key="date",
translation_key="date",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"dipsw": SensorEntityDescription(
key="dipsw",
translation_key="dip_switch_settings",
@@ -125,11 +132,23 @@ SENSORS: dict[str, SensorEntityDescription] = {
translation_key="wake_delay",
entity_category=EntityCategory.DIAGNOSTIC,
),
"end apc": SensorEntityDescription(
key="end apc",
translation_key="date_and_time",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"extbatts": SensorEntityDescription(
key="extbatts",
translation_key="external_batteries",
entity_category=EntityCategory.DIAGNOSTIC,
),
"firmware": SensorEntityDescription(
key="firmware",
translation_key="firmware_version",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"hitrans": SensorEntityDescription(
key="hitrans",
translation_key="transfer_high",
@@ -245,6 +264,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
translation_key="min_time",
entity_category=EntityCategory.DIAGNOSTIC,
),
"model": SensorEntityDescription(
key="model",
translation_key="model",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"nombattv": SensorEntityDescription(
key="nombattv",
translation_key="battery_nominal_voltage",
@@ -333,6 +358,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"serialno": SensorEntityDescription(
key="serialno",
translation_key="serial_number",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"starttime": SensorEntityDescription(
key="starttime",
translation_key="startup_time",
@@ -373,6 +404,18 @@ SENSORS: dict[str, SensorEntityDescription] = {
translation_key="ups_mode",
entity_category=EntityCategory.DIAGNOSTIC,
),
"upsname": SensorEntityDescription(
key="upsname",
translation_key="ups_name",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"version": SensorEntityDescription(
key="version",
translation_key="version",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"xoffbat": SensorEntityDescription(
key="xoffbat",
translation_key="transfer_from_battery",
@@ -438,10 +481,9 @@ async def async_setup_entry(
# as unknown initially.
#
# We also sort the resources to ensure the order of entities
# created is deterministic
# created is deterministic since "APCMODEL" and "MODEL"
# resources map to the same "Model" name.
for resource in sorted(available_resources | {LAST_S_TEST}):
if resource in IGNORED_SENSORS:
continue
if resource not in SENSORS:
_LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper())
continue
@@ -519,3 +561,63 @@ class APCUPSdSensor(APCUPSdEntity, SensorEntity):
self._attr_native_value, inferred_unit = infer_unit(data)
if not self.native_unit_of_measurement:
self._attr_native_unit_of_measurement = inferred_unit
async def async_added_to_hass(self) -> None:
"""Handle when entity is added to Home Assistant.
If this is a deprecated sensor entity, create a repair issue to guide
the user to disable it.
"""
await super().async_added_to_hass()
if not self.enabled:
return
reason = DEPRECATED_SENSORS.get(self.entity_description.key)
if not reason:
return
automations = automations_with_entity(self.hass, self.entity_id)
scripts = scripts_with_entity(self.hass, self.entity_id)
if not automations and not scripts:
return
entity_registry = er.async_get(self.hass)
items = [
f"- [{entry.name or entry.original_name or entity_id}]"
f"(/config/{integration}/edit/"
f"{entry.unique_id or entity_id.split('.', 1)[-1]})"
for integration, entities in (
("automation", automations),
("script", scripts),
)
for entity_id in entities
if (entry := entity_registry.async_get(entity_id))
]
placeholders = {
"entity_name": str(self.name or self.entity_id),
"entity_id": self.entity_id,
"items": "\n".join(items),
}
if via_attr := AVAILABLE_VIA_DEVICE_ATTR.get(self.entity_description.key):
placeholders["available_via_device_attr"] = via_attr
if device_entry := self.device_entry:
placeholders["device_id"] = device_entry.id
ir.async_create_issue(
self.hass,
DOMAIN,
f"{reason}_{self.entity_id}",
breaks_in_ha_version="2026.6.0",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key=reason,
translation_placeholders=placeholders,
)
async def async_will_remove_from_hass(self) -> None:
"""Handle when entity will be removed from Home Assistant."""
await super().async_will_remove_from_hass()
if issue_key := DEPRECATED_SENSORS.get(self.entity_description.key):
ir.async_delete_issue(self.hass, DOMAIN, f"{issue_key}_{self.entity_id}")
@@ -241,5 +241,19 @@
"cannot_connect": {
"message": "Cannot connect to APC UPS Daemon."
}
},
"issues": {
"apc_deprecated": {
"description": "The {entity_name} sensor (`{entity_id}`) is deprecated because it exposes internal details of the APC UPS Daemon response.\n\nIt is still referenced in the following automations or scripts:\n{items}\n\nUpdate those automations or scripts to use supported APC UPS entities instead. Reload the APC UPS Daemon integration afterwards to resolve this issue.",
"title": "{entity_name} sensor is deprecated"
},
"available_via_device_info": {
"description": "The {entity_name} sensor (`{entity_id}`) is deprecated because the same value is available from the device registry via `device_attr(\"{device_id}\", \"{available_via_device_attr}\")`.\n\nIt is still referenced in the following automations or scripts:\n{items}\n\nUpdate those automations or scripts to use the `device_attr` helper instead of this sensor. Reload the APC UPS Daemon integration afterwards to resolve this issue.",
"title": "{entity_name} sensor is deprecated"
},
"date_deprecated": {
"description": "The {entity_name} sensor (`{entity_id}`) is deprecated because the timestamp is already available from other APC UPS sensors via their last updated time.\n\nIt is still referenced in the following automations or scripts:\n{items}\n\nUpdate those automations or scripts to reference any entity's `last_updated` attribute instead (for example, `states.binary_sensor.apcups_online_status.last_updated`). Reload the APC UPS Daemon integration afterwards to resolve this issue.",
"title": "{entity_name} sensor is deprecated"
}
}
}
@@ -24,7 +24,6 @@ from pyatv.interface import (
PushListener,
PushUpdater,
)
from yarl import URL
from homeassistant.components import media_source
from homeassistant.components.media_player import (
@@ -346,10 +345,7 @@ class AppleTvMediaPlayer(
play_item = await media_source.async_resolve_media(
self.hass, media_id, self.entity_id
)
if play_item.path and self._is_feature_available(FeatureName.StreamFile):
media_id = str(play_item.path)
else:
media_id = async_process_play_media_url(self.hass, play_item.url)
media_id = async_process_play_media_url(self.hass, play_item.url)
media_type = MediaType.MUSIC
if self._is_feature_available(FeatureName.StreamFile) and (
@@ -357,16 +353,11 @@ class AppleTvMediaPlayer(
):
_LOGGER.debug("Streaming %s via RAOP", media_id)
await self.atv.stream.stream_file(media_id)
elif self._is_feature_available(FeatureName.PlayUrl) and (
(parsed_url := URL(media_id)).is_absolute() and parsed_url.host
):
elif self._is_feature_available(FeatureName.PlayUrl):
_LOGGER.debug("Playing %s via AirPlay", media_id)
await self.atv.stream.play_url(media_id)
else:
_LOGGER.error(
"Media streaming is not possible with current configuration for %s",
media_id,
)
_LOGGER.error("Media streaming is not possible with current configuration")
@property
def media_image_hash(self) -> str | None:
+1 -5
View File
@@ -193,11 +193,7 @@ async def async_setup_entry(
Aranet4BluetoothSensorEntity, async_add_entities
)
)
entry.async_on_unload(
entry.runtime_data.async_register_processor(
processor, AranetSensorEntityDescription
)
)
entry.async_on_unload(entry.runtime_data.async_register_processor(processor))
class Aranet4BluetoothSensorEntity(
+14 -14
View File
@@ -49,20 +49,6 @@ SENSORS_TYPE_COUNT = "sensors_count"
_LOGGER = logging.getLogger(__name__)
_ENTITY_MIGRATION_ID = {
"sensor_connected_device": "Devices Connected",
"sensor_rx_bytes": "Download",
"sensor_tx_bytes": "Upload",
"sensor_rx_rates": "Download Speed",
"sensor_tx_rates": "Upload Speed",
"sensor_load_avg1": "Load Avg (1m)",
"sensor_load_avg5": "Load Avg (5m)",
"sensor_load_avg15": "Load Avg (15m)",
"2.4GHz": "2.4GHz Temperature",
"5.0GHz": "5GHz Temperature",
"CPU": "CPU Temperature",
}
class AsusWrtSensorDataHandler:
"""Data handler for AsusWrt sensor."""
@@ -201,6 +187,20 @@ class AsusWrtRouter:
def _migrate_entities_unique_id(self) -> None:
"""Migrate router entities to new unique id format."""
_ENTITY_MIGRATION_ID = {
"sensor_connected_device": "Devices Connected",
"sensor_rx_bytes": "Download",
"sensor_tx_bytes": "Upload",
"sensor_rx_rates": "Download Speed",
"sensor_tx_rates": "Upload Speed",
"sensor_load_avg1": "Load Avg (1m)",
"sensor_load_avg5": "Load Avg (5m)",
"sensor_load_avg15": "Load Avg (15m)",
"2.4GHz": "2.4GHz Temperature",
"5.0GHz": "5GHz Temperature",
"CPU": "CPU Temperature",
}
entity_reg = er.async_get(self.hass)
router_entries = er.async_entries_for_config_entry(
entity_reg, self._entry.entry_id
+4 -44
View File
@@ -1,6 +1,5 @@
"""Light platform for Avea."""
from collections.abc import Callable
from contextlib import suppress
import logging
from typing import Any
@@ -20,7 +19,6 @@ from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@@ -29,7 +27,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import color as color_util
from . import AveaConfigEntry
from .const import DOMAIN, INTEGRATION_TITLE, MODEL, UNKNOWN_NAME
from .const import DOMAIN, INTEGRATION_TITLE, UNKNOWN_NAME
_LOGGER = logging.getLogger(__name__)
UPDATE_EXCEPTIONS = (BleakError, OSError, RuntimeError)
@@ -44,13 +42,6 @@ def _normalize_name(name: str | None) -> str | None:
return name
def _read_device_info_value(read: Callable[[], str | None]) -> str | None:
"""Read a device information value from an Avea bulb."""
with suppress(*UPDATE_EXCEPTIONS):
return _normalize_name(read())
return None
def _ha_brightness_to_avea(brightness: int) -> int:
"""Convert Home Assistant brightness to Avea brightness."""
return round((brightness / 255) * AVEA_MAX_BRIGHTNESS)
@@ -105,8 +96,7 @@ async def async_setup_entry(
) -> None:
"""Set up the Avea light platform."""
async_add_entities(
[AveaLight(entry.runtime_data, entry.data[CONF_ADDRESS])],
update_before_add=True,
[AveaLight(entry.runtime_data, entry.title)], update_before_add=True
)
@@ -190,42 +180,14 @@ class AveaLight(LightEntity):
"""Representation of an Avea."""
_attr_color_mode = ColorMode.HS
_attr_has_entity_name = True
_attr_name = None
_attr_supported_color_modes = {ColorMode.HS}
def __init__(self, light: avea.Bulb, address: str) -> None:
def __init__(self, light: avea.Bulb, entry_title: str) -> None:
"""Initialize an AveaLight."""
self._light = light
self._attr_unique_id = address
self._attr_name = entry_title
self._attr_brightness = light.brightness
self._last_brightness = 255
self._device_info_updated = False
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_BLUETOOTH, address)},
model=MODEL,
)
def _update_device_info(self) -> None:
"""Fetch device information from the Avea bulb."""
device_info = self._attr_device_info
assert device_info is not None
manufacturer = _read_device_info_value(self._light.get_manufacturer_name)
hardware_revision = _read_device_info_value(self._light.get_hardware_revision)
firmware_version = _read_device_info_value(self._light.get_fw_version)
serial_number = _read_device_info_value(self._light.get_serial_number)
if manufacturer:
device_info["manufacturer"] = manufacturer
if hardware_revision:
device_info["hw_version"] = hardware_revision
if firmware_version:
device_info["sw_version"] = firmware_version
if serial_number:
device_info["serial_number"] = serial_number
self._device_info_updated = True
def turn_on(self, **kwargs: Any) -> None:
"""Instruct the light to turn on."""
@@ -252,8 +214,6 @@ class AveaLight(LightEntity):
connected = self._light.connect()
try:
if not self._device_info_updated:
self._update_device_info()
brightness = self._light.get_brightness()
rgb_color = self._light.get_rgb()
finally:
+1 -2
View File
@@ -17,11 +17,10 @@ from homeassistant.components.backup import (
OnProgressCallback,
suggested_filename,
)
from homeassistant.const import CONF_PREFIX
from homeassistant.core import HomeAssistant, callback
from . import S3ConfigEntry
from .const import CONF_BUCKET, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .const import CONF_BUCKET, CONF_PREFIX, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .helpers import async_list_backups_from_s3
_LOGGER = logging.getLogger(__name__)
@@ -8,7 +8,6 @@ from botocore.exceptions import ClientError, ConnectionError, ParamValidationErr
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PREFIX
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import (
TextSelector,
@@ -21,6 +20,7 @@ from .const import (
CONF_ACCESS_KEY_ID,
CONF_BUCKET,
CONF_ENDPOINT_URL,
CONF_PREFIX,
CONF_SECRET_ACCESS_KEY,
DEFAULT_ENDPOINT_URL,
DESCRIPTION_AWS_S3_DOCS_URL,
+2
View File
@@ -11,6 +11,8 @@ CONF_ACCESS_KEY_ID = "access_key_id"
CONF_SECRET_ACCESS_KEY = "secret_access_key"
CONF_ENDPOINT_URL = "endpoint_url"
CONF_BUCKET = "bucket"
# pylint: disable-next=home-assistant-duplicate-const
CONF_PREFIX = "prefix"
AWS_DOMAIN = "amazonaws.com"
DEFAULT_ENDPOINT_URL = f"https://s3.eu-central-1.{AWS_DOMAIN}/"
@@ -8,11 +8,10 @@ from aiobotocore.client import AioBaseClient as S3Client
from botocore.exceptions import BotoCoreError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PREFIX
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_BUCKET, DOMAIN
from .const import CONF_BUCKET, CONF_PREFIX, DOMAIN
from .helpers import async_list_backups_from_s3
SCAN_INTERVAL = timedelta(hours=6)
@@ -5,10 +5,15 @@ from typing import Any
from homeassistant.components.backup import DATA_MANAGER as BACKUP_DATA_MANAGER
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PREFIX
from homeassistant.core import HomeAssistant
from .const import CONF_ACCESS_KEY_ID, CONF_BUCKET, CONF_SECRET_ACCESS_KEY, DOMAIN
from .const import (
CONF_ACCESS_KEY_ID,
CONF_BUCKET,
CONF_PREFIX,
CONF_SECRET_ACCESS_KEY,
DOMAIN,
)
from .coordinator import S3ConfigEntry
from .helpers import async_list_backups_from_s3
+19 -25
View File
@@ -2,12 +2,13 @@
from collections.abc import Mapping
from ipaddress import ip_address
from typing import TYPE_CHECKING, Any
from typing import Any
from urllib.parse import urlsplit
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_IGNORE,
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigEntry,
@@ -49,9 +50,6 @@ from .const import (
from .errors import AuthenticationRequired, CannotConnect
from .hub import AxisHub, get_axis_api
if TYPE_CHECKING:
import axis
AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f", "e8:27:25"}
DEFAULT_PORT = 443
DEFAULT_PROTOCOL = "https"
@@ -96,8 +94,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
else:
if (serial := self._get_serial_number(api)) is None:
return self.async_abort(reason="no_serial_number")
serial = api.vapix.serial_number
config = {
CONF_PROTOCOL: user_input[CONF_PROTOCOL],
CONF_HOST: user_input[CONF_HOST],
@@ -142,15 +139,25 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
async def _create_entry(self, serial: str) -> ConfigFlowResult:
"""Create entry for device.
Use the discovered device name when available.
Generate a name to be used as a prefix for device entities.
"""
if (title_placeholders := self.context.get("title_placeholders")) is not None:
name = title_placeholders[CONF_NAME]
else:
name = f"{self.config[CONF_MODEL]} - {serial}"
model = self.config[CONF_MODEL]
same_model = [
entry.data[CONF_NAME]
for entry in self.hass.config_entries.async_entries(DOMAIN)
if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model
]
name = model
for idx in range(len(same_model) + 1):
name = f"{model} {idx}"
if name not in same_model:
break
self.config[CONF_NAME] = name
return self.async_create_entry(title=name, data=self.config)
title = f"{model} - {serial}"
return self.async_create_entry(title=title, data=self.config)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
@@ -262,19 +269,6 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
return await self.async_step_user()
@staticmethod
def _get_serial_number(api: axis.AxisDevice) -> str | None:
"""Retrieve the device serial number from the Axis API.
Tries basic_device_info first, then property_handler. Returns None if not found.
"""
vapix = api.vapix
if vapix.basic_device_info.initialized:
return vapix.basic_device_info["0"].serial_number
if vapix.params.property_handler.initialized:
return vapix.params.property_handler["0"].system_serial_number
return None
class AxisOptionsFlowHandler(OptionsFlow):
"""Handle Axis device options."""
@@ -3,7 +3,6 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"link_local_address": "Link local addresses are not supported",
"no_serial_number": "Could not retrieve a serial number from the device. Please check device connectivity and try again.",
"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%]",
@@ -32,7 +32,6 @@ PLATFORMS = [
Platform.LIGHT,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
]
PARALLEL_UPDATES = 0
+3 -31
View File
@@ -3,7 +3,7 @@
from typing import Any
import blebox_uniapi.cover
from blebox_uniapi.cover import BleboxCoverState, UnifiedCoverType
from blebox_uniapi.cover import BleboxCoverState
from homeassistant.components.cover import (
ATTR_POSITION,
@@ -25,19 +25,6 @@ BLEBOX_TO_COVER_DEVICE_CLASSES = {
"shutter": CoverDeviceClass.SHUTTER,
}
UNIFIED_COVER_TYPE_TO_DEVICE_CLASS = {
UnifiedCoverType.AWNING: CoverDeviceClass.AWNING,
UnifiedCoverType.BLIND: CoverDeviceClass.BLIND,
UnifiedCoverType.CURTAIN: CoverDeviceClass.CURTAIN,
UnifiedCoverType.DAMPER: CoverDeviceClass.DAMPER,
UnifiedCoverType.DOOR: CoverDeviceClass.DOOR,
UnifiedCoverType.GARAGE: CoverDeviceClass.GARAGE,
UnifiedCoverType.GATE: CoverDeviceClass.GATE,
UnifiedCoverType.SHADE: CoverDeviceClass.SHADE,
UnifiedCoverType.SHUTTER: CoverDeviceClass.SHUTTER,
UnifiedCoverType.WINDOW: CoverDeviceClass.WINDOW,
}
BLEBOX_TO_HASS_COVER_STATES = {
None: None,
# all blebox covers
@@ -72,6 +59,7 @@ class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
def __init__(self, feature: blebox_uniapi.cover.Cover) -> None:
"""Initialize a BleBox cover feature."""
super().__init__(feature)
self._attr_device_class = BLEBOX_TO_COVER_DEVICE_CLASSES[feature.device_class]
self._attr_supported_features = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
)
@@ -88,21 +76,6 @@ class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
| CoverEntityFeature.CLOSE_TILT
)
if feature.tilt_only:
self._attr_supported_features &= ~(
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.SET_POSITION
| CoverEntityFeature.STOP
)
@property
def device_class(self) -> CoverDeviceClass | None:
"""Return the device class based on cover type when available."""
if (cover_type := self._feature.cover_type) is not None:
return UNIFIED_COVER_TYPE_TO_DEVICE_CLASS[cover_type]
return BLEBOX_TO_COVER_DEVICE_CLASSES[self._feature.device_class]
@property
def current_cover_position(self) -> int | None:
"""Return the current cover position."""
@@ -145,8 +118,7 @@ class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Fully open the cover tilt."""
position = 50 if self._feature.is_tilt_180 else 0
await self._feature.async_set_tilt_position(position)
await self._feature.async_set_tilt_position(0)
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Fully close the cover tilt."""
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["blebox_uniapi"],
"requirements": ["blebox-uniapi==2.5.4"],
"requirements": ["blebox-uniapi==2.5.3"],
"zeroconf": ["_bbxsrv._tcp.local."]
}
-141
View File
@@ -1,141 +0,0 @@
"""BleBox update entities implementation."""
from datetime import timedelta
from typing import Any, Final
from blebox_uniapi.error import ConnectionError as BleBoxConnectionError, Error
import blebox_uniapi.update
from homeassistant.components.update import (
UpdateDeviceClass,
UpdateEntity,
UpdateEntityFeature,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_call_later
from . import BleBoxConfigEntry
from .entity import BleBoxEntity
SCAN_INTERVAL = timedelta(hours=1)
_POLL_INTERVAL_SECONDS: Final = 10
_MAX_POLL_ATTEMPTS: Final = 30
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BleBoxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a BleBox update entry."""
entities = [
BleBoxUpdateEntity(feature)
for feature in config_entry.runtime_data.features.get("updates", [])
]
async_add_entities(entities, True)
class BleBoxUpdateEntity(BleBoxEntity[blebox_uniapi.update.Update], UpdateEntity):
"""Representation of BleBox updates."""
_attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_supported_features = (
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
)
def __init__(self, feature: blebox_uniapi.update.Update) -> None:
"""Initialize the update entity."""
super().__init__(feature)
self._in_progress_old_version: str | None = None
self._poll_cancel: CALLBACK_TYPE | None = None
self._poll_attempts: int = 0
@property
def in_progress(self) -> bool:
"""Return True while the device hasn't yet rebooted to the new firmware."""
return (
self._in_progress_old_version is not None
and self._in_progress_old_version == self._feature.installed_version
)
def _sync_sw_version(self) -> None:
"""Sync installed firmware version to the device registry."""
if self.device_entry:
dr.async_get(self.hass).async_update_device(
self.device_entry.id,
sw_version=self._feature.installed_version,
)
async def async_update(self) -> None:
"""Update state and refresh sw_version in device registry."""
try:
await self._feature.async_update()
except Error as ex:
raise HomeAssistantError(ex) from ex
self._sync_sw_version()
@property
def installed_version(self) -> str | None:
"""Version installed and in use."""
return self._feature.installed_version
@property
def latest_version(self) -> str | None:
"""Latest version available for install."""
return self._feature.latest_version
def _cancel_poll(self) -> None:
if self._poll_cancel is not None:
self._poll_cancel()
self._poll_cancel = None
def _reset_progress(self) -> None:
self._in_progress_old_version = None
self._poll_attempts = 0
self.async_write_ha_state()
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install an update."""
self._cancel_poll()
self._in_progress_old_version = self._feature.installed_version
self._poll_attempts = 0
self.async_write_ha_state()
try:
await self._feature.async_install()
except Error as ex:
self._reset_progress()
raise HomeAssistantError(ex) from ex
self._poll_cancel = async_call_later(
self.hass, _POLL_INTERVAL_SECONDS, self._poll_until_updated
)
async def async_will_remove_from_hass(self) -> None:
"""Cancel any pending poll timer when the entity is removed."""
self._cancel_poll()
async def _poll_until_updated(self, _now: Any) -> None:
"""Poll device until the installed version changes after OTA reboot."""
self._poll_cancel = None
self._poll_attempts += 1
try:
await self._feature.async_update()
except BleBoxConnectionError:
pass
except Error:
self._reset_progress()
return
else:
self._sync_sw_version()
if self.in_progress and self._poll_attempts < _MAX_POLL_ATTEMPTS:
self._poll_cancel = async_call_later(
self.hass, _POLL_INTERVAL_SECONDS, self._poll_until_updated
)
else:
self._reset_progress()
@@ -124,9 +124,7 @@ async def async_setup_entry(
BlueMaestroBluetoothSensorEntity, async_add_entities
)
)
entry.async_on_unload(
coordinator.async_register_processor(processor, SensorEntityDescription)
)
entry.async_on_unload(coordinator.async_register_processor(processor))
class BlueMaestroBluetoothSensorEntity(
@@ -22,7 +22,6 @@ from homeassistant.config_entries import (
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_SOURCE
from homeassistant.core import callback
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
@@ -41,6 +40,7 @@ from .const import (
CONF_DETAILS,
CONF_MODE,
CONF_PASSIVE,
CONF_SOURCE,
CONF_SOURCE_CONFIG_ENTRY_ID,
CONF_SOURCE_DEVICE_ID,
CONF_SOURCE_DOMAIN,
@@ -22,6 +22,9 @@ CONF_PASSIVE = "passive"
DEFAULT_MODE = BluetoothScanningMode.AUTO.value
# pylint: disable-next=home-assistant-duplicate-const
CONF_SOURCE: Final = "source"
CONF_SOURCE_DOMAIN: Final = "source_domain"
CONF_SOURCE_MODEL: Final = "source_model"
CONF_SOURCE_CONFIG_ENTRY_ID: Final = "source_config_entry_id"
@@ -21,11 +21,7 @@ from habluetooth import (
)
from homeassistant import config_entries
from homeassistant.const import (
CONF_SOURCE,
EVENT_HOMEASSISTANT_STOP,
EVENT_LOGGING_CHANGED,
)
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, EVENT_LOGGING_CHANGED
from homeassistant.core import (
CALLBACK_TYPE,
Event,
@@ -37,6 +33,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.package import is_docker_env
from .const import (
CONF_SOURCE,
CONF_SOURCE_CONFIG_ENTRY_ID,
CONF_SOURCE_DEVICE_ID,
CONF_SOURCE_DOMAIN,
@@ -15,12 +15,12 @@
],
"quality_scale": "internal",
"requirements": [
"bleak==3.0.2",
"bleak-retry-connector==4.6.1",
"bluetooth-adapters==2.3.0",
"bluetooth-auto-recovery==1.6.4",
"bleak==2.1.1",
"bleak-retry-connector==4.6.0",
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.29.18",
"dbus-fast==5.0.14",
"habluetooth==6.7.4"
"dbus-fast==5.0.3",
"habluetooth==6.7.2"
]
}
-1
View File
@@ -17,7 +17,6 @@ BTHOME_BLE_EVENT: Final = "bthome_ble_event"
EVENT_CLASS_BUTTON: Final = "button"
EVENT_CLASS_DIMMER: Final = "dimmer"
EVENT_CLASS_COMMAND: Final = "command"
CONF_EVENT_CLASS: Final = "event_class"
CONF_EVENT_PROPERTIES: Final = "event_properties"
@@ -28,7 +28,6 @@ from .const import (
DOMAIN,
EVENT_CLASS,
EVENT_CLASS_BUTTON,
EVENT_CLASS_COMMAND,
EVENT_CLASS_DIMMER,
EVENT_TYPE,
)
@@ -44,7 +43,6 @@ EVENT_TYPES_BY_EVENT_CLASS = {
"hold_press",
},
EVENT_CLASS_DIMMER: {"rotate_left", "rotate_right"},
EVENT_CLASS_COMMAND: {"off", "on", "toggle", "step_up", "step_down"},
}
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
-6
View File
@@ -16,7 +16,6 @@ from . import format_discovered_event_class, format_event_dispatcher_name
from .const import (
DOMAIN,
EVENT_CLASS_BUTTON,
EVENT_CLASS_COMMAND,
EVENT_CLASS_DIMMER,
EVENT_PROPERTIES,
EVENT_TYPE,
@@ -44,11 +43,6 @@ DESCRIPTIONS_BY_EVENT_CLASS = {
translation_key="dimmer",
event_types=["rotate_left", "rotate_right"],
),
EVENT_CLASS_COMMAND: EventEntityDescription(
key=EVENT_CLASS_COMMAND,
translation_key="command",
event_types=["off", "on", "toggle", "step_up", "step_down"],
),
}
@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==3.23.2"]
"requirements": ["bthome-ble==3.17.0"]
}
-12
View File
@@ -192,12 +192,6 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
),
# Light level (-)
(BTHomeExtendedSensorDeviceClass.LIGHT_LEVEL, None): SensorEntityDescription(
key=str(BTHomeExtendedSensorDeviceClass.LIGHT_LEVEL),
state_class=SensorStateClass.MEASUREMENT,
translation_key="light_level",
),
# Mass sensor (kg)
(BTHomeSensorDeviceClass.MASS, Units.MASS_KILOGRAMS): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_KILOGRAMS}",
@@ -293,12 +287,6 @@ SENSOR_DESCRIPTIONS = {
state_class=SensorStateClass.MEASUREMENT,
translation_key="rotational_speed",
),
# Settings revision (-)
(BTHomeExtendedSensorDeviceClass.SETTINGS_REVISION, None): SensorEntityDescription(
key=str(BTHomeExtendedSensorDeviceClass.SETTINGS_REVISION),
entity_category=EntityCategory.DIAGNOSTIC,
translation_key="settings_revision",
),
# Signal Strength (RSSI) (dB)
(
BTHomeSensorDeviceClass.SIGNAL_STRENGTH,
@@ -36,19 +36,13 @@
"long_double_press": "Long Double Press",
"long_press": "Long Press",
"long_triple_press": "Long Triple Press",
"off": "Off",
"on": "On",
"press": "Press",
"rotate_left": "Rotate Left",
"rotate_right": "Rotate Right",
"step_down": "Step Down",
"step_up": "Step Up",
"toggle": "Toggle",
"triple_press": "Triple Press"
},
"trigger_type": {
"button": "Button \"{subtype}\"",
"command": "Command \"{subtype}\"",
"dimmer": "Dimmer \"{subtype}\""
}
},
@@ -74,19 +68,6 @@
}
}
},
"command": {
"state_attributes": {
"event_type": {
"state": {
"off": "Off",
"on": "On",
"step_down": "Step down",
"step_up": "Step up",
"toggle": "Toggle"
}
}
}
},
"dimmer": {
"state_attributes": {
"event_type": {
@@ -117,9 +98,6 @@
"gyroscope": {
"name": "Gyroscope"
},
"light_level": {
"name": "Light level"
},
"packet_id": {
"name": "Packet ID"
},
@@ -132,9 +110,6 @@
"rotational_speed": {
"name": "Rotational speed"
},
"settings_revision": {
"name": "Settings revision"
},
"text": {
"name": "Text"
},
+6 -1
View File
@@ -3,7 +3,7 @@
from datetime import datetime
from functools import partial
import logging
from typing import Any
from typing import Any, override
import caldav
from caldav.lib.error import DAVError
@@ -204,17 +204,20 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
self._attr_unique_id = unique_id
self._supports_offset = supports_offset
@override
@property
def event(self) -> CalendarEvent | None:
"""Return the next upcoming event."""
return self._event
@override
async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]:
"""Get all events in a specific time frame."""
return await self.coordinator.async_get_events(hass, start_date, end_date)
@override
async def async_create_event(self, **kwargs: Any) -> None:
"""Create a new event in the calendar."""
_LOGGER.debug("Event: %s", kwargs)
@@ -240,6 +243,7 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
@override
@callback
def _handle_coordinator_update(self) -> None:
"""Update event data."""
@@ -255,6 +259,7 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
}
super()._handle_coordinator_update()
@override
async def async_added_to_hass(self) -> None:
"""When entity is added to hass update state from existing coordinator data."""
await super().async_added_to_hass()
@@ -2,7 +2,7 @@
from collections.abc import Mapping
import logging
from typing import Any
from typing import Any, override
import caldav
from caldav.lib.error import AuthorizationError, DAVError
@@ -33,6 +33,7 @@ class CalDavConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -4,7 +4,7 @@ from datetime import date, datetime, time, timedelta
from functools import partial
import logging
import re
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, override
import caldav
@@ -90,6 +90,7 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
return event_list
@override
async def _async_update_data(self) -> CalendarEvent | None:
"""Get the latest data."""
start_of_today = dt_util.start_of_local_day()
+4 -1
View File
@@ -4,7 +4,7 @@ import asyncio
from datetime import date, datetime, timedelta
from functools import partial
import logging
from typing import Any, cast
from typing import Any, cast, override
import caldav
from caldav.lib.error import DAVError, NotFoundError
@@ -121,6 +121,7 @@ class WebDavTodoListEntity(TodoListEntity):
if (todo_item := _todo_item(resource)) is not None
]
@override
async def async_create_todo_item(self, item: TodoItem) -> None:
"""Add an item to the To-do list."""
item_data: dict[str, Any] = {}
@@ -141,6 +142,7 @@ class WebDavTodoListEntity(TodoListEntity):
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
@override
async def async_update_todo_item(self, item: TodoItem) -> None:
"""Update a To-do item."""
uid: str = cast(str, item.uid)
@@ -177,6 +179,7 @@ class WebDavTodoListEntity(TodoListEntity):
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
@override
async def async_delete_todo_items(self, uids: list[str]) -> None:
"""Delete To-do items."""
tasks = (
@@ -7,7 +7,7 @@ from http import HTTPStatus
from itertools import groupby
import logging
import re
from typing import Any, Final, cast, final
from typing import Any, Final, cast, final, override
from aiohttp import web
from dateutil.rrule import rrulestr
@@ -546,6 +546,7 @@ class CalendarEntity(Entity):
return self.entity_description.initial_color
return None
@override
def get_initial_entity_options(self) -> er.EntityOptionsType | None:
"""Return initial entity options."""
if self.initial_color is None:
@@ -564,6 +565,7 @@ class CalendarEntity(Entity):
"""Return the next upcoming event."""
raise NotImplementedError
@override
@final
@property
def state_attributes(self) -> dict[str, Any] | None:
@@ -580,6 +582,7 @@ class CalendarEntity(Entity):
"description": event.description or "",
}
@override
@final
@property
def state(self) -> str:
@@ -594,6 +597,7 @@ class CalendarEntity(Entity):
return STATE_OFF
@override
@callback
def _async_write_ha_state(self) -> None:
"""Write the state to the state machine.
@@ -653,6 +657,7 @@ class CalendarEntity(Entity):
self._event_listener_debouncer.async_cancel()
self._event_listener_debouncer = None
@override
async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass.
+9 -1
View File
@@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import datetime
import logging
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any, cast, override
import voluptuous as vol
@@ -113,6 +113,7 @@ class Timespan:
"""
return Timespan(self.end, max(self.end, now) + interval)
@override
def __str__(self) -> str:
"""Return a string representing the half open interval time span."""
return f"[{self.start}, {self.end})"
@@ -325,6 +326,7 @@ class TargetCalendarEventListener(TargetEntityChangeTracker):
self._pending_listener_task: asyncio.Task[None] | None = None
self._calendar_event_listener: CalendarEventListener | None = None
@override
@callback
def _handle_entities_update(self, tracked_entities: set[str]) -> None:
"""Restart listeners when tracked target entities update."""
@@ -351,6 +353,7 @@ class TargetCalendarEventListener(TargetEntityChangeTracker):
)
await self._calendar_event_listener.async_attach()
@override
def _unsubscribe(self) -> None:
"""Unsubscribe from all events."""
super()._unsubscribe()
@@ -367,6 +370,7 @@ class SingleEntityEventTrigger(Trigger):
_options: dict[str, Any]
@override
@classmethod
async def async_validate_complete_config(
cls, hass: HomeAssistant, complete_config: ConfigType
@@ -377,6 +381,7 @@ class SingleEntityEventTrigger(Trigger):
)
return await super().async_validate_complete_config(hass, complete_config)
@override
@classmethod
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
@@ -392,6 +397,7 @@ class SingleEntityEventTrigger(Trigger):
assert config.options is not None
self._options = config.options
@override
async def async_attach_runner(
self, run_action: TriggerActionRunner
) -> CALLBACK_TYPE:
@@ -426,6 +432,7 @@ class EventTrigger(Trigger):
_options: dict[str, Any]
_event_type: str
@override
@classmethod
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
@@ -443,6 +450,7 @@ class EventTrigger(Trigger):
self._target = config.target
self._options = config.options
@override
async def async_attach_runner(
self, run_action: TriggerActionRunner
) -> CALLBACK_TYPE:
@@ -1,7 +1,7 @@
"""Config flow for Cambridge Audio."""
import asyncio
from typing import Any
from typing import Any, override
from aiostreammagic import StreamMagicClient
import voluptuous as vol
@@ -29,6 +29,7 @@ class CambridgeAudioConfigFlow(ConfigFlow, domain=DOMAIN):
"""Initialize the config flow."""
self.data: dict[str, Any] = {}
@override
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
@@ -81,6 +82,7 @@ class CambridgeAudioConfigFlow(ConfigFlow, domain=DOMAIN):
)
return await self.async_step_user(user_input)
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -2,7 +2,7 @@
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Concatenate
from typing import Any, Concatenate, override
from aiostreammagic import StreamMagicClient
from aiostreammagic.models import CallbackType
@@ -60,15 +60,18 @@ class CambridgeAudioEntity(Entity):
"""Call when the device is notified of changes."""
self.async_write_ha_state()
@override
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.client.is_connected()
@override
async def async_added_to_hass(self) -> None:
"""Register callback handlers."""
await self.client.register_state_update_callbacks(self._state_update_callback)
@override
async def async_will_remove_from_hass(self) -> None:
"""Remove callbacks."""
self.client.unregister_state_update_callbacks(self._state_update_callback)
@@ -1,7 +1,7 @@
"""Support for Cambridge Audio AV Receiver."""
from datetime import datetime
from typing import Any
from typing import Any, override
from aiostreammagic import (
RepeatMode as CambridgeRepeatMode,
@@ -83,6 +83,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
super().__init__(client)
self._attr_unique_id = client.info.unit_id
@override
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Supported features for the media player."""
@@ -100,6 +101,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
features |= feature
return features
@override
@property
def state(self) -> MediaPlayerState:
"""Return the state of the device."""
@@ -118,11 +120,13 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
return MediaPlayerState.ON
return MediaPlayerState.OFF
@override
@property
def source_list(self) -> list[str]:
"""Return a list of available input sources."""
return [item.name for item in self.client.sources]
@override
@property
def source(self) -> str | None:
"""Return the current input source."""
@@ -135,11 +139,13 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
None,
)
@override
@property
def media_title(self) -> str | None:
"""Title of current playing media."""
return self.client.play_state.metadata.title
@override
@property
def media_artist(self) -> str | None:
"""Artist of current playing media, music track only."""
@@ -151,52 +157,62 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
return self.client.play_state.metadata.station
return self.client.play_state.metadata.artist
@override
@property
def media_album_name(self) -> str | None:
"""Album name of current playing media, music track only."""
return self.client.play_state.metadata.album
@override
@property
def media_image_url(self) -> str | None:
"""Image url of current playing media."""
return self.client.play_state.metadata.art_url
@override
@property
def media_duration(self) -> int | None:
"""Duration of the current media."""
return self.client.play_state.metadata.duration
@override
@property
def media_position(self) -> int | None:
"""Position of the current media."""
return self.client.play_state.position
@override
@property
def media_position_updated_at(self) -> datetime:
"""Last time the media position was updated."""
return self.client.position_last_updated
@override
@property
def media_channel(self) -> str | None:
"""Channel currently playing."""
return self.client.play_state.metadata.station
@override
@property
def is_volume_muted(self) -> bool | None:
"""Volume mute status."""
return self.client.state.mute
@override
@property
def volume_level(self) -> float | None:
"""Current pre-amp volume level."""
volume = self.client.state.volume_percent or 0
return volume / 100
@override
@property
def shuffle(self) -> bool:
"""Current shuffle configuration."""
return self.client.play_state.mode_shuffle != ShuffleMode.OFF
@override
@property
def repeat(self) -> RepeatMode | None:
"""Current repeat configuration."""
@@ -205,11 +221,13 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
mode_repeat = RepeatMode.ALL
return mode_repeat
@override
@command
async def async_media_play_pause(self) -> None:
"""Toggle play/pause the current media."""
await self.client.play_pause()
@override
@command
async def async_media_pause(self) -> None:
"""Pause the current media."""
@@ -222,11 +240,13 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
else:
await self.client.pause()
@override
@command
async def async_media_stop(self) -> None:
"""Stop the current media."""
await self.client.stop()
@override
@command
async def async_media_play(self) -> None:
"""Play the current media."""
@@ -239,16 +259,19 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
else:
await self.client.play()
@override
@command
async def async_media_next_track(self) -> None:
"""Skip to the next track."""
await self.client.next_track()
@override
@command
async def async_media_previous_track(self) -> None:
"""Skip to the previous track."""
await self.client.previous_track()
@override
@command
async def async_select_source(self, source: str) -> None:
"""Select the source."""
@@ -257,41 +280,49 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
await self.client.set_source_by_id(src.id)
break
@override
@command
async def async_turn_on(self) -> None:
"""Power on the device."""
await self.client.power_on()
@override
@command
async def async_turn_off(self) -> None:
"""Power off the device."""
await self.client.power_off()
@override
@command
async def async_volume_up(self) -> None:
"""Step the volume up."""
await self.client.volume_up()
@override
@command
async def async_volume_down(self) -> None:
"""Step the volume down."""
await self.client.volume_down()
@override
@command
async def async_set_volume_level(self, volume: float) -> None:
"""Set the volume level."""
await self.client.set_volume(int(volume * 100))
@override
@command
async def async_mute_volume(self, mute: bool) -> None:
"""Set the mute state."""
await self.client.set_mute(mute)
@override
@command
async def async_media_seek(self, position: float) -> None:
"""Seek to a position in the current media."""
await self.client.media_seek(int(position))
@override
@command
async def async_set_shuffle(self, shuffle: bool) -> None:
"""Set the shuffle mode for the current queue."""
@@ -300,6 +331,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
shuffle_mode = ShuffleMode.ALL
await self.client.set_shuffle(shuffle_mode)
@override
@command
async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set the repeat mode for the current queue."""
@@ -308,6 +340,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
repeat_mode = CambridgeRepeatMode.ALL
await self.client.set_repeat(repeat_mode)
@override
@command
async def async_play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
@@ -353,6 +386,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
if media_type == CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO:
await self.client.play_radio_url("Radio", media_id)
@override
async def async_browse_media(
self,
media_content_type: MediaType | str | None = None,
@@ -2,7 +2,7 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, override
from aiostreammagic import StreamMagicClient
@@ -90,16 +90,19 @@ class CambridgeAudioNumber(CambridgeAudioEntity, NumberEntity):
self.entity_description = description
self._attr_unique_id = f"{client.info.unit_id}-{description.key}"
@override
@property
def native_value(self) -> int | None:
"""Return the state of the number."""
return self.entity_description.value_fn(self.client)
@override
@command
async def async_set_native_value(self, value: float) -> None:
"""Set the selected value."""
await self.entity_description.set_value_fn(self.client, int(value))
@override
@property
def available(self) -> bool:
"""Return True if entity is available."""
@@ -2,6 +2,7 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import override
from aiostreammagic import StreamMagicClient
from aiostreammagic.models import ControlBusMode, DisplayBrightness
@@ -127,11 +128,13 @@ class CambridgeAudioSelect(CambridgeAudioEntity, SelectEntity):
if options_fn:
self._attr_options = options_fn
@override
@property
def current_option(self) -> str | None:
"""Return the state of the select."""
return self.entity_description.value_fn(self.client)
@override
@command
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
@@ -2,7 +2,7 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, override
from aiostreammagic import StreamMagicClient
@@ -103,16 +103,19 @@ class CambridgeAudioSwitch(CambridgeAudioEntity, SwitchEntity):
self.entity_description = description
self._attr_unique_id = f"{client.info.unit_id}-{description.key}"
@override
@property
def is_on(self) -> bool:
"""Return the state of the switch."""
return self.entity_description.value_fn(self.client)
@override
@command
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.entity_description.set_value_fn(self.client, True)
@override
@command
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
+11 -1
View File
@@ -12,7 +12,7 @@ import logging
import os
from random import SystemRandom
import time
from typing import Any, Final, final
from typing import Any, Final, final, override
from aiohttp import hdrs, web
import attr
@@ -455,6 +455,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
!= Camera.async_handle_async_webrtc_offer
)
@override
@cached_property
def entity_picture(self) -> str:
"""Return a link to the camera feed as entity picture."""
@@ -467,6 +468,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Whether or not to use stream to generate stills."""
return False
@override
@cached_property
def supported_features(self) -> CameraEntityFeature:
"""Flag supported features."""
@@ -502,6 +504,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the interval between frames of the mjpeg stream."""
return self._attr_frame_interval
@override
@property
def available(self) -> bool:
"""Return True if entity is available."""
@@ -595,6 +598,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""
return await self.handle_async_still_stream(request, self.frame_interval)
@override
@property
@final
def state(self) -> str:
@@ -642,6 +646,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Call the job and disable motion detection."""
await self.hass.async_add_executor_job(self.disable_motion_detection)
@override
@final
@property
def state_attributes(self) -> dict[str, str | None]:
@@ -665,6 +670,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self.access_tokens.append(hex(_RND.getrandbits(256))[2:])
self.__dict__.pop("entity_picture", None)
@override
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()
@@ -758,6 +764,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return CameraCapabilities(frontend_stream_types)
@override
@callback
def _async_write_ha_state(self) -> None:
"""Write the state to the state machine.
@@ -817,6 +824,7 @@ class CameraImageView(CameraView):
url = "/api/camera_proxy/{entity_id}"
name = "api:camera:image"
@override
async def handle(self, request: web.Request, camera: Camera) -> web.Response:
"""Serve camera image."""
width = request.query.get("width")
@@ -840,6 +848,7 @@ class CameraMjpegStream(CameraView):
url = "/api/camera_proxy_stream/{entity_id}"
name = "api:camera:stream"
@override
async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse:
"""Serve camera stream, possibly with interval."""
if (interval_str := request.query.get("interval")) is None:
@@ -985,6 +994,7 @@ class _TemplateCameraEntity:
self._report_issue()
return getattr(self._camera, name)
@override
def __str__(self) -> str:
"""Forward to the camera entity."""
self._report_issue()
@@ -1,6 +1,7 @@
"""Expose cameras as media sources."""
import asyncio
from typing import override
from homeassistant.components.media_player import BrowseError, MediaClass
from homeassistant.components.media_source import (
@@ -54,6 +55,7 @@ class CameraMediaSource(MediaSource):
super().__init__(DOMAIN)
self.hass = hass
@override
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve media to a url."""
component = self.hass.data[DATA_COMPONENT]
@@ -82,6 +84,7 @@ class CameraMediaSource(MediaSource):
return PlayMedia(url, FORMAT_CONTENT_TYPE[HLS_PROVIDER])
@override
async def async_browse_media(
self,
item: MediaSourceItem,
+2 -1
View File
@@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable
from dataclasses import asdict, dataclass, field
from functools import cache, partial, wraps
import logging
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, override
from mashumaro import MissingField
import voluptuous as vol
@@ -73,6 +73,7 @@ class WebRTCCandidate(WebRTCMessage):
candidate: RTCIceCandidate | RTCIceCandidateInit
@override
def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the message."""
return {
@@ -1,6 +1,6 @@
"""Support for Canary alarm."""
from typing import Any
from typing import Any, override
from canary.const import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT
from canary.model import Location
@@ -58,6 +58,7 @@ class CanaryAlarm(
"""Return information about the location."""
return self.coordinator.data["locations"][self._location_id]
@override
@property
def alarm_state(self) -> AlarmControlPanelState | None:
"""Return the state of the device."""
@@ -74,25 +75,30 @@ class CanaryAlarm(
return None
@override
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return {"private": self.location.is_private}
@override
def alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
self.coordinator.canary.set_location_mode(
self._location_id, self.location.mode.name, True
)
@override
def alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_HOME)
@override
def alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
@override
def alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
self.coordinator.canary.set_location_mode(
+5 -1
View File
@@ -2,7 +2,7 @@
from datetime import timedelta
import logging
from typing import Final
from typing import Final, override
from aiohttp.web import Request, StreamResponse
from canary.live_stream_api import LiveStreamSession
@@ -108,16 +108,19 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera):
"""Return information about the location."""
return self.coordinator.data["locations"][self._location_id]
@override
@property
def is_recording(self) -> bool:
"""Return true if the device is recording."""
return self.location.is_recording # type: ignore[no-any-return]
@override
@property
def motion_detection_enabled(self) -> bool:
"""Return the camera motion detection status."""
return not self.location.is_recording
@override
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
@@ -150,6 +153,7 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera):
return self._image
@override
async def handle_async_mjpeg_stream(
self, request: Request
) -> StreamResponse | None:
@@ -1,7 +1,7 @@
"""Config flow for Canary."""
import logging
from typing import Any, Final
from typing import Any, Final, override
from canary.api import Api
from requests.exceptions import ConnectTimeout, HTTPError
@@ -46,12 +46,14 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
@override
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
return CanaryOptionsFlowHandler()
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -4,6 +4,7 @@ import asyncio
from collections.abc import ValuesView
from datetime import timedelta
import logging
from typing import override
from canary.api import Api
from canary.model import Location, Reading
@@ -60,6 +61,7 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator[CanaryData]):
"readings": readings_by_device_id,
}
@override
async def _async_update_data(self) -> CanaryData:
"""Fetch data from Canary."""
+3 -1
View File
@@ -1,6 +1,6 @@
"""Support for Canary sensors."""
from typing import Final
from typing import Final, override
from canary.model import Device, Location, SensorType
@@ -143,11 +143,13 @@ class CanarySensor(CoordinatorEntity[CanaryDataUpdateCoordinator], SensorEntity)
return None
@override
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
return self.reading
@override
@property
def extra_state_attributes(self) -> dict[str, str] | None:
"""Return the state attributes."""
@@ -1,5 +1,7 @@
"""Casper Glow integration binary sensor platform."""
from typing import override
from pycasperglow import GlowState
from homeassistant.components.binary_sensor import (
@@ -43,6 +45,7 @@ class CasperGlowPausedBinarySensor(CasperGlowEntity, BinarySensorEntity):
if coordinator.device.state.is_paused is not None:
self._attr_is_on = coordinator.device.state.is_paused
@override
async def async_added_to_hass(self) -> None:
"""Register state update callback when entity is added."""
await super().async_added_to_hass()
@@ -71,6 +74,7 @@ class CasperGlowChargingBinarySensor(CasperGlowEntity, BinarySensorEntity):
if coordinator.device.state.is_charging is not None:
self._attr_is_on = coordinator.device.state.is_charging
@override
async def async_added_to_hass(self) -> None:
"""Register state update callback when entity is added."""
await super().async_added_to_hass()
@@ -2,6 +2,7 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import override
from pycasperglow import CasperGlow
@@ -66,6 +67,7 @@ class CasperGlowButton(CasperGlowEntity, ButtonEntity):
f"{format_mac(coordinator.device.address)}_{description.key}"
)
@override
async def async_press(self) -> None:
"""Press the button."""
await self._async_command(self.entity_description.press_fn(self._device))
@@ -1,7 +1,7 @@
"""Config flow for Casper Glow integration."""
import logging
from typing import Any
from typing import Any, override
from bluetooth_data_tools import human_readable_name
from pycasperglow import CasperGlow, CasperGlowError
@@ -31,6 +31,7 @@ class CasperGlowConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovery_info: BluetoothServiceInfoBleak | None = None
self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {}
@override
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
) -> ConfigFlowResult:
@@ -73,6 +74,7 @@ class CasperGlowConfigFlow(ConfigFlow, domain=DOMAIN):
description_placeholders=self.context["title_placeholders"],
)
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -1,6 +1,7 @@
"""Coordinator for the Casper Glow integration."""
import logging
from typing import override
from bleak import BleakError
from bluetooth_data_tools import monotonic_time_coarse
@@ -74,6 +75,7 @@ class CasperGlowCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
"""Poll device state."""
await self.device.query_state()
@override
async def _async_poll(self) -> None:
"""Poll the device and log availability changes."""
assert self._last_service_info
@@ -99,6 +101,7 @@ class CasperGlowCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
self._async_handle_bluetooth_poll()
@override
@callback
def _async_handle_bluetooth_event(
self,
@@ -1,6 +1,6 @@
"""Casper Glow integration light platform."""
from typing import Any
from typing import Any, override
from pycasperglow import GlowState
@@ -54,6 +54,7 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
self._attr_unique_id = format_mac(coordinator.device.address)
self._update_from_state(coordinator.device.state)
@override
async def async_added_to_hass(self) -> None:
"""Register state update callback when entity is added."""
await super().async_added_to_hass()
@@ -77,6 +78,7 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
self._update_from_state(state)
self.async_write_ha_state()
@override
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
brightness_pct: int | None = None
@@ -98,6 +100,7 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
self._attr_brightness = _device_pct_to_ha_brightness(brightness_pct)
self.coordinator.last_brightness_pct = brightness_pct
@override
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
await self._async_command(self._device.turn_off())
@@ -1,5 +1,7 @@
"""Casper Glow integration select platform for dimming time."""
from typing import override
from pycasperglow import GlowState
from homeassistant.components.select import SelectEntity
@@ -38,6 +40,7 @@ class CasperGlowDimmingTimeSelect(CasperGlowEntity, SelectEntity, RestoreEntity)
super().__init__(coordinator)
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_dimming_time"
@override
@property
def current_option(self) -> str | None:
"""Return the currently selected dimming time from the coordinator."""
@@ -45,6 +48,7 @@ class CasperGlowDimmingTimeSelect(CasperGlowEntity, SelectEntity, RestoreEntity)
return None
return str(self.coordinator.last_dimming_time_minutes)
@override
async def async_added_to_hass(self) -> None:
"""Restore last known dimming time and register state update callback."""
await super().async_added_to_hass()
@@ -75,6 +79,7 @@ class CasperGlowDimmingTimeSelect(CasperGlowEntity, SelectEntity, RestoreEntity)
# to update the current state.
self.async_write_ha_state()
@override
async def async_select_option(self, option: str) -> None:
"""Set the dimming time."""
await self._async_command(
@@ -1,6 +1,7 @@
"""Casper Glow integration sensor platform."""
from datetime import datetime, timedelta
from typing import override
from pycasperglow import GlowState
@@ -51,6 +52,7 @@ class CasperGlowBatterySensor(CasperGlowEntity, SensorEntity):
if coordinator.device.state.battery_level is not None:
self._attr_native_value = coordinator.device.state.battery_level.percentage
@override
async def async_added_to_hass(self) -> None:
"""Register state update callback when entity is added."""
await super().async_added_to_hass()
@@ -93,6 +95,7 @@ class CasperGlowDimmingEndTimeSensor(CasperGlowEntity, SensorEntity):
"""Calculate projected dimming end time from remaining milliseconds."""
return utcnow() + timedelta(milliseconds=remaining_ms)
@override
async def async_added_to_hass(self) -> None:
"""Register state update callback when entity is added."""
await super().async_added_to_hass()
+25 -15
View File
@@ -1,6 +1,6 @@
"""Config flow for Cast."""
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, override
import voluptuous as vol
@@ -32,16 +32,8 @@ OPTIONS_SCHEMA = KNOWN_HOSTS_SCHEMA.extend(
vol.Required(CONF_MORE_OPTIONS): section(
vol.Schema(
{
vol.Optional(CONF_UUID): SelectSelector(
SelectSelectorConfig(
custom_value=True, options=[], multiple=True
),
),
vol.Optional(CONF_IGNORE_CEC): SelectSelector(
SelectSelectorConfig(
custom_value=True, options=[], multiple=True
),
),
vol.Optional(CONF_UUID): str,
vol.Optional(CONF_IGNORE_CEC): str,
}
),
SectionConfig(collapsed=True),
@@ -55,6 +47,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
@override
@staticmethod
@callback
def async_get_options_flow(
@@ -63,12 +56,14 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler."""
return CastOptionsFlowHandler()
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
return await self.async_step_config()
@override
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
@@ -117,11 +112,13 @@ class CastOptionsFlowHandler(OptionsFlow):
) -> ConfigFlowResult:
"""Manage the Google Cast options."""
if user_input is not None:
ignore_cec = _trim_items(
user_input[CONF_MORE_OPTIONS].get(CONF_IGNORE_CEC, [])
ignore_cec = _string_to_list(
user_input[CONF_MORE_OPTIONS].get(CONF_IGNORE_CEC, "")
)
known_hosts = _trim_items(user_input.get(CONF_KNOWN_HOSTS, []))
wanted_uuid = _trim_items(user_input[CONF_MORE_OPTIONS].get(CONF_UUID, []))
wanted_uuid = _string_to_list(
user_input[CONF_MORE_OPTIONS].get(CONF_UUID, "")
)
updated_config = dict(self.config_entry.data)
updated_config[CONF_IGNORE_CEC] = ignore_cec
updated_config[CONF_KNOWN_HOSTS] = known_hosts
@@ -138,7 +135,9 @@ class CastOptionsFlowHandler(OptionsFlow):
for key in (CONF_UUID, CONF_IGNORE_CEC):
if key not in self.config_entry.data:
continue
suggested[CONF_MORE_OPTIONS][key] = self.config_entry.data[key]
suggested[CONF_MORE_OPTIONS][key] = _list_to_string(
self.config_entry.data[key]
)
return self.async_show_form(
step_id="init",
@@ -147,5 +146,16 @@ class CastOptionsFlowHandler(OptionsFlow):
)
def _list_to_string(items: list[str]) -> str:
comma_separated_string = ""
if items:
comma_separated_string = ",".join(items)
return comma_separated_string
def _string_to_list(string: str) -> list[str]:
return [x.strip() for x in string.split(",") if x.strip()]
def _trim_items(items: list[str]) -> list[str]:
return [x.strip() for x in items if x.strip()]
+4 -1
View File
@@ -2,7 +2,7 @@
import logging
import threading
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, override
import pychromecast.discovery
import pychromecast.models
@@ -67,14 +67,17 @@ def setup_internal_discovery(
class CastListener(pychromecast.discovery.AbstractCastListener):
"""Listener for discovering chromecasts."""
@override
def add_cast(self, uuid, _):
"""Handle zeroconf discovery of a new chromecast."""
discover_chromecast(hass, browser.devices[uuid], config_entry)
@override
def update_cast(self, uuid, _):
"""Handle zeroconf discovery of an updated chromecast."""
discover_chromecast(hass, browser.devices[uuid], config_entry)
@override
def remove_cast(self, uuid, service, cast_info):
"""Handle zeroconf discovery of a removed chromecast."""
_remove_chromecast(
+9 -1
View File
@@ -3,7 +3,7 @@
import configparser
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING, ClassVar
from typing import TYPE_CHECKING, ClassVar, override
from urllib.parse import urlparse
from uuid import UUID
@@ -180,37 +180,45 @@ class CastStatusListener(
if not cast_device._cast_info.is_audio_group: # noqa: SLF001
self._mz_mgr.register_listener(chromecast.uuid, self)
@override
def new_cast_status(self, status):
"""Handle reception of a new CastStatus."""
if self._valid:
self._cast_device.new_cast_status(status)
@override
def new_media_status(self, status):
"""Handle reception of a new MediaStatus."""
if self._valid:
self._cast_device.new_media_status(status)
@override
def load_media_failed(self, queue_item_id, error_code):
"""Handle reception of a new MediaStatus."""
if self._valid:
self._cast_device.load_media_failed(queue_item_id, error_code)
@override
def new_connection_status(self, status):
"""Handle reception of a new ConnectionStatus."""
if self._valid:
self._cast_device.new_connection_status(status)
@override
def added_to_multizone(self, group_uuid):
"""Handle the cast added to a group."""
@override
def removed_from_multizone(self, group_uuid):
"""Handle the cast removed from a group."""
if self._valid:
self._cast_device.multizone_new_media_status(group_uuid, None)
@override
def multizone_new_cast_status(self, group_uuid, cast_status):
"""Handle reception of a new CastStatus for a group."""
@override
def multizone_new_media_status(self, group_uuid, media_status):
"""Handle reception of a new MediaStatus for a group."""
if self._valid:
+37 -1
View File
@@ -6,7 +6,7 @@ from datetime import datetime
from functools import wraps
import json
import logging
from typing import TYPE_CHECKING, Any, Concatenate
from typing import TYPE_CHECKING, Any, Concatenate, override
import pychromecast.config
import pychromecast.const
@@ -338,6 +338,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
]:
self._attr_device_class = MediaPlayerDeviceClass.SPEAKER
@override
async def async_added_to_hass(self) -> None:
"""Create chromecast object when added to hass."""
self._async_setup(self.entity_id)
@@ -346,6 +347,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
self.hass, SIGNAL_HASS_CAST_SHOW_VIEW, self._handle_signal_show_view
)
@override
async def async_will_remove_from_hass(self) -> None:
"""Disconnect Chromecast object when removed."""
await self._async_tear_down()
@@ -354,6 +356,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
self._cast_view_remove_handler()
self._cast_view_remove_handler = None
@override
async def _async_connect_to_chromecast(self):
"""Set up the chromecast object."""
await super()._async_connect_to_chromecast()
@@ -363,6 +366,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
self.media_status = self._chromecast.media_controller.status
self.async_write_ha_state()
@override
async def _async_disconnect(self):
"""Disconnect Chromecast object if it is set."""
await super()._async_disconnect()
@@ -370,6 +374,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
self._attr_available = False
self.async_write_ha_state()
@override
def _invalidate(self):
"""Invalidate some attributes."""
super()._invalidate()
@@ -528,6 +533,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
"""Start an app."""
self._get_chromecast().start_app(app_id)
@override
def turn_on(self) -> None:
"""Turn on the cast device."""
@@ -547,51 +553,60 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
else:
self._start_app(pychromecast.config.APP_MEDIA_RECEIVER)
@override
@api_error
def turn_off(self) -> None:
"""Turn off the cast device."""
self._get_chromecast().quit_app()
@override
@api_error
def mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
self._get_chromecast().set_volume_muted(mute)
@override
@api_error
def set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
self._get_chromecast().set_volume(volume)
@override
@api_error
def media_play(self) -> None:
"""Send play command."""
media_controller = self._media_controller()
media_controller.play()
@override
@api_error
def media_pause(self) -> None:
"""Send pause command."""
media_controller = self._media_controller()
media_controller.pause()
@override
@api_error
def media_stop(self) -> None:
"""Send stop command."""
media_controller = self._media_controller()
media_controller.stop()
@override
@api_error
def media_previous_track(self) -> None:
"""Send previous track command."""
media_controller = self._media_controller()
media_controller.queue_prev()
@override
@api_error
def media_next_track(self) -> None:
"""Send next track command."""
media_controller = self._media_controller()
media_controller.queue_next()
@override
@api_error
def media_seek(self, position: float) -> None:
"""Seek the media to a specific location."""
@@ -636,6 +651,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
children=sorted(children, key=lambda c: c.title),
)
@override
async def async_browse_media(
self,
media_content_type: MediaType | str | None = None,
@@ -675,6 +691,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
self.hass, media_content_id, content_filter=content_filter
)
@override
async def async_play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
) -> None:
@@ -816,6 +833,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return (media_status, media_status_received)
@override
@property
def state(self) -> MediaPlayerState | None:
"""Return the state of the player."""
@@ -858,6 +876,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return MediaPlayerState.IDLE
@override
@property
def media_content_id(self) -> str | None:
"""Content ID of current playing media."""
@@ -867,6 +886,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
media_status = self._media_status()[0]
return media_status.content_id if media_status else None
@override
@property
def media_content_type(self) -> MediaType | None:
"""Content type of current playing media."""
@@ -891,6 +911,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return MediaType.VIDEO
@override
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
@@ -900,6 +921,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
media_status = self._media_status()[0]
return media_status.duration if media_status else None
@override
@property
def media_image_url(self):
"""Image url of current playing media."""
@@ -910,64 +932,75 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return images[0].url if images and images[0].url else None
@override
@property
def media_title(self):
"""Title of current playing media."""
media_status = self._media_status()[0]
return media_status.title if media_status else None
@override
@property
def media_artist(self):
"""Artist of current playing media (Music track only)."""
media_status = self._media_status()[0]
return media_status.artist if media_status else None
@override
@property
def media_album_name(self):
"""Album of current playing media (Music track only)."""
media_status = self._media_status()[0]
return media_status.album_name if media_status else None
@override
@property
def media_album_artist(self):
"""Album artist of current playing media (Music track only)."""
media_status = self._media_status()[0]
return media_status.album_artist if media_status else None
@override
@property
def media_track(self):
"""Track number of current playing media (Music track only)."""
media_status = self._media_status()[0]
return media_status.track if media_status else None
@override
@property
def media_series_title(self):
"""Return the title of the series of current playing media."""
media_status = self._media_status()[0]
return media_status.series_title if media_status else None
@override
@property
def media_season(self):
"""Season of current playing media (TV Show only)."""
media_status = self._media_status()[0]
return media_status.season if media_status else None
@override
@property
def media_episode(self):
"""Episode of current playing media (TV Show only)."""
media_status = self._media_status()[0]
return media_status.episode if media_status else None
@override
@property
def app_id(self):
"""Return the ID of the current running app."""
return self._chromecast.app_id if self._chromecast else None
@override
@property
def app_name(self):
"""Name of the current running app."""
return self._chromecast.app_display_name if self._chromecast else None
@override
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag media player features that are supported."""
@@ -1006,6 +1039,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return support
@override
@property
def media_position(self):
"""Position of current playing media in seconds."""
@@ -1021,6 +1055,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return None
return media_status.current_time
@override
@property
def media_position_updated_at(self):
"""When was the position of the current playing media valid.
@@ -1075,6 +1110,7 @@ class DynamicCastGroup(CastDevice):
"""Create chromecast object."""
self._async_setup("Dynamic group")
@override
async def _async_cast_removed(self, discover: ChromecastInfo):
"""Handle removal of Chromecast."""
if self._cast_info.uuid != discover.uuid:
+13 -1
View File
@@ -1,7 +1,7 @@
"""Climate device for CCM15 coordinator."""
import logging
from typing import Any
from typing import Any, override
from ccm15 import CCM15DeviceState, CCM15SlaveDevice
@@ -93,6 +93,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
"""Return device data."""
return self.coordinator.get_ac_data(self._ac_index)
@override
@property
def current_temperature(self) -> int | None:
"""Return current temperature."""
@@ -100,6 +101,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
return data.temperature
return None
@override
@property
def target_temperature(self) -> int | None:
"""Return target temperature."""
@@ -107,6 +109,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
return data.temperature_setpoint
return None
@override
@property
def hvac_mode(self) -> HVACMode | None:
"""Return hvac mode."""
@@ -115,6 +118,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
return CONST_CMD_STATE_MAP[mode]
return None
@override
@property
def fan_mode(self) -> str | None:
"""Return fan mode."""
@@ -123,6 +127,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
return CONST_CMD_FAN_MAP[mode]
return None
@override
@property
def swing_mode(self) -> str | None:
"""Return swing mode."""
@@ -130,11 +135,13 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
return SWING_ON if data.is_swing_on else SWING_OFF
return None
@override
@property
def available(self) -> bool:
"""Return the availability of the entity."""
return self.data is not None
@override
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
@@ -142,6 +149,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
return {"error_code": data.error_code}
return {}
@override
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
@@ -149,18 +157,22 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
self._ac_index, self.data, temperature, kwargs.get(ATTR_HVAC_MODE)
)
@override
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the hvac mode."""
await self.coordinator.async_set_hvac_mode(self._ac_index, self.data, hvac_mode)
@override
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set the fan mode."""
await self.coordinator.async_set_fan_mode(self._ac_index, self.data, fan_mode)
@override
async def async_turn_off(self) -> None:
"""Turn off."""
await self.async_set_hvac_mode(HVACMode.OFF)
@override
async def async_turn_on(self) -> None:
"""Turn on."""
await self.async_set_hvac_mode(HVACMode.AUTO)
@@ -1,7 +1,7 @@
"""Config flow for Midea ccm15 AC Controller integration."""
import logging
from typing import Any
from typing import Any, override
from ccm15 import CCM15Device
import voluptuous as vol
@@ -27,6 +27,7 @@ class CCM15ConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -2,6 +2,7 @@
import datetime
import logging
from typing import override
from ccm15 import CCM15Device, CCM15DeviceState, CCM15SlaveDevice
import httpx
@@ -44,6 +45,7 @@ class CCM15Coordinator(DataUpdateCoordinator[CCM15DeviceState]):
"""Get the host."""
return self._host
@override
async def _async_update_data(self) -> CCM15DeviceState:
"""Fetch data from Rain Bird device."""
try:
@@ -1,7 +1,7 @@
"""Config flow for the CentriConnect/MyPropane API integration."""
import logging
from typing import Any
from typing import Any, override
from aiocentriconnect import CentriConnect
from aiocentriconnect.exceptions import (
@@ -58,6 +58,7 @@ class CentriConnectConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -6,6 +6,7 @@ Responsible for polling the device API endpoint and normalizing data for entitie
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import override
from aiocentriconnect import CentriConnect, Tank
from aiocentriconnect.exceptions import CentriConnectConnectionError, CentriConnectError
@@ -65,6 +66,7 @@ class CentriConnectCoordinator(DataUpdateCoordinator[Tank]):
session=async_get_clientsession(hass),
)
@override
async def _async_setup(self) -> None:
try:
tank_data = await self.api_client.async_get_tank_data()
@@ -79,6 +81,7 @@ class CentriConnectCoordinator(DataUpdateCoordinator[Tank]):
tank_size_unit=tank_data.tank_size_unit,
)
@override
async def _async_update_data(self) -> Tank:
"""Fetch device state."""
try:
@@ -4,6 +4,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
from typing import override
from homeassistant.components.sensor import (
EntityCategory,
@@ -236,6 +237,7 @@ class CentriConnectSensor(CentriConnectBaseEntity, SensorEntity):
entity_description: CentriConnectSensorEntityDescription
@override
@property
def native_value(self) -> StateType | datetime | None:
"""Return the state of the sensor."""
@@ -1,7 +1,7 @@
"""Config flow for the Cert Expiry platform."""
from collections.abc import Mapping
from typing import Any
from typing import Any, override
import voluptuous as vol
@@ -53,6 +53,7 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
return True
return False
@override
async def async_step_user(
self,
user_input: Mapping[str, Any] | None = None,
@@ -2,6 +2,7 @@
from datetime import datetime, timedelta
import logging
from typing import override
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -46,6 +47,7 @@ class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime | None]):
always_update=False,
)
@override
async def _async_update_data(self) -> datetime | None:
"""Fetch certificate."""
try:
@@ -1,57 +0,0 @@
"""Diagnostics for the cert_expiry integration."""
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from .coordinator import CertExpiryConfigEntry
TO_REDACT = {CONF_HOST, "name", "title", "unique_id"}
async def async_get_config_entry_diagnostics(
_hass: HomeAssistant,
entry: CertExpiryConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
entry_diagnostics = entry.as_dict()
coordinator = getattr(entry, "runtime_data", None)
coordinator_diagnostics: dict[str, Any] = {
"host": None,
"port": None,
"name": None,
"expiry_datetime": None,
"is_cert_valid": None,
"cert_error": None,
"last_update_success": None,
}
if coordinator is not None:
expiry = coordinator.data.isoformat() if coordinator.data else None
cert_error = (
(
f"{type(coordinator.cert_error).__module__}."
f"{type(coordinator.cert_error).__qualname__}"
)
if coordinator.cert_error
else None
)
coordinator_diagnostics = {
"host": coordinator.host,
"port": coordinator.port,
"name": coordinator.name,
"expiry_datetime": expiry,
"is_cert_valid": coordinator.is_cert_valid,
"cert_error": cert_error,
"last_update_success": coordinator.last_update_success,
}
return {
"entry": async_redact_data(entry_diagnostics, TO_REDACT),
"coordinator": async_redact_data(coordinator_diagnostics, TO_REDACT),
}
@@ -1,6 +1,6 @@
"""Counter for the days until an HTTPS (TLS) certificate will expire."""
from typing import Any
from typing import Any, override
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -12,6 +12,7 @@ class CertExpiryEntity(CoordinatorEntity[CertExpiryDataUpdateCoordinator]):
_attr_has_entity_name = True
@override
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional sensor state attributes."""
@@ -1,81 +0,0 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register custom actions.
appropriate-polling:
status: done
comment: Certificates are checked every 12 hours via DataUpdateCoordinator.
brands: done
common-modules: done
config-flow-test-coverage:
status: done
comment: test_abort_on_socket_failed can be parametrized and should end in CREATE_ENTRY to test flow recovery.
config-flow: done
dependency-transparency:
status: exempt
comment: Integration has no external library dependencies.
docs-actions:
status: exempt
comment: Integration does not register custom actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Integration does not subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: todo
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: Integration does not register custom actions.
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: todo
reauthentication-flow:
status: exempt
comment: Config flow only collects host/port; the integration does not authenticate.
test-coverage:
status: todo
comment: Consider creating a mock_config_entry fixture and use that throughout tests.
# Gold
devices: done
diagnostics: todo
discovery: todo
discovery-update-info: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: Integration supports a single device per config entry.
entity-category:
status: todo
comment: Extra state attributes (is_valid, error) should be moved to separate entities in the future.
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: done
repair-issues: todo
stale-devices:
status: exempt
comment: Integration supports a single device per config entry.
# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo
@@ -1,6 +1,7 @@
"""Counter for the days until an HTTPS (TLS) certificate will expire."""
from datetime import datetime
from typing import override
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.core import HomeAssistant
@@ -44,6 +45,7 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity):
entry_type=DeviceEntryType.SERVICE,
)
@override
@property
def native_value(self) -> datetime | None:
"""Return the state of the sensor."""
@@ -16,10 +16,6 @@
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of the server to monitor.",
"port": "The port to connect to on the server."
},
"title": "Reconfigure the certificate to test"
},
"user": {
@@ -28,10 +24,6 @@
"name": "The name of the certificate",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of the server to monitor.",
"port": "The port to connect to on the server."
},
"title": "Define the certificate to test"
}
}
@@ -1,7 +1,7 @@
"""Config flow for chacon_dio integration."""
import logging
from typing import Any
from typing import Any, override
from dio_chacon_wifi_api import DIOChaconAPIClient
from dio_chacon_wifi_api.exceptions import DIOChaconAPIError, DIOChaconInvalidAuthError
@@ -25,6 +25,7 @@ DATA_SCHEMA = vol.Schema(
class ChaconDioConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for chacon_dio."""
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
+6 -1
View File
@@ -1,7 +1,7 @@
"""Cover Platform for Chacon Dio REV-SHUTTER devices."""
import logging
from typing import Any
from typing import Any, override
from dio_chacon_wifi_api.const import DeviceTypeEnum, ShutterMoveEnum
@@ -49,6 +49,7 @@ class ChaconDioCover(ChaconDioEntity, CoverEntity):
| CoverEntityFeature.SET_POSITION
)
@override
def _update_attr(self, data: dict[str, Any]) -> None:
"""Recompute the attribute values on init or state change."""
self._attr_available = data["connected"]
@@ -57,6 +58,7 @@ class ChaconDioCover(ChaconDioEntity, CoverEntity):
self._attr_is_opening = data["movement"] == ShutterMoveEnum.UP.value
self._attr_is_closed = self._attr_current_cover_position == 0
@override
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover.
@@ -80,6 +82,7 @@ class ChaconDioCover(ChaconDioEntity, CoverEntity):
self.target_id, ShutterMoveEnum.DOWN
)
@override
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover.
@@ -101,6 +104,7 @@ class ChaconDioCover(ChaconDioEntity, CoverEntity):
await self.client.move_shutter_direction(self.target_id, ShutterMoveEnum.UP)
@override
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
@@ -112,6 +116,7 @@ class ChaconDioCover(ChaconDioEntity, CoverEntity):
await self.client.move_shutter_direction(self.target_id, ShutterMoveEnum.STOP)
@override
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Set the cover open position in percentage.
@@ -1,7 +1,7 @@
"""Base entity for the Chacon Dio entity."""
import logging
from typing import Any
from typing import Any, override
from dio_chacon_wifi_api import DIOChaconAPIClient
@@ -38,6 +38,7 @@ class ChaconDioEntity(Entity):
def _update_attr(self, data: dict[str, Any]) -> None:
"""Recomputes the attributes values."""
@override
async def async_added_to_hass(self) -> None:
"""Register the callback for server side events."""
await super().async_added_to_hass()
@@ -1,7 +1,7 @@
"""Switch Platform for Chacon Dio REV-LIGHT and switch plug devices."""
import logging
from typing import Any
from typing import Any, override
from dio_chacon_wifi_api.const import DeviceTypeEnum
@@ -38,11 +38,13 @@ class ChaconDioSwitch(ChaconDioEntity, SwitchEntity):
_attr_device_class = SwitchDeviceClass.SWITCH
_attr_name = None
@override
def _update_attr(self, data: dict[str, Any]) -> None:
"""Recompute the attribute values on init or state change."""
self._attr_available = data["connected"]
self._attr_is_on = data["is_on"]
@override
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the switch.
@@ -59,6 +61,7 @@ class ChaconDioSwitch(ChaconDioEntity, SwitchEntity):
await self.client.switch_switch(self.target_id, True)
@override
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the switch.
@@ -1,6 +1,6 @@
"""Support for interfacing with an instance of getchannels.com."""
from typing import Any
from typing import Any, override
from pychannels import Channels
import voluptuous as vol
@@ -138,11 +138,13 @@ class ChannelsPlayer(MediaPlayerEntity):
self.now_playing_summary = None
self.now_playing_image_url = None
@override
@property
def name(self):
"""Return the name of the player."""
return self._name
@override
@property
def state(self) -> MediaPlayerState | None:
"""Return the state of the player."""
@@ -162,21 +164,25 @@ class ChannelsPlayer(MediaPlayerEntity):
self.update_favorite_channels()
self.update_state(self.client.status())
@override
@property
def source_list(self):
"""List of favorite channels."""
return [channel["name"] for channel in self.favorite_channels]
@override
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self.muted
@override
@property
def media_content_id(self):
"""Content ID of current playing channel."""
return self.channel_number
@override
@property
def media_image_url(self):
"""Image url of current playing media."""
@@ -187,6 +193,7 @@ class ChannelsPlayer(MediaPlayerEntity):
return "https://getchannels.com/assets/img/icon-1024.png"
@override
@property
def media_title(self):
"""Title of current playing media."""
@@ -195,38 +202,45 @@ class ChannelsPlayer(MediaPlayerEntity):
return None
@override
def mute_volume(self, mute: bool) -> None:
"""Mute (true) or unmute (false) player."""
if mute != self.muted:
response = self.client.toggle_muted()
self.update_state(response)
@override
def media_stop(self) -> None:
"""Send media_stop command to player."""
self.status = "stopped"
response = self.client.stop()
self.update_state(response)
@override
def media_play(self) -> None:
"""Send media_play command to player."""
response = self.client.resume()
self.update_state(response)
@override
def media_pause(self) -> None:
"""Send media_pause command to player."""
response = self.client.pause()
self.update_state(response)
@override
def media_next_track(self) -> None:
"""Seek ahead."""
response = self.client.skip_forward()
self.update_state(response)
@override
def media_previous_track(self) -> None:
"""Seek back."""
response = self.client.skip_backward()
self.update_state(response)
@override
def select_source(self, source: str) -> None:
"""Select a channel to tune to."""
for channel in self.favorite_channels:
@@ -235,6 +249,7 @@ class ChannelsPlayer(MediaPlayerEntity):
self.update_state(response)
break
@override
def play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
) -> None:
@@ -1,7 +1,7 @@
"""Config flow for the Chess.com integration."""
import logging
from typing import Any
from typing import Any, override
from chess_com_api import ChessComClient, NotFoundError
import voluptuous as vol
@@ -18,6 +18,7 @@ _LOGGER = logging.getLogger(__name__)
class ChessConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Chess.com."""
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -3,6 +3,7 @@
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import override
from chess_com_api import ChessComAPIError, ChessComClient, Player, PlayerStats
@@ -45,6 +46,7 @@ class ChessCoordinator(DataUpdateCoordinator[ChessData]):
)
self.client = ChessComClient(session=async_get_clientsession(hass))
@override
async def _async_update_data(self) -> ChessData:
"""Update data from Chess.com."""
try:
+3 -1
View File
@@ -2,7 +2,7 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, override
from chess_com_api import PlayerStats
@@ -121,6 +121,7 @@ class ChessPlayerSensor(ChessEntity, SensorEntity):
self.entity_description = description
self._attr_unique_id = f"{coordinator.config_entry.unique_id}.{description.key}"
@override
@property
def native_value(self) -> float:
"""Return the state of the sensor."""
@@ -148,6 +149,7 @@ class ChessGameModeSensor(ChessEntity, SensorEntity):
)
self._attr_translation_key = f"{game_mode}_{description.translation_key}"
@override
@property
def native_value(self) -> float:
"""Return the state of the sensor."""
+27 -1
View File
@@ -2,7 +2,7 @@
import asyncio
from collections.abc import Callable, Coroutine
from typing import Any, Concatenate
from typing import Any, Concatenate, override
from cieloconnectapi.exceptions import AuthenticationError
@@ -103,6 +103,7 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
super().__init__(coordinator, device_id)
self._attr_unique_id = device_id
@override
@property
def temperature_unit(self) -> str:
"""Return the unit of temperature in Home Assistant format.
@@ -124,6 +125,7 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
return UnitOfTemperature.CELSIUS
@override
@property
def supported_features(self) -> ClimateEntityFeature:
"""Return dynamic feature flags based on the current mode."""
@@ -147,6 +149,7 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
return flags
@override
@property
def current_humidity(self) -> int | None:
"""Return the current humidity, if available."""
@@ -154,58 +157,69 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
return self.device_data.humidity
return None
@override
@property
def target_temperature_low(self) -> float | None:
"""Return the low target temperature for HEAT_COOL mode."""
return self.client.target_temperature_low(self.temperature_unit)
@override
@property
def target_temperature_high(self) -> float | None:
"""Return the high target temperature for HEAT_COOL mode."""
return self.client.target_temperature_high(self.temperature_unit)
@override
@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
mode = self.client.hvac_mode()
return CIELO_TO_HA_HVAC.get(mode, mode)
@override
@property
def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available HVAC modes."""
modes = self.client.hvac_modes() or []
return [CIELO_TO_HA_HVAC.get(m, m) for m in modes]
@override
@property
def current_temperature(self) -> float | None:
"""Return the current indoor temperature."""
return self.client.current_temperature()
@override
@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
return self.client.target_temperature()
@override
@property
def min_temp(self) -> float:
"""Return the minimum possible target temperature."""
return self.client.min_temp()
@override
@property
def max_temp(self) -> float:
"""Return the maximum possible target temperature."""
return self.client.max_temp()
@override
@property
def target_temperature_step(self) -> float | None:
"""Return the precision of the thermostat."""
return self.client.target_temperature_step(self.temperature_unit)
@override
@property
def fan_mode(self) -> str | None:
"""Return the current fan mode."""
return self.client.fan_mode()
@override
@property
def fan_modes(self) -> list[str] | None:
"""Return the list of available fan modes.
@@ -217,6 +231,7 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
"""
return self.client.fan_modes()
@override
@property
def swing_modes(self) -> list[str] | None:
"""Return the list of available swing modes.
@@ -228,11 +243,13 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
"""
return self.client.swing_modes()
@override
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
return self.client.preset_mode()
@override
@property
def preset_modes(self) -> list[str] | None:
"""Return the list of available preset modes.
@@ -244,16 +261,19 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
"""
return self.client.preset_modes()
@override
@property
def swing_mode(self) -> str | None:
"""Return the current swing mode."""
return self.device_data.swing_mode if self.device_data else None
@override
@property
def precision(self) -> float:
"""Return the precision of the thermostat."""
return self.client.precision(self.temperature_unit)
@override
@async_handle_api_call
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
@@ -270,27 +290,32 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
**{ATTR_TEMPERATURE: kwargs.get(ATTR_TEMPERATURE)},
)
@override
@async_handle_api_call
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new fan mode."""
return await self.client.async_set_fan_mode(fan_mode)
@override
@async_handle_api_call
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
return await self.client.async_set_preset_mode(preset_mode)
@override
@async_handle_api_call
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new HVAC mode."""
cielo_mode = HA_TO_CIELO_HVAC.get(hvac_mode)
return await self.client.async_set_hvac_mode(cielo_mode)
@override
@async_handle_api_call
async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set new swing mode."""
return await self.client.async_set_swing_mode(swing_mode)
@override
async def async_turn_on(self) -> None:
"""Turn the climate device on."""
modes = self.hvac_modes or []
@@ -303,6 +328,7 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
raise HomeAssistantError("No non-off HVAC modes available to turn on device")
@override
async def async_turn_off(self) -> None:
"""Turn the climate device off."""
await self.async_set_hvac_mode(HVACMode.OFF)
@@ -1,6 +1,6 @@
"""Config Flow for Cielo integration."""
from typing import Any, Final
from typing import Any, Final, override
from aiohttp import ClientError
from cieloconnectapi import CieloClient
@@ -61,6 +61,7 @@ class CieloConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return client.user_id, {CONF_TOKEN: token}
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:

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