mirror of
https://github.com/home-assistant/core.git
synced 2026-05-27 11:15:48 +02:00
Compare commits
485 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51cddb88f5 | |||
| 0a7293dbbd | |||
| 057788d531 | |||
| 74cb4e2448 | |||
| 62aa79a304 | |||
| da74ae1955 | |||
| 2a4728463b | |||
| 3c5bcad0e9 | |||
| 2388353bd2 | |||
| 98823d6816 | |||
| cdd09f2535 | |||
| 2c900c59eb | |||
| 68757996de | |||
| 0fa3985b1d | |||
| a2551647b8 | |||
| e19601f991 | |||
| bc6060f98b | |||
| 0e2190fb25 | |||
| dd75a39e25 | |||
| 6efb3fffa3 | |||
| 4ef409f3cd | |||
| 0842c1cdfc | |||
| 49c045236c | |||
| 0b687df9f8 | |||
| ffcab49087 | |||
| 06c92cd328 | |||
| 66d4124439 | |||
| 99877d79e3 | |||
| 978171b600 | |||
| 4bd011702e | |||
| 64bc689bcf | |||
| 2f3f91ec82 | |||
| f6e8394771 | |||
| 27b0ba1a25 | |||
| 1070226acf | |||
| e8d7df7770 | |||
| 31f87b3a8a | |||
| 81efe6ddbf | |||
| af53865b2a | |||
| beeb8aca4c | |||
| b4063aaac9 | |||
| 7087cb2046 | |||
| 0044c43f3a | |||
| 0bb6113bfd | |||
| b6fa89c032 | |||
| 6a18e05bda | |||
| b312bd010b | |||
| 3487eaf8c5 | |||
| 9db7b3d012 | |||
| 23ecc311fd | |||
| 3355581bbf | |||
| 995707160f | |||
| b3199bac88 | |||
| 97de25d55a | |||
| 16ef7f967e | |||
| 4864a4125e | |||
| e2b71cee1f | |||
| 5bd92d47a9 | |||
| 7f0133e2ce | |||
| ba1ed66f7a | |||
| 2bc91e7a3e | |||
| 1c3a080506 | |||
| 5ecbfea028 | |||
| 9b67a24d92 | |||
| 4bd829a6a8 | |||
| 6feeba1f4f | |||
| 71b849cb58 | |||
| ba5855f5d2 | |||
| 4b04006302 | |||
| 430e03f299 | |||
| 7a2422013c | |||
| c906dc3d0c | |||
| f2fa25d449 | |||
| 0426f9beb6 | |||
| b6f0ca13f9 | |||
| 83e8f4991c | |||
| 3b38208e07 | |||
| 1a15f925a0 | |||
| 10d944eab7 | |||
| 1f873927aa | |||
| fe071ff66b | |||
| e4b79d4f3d | |||
| f6d4d0289e | |||
| 3089f3cc06 | |||
| e24f35473c | |||
| 1da605230d | |||
| fd572d83b7 | |||
| 305d4429ec | |||
| b95a3f5b2d | |||
| 4e986b181b | |||
| 65c074af9a | |||
| 58eae0b815 | |||
| c201c62b3d | |||
| 8b9b21c006 | |||
| b9c00dd82b | |||
| 910b87b847 | |||
| e37459c16b | |||
| c347afe28d | |||
| c8270fcb91 | |||
| ed399a6d14 | |||
| afa01d3d8c | |||
| ba03aaa2fa | |||
| 33f3640f66 | |||
| 46fc47bcdf | |||
| 71ec3c31fa | |||
| 2d54070cab | |||
| 67e4f04f09 | |||
| 78db1e3407 | |||
| 2368a3614d | |||
| 5053392cf2 | |||
| 6ec11460ed | |||
| 975e30c048 | |||
| 7655cb0fc6 | |||
| 7566839e9d | |||
| 7db5e82f58 | |||
| 7e67c53417 | |||
| 89fb856302 | |||
| a2fbd2b1ea | |||
| 231ed34133 | |||
| 6cff433b2e | |||
| eca83fb7b1 | |||
| 2c5adaec5c | |||
| 5d75f1c33b | |||
| d628d2314e | |||
| a9547ec349 | |||
| 2ec637df84 | |||
| 4f50ee5675 | |||
| 0faf96b983 | |||
| c3dacbc601 | |||
| 2659484000 | |||
| 6830ca75f5 | |||
| 38b4184dc3 | |||
| cfde7975d8 | |||
| d7ab696a4c | |||
| 7f7dad7f71 | |||
| 0ed21dbed7 | |||
| d2b37ee28b | |||
| b82c95e77f | |||
| baa61982a1 | |||
| 8ff6de788d | |||
| 640f82642a | |||
| 64ed269f9c | |||
| 2b58ef96eb | |||
| 74ca79ac28 | |||
| afb27bc165 | |||
| 0cbf27f44f | |||
| a5ceafa544 | |||
| cd4d669231 | |||
| cc411d06b5 | |||
| 1329f12d37 | |||
| 3899f5347b | |||
| cf02cfaa7c | |||
| e77c16ea1b | |||
| f1e2f94ee0 | |||
| 3516883b0a | |||
| c8b70b1a38 | |||
| 946625e281 | |||
| f4b7840d5c | |||
| 060f447e4a | |||
| d5bae0a2cf | |||
| f9bef804b1 | |||
| 6de03f4ed6 | |||
| e7f3e5637f | |||
| 80cefc74ec | |||
| 2f33b4b7f9 | |||
| cf52a7a509 | |||
| f5835f849a | |||
| ec5210dca8 | |||
| 422ea1a9b1 | |||
| b6f69f6b99 | |||
| a2a3819241 | |||
| 3ce33b0ac6 | |||
| e507a97d8b | |||
| 5801fdad14 | |||
| 2f4abd6a25 | |||
| 1c045ab715 | |||
| d4ca541a96 | |||
| a07a9dc6c8 | |||
| 6d60b3a23a | |||
| 37bb895b91 | |||
| f87dc917a6 | |||
| 71a15c188e | |||
| 003ecdb867 | |||
| ec7e5e5a75 | |||
| 7587f062e1 | |||
| 11970144e4 | |||
| 70750a6d79 | |||
| a53437315f | |||
| 5c73ad0310 | |||
| 4a04a271ec | |||
| 52c27bdea5 | |||
| 6fdc52c002 | |||
| e560bbc103 | |||
| b8c573685f | |||
| 3764b70b90 | |||
| 5d2de6f82b | |||
| 64d17f44fa | |||
| 6f67d44cfe | |||
| def3befb0e | |||
| 05716ae196 | |||
| c0a864297f | |||
| 04bb84cd03 | |||
| cb55accc3b | |||
| d21c227804 | |||
| 1ebccd9fa2 | |||
| cfbd0f3217 | |||
| 4afb7c0997 | |||
| 105caccc51 | |||
| 6419551117 | |||
| 585bd6616a | |||
| b8dd97cf21 | |||
| 68fc4aed78 | |||
| 7dbb259625 | |||
| 057eac7fb6 | |||
| 31c9cdf742 | |||
| 3147104132 | |||
| d6d0f37b52 | |||
| 75e48745a8 | |||
| 533417778c | |||
| e49fd4ebbd | |||
| 8412b029b1 | |||
| c65de7521f | |||
| 752c17917e | |||
| f643c7ddc6 | |||
| 6f5d4cf991 | |||
| b52466fed1 | |||
| 189534e32b | |||
| 684ae23b18 | |||
| f4d2f65602 | |||
| 65879ff37b | |||
| d902104bee | |||
| 7bad27c412 | |||
| 74a7102cf6 | |||
| e88fb03388 | |||
| 0723d8d83f | |||
| 73c9edd3e8 | |||
| 18f30bd97b | |||
| eae6e79b61 | |||
| 5bb42801d9 | |||
| 98271265d3 | |||
| 92d20477bc | |||
| 9352a0057e | |||
| 5fb874277a | |||
| d65f605398 | |||
| 7e5b448f70 | |||
| ef5da5ef36 | |||
| 410f00c4ed | |||
| 33c205dc04 | |||
| 267b3e279d | |||
| 9c1cd8093d | |||
| 201c0c2470 | |||
| 281d6e0e8b | |||
| 88746534a4 | |||
| 135f91c3c5 | |||
| 49d8dc88d9 | |||
| a7a2c1eb02 | |||
| 6596f956d2 | |||
| 9d8859833b | |||
| 65a4c10660 | |||
| 1737b50558 | |||
| 614c7006f6 | |||
| 8c901cc405 | |||
| 5d0fdfd38b | |||
| 748a9842af | |||
| 55786dbdfc | |||
| e88c03a437 | |||
| dbc0dc1ea6 | |||
| 31271876bf | |||
| d5c31332b5 | |||
| 3f0c93c26c | |||
| 07ed913ba2 | |||
| b7905b163f | |||
| c712b07da3 | |||
| c9ed57bc56 | |||
| 0e0901993d | |||
| 54aba11091 | |||
| dc9116a7a7 | |||
| 1e90882918 | |||
| e8295e14b1 | |||
| 7ebaaf129a | |||
| ee734dede6 | |||
| ebc582c813 | |||
| 311e5a9bd2 | |||
| cd6c3c878b | |||
| 51589ec2ff | |||
| 8e1a04dc82 | |||
| 6b15f9a2ec | |||
| 8d66752556 | |||
| 266767e37d | |||
| d39775ac34 | |||
| a314f7bf64 | |||
| 37478d33eb | |||
| 5a76f3bd19 | |||
| 17e105083e | |||
| db8589b2bc | |||
| 771b016f33 | |||
| 0bc0745e8c | |||
| ea084797d3 | |||
| 2456753caf | |||
| 070de13c14 | |||
| 5e45f37ee6 | |||
| 4a96880f51 | |||
| 228ac01124 | |||
| d366027e6b | |||
| 2f35ad2a8a | |||
| 95cc9aed64 | |||
| 37d6449a49 | |||
| 249b5435d9 | |||
| 3293ebcea5 | |||
| 47d8adc77c | |||
| 356e6a691b | |||
| b26c2f3854 | |||
| 0830988687 | |||
| 456202325a | |||
| 1e47149764 | |||
| 116b63ca3a | |||
| 3096bcf8a9 | |||
| a4027029d0 | |||
| fffc9d0695 | |||
| 3ca5cf5add | |||
| 087cb77042 | |||
| 8bd1c07ec9 | |||
| 9ecb59590b | |||
| e14eb9fbc5 | |||
| 149c796227 | |||
| 3383e5b1e9 | |||
| 05862c6dc8 | |||
| b35ac41470 | |||
| 20cec56512 | |||
| 74580262b6 | |||
| f75cdae602 | |||
| 8c95f4f7ae | |||
| c3ec51c471 | |||
| 0f80a4bc18 | |||
| 0761d618f1 | |||
| 03e3c46faf | |||
| d1962b0df2 | |||
| 7a38a2303a | |||
| 6f5c2a8614 | |||
| ff36498698 | |||
| 23e19ea2e4 | |||
| c33f174041 | |||
| bbe64d74e3 | |||
| ed3a71f2ee | |||
| 46c49daba4 | |||
| a2f2ded188 | |||
| 7be061796d | |||
| 27c7d8de0c | |||
| 07542523b5 | |||
| 18597bb653 | |||
| c4be57a294 | |||
| 7ceaebb086 | |||
| 7c5ef09734 | |||
| b4d8ba66fe | |||
| 308221ce67 | |||
| 1344213335 | |||
| 7e405e9014 | |||
| b0c45132ed | |||
| 7d7738303a | |||
| dd0cdc4fc4 | |||
| 18ea40c46d | |||
| a23131efc8 | |||
| 4940a0abae | |||
| 5f98d5ae52 | |||
| ba18cded30 | |||
| fb7504e9df | |||
| 106f815a1e | |||
| 167757762b | |||
| 3a902e1a16 | |||
| 85c11672d8 | |||
| 89649df20d | |||
| 7b749b95ce | |||
| cc140be85c | |||
| e1ad765414 | |||
| 44b1fea745 | |||
| 5dd04363b2 | |||
| 03aa979309 | |||
| 6fabbb354b | |||
| f644448d0f | |||
| 4e61581cd8 | |||
| 6f87d02b72 | |||
| 348f6149b4 | |||
| a4227ef1bc | |||
| aac49a567f | |||
| 76b878b136 | |||
| 2d05931683 | |||
| b10582b0a9 | |||
| b193d951d7 | |||
| 4cd0d9dcec | |||
| 32f65b2e11 | |||
| 8c79d1e44b | |||
| 8d53f7a520 | |||
| cc83ee88fb | |||
| 0c5b02eff3 | |||
| 9da9f8fd50 | |||
| d70ffcd3e9 | |||
| 3e26d0dfe3 | |||
| eab9747b32 | |||
| 9e955d8294 | |||
| f08cd01ff8 | |||
| eabaf3b0fe | |||
| 65ca790d15 | |||
| d177944f7a | |||
| 7f186f4430 | |||
| 4f4f4642a7 | |||
| 12e443cd31 | |||
| 22a7daabe7 | |||
| c139e99abd | |||
| 2bfdb96a3f | |||
| 4b24ca924b | |||
| 1d3d714e4f | |||
| ffae6eda8a | |||
| 4dd996b728 | |||
| afad1e8dac | |||
| 8e41933251 | |||
| c581eaad53 | |||
| 3050e79d06 | |||
| 0e8ecd1065 | |||
| 94732139f4 | |||
| c5e08b2409 | |||
| c12e1b5f4a | |||
| 6cfedb55e6 | |||
| af4cb9530b | |||
| 58e97e7d5f | |||
| 2945b51617 | |||
| 9d0e2df627 | |||
| 643ae080db | |||
| a7eaa51179 | |||
| e15852ff38 | |||
| f6dec34136 | |||
| 53905fbc49 | |||
| 8218ff0fe8 | |||
| 663f7e3e6b | |||
| 4dfa2b8b88 | |||
| f828b165b1 | |||
| c56c506648 | |||
| 8e5bf2a35f | |||
| 4d575e69a4 | |||
| 4f78bbccc0 | |||
| 2d66ebe54a | |||
| a3e1209778 | |||
| 7c44a0b88d | |||
| 126058e0fa | |||
| 28742822cb | |||
| 179d370c2a | |||
| 2d8f3691cf | |||
| ce4fc9e880 | |||
| 9e357e7e5a | |||
| ed35b23e62 | |||
| 191d2d1f12 | |||
| b165d8251f | |||
| 5e8886aeb7 | |||
| bdb66635f8 | |||
| 5ba6e348da | |||
| ed52b0ce80 | |||
| 33ee3d6967 | |||
| f36676c32c | |||
| 77beddb1e7 | |||
| 1677e410b3 | |||
| 1be09347cd | |||
| c30ac2c0f3 | |||
| 145c7435a5 | |||
| 60f3b3bcc0 | |||
| 03e6d3bd30 | |||
| ee4d150e13 | |||
| 148603a10e | |||
| 1dbd933d3c | |||
| f7ee7423fe | |||
| 6322f1e37a | |||
| 0d8c7fbb9d | |||
| 70e30b02a4 | |||
| ebd21ea9b2 | |||
| 9aa092cd34 | |||
| b274fe85b7 | |||
| 777c36998c | |||
| a3977428f9 | |||
| 2d626c263c | |||
| d1461f2e68 | |||
| 3b778d2cc7 | |||
| 67b7d17a2f | |||
| 1afeadc342 | |||
| f6aa4e2092 | |||
| 3b00c5bb96 | |||
| ef7eed579b | |||
| 568a0085fe |
@@ -0,0 +1,52 @@
|
||||
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
|
||||
@@ -0,0 +1,42 @@
|
||||
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}"
|
||||
+135
-244
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 3
|
||||
CACHE_VERSION: 4
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.6"
|
||||
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
||||
@@ -60,9 +60,7 @@ 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_BASE: /home/runner/work/apt
|
||||
APT_CACHE_DIR: /home/runner/work/apt/cache
|
||||
APT_LIST_CACHE_DIR: /home/runner/work/apt/lists
|
||||
APT_CACHE_VERSION: 1
|
||||
SQLALCHEMY_WARN_20: 1
|
||||
PYTHONASYNCIODEBUG: 1
|
||||
HASS_CI: 1
|
||||
@@ -86,12 +84,13 @@ 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 }}
|
||||
@@ -116,10 +115,6 @@ 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
|
||||
@@ -242,6 +237,11 @@ 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 Python ${{ matrix.python-version }}
|
||||
- name: Set up uv and Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
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,80 +384,40 @@ jobs:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: ${{ steps.generate-uv-key.outputs.full_key }}
|
||||
restore-keys: ${{ steps.generate-uv-key.outputs.partial_key }}
|
||||
- name: Check if apt cache exists
|
||||
id: cache-apt-check
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
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
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
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
|
||||
|
||||
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
|
||||
uses: ./.github/actions/cache-apt-packages
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
${{ env.APT_LIST_CACHE_DIR }}
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
|
||||
packages: >-
|
||||
bluez
|
||||
ffmpeg
|
||||
libturbojpeg
|
||||
libxml2-utils
|
||||
libavcodec-dev
|
||||
libavdevice-dev
|
||||
libavfilter-dev
|
||||
libavformat-dev
|
||||
libavutil-dev
|
||||
libswresample-dev
|
||||
libswscale-dev
|
||||
libudev-dev
|
||||
version: ${{ env.APT_CACHE_VERSION }}
|
||||
execute_install_scripts: true
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
id: create-venv
|
||||
env:
|
||||
PYTHON_VERSION: ${{ steps.python.outputs.python-version }}
|
||||
run: |
|
||||
python -m venv venv
|
||||
uv venv venv --python "${PYTHON_VERSION}"
|
||||
. 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
|
||||
@@ -506,36 +466,22 @@ 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: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -569,10 +515,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -605,10 +551,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
- name: Run gen_copilot_instructions.py
|
||||
run: |
|
||||
python -m script.gen_copilot_instructions validate
|
||||
@@ -660,10 +606,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
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
|
||||
@@ -711,10 +657,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -764,10 +710,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -815,10 +761,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
- name: Generate partial mypy restore key
|
||||
id: generate-mypy-key
|
||||
run: |
|
||||
@@ -876,38 +822,26 @@ 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: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -952,39 +886,27 @@ 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: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
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
|
||||
@@ -1105,40 +1027,28 @@ 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: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
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
|
||||
@@ -1266,42 +1176,35 @@ 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: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
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
|
||||
@@ -1421,7 +1324,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
@@ -1449,39 +1352,27 @@ 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: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
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
|
||||
@@ -1592,7 +1483,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
||||
@@ -1620,7 +1511,7 @@ jobs:
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
report_type: test_results
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -55,11 +55,11 @@ jobs:
|
||||
- name: Generate app token
|
||||
id: token
|
||||
# Pinned to a specific version of the action for security reasons
|
||||
# v1.7.0
|
||||
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
|
||||
# v3.2.0
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
||||
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:
|
||||
|
||||
@@ -337,6 +337,7 @@ 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.*
|
||||
@@ -428,6 +429,7 @@ 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.*
|
||||
@@ -565,6 +567,7 @@ 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.*
|
||||
@@ -609,6 +612,7 @@ 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
@@ -987,6 +987,8 @@ 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
|
||||
@@ -1290,6 +1292,8 @@ 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
|
||||
@@ -1317,6 +1321,8 @@ 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
|
||||
@@ -1930,6 +1936,8 @@ 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
|
||||
@@ -2054,6 +2062,8 @@ 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,9 +1,13 @@
|
||||
"""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
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv, httpx_client
|
||||
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
|
||||
@@ -12,6 +16,8 @@ from .services import async_setup_services
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.EVENT,
|
||||
Platform.MEDIA_PLAYER,
|
||||
Platform.NOTIFY,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
@@ -34,6 +40,28 @@ 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,13 +8,18 @@ from aioamazondevices.exceptions import (
|
||||
CannotConnect,
|
||||
CannotRetrieveData,
|
||||
)
|
||||
from aioamazondevices.structures import AmazonDevice
|
||||
from aioamazondevices.structures import (
|
||||
AmazonDevice,
|
||||
AmazonMediaState,
|
||||
AmazonVocalRecord,
|
||||
AmazonVolumeState,
|
||||
)
|
||||
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
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
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
|
||||
@@ -73,6 +78,18 @@ 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:
|
||||
@@ -149,3 +166,66 @@ 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
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""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,5 +1,10 @@
|
||||
{
|
||||
"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.7.0"]
|
||||
"requirements": ["aioamazondevices==13.8.0"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
"""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,6 +58,18 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"event": {
|
||||
"voice_event": {
|
||||
"name": "Voice event",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"triggered": "Triggered"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"announce": {
|
||||
"name": "Announce"
|
||||
|
||||
@@ -7,10 +7,11 @@ 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 CONF_HOST, DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
"""Constants for the Altruist integration."""
|
||||
|
||||
DOMAIN = "altruist"
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_HOST = "host"
|
||||
|
||||
@@ -10,13 +10,12 @@ 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
|
||||
CONF_TEMPERATURE = "temperature"
|
||||
temperature_key = "temperature"
|
||||
|
||||
for subentry in entry.subentries.values():
|
||||
data = subentry.data.copy()
|
||||
if CONF_TEMPERATURE not in data:
|
||||
if temperature_key not in data:
|
||||
continue
|
||||
data.pop(CONF_TEMPERATURE, None)
|
||||
data.pop(temperature_key, None)
|
||||
hass.config_entries.async_update_subentry(entry, subentry, data=data)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, minor_version=4)
|
||||
|
||||
@@ -7,27 +7,3 @@ 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",
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""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,
|
||||
@@ -24,11 +23,9 @@ 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 AVAILABLE_VIA_DEVICE_ATTR, DEPRECATED_SENSORS, DOMAIN, LAST_S_TEST
|
||||
from .const import LAST_S_TEST
|
||||
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
||||
from .entity import APCUPSdEntity
|
||||
|
||||
@@ -36,6 +33,20 @@ 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",
|
||||
@@ -49,18 +60,6 @@ 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",
|
||||
@@ -100,12 +99,6 @@ 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",
|
||||
@@ -132,23 +125,11 @@ 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",
|
||||
@@ -264,12 +245,6 @@ 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",
|
||||
@@ -358,12 +333,6 @@ 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",
|
||||
@@ -404,18 +373,6 @@ 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",
|
||||
@@ -481,9 +438,10 @@ async def async_setup_entry(
|
||||
# as unknown initially.
|
||||
#
|
||||
# We also sort the resources to ensure the order of entities
|
||||
# created is deterministic since "APCMODEL" and "MODEL"
|
||||
# resources map to the same "Model" name.
|
||||
# created is deterministic
|
||||
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
|
||||
@@ -561,63 +519,3 @@ 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,19 +241,5 @@
|
||||
"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,6 +24,7 @@ from pyatv.interface import (
|
||||
PushListener,
|
||||
PushUpdater,
|
||||
)
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -345,7 +346,10 @@ class AppleTvMediaPlayer(
|
||||
play_item = await media_source.async_resolve_media(
|
||||
self.hass, media_id, self.entity_id
|
||||
)
|
||||
media_id = async_process_play_media_url(self.hass, play_item.url)
|
||||
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_type = MediaType.MUSIC
|
||||
|
||||
if self._is_feature_available(FeatureName.StreamFile) and (
|
||||
@@ -353,11 +357,16 @@ 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):
|
||||
elif self._is_feature_available(FeatureName.PlayUrl) and (
|
||||
(parsed_url := URL(media_id)).is_absolute() and parsed_url.host
|
||||
):
|
||||
_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")
|
||||
_LOGGER.error(
|
||||
"Media streaming is not possible with current configuration for %s",
|
||||
media_id,
|
||||
)
|
||||
|
||||
@property
|
||||
def media_image_hash(self) -> str | None:
|
||||
|
||||
@@ -193,7 +193,11 @@ async def async_setup_entry(
|
||||
Aranet4BluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(entry.runtime_data.async_register_processor(processor))
|
||||
entry.async_on_unload(
|
||||
entry.runtime_data.async_register_processor(
|
||||
processor, AranetSensorEntityDescription
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Aranet4BluetoothSensorEntity(
|
||||
|
||||
@@ -49,6 +49,20 @@ 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."""
|
||||
@@ -187,20 +201,6 @@ 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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Light platform for Avea."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
from typing import Any
|
||||
@@ -19,6 +20,7 @@ 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,
|
||||
@@ -27,7 +29,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, UNKNOWN_NAME
|
||||
from .const import DOMAIN, INTEGRATION_TITLE, MODEL, UNKNOWN_NAME
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
UPDATE_EXCEPTIONS = (BleakError, OSError, RuntimeError)
|
||||
@@ -42,6 +44,13 @@ 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)
|
||||
@@ -96,7 +105,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Avea light platform."""
|
||||
async_add_entities(
|
||||
[AveaLight(entry.runtime_data, entry.title)], update_before_add=True
|
||||
[AveaLight(entry.runtime_data, entry.data[CONF_ADDRESS])],
|
||||
update_before_add=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -180,14 +190,42 @@ 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, entry_title: str) -> None:
|
||||
def __init__(self, light: avea.Bulb, address: str) -> None:
|
||||
"""Initialize an AveaLight."""
|
||||
self._light = light
|
||||
self._attr_name = entry_title
|
||||
self._attr_unique_id = address
|
||||
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."""
|
||||
@@ -214,6 +252,8 @@ 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:
|
||||
|
||||
@@ -17,10 +17,11 @@ 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, CONF_PREFIX, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .const import CONF_BUCKET, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .helpers import async_list_backups_from_s3
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -8,6 +8,7 @@ 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,
|
||||
@@ -20,7 +21,6 @@ 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,
|
||||
|
||||
@@ -11,8 +11,6 @@ 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,10 +8,11 @@ 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, CONF_PREFIX, DOMAIN
|
||||
from .const import CONF_BUCKET, DOMAIN
|
||||
from .helpers import async_list_backups_from_s3
|
||||
|
||||
SCAN_INTERVAL = timedelta(hours=6)
|
||||
|
||||
@@ -5,15 +5,10 @@ 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_PREFIX,
|
||||
CONF_SECRET_ACCESS_KEY,
|
||||
DOMAIN,
|
||||
)
|
||||
from .const import CONF_ACCESS_KEY_ID, CONF_BUCKET, CONF_SECRET_ACCESS_KEY, DOMAIN
|
||||
from .coordinator import S3ConfigEntry
|
||||
from .helpers import async_list_backups_from_s3
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
from collections.abc import Mapping
|
||||
from ipaddress import ip_address
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigEntry,
|
||||
@@ -50,6 +49,9 @@ 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"
|
||||
@@ -94,7 +96,8 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
else:
|
||||
serial = api.vapix.serial_number
|
||||
if (serial := self._get_serial_number(api)) is None:
|
||||
return self.async_abort(reason="no_serial_number")
|
||||
config = {
|
||||
CONF_PROTOCOL: user_input[CONF_PROTOCOL],
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
@@ -139,25 +142,15 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
async def _create_entry(self, serial: str) -> ConfigFlowResult:
|
||||
"""Create entry for device.
|
||||
|
||||
Generate a name to be used as a prefix for device entities.
|
||||
Use the discovered device name when available.
|
||||
"""
|
||||
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
|
||||
|
||||
if (title_placeholders := self.context.get("title_placeholders")) is not None:
|
||||
name = title_placeholders[CONF_NAME]
|
||||
else:
|
||||
name = f"{self.config[CONF_MODEL]} - {serial}"
|
||||
self.config[CONF_NAME] = name
|
||||
|
||||
title = f"{model} - {serial}"
|
||||
return self.async_create_entry(title=title, data=self.config)
|
||||
return self.async_create_entry(title=name, data=self.config)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -269,6 +262,19 @@ 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,6 +3,7 @@
|
||||
"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,6 +32,7 @@ PLATFORMS = [
|
||||
Platform.LIGHT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.UPDATE,
|
||||
]
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from typing import Any
|
||||
|
||||
import blebox_uniapi.cover
|
||||
from blebox_uniapi.cover import BleboxCoverState
|
||||
from blebox_uniapi.cover import BleboxCoverState, UnifiedCoverType
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
@@ -25,6 +25,19 @@ 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
|
||||
@@ -59,7 +72,6 @@ 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
|
||||
)
|
||||
@@ -76,6 +88,21 @@ 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."""
|
||||
@@ -118,7 +145,8 @@ class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
|
||||
|
||||
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Fully open the cover tilt."""
|
||||
await self._feature.async_set_tilt_position(0)
|
||||
position = 50 if self._feature.is_tilt_180 else 0
|
||||
await self._feature.async_set_tilt_position(position)
|
||||
|
||||
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.3"],
|
||||
"requirements": ["blebox-uniapi==2.5.4"],
|
||||
"zeroconf": ["_bbxsrv._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"""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,7 +124,9 @@ async def async_setup_entry(
|
||||
BlueMaestroBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
entry.async_on_unload(
|
||||
coordinator.async_register_processor(processor, SensorEntityDescription)
|
||||
)
|
||||
|
||||
|
||||
class BlueMaestroBluetoothSensorEntity(
|
||||
|
||||
@@ -22,6 +22,7 @@ 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,
|
||||
@@ -40,7 +41,6 @@ from .const import (
|
||||
CONF_DETAILS,
|
||||
CONF_MODE,
|
||||
CONF_PASSIVE,
|
||||
CONF_SOURCE,
|
||||
CONF_SOURCE_CONFIG_ENTRY_ID,
|
||||
CONF_SOURCE_DEVICE_ID,
|
||||
CONF_SOURCE_DOMAIN,
|
||||
|
||||
@@ -22,9 +22,6 @@ 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,7 +21,11 @@ from habluetooth import (
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, EVENT_LOGGING_CHANGED
|
||||
from homeassistant.const import (
|
||||
CONF_SOURCE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_LOGGING_CHANGED,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
@@ -33,7 +37,6 @@ 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==2.1.1",
|
||||
"bleak-retry-connector==4.6.0",
|
||||
"bluetooth-adapters==2.1.0",
|
||||
"bluetooth-auto-recovery==1.5.3",
|
||||
"bleak==3.0.2",
|
||||
"bleak-retry-connector==4.6.1",
|
||||
"bluetooth-adapters==2.3.0",
|
||||
"bluetooth-auto-recovery==1.6.4",
|
||||
"bluetooth-data-tools==1.29.18",
|
||||
"dbus-fast==5.0.3",
|
||||
"habluetooth==6.7.2"
|
||||
"dbus-fast==5.0.14",
|
||||
"habluetooth==6.7.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ 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,6 +28,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
EVENT_CLASS,
|
||||
EVENT_CLASS_BUTTON,
|
||||
EVENT_CLASS_COMMAND,
|
||||
EVENT_CLASS_DIMMER,
|
||||
EVENT_TYPE,
|
||||
)
|
||||
@@ -43,6 +44,7 @@ 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(
|
||||
|
||||
@@ -16,6 +16,7 @@ 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,
|
||||
@@ -43,6 +44,11 @@ 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.17.0"]
|
||||
"requirements": ["bthome-ble==3.23.2"]
|
||||
}
|
||||
|
||||
@@ -192,6 +192,12 @@ 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}",
|
||||
@@ -287,6 +293,12 @@ 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,13 +36,19 @@
|
||||
"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}\""
|
||||
}
|
||||
},
|
||||
@@ -68,6 +74,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@@ -98,6 +117,9 @@
|
||||
"gyroscope": {
|
||||
"name": "Gyroscope"
|
||||
},
|
||||
"light_level": {
|
||||
"name": "Light level"
|
||||
},
|
||||
"packet_id": {
|
||||
"name": "Packet ID"
|
||||
},
|
||||
@@ -110,6 +132,9 @@
|
||||
"rotational_speed": {
|
||||
"name": "Rotational speed"
|
||||
},
|
||||
"settings_revision": {
|
||||
"name": "Settings revision"
|
||||
},
|
||||
"text": {
|
||||
"name": "Text"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, override
|
||||
from typing import Any
|
||||
|
||||
import caldav
|
||||
from caldav.lib.error import DAVError
|
||||
@@ -204,20 +204,17 @@ 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)
|
||||
@@ -243,7 +240,6 @@ 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."""
|
||||
@@ -259,7 +255,6 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
import caldav
|
||||
from caldav.lib.error import AuthorizationError, DAVError
|
||||
@@ -33,7 +33,6 @@ 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, override
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import caldav
|
||||
|
||||
@@ -90,7 +90,6 @@ 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,7 +4,7 @@ import asyncio
|
||||
from datetime import date, datetime, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, cast, override
|
||||
from typing import Any, cast
|
||||
|
||||
import caldav
|
||||
from caldav.lib.error import DAVError, NotFoundError
|
||||
@@ -121,7 +121,6 @@ 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] = {}
|
||||
@@ -142,7 +141,6 @@ 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)
|
||||
@@ -179,7 +177,6 @@ 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, override
|
||||
from typing import Any, Final, cast, final
|
||||
|
||||
from aiohttp import web
|
||||
from dateutil.rrule import rrulestr
|
||||
@@ -546,7 +546,6 @@ 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:
|
||||
@@ -565,7 +564,6 @@ class CalendarEntity(Entity):
|
||||
"""Return the next upcoming event."""
|
||||
raise NotImplementedError
|
||||
|
||||
@override
|
||||
@final
|
||||
@property
|
||||
def state_attributes(self) -> dict[str, Any] | None:
|
||||
@@ -582,7 +580,6 @@ class CalendarEntity(Entity):
|
||||
"description": event.description or "",
|
||||
}
|
||||
|
||||
@override
|
||||
@final
|
||||
@property
|
||||
def state(self) -> str:
|
||||
@@ -597,7 +594,6 @@ class CalendarEntity(Entity):
|
||||
|
||||
return STATE_OFF
|
||||
|
||||
@override
|
||||
@callback
|
||||
def _async_write_ha_state(self) -> None:
|
||||
"""Write the state to the state machine.
|
||||
@@ -657,7 +653,6 @@ 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.
|
||||
|
||||
|
||||
@@ -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, override
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -113,7 +113,6 @@ 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})"
|
||||
@@ -326,7 +325,6 @@ 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."""
|
||||
@@ -353,7 +351,6 @@ class TargetCalendarEventListener(TargetEntityChangeTracker):
|
||||
)
|
||||
await self._calendar_event_listener.async_attach()
|
||||
|
||||
@override
|
||||
def _unsubscribe(self) -> None:
|
||||
"""Unsubscribe from all events."""
|
||||
super()._unsubscribe()
|
||||
@@ -370,7 +367,6 @@ class SingleEntityEventTrigger(Trigger):
|
||||
|
||||
_options: dict[str, Any]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_complete_config(
|
||||
cls, hass: HomeAssistant, complete_config: ConfigType
|
||||
@@ -381,7 +377,6 @@ 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
|
||||
@@ -397,7 +392,6 @@ 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:
|
||||
@@ -432,7 +426,6 @@ class EventTrigger(Trigger):
|
||||
_options: dict[str, Any]
|
||||
_event_type: str
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
@@ -450,7 +443,6 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
from aiostreammagic import StreamMagicClient
|
||||
import voluptuous as vol
|
||||
@@ -29,7 +29,6 @@ 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:
|
||||
@@ -82,7 +81,6 @@ 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, override
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aiostreammagic import StreamMagicClient
|
||||
from aiostreammagic.models import CallbackType
|
||||
@@ -60,18 +60,15 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
from aiostreammagic import (
|
||||
RepeatMode as CambridgeRepeatMode,
|
||||
@@ -83,7 +83,6 @@ 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."""
|
||||
@@ -101,7 +100,6 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
features |= feature
|
||||
return features
|
||||
|
||||
@override
|
||||
@property
|
||||
def state(self) -> MediaPlayerState:
|
||||
"""Return the state of the device."""
|
||||
@@ -120,13 +118,11 @@ 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."""
|
||||
@@ -139,13 +135,11 @@ 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."""
|
||||
@@ -157,62 +151,52 @@ 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."""
|
||||
@@ -221,13 +205,11 @@ 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."""
|
||||
@@ -240,13 +222,11 @@ 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."""
|
||||
@@ -259,19 +239,16 @@ 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."""
|
||||
@@ -280,49 +257,41 @@ 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."""
|
||||
@@ -331,7 +300,6 @@ 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."""
|
||||
@@ -340,7 +308,6 @@ 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
|
||||
@@ -386,7 +353,6 @@ 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, override
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiostreammagic import StreamMagicClient
|
||||
|
||||
@@ -90,19 +90,16 @@ 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,7 +2,6 @@
|
||||
|
||||
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
|
||||
@@ -128,13 +127,11 @@ 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, override
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiostreammagic import StreamMagicClient
|
||||
|
||||
@@ -103,19 +103,16 @@ 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."""
|
||||
|
||||
@@ -12,7 +12,7 @@ import logging
|
||||
import os
|
||||
from random import SystemRandom
|
||||
import time
|
||||
from typing import Any, Final, final, override
|
||||
from typing import Any, Final, final
|
||||
|
||||
from aiohttp import hdrs, web
|
||||
import attr
|
||||
@@ -455,7 +455,6 @@ 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."""
|
||||
@@ -468,7 +467,6 @@ 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."""
|
||||
@@ -504,7 +502,6 @@ 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."""
|
||||
@@ -598,7 +595,6 @@ 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:
|
||||
@@ -646,7 +642,6 @@ 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]:
|
||||
@@ -670,7 +665,6 @@ 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()
|
||||
@@ -764,7 +758,6 @@ 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.
|
||||
@@ -824,7 +817,6 @@ 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")
|
||||
@@ -848,7 +840,6 @@ 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:
|
||||
@@ -994,7 +985,6 @@ class _TemplateCameraEntity:
|
||||
self._report_issue()
|
||||
return getattr(self._camera, name)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
"""Forward to the camera entity."""
|
||||
self._report_issue()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""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 (
|
||||
@@ -55,7 +54,6 @@ 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]
|
||||
@@ -84,7 +82,6 @@ class CameraMediaSource(MediaSource):
|
||||
|
||||
return PlayMedia(url, FORMAT_CONTENT_TYPE[HLS_PROVIDER])
|
||||
|
||||
@override
|
||||
async def async_browse_media(
|
||||
self,
|
||||
item: MediaSourceItem,
|
||||
|
||||
@@ -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, override
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from mashumaro import MissingField
|
||||
import voluptuous as vol
|
||||
@@ -73,7 +73,6 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
from canary.const import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT
|
||||
from canary.model import Location
|
||||
@@ -58,7 +58,6 @@ 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."""
|
||||
@@ -75,30 +74,25 @@ 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(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Final, override
|
||||
from typing import Final
|
||||
|
||||
from aiohttp.web import Request, StreamResponse
|
||||
from canary.live_stream_api import LiveStreamSession
|
||||
@@ -108,19 +108,16 @@ 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:
|
||||
@@ -153,7 +150,6 @@ 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, override
|
||||
from typing import Any, Final
|
||||
|
||||
from canary.api import Api
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
@@ -46,14 +46,12 @@ 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,7 +4,6 @@ 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
|
||||
@@ -61,7 +60,6 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator[CanaryData]):
|
||||
"readings": readings_by_device_id,
|
||||
}
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> CanaryData:
|
||||
"""Fetch data from Canary."""
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Support for Canary sensors."""
|
||||
|
||||
from typing import Final, override
|
||||
from typing import Final
|
||||
|
||||
from canary.model import Device, Location, SensorType
|
||||
|
||||
@@ -143,13 +143,11 @@ 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,7 +1,5 @@
|
||||
"""Casper Glow integration binary sensor platform."""
|
||||
|
||||
from typing import override
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -45,7 +43,6 @@ 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()
|
||||
@@ -74,7 +71,6 @@ 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,7 +2,6 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import override
|
||||
|
||||
from pycasperglow import CasperGlow
|
||||
|
||||
@@ -67,7 +66,6 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
from bluetooth_data_tools import human_readable_name
|
||||
from pycasperglow import CasperGlow, CasperGlowError
|
||||
@@ -31,7 +31,6 @@ 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:
|
||||
@@ -74,7 +73,6 @@ 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,7 +1,6 @@
|
||||
"""Coordinator for the Casper Glow integration."""
|
||||
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from bleak import BleakError
|
||||
from bluetooth_data_tools import monotonic_time_coarse
|
||||
@@ -75,7 +74,6 @@ 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
|
||||
@@ -101,7 +99,6 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
@@ -54,7 +54,6 @@ 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()
|
||||
@@ -78,7 +77,6 @@ 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
|
||||
@@ -100,7 +98,6 @@ 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,7 +1,5 @@
|
||||
"""Casper Glow integration select platform for dimming time."""
|
||||
|
||||
from typing import override
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
@@ -40,7 +38,6 @@ 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."""
|
||||
@@ -48,7 +45,6 @@ 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()
|
||||
@@ -79,7 +75,6 @@ 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,7 +1,6 @@
|
||||
"""Casper Glow integration sensor platform."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import override
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
@@ -52,7 +51,6 @@ 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()
|
||||
@@ -95,7 +93,6 @@ 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()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Config flow for Cast."""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -32,8 +32,16 @@ OPTIONS_SCHEMA = KNOWN_HOSTS_SCHEMA.extend(
|
||||
vol.Required(CONF_MORE_OPTIONS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_UUID): str,
|
||||
vol.Optional(CONF_IGNORE_CEC): str,
|
||||
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
|
||||
),
|
||||
),
|
||||
}
|
||||
),
|
||||
SectionConfig(collapsed=True),
|
||||
@@ -47,7 +55,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
@@ -56,14 +63,12 @@ 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:
|
||||
@@ -112,13 +117,11 @@ class CastOptionsFlowHandler(OptionsFlow):
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the Google Cast options."""
|
||||
if user_input is not None:
|
||||
ignore_cec = _string_to_list(
|
||||
user_input[CONF_MORE_OPTIONS].get(CONF_IGNORE_CEC, "")
|
||||
ignore_cec = _trim_items(
|
||||
user_input[CONF_MORE_OPTIONS].get(CONF_IGNORE_CEC, [])
|
||||
)
|
||||
known_hosts = _trim_items(user_input.get(CONF_KNOWN_HOSTS, []))
|
||||
wanted_uuid = _string_to_list(
|
||||
user_input[CONF_MORE_OPTIONS].get(CONF_UUID, "")
|
||||
)
|
||||
wanted_uuid = _trim_items(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
|
||||
@@ -135,9 +138,7 @@ 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] = _list_to_string(
|
||||
self.config_entry.data[key]
|
||||
)
|
||||
suggested[CONF_MORE_OPTIONS][key] = self.config_entry.data[key]
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
@@ -146,16 +147,5 @@ 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()]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, override
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pychromecast.discovery
|
||||
import pychromecast.models
|
||||
@@ -67,17 +67,14 @@ 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(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import configparser
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, ClassVar, override
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
from urllib.parse import urlparse
|
||||
from uuid import UUID
|
||||
|
||||
@@ -180,45 +180,37 @@ 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:
|
||||
|
||||
@@ -6,7 +6,7 @@ from datetime import datetime
|
||||
from functools import wraps
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Concatenate, override
|
||||
from typing import TYPE_CHECKING, Any, Concatenate
|
||||
|
||||
import pychromecast.config
|
||||
import pychromecast.const
|
||||
@@ -338,7 +338,6 @@ 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)
|
||||
@@ -347,7 +346,6 @@ 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()
|
||||
@@ -356,7 +354,6 @@ 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()
|
||||
@@ -366,7 +363,6 @@ 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()
|
||||
@@ -374,7 +370,6 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
self._attr_available = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
def _invalidate(self):
|
||||
"""Invalidate some attributes."""
|
||||
super()._invalidate()
|
||||
@@ -533,7 +528,6 @@ 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."""
|
||||
|
||||
@@ -553,60 +547,51 @@ 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."""
|
||||
@@ -651,7 +636,6 @@ 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,
|
||||
@@ -691,7 +675,6 @@ 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:
|
||||
@@ -833,7 +816,6 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
|
||||
return (media_status, media_status_received)
|
||||
|
||||
@override
|
||||
@property
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the player."""
|
||||
@@ -876,7 +858,6 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_content_id(self) -> str | None:
|
||||
"""Content ID of current playing media."""
|
||||
@@ -886,7 +867,6 @@ 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."""
|
||||
@@ -911,7 +891,6 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
|
||||
return MediaType.VIDEO
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
@@ -921,7 +900,6 @@ 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."""
|
||||
@@ -932,75 +910,64 @@ 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."""
|
||||
@@ -1039,7 +1006,6 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
|
||||
return support
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_position(self):
|
||||
"""Position of current playing media in seconds."""
|
||||
@@ -1055,7 +1021,6 @@ 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.
|
||||
@@ -1110,7 +1075,6 @@ 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:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Climate device for CCM15 coordinator."""
|
||||
|
||||
import logging
|
||||
from typing import Any, override
|
||||
from typing import Any
|
||||
|
||||
from ccm15 import CCM15DeviceState, CCM15SlaveDevice
|
||||
|
||||
@@ -93,7 +93,6 @@ 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."""
|
||||
@@ -101,7 +100,6 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
return data.temperature
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def target_temperature(self) -> int | None:
|
||||
"""Return target temperature."""
|
||||
@@ -109,7 +107,6 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
return data.temperature_setpoint
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
"""Return hvac mode."""
|
||||
@@ -118,7 +115,6 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
return CONST_CMD_STATE_MAP[mode]
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Return fan mode."""
|
||||
@@ -127,7 +123,6 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
return CONST_CMD_FAN_MAP[mode]
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def swing_mode(self) -> str | None:
|
||||
"""Return swing mode."""
|
||||
@@ -135,13 +130,11 @@ 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."""
|
||||
@@ -149,7 +142,6 @@ 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:
|
||||
@@ -157,22 +149,18 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
from ccm15 import CCM15Device
|
||||
import voluptuous as vol
|
||||
@@ -27,7 +27,6 @@ class CCM15ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from ccm15 import CCM15Device, CCM15DeviceState, CCM15SlaveDevice
|
||||
import httpx
|
||||
@@ -45,7 +44,6 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
from aiocentriconnect import CentriConnect
|
||||
from aiocentriconnect.exceptions import (
|
||||
@@ -58,7 +58,6 @@ class CentriConnectConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
@@ -66,7 +65,6 @@ 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()
|
||||
@@ -81,7 +79,6 @@ 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,7 +4,6 @@ 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,
|
||||
@@ -237,7 +236,6 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -53,7 +53,6 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return True
|
||||
return False
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self,
|
||||
user_input: Mapping[str, Any] | None = None,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -47,7 +46,6 @@ class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime | None]):
|
||||
always_update=False,
|
||||
)
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> datetime | None:
|
||||
"""Fetch certificate."""
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""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, override
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -12,7 +12,6 @@ class CertExpiryEntity(CoordinatorEntity[CertExpiryDataUpdateCoordinator]):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
@override
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return additional sensor state attributes."""
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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,7 +1,6 @@
|
||||
"""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
|
||||
@@ -45,7 +44,6 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity):
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@override
|
||||
@property
|
||||
def native_value(self) -> datetime | None:
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
"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": {
|
||||
@@ -24,6 +28,10 @@
|
||||
"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, override
|
||||
from typing import Any
|
||||
|
||||
from dio_chacon_wifi_api import DIOChaconAPIClient
|
||||
from dio_chacon_wifi_api.exceptions import DIOChaconAPIError, DIOChaconInvalidAuthError
|
||||
@@ -25,7 +25,6 @@ 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:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Cover Platform for Chacon Dio REV-SHUTTER devices."""
|
||||
|
||||
import logging
|
||||
from typing import Any, override
|
||||
from typing import Any
|
||||
|
||||
from dio_chacon_wifi_api.const import DeviceTypeEnum, ShutterMoveEnum
|
||||
|
||||
@@ -49,7 +49,6 @@ 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"]
|
||||
@@ -58,7 +57,6 @@ 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.
|
||||
|
||||
@@ -82,7 +80,6 @@ class ChaconDioCover(ChaconDioEntity, CoverEntity):
|
||||
self.target_id, ShutterMoveEnum.DOWN
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover.
|
||||
|
||||
@@ -104,7 +101,6 @@ 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."""
|
||||
|
||||
@@ -116,7 +112,6 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
from dio_chacon_wifi_api import DIOChaconAPIClient
|
||||
|
||||
@@ -38,7 +38,6 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
from dio_chacon_wifi_api.const import DeviceTypeEnum
|
||||
|
||||
@@ -38,13 +38,11 @@ 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.
|
||||
|
||||
@@ -61,7 +59,6 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
from pychannels import Channels
|
||||
import voluptuous as vol
|
||||
@@ -138,13 +138,11 @@ 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."""
|
||||
@@ -164,25 +162,21 @@ 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."""
|
||||
@@ -193,7 +187,6 @@ class ChannelsPlayer(MediaPlayerEntity):
|
||||
|
||||
return "https://getchannels.com/assets/img/icon-1024.png"
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
@@ -202,45 +195,38 @@ 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:
|
||||
@@ -249,7 +235,6 @@ 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, override
|
||||
from typing import Any
|
||||
|
||||
from chess_com_api import ChessComClient, NotFoundError
|
||||
import voluptuous as vol
|
||||
@@ -18,7 +18,6 @@ _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,7 +3,6 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from chess_com_api import ChessComAPIError, ChessComClient, Player, PlayerStats
|
||||
|
||||
@@ -46,7 +45,6 @@ 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:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from chess_com_api import PlayerStats
|
||||
|
||||
@@ -121,7 +121,6 @@ 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."""
|
||||
@@ -149,7 +148,6 @@ 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."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any, Concatenate, override
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from cieloconnectapi.exceptions import AuthenticationError
|
||||
|
||||
@@ -103,7 +103,6 @@ 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.
|
||||
@@ -125,7 +124,6 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
|
||||
|
||||
return UnitOfTemperature.CELSIUS
|
||||
|
||||
@override
|
||||
@property
|
||||
def supported_features(self) -> ClimateEntityFeature:
|
||||
"""Return dynamic feature flags based on the current mode."""
|
||||
@@ -149,7 +147,6 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
|
||||
|
||||
return flags
|
||||
|
||||
@override
|
||||
@property
|
||||
def current_humidity(self) -> int | None:
|
||||
"""Return the current humidity, if available."""
|
||||
@@ -157,69 +154,58 @@ 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.
|
||||
@@ -231,7 +217,6 @@ 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.
|
||||
@@ -243,13 +228,11 @@ 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.
|
||||
@@ -261,19 +244,16 @@ 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."""
|
||||
@@ -290,32 +270,27 @@ 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 []
|
||||
@@ -328,7 +303,6 @@ 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, override
|
||||
from typing import Any, Final
|
||||
|
||||
from aiohttp import ClientError
|
||||
from cieloconnectapi import CieloClient
|
||||
@@ -61,7 +61,6 @@ 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
Reference in New Issue
Block a user