Compare commits

..

98 Commits

Author SHA1 Message Date
J. Nick Koston bf48806ee9 Merge remote-tracking branch 'upstream/cache-split-tests' into cache-split-tests 2026-05-22 14:48:53 -05:00
J. Nick Koston 2c25c5ad26 another round of copilot comments 2026-05-22 14:48:31 -05:00
J. Nick Koston ff1177dde4 Merge branch 'dev' into cache-split-tests 2026-05-22 14:33:56 -05:00
Martin Hjelmare 4306863729 Fix homekit test_reload flaky test (#171878) 2026-05-22 14:33:27 -05:00
J. Nick Koston 1cc91cd3b6 another round of copilot 2026-05-22 13:32:08 -05:00
J. Nick Koston ecac38a359 Merge branch 'dev' into cache-split-tests 2026-05-22 13:20:15 -05:00
Martin Hjelmare ba2f66e751 Remove not needed default force_update in flo (#171854) 2026-05-22 20:15:00 +02:00
J. Nick Koston 8301addc94 bot comments 2026-05-22 13:06:55 -05:00
J. Nick Koston 77bc932cf0 will copilot ever end 2026-05-22 12:57:17 -05:00
J. Nick Koston 11903ac62e dry 2026-05-22 12:54:51 -05:00
J. Nick Koston 878761cb41 preen 2026-05-22 12:49:43 -05:00
J. Nick Koston d7bf7df59f preen 2026-05-22 12:49:16 -05:00
J. Nick Koston e5890172a0 preen 2026-05-22 12:48:39 -05:00
J. Nick Koston e9a58cdd20 restore 2026-05-22 12:47:15 -05:00
J. Nick Koston cab7c41a7f more cleanups 2026-05-22 12:37:54 -05:00
J. Nick Koston 277a2d847a more cleanups 2026-05-22 12:30:30 -05:00
J. Nick Koston 7835a4992a simplify 2026-05-22 11:51:05 -05:00
Manu 94581d8ab6 Move service registration in System Bridge integration to async_setup (#171761)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-22 18:43:22 +02:00
J. Nick Koston 9dc37a2f46 Merge remote-tracking branch 'upstream/cache-split-tests' into cache-split-tests 2026-05-22 11:43:16 -05:00
J. Nick Koston 7534c438c1 fix cache bust 2026-05-22 11:42:22 -05:00
Ingo Fischer 7d6ec7fc58 Bump matter-python-client to 0.7.1 (#171764)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2026-05-22 17:34:20 +01:00
J. Nick Koston 69efa8ee1a Merge branch 'dev' into cache-split-tests 2026-05-22 11:28:43 -05:00
Jan Bouwhuis f49de3548e Add MQTT message expiry interval option (#171143) 2026-05-22 18:27:22 +02:00
J. Nick Koston 305b5d6e00 preen 2026-05-22 11:00:46 -05:00
J. Nick Koston d94226260b handle bot review comments 2026-05-22 10:40:23 -05:00
J. Nick Koston ecc8e52f3e make bot happy 2026-05-22 10:33:22 -05:00
J. Nick Koston 5771b0c86c Merge remote-tracking branch 'refs/remotes/upstream/cache-split-tests' into cache-split-tests 2026-05-22 10:31:11 -05:00
J. Nick Koston 3e289da366 drop bad copilot suggest 2026-05-22 10:31:00 -05:00
J. Nick Koston 944fb1ef67 Merge branch 'dev' into cache-split-tests 2026-05-22 10:17:10 -05:00
Manu 49ab42d3a2 Fix dead link in System Bridge service action (#171855) 2026-05-22 17:00:30 +02:00
Franck Nijhof 383f6142f0 Fix ZBT-2 hardware page crash when entry data is missing VID (#171828) 2026-05-22 16:58:01 +02:00
Kamil Breguła 2f120cf604 Fix rgb_color passed as RGBColor NamedTuple instead of plain tuple to light entity turn_on (#171795)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 16:56:34 +02:00
J. Nick Koston 1b6e9f5094 trim 2026-05-22 09:53:51 -05:00
Franck Nijhof 37288849b3 Register Insteon modem device before platform setup (#171839) 2026-05-22 10:23:47 -04:00
J. Nick Koston b2257caeb7 touch ups 2026-05-22 09:11:01 -05:00
J. Nick Koston 0ec0ea30ac single pass 2026-05-22 09:06:15 -05:00
J. Nick Koston 584b32c8b3 address copilot, cleanups 2026-05-22 09:01:39 -05:00
zhangluofeng aa8659f507 Add xthings cloud lock (#171176)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-22 15:54:37 +02:00
J. Nick Koston 4033a8b83a Apply suggestions from code review
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-05-22 08:53:47 -05:00
J. Nick Koston add8a5f799 Merge branch 'dev' into cache-split-tests 2026-05-22 08:53:27 -05:00
DeerMaximum 40c0d79d1d Replaced duplicate constant with homeassistant.const in NINA (#171852) 2026-05-22 15:41:04 +02:00
J. Nick Koston 7c137b5c73 cleanup 2026-05-22 08:34:32 -05:00
Franck Nijhof bef8632d78 Fix OpenHome config flow crash when UDN is a list (#171841) 2026-05-22 15:23:42 +02:00
Duco Sebel f00decfaa3 Use uptime device class for HomeWizard uptime sensor (#171830) 2026-05-22 15:23:09 +02:00
Manu 42e7add026 Add selector options translations to System Bridge integration (#171771) 2026-05-22 15:22:22 +02:00
Franck Nijhof 263aa3f16e Fix Hue device trigger crash for devices removed from bridge (#171844) 2026-05-22 15:18:00 +02:00
mhuiskes 03b364dcf0 Refactor zeversolar tests: use fixtures, patch at use site, add unique_id (#171697) 2026-05-22 14:58:56 +02:00
Duco Sebel 3b1aaf39af Bumb python homewizard energy 10.1.0 (#171826) 2026-05-22 14:51:58 +02:00
J. Nick Koston 4a6c5b5a22 cleanups 2026-05-22 07:46:31 -05:00
Franck Nijhof b82ba43fa4 Add pylint checker for invalid MDI icon references (#171824)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 13:45:40 +02:00
starkillerOG d81ef5593c Bump reolink_aio to 0.20.0: Reolink battery camera support (#171836) 2026-05-22 12:59:33 +02:00
Manu 5c5e50f024 Fix platform unloading in System Bridge integration (#171822) 2026-05-22 12:56:03 +02:00
Lex Postma e796d9c467 Update strings.json to align with HomeWizard app (#171740)
Co-authored-by: Duco Sebel <74970928+DCSBL@users.noreply.github.com>
2026-05-22 12:54:58 +02:00
Karl Beecken 342f23526f Remove empty requirements_test_all.txt (#171530 follow-up) (#171834) 2026-05-22 12:38:58 +02:00
Erik Montnemery 814ec697cf Remove advanced mode from hue service actions (#171442) 2026-05-22 11:45:33 +02:00
Erik Montnemery 120f1446d4 Rename advanced section to additional options in telegram_bot service actions (#171460) 2026-05-22 11:44:05 +02:00
Franck Nijhof 170af75b7d Fix Lutron Caseta battery sensor crash on unsupported devices (#171829) 2026-05-22 11:37:05 +02:00
Ariel Ebersberger 5432d29489 Use is/is not for same-enum identity comparisons (tests) (#171689) 2026-05-22 11:32:27 +02:00
Franck Nijhof 8098f4f6bc Fix invalid MDI icon references (#171831)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 11:25:28 +02:00
Simone Chemelli 6a70077687 Fix exception translation placeholder mismatches in comelit (#171748) 2026-05-22 11:17:17 +02:00
Max Michels 5dbb0464ba Replace duplicate constants with homeassistant.const imports (#171815) 2026-05-22 11:10:21 +02:00
dependabot[bot] 1df165ea02 Bump j178/prek-action from 2.0.3 to 2.0.4 (#171812)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 10:39:43 +02:00
Manu 62542eb911 Replace duplicate constants with homeassistant.const imports in xiaomi_miio (#171823) 2026-05-22 10:39:09 +02:00
Max Michels a842cac34c Replace duplicate constants with homeassistant.const imports (#171817) 2026-05-22 10:38:06 +02:00
Simone Chemelli 2460f688e3 Add missing exception translation keys in alexa_devices (#171749) 2026-05-22 10:34:00 +02:00
Simone Chemelli a868ea443c Fix hardcoded exception strings in uptimerobot (#171744) 2026-05-22 10:33:07 +02:00
Franck Nijhof 1d8565483b Apply web search citation stripping for GPT-5.x models in OpenAI conversation (#170956) 2026-05-22 10:31:10 +02:00
dependabot[bot] 1ef3301253 Bump github/codeql-action from 4.35.4 to 4.35.5 (#171813)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 09:47:16 +02:00
Manu 525952f016 Add entity translations to System Bridge integration (#171807) 2026-05-22 09:00:54 +02:00
Shay Levy 3257275c5a Fix LG webOS TV hardcoded exception strings (#171777) 2026-05-22 08:28:19 +02:00
Max Michels cb54fd4921 Replace duplicate constants with homeassistant.const imports (#171809) 2026-05-22 07:57:08 +02:00
Max Michels b391fc61ea Replace duplicate constants with homeassistant.const imports (#171808) 2026-05-22 07:56:29 +02:00
J. Nick Koston 1009ce4180 Merge branch 'dev' into cache-split-tests 2026-05-21 23:09:44 -05:00
J. Nick Koston fcd4e4939c Bump habluetooth to 6.2.0 (#171800) 2026-05-21 23:08:17 -05:00
J. Nick Koston deb8b5da05 Parallelize pytest --collect-only in split_tests.py (#171772) 2026-05-21 22:58:01 -04:00
g4bri3lDev c7754a6ce9 Bump py-opendisplay to 7.2.3 (#171775) 2026-05-21 22:52:36 -04:00
J. Nick Koston 242724bd50 Bump aiodiscover to 3.2.3 (#171803) 2026-05-21 22:51:54 -04:00
Max Michels 42454563db Replace duplicate constants with homeassistant.const imports (#171790)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2026-05-21 22:51:34 -04:00
J. Nick Koston bf03d0c216 Bump dbus-fast to 5.0.3 (#171595) 2026-05-21 21:11:35 -05:00
Max Michels 568107e06b Replace duplicate constants with homeassistant.const imports (#171784) 2026-05-22 01:33:48 +03:00
Jens Timmerman 7da44428b6 Bump guntamatic to v1.9.0 (#171631) 2026-05-21 22:55:29 +01:00
J. Nick Koston 22fb68b7a1 Revert "DNM: test cache, touch cloud manifest only"
This reverts commit a8bc244a7a.
2026-05-21 16:53:19 -05:00
J. Nick Koston 81e06539e6 Revert "DNM: test cache bust, touch cloud conftest"
This reverts commit 7c18b67b2e.
2026-05-21 16:53:19 -05:00
Max Michels 0a27f31949 Replace duplicate constants with homeassistant.const imports (#171781) 2026-05-21 22:53:07 +01:00
Erwin Douna 905b868c82 Add recreate services to Portainer (#167225)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2026-05-21 22:52:07 +01:00
J. Nick Koston 7c18b67b2e DNM: test cache bust, touch cloud conftest 2026-05-21 16:48:05 -05:00
J. Nick Koston a8bc244a7a DNM: test cache, touch cloud manifest only 2026-05-21 16:44:43 -05:00
Max Michels 3187289913 Replace duplicate constants with homeassistant.const imports (#171776) 2026-05-22 00:18:54 +03:00
Max Michels 87cecd4a44 Replace duplicate constants with homeassistant.const imports (#171778) 2026-05-22 00:18:23 +03:00
Robert Svensson fed38b0e38 Replace duplicate ATTR_LOCKED constant with homeassistant.const import in deconz (#171779) 2026-05-22 00:17:22 +03:00
J. Nick Koston 5975f4b179 Skip cache walking when --cache is not passed
Address Copilot review feedback on the cache PR:

* Split collect_tests into _collect_tests_uncached (the original
  directory-based pre-cache flow) and _collect_tests_cached (walks
  the tree to build per-file hashes).  Without --cache we now skip
  the walk + per-file hash entirely.
* A single-file root has no ancestor conftests to walk, so the
  conftest_hash would always be empty and stale counts could survive
  a real conftest change; bypass the cache for the file-root case.
* Save the cache file with explicit utf-8 encoding and
  ensure_ascii=False.
2026-05-21 16:08:44 -05:00
J. Nick Koston 9ed16b63a3 Cache per-file test counts in split_tests
Persist the result of pytest --collect-only between CI runs as a JSON
file keyed by content hash, so unchanged test files are served from
cache and only edited or new files are re-collected.  The cache is
self-healing:

* Missing, corrupt, or wrong-version files fall back to a full collect.
* Any conftest.py change anywhere under the test root invalidates the
  whole cache, so fixture parametrization shifts cannot silently skew
  counts.
* Files pytest returns nothing for (helper modules named test_*.py with
  no test functions) are cached as zero so they don't get re-collected
  forever.

Walking is done once with os.walk (~2x faster than Path.rglob) and
collects test files plus conftests in a single pass.  When the cache
is fully cold we feed pytest top-level directories rather than
thousands of file paths so cold runs stay as fast as before the cache
landed.

Wire the new --cache flag through the prepare-pytest-full job and back
the cache file with actions/cache so PRs can pick up the latest dev
snapshot via restore-keys.  Local timings: cold 11s, warm with no diff
0.4s, warm with one file edited 2.3s.
2026-05-21 15:56:08 -05:00
Raphael Hehl 6a36d1260b Bump uiprotect to 10.5.0 (#171768)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-05-21 15:42:31 -05:00
J. Nick Koston 8dadaa2f9e Filter fan-out children and fail fast on empty batch list
Only pass directories and test_*.py files to pytest --collect-only so
helpers like tests/components/conftest.py and tests/components/common.py
are not treated as explicit collection targets, and bail out with a
clear error if no eligible paths are found instead of running pytest
with no arguments.
2026-05-21 15:17:42 -05:00
J. Nick Koston 4f98c71586 Run pytest --collect-only in parallel batches in split_tests
cProfile showed 99.6% of split_tests.py wall time was spent in the
single pytest --collect-only subprocess.  Fan out the collection across
``os.cpu_count()`` workers; round-robin chunking keeps each batch
roughly equal, and tests/components is expanded one level deeper so
the ~1000 integration subdirectories distribute evenly.  Local wall
time dropped from ~132s to ~11s on an 18-core box.  Bucket output is
unchanged because we still parse the same pytest -qq output, just
aggregated from multiple invocations.
2026-05-21 15:10:01 -05:00
Raphael Hehl 49fc1b413d Bump pydantic to 2.13.4 (#171763)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-05-21 14:42:06 -05:00
Abílio Costa bffb0417cc Instruct agents to run prek after doing changes (#171757) 2026-05-21 20:16:26 +01:00
G Johansson 8b8c687fc3 Remove not needed exception handling in dnsip (#171758) 2026-05-21 20:58:32 +02:00
270 changed files with 14610 additions and 1805 deletions
+1
View File
@@ -15,6 +15,7 @@ Dockerfile.dev linguist-language=Dockerfile
# Generated files
CODEOWNERS linguist-generated=true
homeassistant/generated/*.py linguist-generated=true
pylint/plugins/pylint_home_assistant/generated/*.py linguist-generated=true
machine/* linguist-generated=true
mypy.ini linguist-generated=true
requirements.txt linguist-generated=true
@@ -1,52 +0,0 @@
name: Cache and install APT packages
description: >-
Wraps awalsh128/cache-apt-pkgs-action with the workarounds Home Assistant CI
needs. Removes the conflicting Microsoft apt source before any apt run, and
points the dynamic linker at the host's multiarch lib subdirectories so
shared libraries that rely on update-alternatives or postinst-managed paths
(eg libblas, liblapack pulled in by ffmpeg) stay reachable since the upstream
action does not execute postinst scripts on cache restore.
inputs:
packages:
description: Space-delimited list of apt packages to install.
required: true
version:
description: Cache version. Bump to invalidate the cache.
required: false
default: "1"
execute_install_scripts:
description: >-
Pass-through to awalsh128/cache-apt-pkgs-action. Postinst scripts are not
actually cached by the upstream action, so this is largely a no-op today.
required: false
default: "false"
runs:
using: composite
steps:
- name: Remove conflicting Microsoft apt source
shell: bash
run: sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
- name: Install apt packages via cache
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
with:
packages: ${{ inputs.packages }}
version: ${{ inputs.version }}
execute_install_scripts: ${{ inputs.execute_install_scripts }}
- name: Refresh dynamic linker cache
shell: bash
run: |
# awalsh128/cache-apt-pkgs-action does not run postinst scripts on
# cache restore, so update-alternatives symlinks (eg the one libblas
# creates at /usr/lib/<multiarch>/libblas.so.3) are never produced.
# Add every /usr/lib/<multiarch> subdirectory that holds shared
# libraries to the ldconfig search path so the dynamic linker still
# finds them. Use dpkg-architecture to derive the host's multiarch
# tuple so this works on non-x86_64 runners too.
multiarch="$(dpkg-architecture -qDEB_HOST_MULTIARCH)"
find "/usr/lib/${multiarch}" -mindepth 2 -maxdepth 2 \
-name '*.so.*' -printf '%h\n' \
| sort -u \
| sudo tee /etc/ld.so.conf.d/zzz-cache-apt-extras.conf > /dev/null
sudo ldconfig
+1
View File
@@ -25,6 +25,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
- .vscode/tasks.json contains useful commands used for development.
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
## Python Syntax Notes
+242 -99
View File
@@ -60,7 +60,9 @@ env:
# - 15.2 is the latest (as of 9 Feb 2023)
POSTGRESQL_VERSIONS: "['postgres:12.14','postgres:15.2']"
UV_CACHE_DIR: /tmp/uv-cache
APT_CACHE_VERSION: 1
APT_CACHE_BASE: /home/runner/work/apt
APT_CACHE_DIR: /home/runner/work/apt/cache
APT_LIST_CACHE_DIR: /home/runner/work/apt/lists
SQLALCHEMY_WARN_20: 1
PYTHONASYNCIODEBUG: 1
HASS_CI: 1
@@ -84,6 +86,7 @@ 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 }}
@@ -113,6 +116,10 @@ jobs:
# Include HA_SHORT_VERSION to force the immediate creation
# of a new uv cache entry after a version bump.
echo "key=venv-${CACHE_VERSION}-${HA_SHORT_VERSION}-${HASH_REQUIREMENTS_TEST}-${HASH_REQUIREMENTS}-${HASH_REQUIREMENTS_ALL}-${HASH_PACKAGE_CONSTRAINTS}-${HASH_GEN_REQUIREMENTS}" >> $GITHUB_OUTPUT
- name: Generate partial apt restore key
id: generate_apt_cache_key
run: |
echo "key=$(lsb_release -rs)-apt-${CACHE_VERSION}-${HA_SHORT_VERSION}" >> $GITHUB_OUTPUT
- name: Filter for core changes
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: core
@@ -274,7 +281,7 @@ jobs:
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run prek
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
RUFF_OUTPUT_FORMAT: github
@@ -295,7 +302,7 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
with:
extra-args: --all-files zizmor
@@ -377,36 +384,65 @@ 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'
if: |
steps.cache-venv.outputs.cache-hit != 'true'
|| steps.cache-apt-check.outputs.cache-hit != 'true'
id: install-os-deps
timeout-minutes: 10
uses: ./.github/actions/cache-apt-packages
with:
packages: >-
bluez
ffmpeg
libturbojpeg
libxml2-utils
libavcodec-dev
libavdevice-dev
libavfilter-dev
libavformat-dev
libavutil-dev
libswresample-dev
libswscale-dev
libudev-dev
version: ${{ env.APT_CACHE_VERSION }}
execute_install_scripts: true
- name: Read uv version from requirements.txt
if: steps.cache-venv.outputs.cache-hit != 'true'
id: read-uv-version
env:
APT_CACHE_HIT: ${{ steps.cache-apt-check.outputs.cache-hit }}
run: |
echo "version=$(grep '^uv==' requirements.txt | cut -d'=' -f3)" >> "$GITHUB_OUTPUT"
- name: Set up uv
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
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
with:
version: ${{ steps.read-uv-version.outputs.version }}
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
id: create-venv
@@ -414,6 +450,7 @@ jobs:
python -m venv venv
. venv/bin/activate
python --version
pip install "$(grep '^uv' < requirements.txt)"
uv pip install -U "pip>=25.2"
uv pip install -r requirements.txt
uv pip install -r requirements_all.txt -r requirements_test.txt
@@ -469,16 +506,30 @@ 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
@@ -825,20 +876,32 @@ 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
@@ -854,12 +917,49 @@ jobs:
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore pytest test counts cache
id: cache-pytest-counts
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: pytest_test_counts.json
# Primary key is a sentinel; restore-keys pick the most recent
# prefix match since the real (content-addressed) key isn't
# known until split_tests.py runs below.
key: >-
pytest-counts-${{ runner.os }}-${{ runner.arch }}-${{
steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}-restore-sentinel
restore-keys: |
pytest-counts-${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }}-
- name: Run split_tests.py
env:
TEST_GROUP_COUNT: ${{ needs.info.outputs.test_group_count }}
run: |
. venv/bin/activate
python -m script.split_tests ${TEST_GROUP_COUNT} tests
python -m script.split_tests \
--cache pytest_test_counts.json \
${TEST_GROUP_COUNT} tests
- name: Hash pytest test counts cache
id: cache-pytest-counts-hash
run: |
echo "hash=$(sha256sum pytest_test_counts.json | cut -d' ' -f1)" \
>> "$GITHUB_OUTPUT"
- name: Save pytest test counts cache
# Content-addressed key: identical content reuses the same entry.
# Skip the save when the restore already matched that hash.
if: >-
!endsWith(
steps.cache-pytest-counts.outputs.cache-matched-key,
steps.cache-pytest-counts-hash.outputs.hash
)
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: pytest_test_counts.json
key: >-
pytest-counts-${{ runner.os }}-${{ runner.arch }}-${{
steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}-${{
steps.cache-pytest-counts-hash.outputs.hash }}
- name: Upload pytest_buckets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -889,21 +989,33 @@ 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
@@ -1030,22 +1142,34 @@ 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
@@ -1179,29 +1303,36 @@ 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
@@ -1355,21 +1486,33 @@ 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
+2 -2
View File
@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
category: "/language:python"
+1
View File
@@ -15,6 +15,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
- .vscode/tasks.json contains useful commands used for development.
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
## Python Syntax Notes
@@ -11,7 +11,7 @@
"service": "mdi:dialpad"
},
"alarm_toggle_chime": {
"service": "mdi:abc"
"service": "mdi:bell-ring"
}
}
}
@@ -91,7 +91,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
translation_placeholders={"error": repr(err)},
) from err
except CannotAuthenticate as err:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
@@ -102,6 +102,9 @@
"entry_not_loaded": {
"message": "Entry not loaded: {entry}"
},
"invalid_auth": {
"message": "Invalid authentication credentials: {error}"
},
"invalid_device_id": {
"message": "Invalid device ID specified: {device_id}"
},
@@ -20,7 +20,7 @@
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.29.11",
"dbus-fast==5.0.0",
"habluetooth==6.1.0"
"dbus-fast==5.0.3",
"habluetooth==6.2.0"
]
}
@@ -65,11 +65,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
translation_placeholders={"error": repr(err)},
) from err
except aiocomelit_exceptions.CannotAuthenticate as err:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise InvalidAuth(
translation_domain=DOMAIN,
translation_key="cannot_authenticate",
translation_placeholders={"error": repr(err)},
) from err
finally:
await api.logout()
@@ -61,7 +61,7 @@ class CurrencylayerSensor(SensorEntity):
"""Implementing the Currencylayer sensor."""
_attr_attribution = "Data provided by currencylayer.com"
_attr_icon = "mdi:currency"
_attr_icon = "mdi:currency-usd"
def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None:
"""Initialize the sensor."""
+2 -2
View File
@@ -26,12 +26,12 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.const import ATTR_LOCKED, ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DeconzConfigEntry
from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE
from .const import ATTR_OFFSET, ATTR_VALVE
from .entity import DeconzDevice
from .hub import DeconzHub
-2
View File
@@ -43,8 +43,6 @@ PLATFORMS = [
]
ATTR_DARK = "dark"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LOCKED = "locked"
ATTR_OFFSET = "offset"
ATTR_ON = "on"
ATTR_VALVE = "valve"
+1 -1
View File
@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.1",
"aiodiscover==3.2.0",
"aiodiscover==3.2.3",
"cached-ipaddress==1.0.1"
]
}
+1 -6
View File
@@ -6,7 +6,6 @@ import logging
import aiodns
from aiodns.error import DNSError
from pycares import AresError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT
@@ -78,11 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DnsIPConfigEntry) -> boo
) from err
errors = [
result
for result in results
if isinstance(
result, (TimeoutError, DNSError, AresError, asyncio.CancelledError)
)
result for result in results if isinstance(result, (TimeoutError, DNSError))
]
if errors and len(errors) == len(results):
await _close_resolvers()
+1 -1
View File
@@ -23,7 +23,7 @@
"service": "mdi:refresh"
},
"set_dhw_override": {
"service": "mdi:water-heater"
"service": "mdi:water-boiler"
},
"set_system_mode": {
"service": "mdi:pencil"
+1 -1
View File
@@ -16,7 +16,7 @@ class DeviceType(Enum):
GAME_CONSOLE = "mdi:nintendo-game-boy"
STREAMING_DONGLE = "mdi:cast"
LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker"
DISC_PLAYER = "mdi:disk-player"
DISC_PLAYER = "mdi:disc-player"
REMOTE_CONTROL = "mdi:remote-tv"
RADIO = "mdi:radio"
PHOTO_CAMERA = PHOTOS = "mdi:camera"
-1
View File
@@ -10,7 +10,6 @@ from .coordinator import FloDeviceDataUpdateCoordinator
class FloEntity(Entity):
"""A base class for Flo entities."""
_attr_force_update = False
_attr_has_entity_name = True
_attr_should_poll = False
+1 -1
View File
@@ -2,7 +2,7 @@
"entity": {
"button": {
"sync_clock": {
"default": "mdi:clock-sync"
"default": "mdi:clock-check"
}
},
"number": {
@@ -14,5 +14,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["guntamatic==1.8.0"]
"requirements": ["guntamatic==1.9.0"]
}
-2
View File
@@ -2,8 +2,6 @@
from homeassistant.const import Platform
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODE = "mode"
ATTR_TIME_PERIOD = "time_period"
ATTR_ONOFF = "on_off"
CONF_CODE = "2fa"
+1 -1
View File
@@ -12,12 +12,12 @@ from homeassistant.components.light import (
ColorMode,
LightEntity,
)
from homeassistant.const import ATTR_MODE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util
from . import HiveConfigEntry, refresh_system
from .const import ATTR_MODE
from .entity import HiveEntity
PARALLEL_UPDATES = 0
+1 -2
View File
@@ -6,12 +6,11 @@ from typing import Any
from apyhiveapi import Hive
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.const import ATTR_MODE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HiveConfigEntry, refresh_system
from .const import ATTR_MODE
from .entity import HiveEntity
PARALLEL_UPDATES = 0
@@ -19,7 +19,9 @@ EXPECTED_ENTRY_VERSION = (
@callback
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
"""Return board info."""
entries = hass.config_entries.async_entries(DOMAIN)
entries = hass.config_entries.async_entries(
DOMAIN, include_ignore=False, include_disabled=False
)
return [
HardwareInfo(
board=None,
@@ -13,6 +13,6 @@
"iot_class": "local_polling",
"loggers": ["homewizard_energy"],
"quality_scale": "platinum",
"requirements": ["python-homewizard-energy==10.0.1"],
"requirements": ["python-homewizard-energy==10.1.0"],
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
}
+2 -10
View File
@@ -35,7 +35,6 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from homeassistant.util.variance import ignore_variance
from .const import DOMAIN
from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
@@ -66,13 +65,6 @@ def to_percentage(value: float | None) -> float | None:
return value * 100 if value is not None else None
def uptime_to_datetime(value: int) -> datetime:
"""Convert seconds to datetime timestamp."""
return utcnow().replace(microsecond=0) - timedelta(seconds=value)
uptime_to_stable_datetime = ignore_variance(uptime_to_datetime, timedelta(minutes=5))
SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
HomeWizardSensorEntityDescription(
key="smr_version",
@@ -643,7 +635,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
HomeWizardSensorEntityDescription(
key="uptime",
translation_key="uptime",
device_class=SensorDeviceClass.TIMESTAMP,
device_class=SensorDeviceClass.UPTIME,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_fn=(
@@ -651,7 +643,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
),
value_fn=(
lambda data: (
uptime_to_stable_datetime(data.system.uptime_s)
utcnow() - timedelta(seconds=data.system.uptime_s)
if data.system is not None and data.system.uptime_s is not None
else None
)
@@ -61,13 +61,14 @@
},
"select": {
"battery_group_mode": {
"name": "Battery group mode",
"name": "Battery group charging strategy",
"state": {
"predictive": "Smart charging",
"standby": "Standby",
"to_full": "Manual charge mode",
"zero": "Zero mode",
"zero_charge_only": "Zero mode (charge only)",
"zero_discharge_only": "Zero mode (discharge only)"
"to_full": "One-time full charge",
"zero": "Net zero",
"zero_charge_only": "Net zero (charge only)",
"zero_discharge_only": "Net zero (discharge only)"
}
}
},
+13 -12
View File
@@ -31,15 +31,16 @@ activate_scene:
dynamic:
selector:
boolean:
speed:
advanced: true
selector:
number:
min: 0
max: 100
brightness:
advanced: true
selector:
number:
min: 1
max: 255
scene_customization:
collapsed: true
fields:
speed:
selector:
number:
min: 0
max: 100
brightness:
selector:
number:
min: 1
max: 255
+6 -1
View File
@@ -184,7 +184,12 @@
"name": "Transition"
}
},
"name": "Activate Hue scene"
"name": "Activate Hue scene",
"sections": {
"scene_customization": {
"name": "Scene customization"
}
}
},
"hue_activate_scene": {
"description": "Activates a Hue scene stored in the Hue hub.",
@@ -87,6 +87,8 @@ def async_get_triggers(
# Get Hue device id from device identifier
hue_dev_id = get_hue_device_id(device_entry)
if hue_dev_id is None or hue_dev_id not in api.devices:
return []
# extract triggers from all button resources of this Hue device
triggers: list[dict[str, Any]] = []
model_id = api.devices[hue_dev_id].product_data.product_name
+2 -2
View File
@@ -118,6 +118,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
device = devices.add_x10_device(housecode, unitcode, x10_type, steps)
create_insteon_device(hass, devices.modem, entry.entry_id)
await hass.config_entries.async_forward_entry_setups(entry, INSTEON_PLATFORMS)
for address in devices:
@@ -131,8 +133,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
register_new_device_callback(hass)
async_setup_services(hass)
create_insteon_device(hass, devices.modem, entry.entry_id)
entry.async_create_background_task(
hass, async_get_device_config(hass, entry), "insteon-get-device-config"
)
@@ -1,7 +1,7 @@
{
"entity": {
"binary_sensor": {
"erev_shabbat_hag": { "default": "mdi:candle-light" },
"erev_shabbat_hag": { "default": "mdi:candle" },
"issur_melacha_in_effect": { "default": "mdi:power-plug-off" },
"motzei_shabbat_hag": { "default": "mdi:fire" }
},
+1 -1
View File
@@ -7,7 +7,7 @@
"service": "mdi:lock-open"
},
"disable": {
"service": "mdi:fash-off"
"service": "mdi:flash-off"
},
"enable": {
"service": "mdi:flash"
+4 -4
View File
@@ -28,25 +28,25 @@
"ice_maker": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_bottom_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_middle_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_top_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
}
},
+1 -1
View File
@@ -241,7 +241,7 @@ def preprocess_turn_on_alternatives(
if (color_name := params.pop(ATTR_COLOR_NAME, None)) is not None:
try:
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
params[ATTR_RGB_COLOR] = tuple(color_util.color_name_to_rgb(color_name))
except ValueError:
_LOGGER.warning("Got unknown color %s, falling back to white", color_name)
params[ATTR_RGB_COLOR] = (255, 255, 255)
@@ -3,7 +3,7 @@
from datetime import timedelta
from typing import Any
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED, BridgeResponseError
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -131,7 +131,11 @@ class LutronCasetaBatterySensor(LutronCasetaEntity, BinarySensorEntity):
async def async_update(self) -> None:
"""Fetch the latest battery status from the bridge."""
status = await self._smartbridge.get_battery_status(self.device_id)
try:
status = await self._smartbridge.get_battery_status(self.device_id)
except BridgeResponseError:
self._attr_is_on = None
return
normalized_status = status.strip().casefold() if status else None
if normalized_status == BATTERY_STATUS_LOW:
self._attr_is_on = True
+1 -1
View File
@@ -60,7 +60,7 @@ def get_matter_device_info(
return None
return MatterDeviceInfo(
unique_id=node.device_info.uniqueID,
unique_id=node.device_info.uniqueID or "",
vendor_id=hex(node.device_info.vendorID),
product_id=hex(node.device_info.productID),
)
@@ -6,13 +6,14 @@ from typing import Any
from chip.clusters import Objects
from matter_server.common.helpers.util import dataclass_to_dict, parse_attribute_path
from homeassistant.components.diagnostics import REDACTED
from homeassistant.components.diagnostics import REDACTED, async_redact_data
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .helpers import MatterConfigEntry, get_matter, get_node_from_device_entry
ATTRIBUTES_TO_REDACT = {Objects.BasicInformation.Attributes.Location}
SERVER_INFO_TO_REDACT = {"wifi_ssid"}
def redact_matter_attributes(node_data: dict[str, Any]) -> dict[str, Any]:
@@ -44,6 +45,7 @@ async def async_get_config_entry_diagnostics(
matter = get_matter(hass)
server_diagnostics = await matter.matter_client.get_diagnostics()
data = dataclass_to_dict(server_diagnostics)
data["info"] = async_redact_data(data["info"], SERVER_INFO_TO_REDACT)
nodes = [redact_matter_attributes(node_data) for node_data in data["nodes"]]
data["nodes"] = nodes
@@ -59,7 +61,9 @@ async def async_get_device_diagnostics(
node = get_node_from_device_entry(hass, device)
return {
"server_info": dataclass_to_dict(server_diagnostics.info),
"server_info": async_redact_data(
dataclass_to_dict(server_diagnostics.info), SERVER_INFO_TO_REDACT
),
"node": redact_matter_attributes(
remove_serialization_type(dataclass_to_dict(node.node_data) if node else {})
),
+1 -1
View File
@@ -102,7 +102,7 @@
"default": "mdi:home-lightning-bolt"
},
"eve_weather_trend": {
"default": "mdi:weather",
"default": "mdi:weather-cloudy",
"state": {
"cloudy": "mdi:weather-cloudy",
"rainy": "mdi:weather-rainy",
@@ -8,6 +8,6 @@
"documentation": "https://www.home-assistant.io/integrations/matter",
"integration_type": "hub",
"iot_class": "local_push",
"requirements": ["matter-python-client==0.6.0"],
"requirements": ["matter-python-client==0.7.1"],
"zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."]
}
+2 -1
View File
@@ -4,12 +4,13 @@ from dataclasses import dataclass
from typing import cast
from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow, llm
from .application_credentials import authorization_server_context
from .const import CONF_ACCESS_TOKEN, CONF_AUTHORIZATION_URL, CONF_TOKEN_URL, DOMAIN
from .const import CONF_AUTHORIZATION_URL, CONF_TOKEN_URL, DOMAIN
from .coordinator import ModelContextProtocolCoordinator, TokenManager
from .types import ModelContextProtocolConfigEntry
+2 -8
View File
@@ -13,7 +13,7 @@ from yarl import URL
from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.const import CONF_TOKEN, CONF_URL
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
@@ -24,13 +24,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
from . import async_get_config_entry_implementation
from .application_credentials import authorization_server_context
from .const import (
CONF_ACCESS_TOKEN,
CONF_AUTHORIZATION_URL,
CONF_SCOPE,
CONF_TOKEN_URL,
DOMAIN,
)
from .const import CONF_AUTHORIZATION_URL, CONF_SCOPE, CONF_TOKEN_URL, DOMAIN
from .coordinator import TokenManager, mcp_client
_LOGGER = logging.getLogger(__name__)
-2
View File
@@ -2,8 +2,6 @@
DOMAIN = "mcp"
# pylint: disable-next=home-assistant-duplicate-const
CONF_ACCESS_TOKEN = "access_token"
CONF_AUTHORIZATION_URL = "authorization_url"
CONF_TOKEN_URL = "token_url"
CONF_SCOPE = "scope"
+1 -5
View File
@@ -41,7 +41,7 @@ from mcp.shared.message import SessionMessage
from homeassistant.components import conversation
from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.const import CONF_LLM_HASS_API
from homeassistant.const import CONF_LLM_HASS_API, CONTENT_TYPE_JSON
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.helpers import llm
@@ -56,10 +56,6 @@ _LOGGER = logging.getLogger(__name__)
STREAMABLE_API = "/api/mcp"
TIMEOUT = 60 # Seconds
# Content types
# pylint: disable-next=home-assistant-duplicate-const
CONTENT_TYPE_JSON = "application/json"
# Legacy SSE endpoint
SSE_API = f"/{DOMAIN}/sse"
MESSAGES_API = f"/{DOMAIN}/messages/{{session_id}}"
@@ -108,6 +108,7 @@ ABBREVIATIONS = {
"mode_stat_t": "mode_state_topic",
"mode_stat_tpl": "mode_state_template",
"modes": "modes",
"msg_exp_int": "message_expiry_interval",
"name": "name",
"o": "origin",
"off_dly": "off_delay",
@@ -120,6 +120,8 @@ from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.selector import (
BooleanSelector,
DurationSelector,
DurationSelectorConfig,
FileSelector,
FileSelectorConfig,
NumberSelector,
@@ -227,6 +229,7 @@ from .const import (
CONF_LAST_RESET_VALUE_TEMPLATE,
CONF_MAX,
CONF_MAX_KELVIN,
CONF_MESSAGE_EXPIRY_INTERVAL,
CONF_MIN,
CONF_MIN_KELVIN,
CONF_MODE_COMMAND_TEMPLATE,
@@ -3721,6 +3724,11 @@ MQTT_DEVICE_PLATFORM_FIELDS = {
default=DEFAULT_QOS,
section="mqtt_settings",
),
CONF_MESSAGE_EXPIRY_INTERVAL: PlatformField(
selector=DurationSelector(DurationSelectorConfig(enable_day=True)),
required=False,
section="mqtt_settings",
),
}
+1
View File
@@ -49,6 +49,7 @@ CONF_IMAGE_TOPIC = "image_topic"
CONF_JSON_ATTRS_TOPIC = "json_attributes_topic"
CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template"
CONF_KEEPALIVE = "keepalive"
CONF_MESSAGE_EXPIRY_INTERVAL = "message_expiry_interval"
CONF_ORIGIN = "origin"
CONF_QOS = ATTR_QOS
CONF_RETAIN = ATTR_RETAIN
+12 -2
View File
@@ -17,7 +17,7 @@ from .models import DATA_MQTT, PublishPayloadType
STORED_MESSAGES = 10
@dataclass
@dataclass(frozen=True, slots=True)
class TimestampedPublishMessage:
"""MQTT Message."""
@@ -26,6 +26,8 @@ class TimestampedPublishMessage:
qos: int
retain: bool
timestamp: float
encoding: str | None
kwargs: dict[str, Any]
def log_message(
@@ -35,6 +37,8 @@ def log_message(
payload: PublishPayloadType,
qos: int,
retain: bool,
encoding: str | None,
**kwargs: Any,
) -> None:
"""Log an outgoing MQTT message."""
entity_info = hass.data[DATA_MQTT].debug_info_entities.setdefault(
@@ -45,7 +49,13 @@ def log_message(
"messages": deque(maxlen=STORED_MESSAGES),
}
msg = TimestampedPublishMessage(
topic, payload, qos, retain, timestamp=time.monotonic()
topic,
payload,
qos,
retain,
timestamp=time.monotonic(),
encoding=encoding,
kwargs=kwargs,
)
entity_info["transmitted"][topic]["messages"].append(msg)
+12 -26
View File
@@ -84,6 +84,7 @@ from .const import (
CONF_JSON_ATTRS_TEMPLATE,
CONF_JSON_ATTRS_TOPIC,
CONF_MANUFACTURER,
CONF_MESSAGE_EXPIRY_INTERVAL,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS,
@@ -94,7 +95,6 @@ from .const import (
CONF_SW_VERSION,
CONF_TOPIC,
CONF_VIA_DEVICE,
DEFAULT_ENCODING,
DOMAIN,
MQTT_CONNECTION_STATE,
)
@@ -153,6 +153,8 @@ MQTT_ATTRIBUTES_BLOCKED = {
"unit_of_measurement",
}
PUBLISH_KWARGS = (CONF_MESSAGE_EXPIRY_INTERVAL,)
@callback
def async_handle_schema_error(
@@ -1539,36 +1541,20 @@ class MqttEntity(
await MqttDiscoveryUpdateMixin.async_will_remove_from_hass(self)
debug_info.remove_entity_data(self.hass, self.entity_id)
async def async_publish(
self,
topic: str,
payload: PublishPayloadType,
qos: int = 0,
retain: bool = False,
encoding: str | None = DEFAULT_ENCODING,
) -> None:
"""Publish message to an MQTT topic."""
log_message(self.hass, self.entity_id, topic, payload, qos, retain)
await async_publish(
self.hass,
topic,
payload,
qos,
retain,
encoding,
)
async def async_publish_with_config(
self, topic: str, payload: PublishPayloadType
) -> None:
"""Publish payload to a topic using config."""
await self.async_publish(
topic,
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
kwargs: dict[str, Any] = {
key: value for key, value in self._config.items() if key in PUBLISH_KWARGS
}
qos: int = self._config[CONF_QOS]
retain: bool = self._config[CONF_RETAIN]
encoding: str = self._config[CONF_ENCODING]
log_message(
self.hass, self.entity_id, topic, payload, qos, retain, encoding, **kwargs
)
await async_publish(self.hass, topic, payload, qos, retain, encoding, **kwargs)
@staticmethod
@abstractmethod
+10
View File
@@ -509,10 +509,20 @@ class MqttComponentConfig:
discovery_payload: MQTTDiscoveryPayload
class MessageExpiryInterval(TypedDict, total=False):
"""Hold the Message Expiry Interval."""
days: float
hours: float
minutes: float
seconds: float
class DeviceMqttOptions(TypedDict, total=False):
"""Hold the shared MQTT specific options for an MQTT device."""
qos: int
message_expiry_interval: MessageExpiryInterval
class MqttDeviceData(TypedDict, total=False):
+12
View File
@@ -40,6 +40,7 @@ from .const import (
CONF_JSON_ATTRS_TEMPLATE,
CONF_JSON_ATTRS_TOPIC,
CONF_MANUFACTURER,
CONF_MESSAGE_EXPIRY_INTERVAL,
CONF_ORIGIN,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
@@ -66,6 +67,7 @@ SHARED_OPTIONS = [
CONF_AVAILABILITY_TOPIC,
CONF_COMMAND_TOPIC,
CONF_ENCODING,
CONF_MESSAGE_EXPIRY_INTERVAL,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_STATE_TOPIC,
@@ -161,6 +163,14 @@ MQTT_ORIGIN_INFO_SCHEMA = vol.All(
),
)
def valid_message_expiry_interval(value: Any) -> int:
"""Return Message Expiry Interval in seconds."""
if isinstance(value, int):
return cv.positive_int(value) # type: ignore[no-any-return]
return int(cv.positive_time_period_dict(value).total_seconds())
MQTT_ENTITY_COMMON_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
@@ -172,6 +182,7 @@ MQTT_ENTITY_COMMON_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template,
vol.Optional(CONF_DEFAULT_ENTITY_ID): cv.string,
vol.Optional(CONF_MESSAGE_EXPIRY_INTERVAL): valid_message_expiry_interval,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
@@ -203,6 +214,7 @@ DEVICE_DISCOVERY_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
vol.Required(CONF_ORIGIN): MQTT_ORIGIN_INFO_SCHEMA,
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_MESSAGE_EXPIRY_INTERVAL): valid_message_expiry_interval,
vol.Optional(CONF_QOS): valid_qos_schema,
vol.Optional(CONF_ENCODING): cv.string,
}
@@ -197,9 +197,11 @@
},
"mqtt_settings": {
"data": {
"message_expiry_interval": "Message Expiry Interval",
"qos": "QoS"
},
"data_description": {
"message_expiry_interval": "Retention time interval for published message.",
"qos": "The Quality of Service value the device's entities should use."
},
"name": "MQTT settings"
@@ -6,6 +6,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.const import ATTR_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -14,7 +15,6 @@ from .const import (
ATTR_DESCRIPTION,
ATTR_EXPIRES,
ATTR_HEADLINE,
ATTR_ID,
ATTR_RECOMMENDED_ACTIONS,
ATTR_SENDER,
ATTR_SENT,
-2
View File
@@ -29,8 +29,6 @@ ATTR_SEVERITY: str = "severity"
ATTR_RECOMMENDED_ACTIONS: str = "recommended_actions"
ATTR_AFFECTED_AREAS: str = "affected_areas"
ATTR_WEB: str = "web"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_ID: str = "id"
ATTR_SENT: str = "sent"
ATTR_START: str = "start"
ATTR_EXPIRES: str = "expires"
@@ -595,8 +595,8 @@ class OpenAIBaseLLMEntity(Entity):
)
)
if "reasoning" not in model_args:
# Reasoning models handle this correctly with just a prompt
if not model_args["model"].startswith("o"):
# o-series models handle this correctly with just a prompt
remove_citations = True
tools.append(web_search)
@@ -15,5 +15,5 @@
"iot_class": "local_push",
"loggers": ["opendisplay"],
"quality_scale": "silver",
"requirements": ["py-opendisplay==5.9.0"]
"requirements": ["py-opendisplay==7.2.3"]
}
@@ -218,7 +218,7 @@ async def _async_upload_image(call: ServiceCall) -> None:
pil_image,
refresh_mode=refresh_mode,
dither_mode=dither_mode,
tone_compression=tone_compression,
tone=tone_compression,
fit=fit_mode,
rotate=rotation,
)
@@ -37,11 +37,15 @@ class OpenhomeConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("async_step_ssdp: Incomplete discovery, ignoring")
return self.async_abort(reason="incomplete_discovery")
_LOGGER.debug(
"async_step_ssdp: setting unique id %s", discovery_info.upnp[ATTR_UPNP_UDN]
)
udn = discovery_info.upnp[ATTR_UPNP_UDN]
if isinstance(udn, list):
if not udn:
return self.async_abort(reason="incomplete_discovery")
udn = udn[0]
await self.async_set_unique_id(discovery_info.upnp[ATTR_UPNP_UDN])
_LOGGER.debug("async_step_ssdp: setting unique id %s", udn)
await self.async_set_unique_id(udn)
self._abort_if_unique_id_configured({CONF_HOST: discovery_info.ssdp_location})
_LOGGER.debug(
@@ -118,6 +118,9 @@
"services": {
"prune_images": {
"service": "mdi:delete-sweep"
},
"recreate_container": {
"service": "mdi:restart"
}
}
}
@@ -20,6 +20,9 @@ from .coordinator import PortainerConfigEntry
ATTR_DATE_UNTIL = "until"
ATTR_DANGLING = "dangling"
ATTR_TIMEOUT = "timeout"
ATTR_PULL_IMAGE = "pull_image"
ATTR_CONTAINER_DEVICE_ID = "container_device_id"
SERVICE_PRUNE_IMAGES = "prune_images"
SERVICE_PRUNE_IMAGES_SCHEMA = vol.Schema(
@@ -32,6 +35,17 @@ SERVICE_PRUNE_IMAGES_SCHEMA = vol.Schema(
},
)
SERVICE_RECREATE_CONTAINER = "recreate_container"
SERVICE_RECREATE_CONTAINER_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONTAINER_DEVICE_ID): cv.string,
vol.Optional(ATTR_TIMEOUT): vol.All(
cv.time_period, vol.Range(min=timedelta(minutes=1))
),
vol.Optional(ATTR_PULL_IMAGE): cv.boolean,
}
)
async def _extract_config_entry(service_call: ServiceCall) -> PortainerConfigEntry:
"""Extract config entry from the service call."""
@@ -75,6 +89,45 @@ async def _get_endpoint_id(
return endpoint_data.endpoint.id
async def _get_container_and_endpoint_ids(
call: ServiceCall,
) -> tuple[PortainerConfigEntry, int, str]:
"""Get config entry, endpoint ID and container ID from the container device ID."""
device_reg = dr.async_get(call.hass)
device = device_reg.async_get(call.data[ATTR_CONTAINER_DEVICE_ID])
if device is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_target",
)
config_entry: PortainerConfigEntry | None = None
for loaded_entry in call.hass.config_entries.async_loaded_entries(DOMAIN):
if loaded_entry.entry_id in device.config_entries:
config_entry = loaded_entry
break
if config_entry is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_target",
)
coordinator = config_entry.runtime_data
for data in coordinator.data.values():
for container_name, container_data in data.containers.items():
if (
DOMAIN,
f"{config_entry.entry_id}_{data.endpoint.id}_{container_name}",
) in device.identifiers:
return config_entry, data.endpoint.id, container_data.container.id
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_target",
)
async def prune_images(call: ServiceCall) -> None:
"""Prune unused images in Portainer, with more controls."""
config_entry = await _extract_config_entry(call)
@@ -104,6 +157,40 @@ async def prune_images(call: ServiceCall) -> None:
) from err
async def recreate_container(call: ServiceCall) -> None:
"""Recreate a container in Portainer, with more controls."""
config_entry, endpoint_id, container_id = await _get_container_and_endpoint_ids(
call
)
coordinator = config_entry.runtime_data
timeout: timedelta | None = call.data.get(ATTR_TIMEOUT)
try:
await coordinator.portainer.container_recreate(
endpoint_id=endpoint_id,
container_id=container_id,
**({"timeout": timeout} if timeout is not None else {}),
pull_image=call.data.get(ATTR_PULL_IMAGE, False),
)
except PortainerAuthenticationError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="invalid_auth_no_details",
) from err
except PortainerConnectionError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect_no_details",
) from err
except PortainerTimeoutError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="timeout_connect_no_details",
) from err
await coordinator.async_request_refresh()
async def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services."""
@@ -113,3 +200,10 @@ async def async_setup_services(hass: HomeAssistant) -> None:
prune_images,
SERVICE_PRUNE_IMAGES_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_RECREATE_CONTAINER,
recreate_container,
SERVICE_RECREATE_CONTAINER_SCHEMA,
)
@@ -16,3 +16,20 @@ prune_images:
required: false
selector:
boolean: {}
recreate_container:
fields:
container_device_id:
required: true
selector:
device:
integration: portainer
model: Container
timeout:
required: false
selector:
duration:
pull_image:
required: false
selector:
boolean:
@@ -235,6 +235,24 @@
}
},
"name": "Prune unused images"
},
"recreate_container": {
"description": "Recreates a container on a Portainer endpoint. This is more disruptive than a restart as the container will be stopped, removed, and then re-created with the same configuration. Use with caution.",
"fields": {
"container_device_id": {
"description": "The container to recreate.",
"name": "Container"
},
"pull_image": {
"description": "Whether to pull the image before recreating the container. This can be used to update the container to the latest version of the image.",
"name": "Pull image"
},
"timeout": {
"description": "The time to wait for the container to stop before killing it. If not provided, a default of 5 minutes will be used.",
"name": "Timeout"
}
},
"name": "Recreate container"
}
},
"system_health": {
+16 -16
View File
@@ -29,29 +29,29 @@
}
},
"sensor": {
"translation_key_0": {
"default": "mdi:abc"
"flow_sensor_clicks_cubic_meter": {
"default": "mdi:water-pump"
},
"translation_key_1": {
"default": "mdi:abc"
"flow_sensor_consumed_liters": {
"default": "mdi:water-pump"
},
"translation_key_2": {
"default": "mdi:abc"
"flow_sensor_leak_clicks": {
"default": "mdi:pipe-leak"
},
"translation_key_3": {
"default": "mdi:abc"
"flow_sensor_leak_volume": {
"default": "mdi:pipe-leak"
},
"translation_key_4": {
"default": "mdi:abc"
"flow_sensor_start_index": {
"default": "mdi:water-pump"
},
"translation_key_5": {
"default": "mdi:abc"
"flow_sensor_watering_clicks": {
"default": "mdi:water-pump"
},
"translation_key_6": {
"default": "mdi:abc"
"last_leak_detected": {
"default": "mdi:pipe-leak"
},
"translation_key_7": {
"default": "mdi:abc"
"rain_sensor_rain_start": {
"default": "mdi:weather-pouring"
}
},
"switch": {
@@ -20,5 +20,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.19.1"]
"requirements": ["reolink-aio==0.20.0"]
}
@@ -23,7 +23,7 @@ from homeassistant.config_entries import (
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_USERNAME
from homeassistant.const import CONF_REGION, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -38,7 +38,6 @@ from . import RoborockConfigEntry
from .const import (
CONF_BASE_URL,
CONF_ENTRY_CODE,
CONF_REGION,
CONF_SHOW_BACKGROUND,
CONF_SHOW_ROOMS,
CONF_SHOW_WALLS,
@@ -13,8 +13,6 @@ CONF_USER_DATA = "user_data"
CONF_SHOW_BACKGROUND = "show_background"
CONF_SHOW_WALLS = "show_walls"
CONF_SHOW_ROOMS = "show_rooms"
# pylint: disable-next=home-assistant-duplicate-const
CONF_REGION = "region"
REGION_OPTIONS = ["auto", "us", "eu", "ru", "cn"]
# Option Flow steps
+1 -2
View File
@@ -15,6 +15,7 @@ from homeassistant.components.sensor import (
SensorEntity,
)
from homeassistant.const import (
ATTR_MODEL,
CONF_MAC,
CONF_NAME,
EVENT_HOMEASSISTANT_STOP,
@@ -30,8 +31,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
ATTR_DEVICE = "device"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODEL = "model"
BLE_TEMP_HANDLE = 0x24
BLE_TEMP_UUID = "0000ff92-0000-1000-8000-00805f9b34fb"
+2 -2
View File
@@ -1,5 +1,7 @@
"""Define constants for the SleepIQ component."""
from homeassistant.const import PRESSURE
DATA_SLEEPIQ = "data_sleepiq"
DOMAIN = "sleepiq"
@@ -11,8 +13,6 @@ FIRMNESS = "firmness"
ICON_EMPTY = "mdi:bed-empty"
ICON_OCCUPIED = "mdi:bed"
IS_IN_BED = "is_in_bed"
# pylint: disable-next=home-assistant-duplicate-const
PRESSURE = "pressure"
SLEEP_NUMBER = "sleep_number"
FOOT_WARMING_TIMER = "foot_warming_timer"
FOOT_WARMER = "foot_warmer"
+1 -2
View File
@@ -11,14 +11,13 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfTime
from homeassistant.const import PRESSURE, UnitOfTime
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
HEART_RATE,
HRV,
PRESSURE,
RESPIRATORY_RATE,
SLEEP_DURATION,
SLEEP_NUMBER,
+1 -2
View File
@@ -7,6 +7,7 @@ import smarttub
import voluptuous as vol
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_MODE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -19,8 +20,6 @@ from .entity import SmartTubOnboardSensorBase
# the desired duration, in hours, of the cycle
ATTR_DURATION = "duration"
ATTR_CYCLE_LAST_UPDATED = "cycle_last_updated"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODE = "mode"
# the hour of the day at which to start the cycle (0-23)
ATTR_START_HOUR = "start_hour"
-4
View File
@@ -38,12 +38,8 @@ PLATFORMS = [
Platform.SENSOR,
]
# pylint: disable-next=home-assistant-duplicate-const
SERVICE_LOCK = "lock"
SERVICE_REMOTE_START = "remote_start"
SERVICE_REMOTE_STOP = "remote_stop"
# pylint: disable-next=home-assistant-duplicate-const
SERVICE_UNLOCK = "unlock"
SERVICE_UNLOCK_SPECIFIC_DOOR = "unlock_specific_door"
ATTR_DOOR = "door"
@@ -4,9 +4,10 @@ import logging
from subarulink.exceptions import SubaruException
from homeassistant.const import SERVICE_UNLOCK
from homeassistant.exceptions import HomeAssistantError
from .const import SERVICE_REMOTE_START, SERVICE_UNLOCK, VEHICLE_NAME, VEHICLE_VIN
from .const import SERVICE_REMOTE_START, VEHICLE_NAME, VEHICLE_VIN
_LOGGER = logging.getLogger(__name__)
@@ -7,14 +7,13 @@ from surepy.enums import Location
from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError
import voluptuous as vol
from homeassistant.const import Platform
from homeassistant.const import ATTR_LOCATION, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from .const import (
ATTR_FLAP_ID,
ATTR_LOCATION,
ATTR_LOCK_STATE,
ATTR_PET_NAME,
DOMAIN,
@@ -18,7 +18,5 @@ SURE_BATT_VOLTAGE_DIFF = SURE_BATT_VOLTAGE_FULL - SURE_BATT_VOLTAGE_LOW
SERVICE_SET_LOCK_STATE = "set_lock_state"
SERVICE_SET_PET_LOCATION = "set_pet_location"
ATTR_FLAP_ID = "flap_id"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LOCATION = "location"
ATTR_LOCK_STATE = "lock_state"
ATTR_PET_NAME = "pet_name"
@@ -8,7 +8,7 @@ from surepy.enums import EntityType, Location, LockState
from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from homeassistant.const import ATTR_LOCATION, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -16,7 +16,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import (
ATTR_FLAP_ID,
ATTR_LOCATION,
ATTR_LOCK_STATE,
ATTR_PET_NAME,
DOMAIN,
@@ -1,9 +1,7 @@
"""The System Bridge integration."""
import asyncio
from dataclasses import asdict
import logging
from typing import Any
from systembridgeconnector.exceptions import (
AuthenticationException,
@@ -11,71 +9,34 @@ from systembridgeconnector.exceptions import (
ConnectionErrorException,
DataMissingException,
)
from systembridgeconnector.models.keyboard_key import KeyboardKey
from systembridgeconnector.models.keyboard_text import KeyboardText
from systembridgeconnector.models.modules.processes import Process
from systembridgeconnector.models.open_path import OpenPath
from systembridgeconnector.models.open_url import OpenUrl
from systembridgeconnector.version import Version
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_COMMAND,
CONF_ENTITY_ID,
CONF_HOST,
CONF_ID,
CONF_NAME,
CONF_PATH,
CONF_PORT,
CONF_TOKEN,
CONF_URL,
Platform,
)
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
ServiceValidationError,
)
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
discovery,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType
from .config_flow import SystemBridgeConfigFlow
from .const import DATA_WAIT_TIMEOUT, DOMAIN, MODULES
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
def _get_coordinator(
hass: HomeAssistant, entry_id: str
) -> SystemBridgeDataUpdateCoordinator:
"""Return the coordinator for a config entry id."""
entry: SystemBridgeConfigEntry | None = hass.config_entries.async_get_entry(
entry_id
)
if entry is None or entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device": entry_id},
)
return entry.runtime_data
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.MEDIA_PLAYER,
@@ -84,26 +45,12 @@ PLATFORMS = [
Platform.UPDATE,
]
CONF_BRIDGE = "bridge"
CONF_KEY = "key"
CONF_TEXT = "text"
SERVICE_GET_PROCESS_BY_ID = "get_process_by_id"
SERVICE_GET_PROCESSES_BY_NAME = "get_processes_by_name"
SERVICE_OPEN_PATH = "open_path"
SERVICE_POWER_COMMAND = "power_command"
SERVICE_OPEN_URL = "open_url"
SERVICE_SEND_KEYPRESS = "send_keypress"
SERVICE_SEND_TEXT = "send_text"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the System Bridge services."""
POWER_COMMAND_MAP = {
"hibernate": "power_hibernate",
"lock": "power_lock",
"logout": "power_logout",
"restart": "power_restart",
"shutdown": "power_shutdown",
"sleep": "power_sleep",
}
async_setup_services(hass)
return True
async def async_setup_entry(
@@ -231,219 +178,6 @@ async def async_setup_entry(
)
)
if hass.services.has_service(DOMAIN, SERVICE_OPEN_URL):
return True
def valid_device(device: str) -> str:
"""Check device is valid."""
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get(device)
if device_entry is not None:
try:
return next(
entry.entry_id
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.entry_id in device_entry.config_entries
)
except StopIteration as exception:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device": device},
) from exception
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device": device},
)
async def handle_get_process_by_id(service_call: ServiceCall) -> ServiceResponse:
"""Handle the get process by id service call."""
_LOGGER.debug("Get process by id: %s", service_call.data)
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
processes: list[Process] = coordinator.data.processes
# Find process.id from list, raise ServiceValidationError if not found
try:
return asdict(
next(
process
for process in processes
if process.id == service_call.data[CONF_ID]
)
)
except StopIteration as exception:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="process_not_found",
translation_placeholders={"id": service_call.data[CONF_ID]},
) from exception
async def handle_get_processes_by_name(
service_call: ServiceCall,
) -> ServiceResponse:
"""Handle the get process by name service call."""
_LOGGER.debug("Get process by name: %s", service_call.data)
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
# Find processes from list
items: list[dict[str, Any]] = [
asdict(process)
for process in coordinator.data.processes
if process.name is not None
and service_call.data[CONF_NAME].lower() in process.name.lower()
]
return {
"count": len(items),
"processes": list(items),
}
async def handle_open_path(service_call: ServiceCall) -> ServiceResponse:
"""Handle the open path service call."""
_LOGGER.debug("Open path: %s", service_call.data)
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.open_path(
OpenPath(path=service_call.data[CONF_PATH])
)
return asdict(response)
async def handle_power_command(service_call: ServiceCall) -> ServiceResponse:
"""Handle the power command service call."""
_LOGGER.debug("Power command: %s", service_call.data)
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await getattr(
coordinator.websocket_client,
POWER_COMMAND_MAP[service_call.data[CONF_COMMAND]],
)()
return asdict(response)
async def handle_open_url(service_call: ServiceCall) -> ServiceResponse:
"""Handle the open url service call."""
_LOGGER.debug("Open URL: %s", service_call.data)
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.open_url(
OpenUrl(url=service_call.data[CONF_URL])
)
return asdict(response)
async def handle_send_keypress(service_call: ServiceCall) -> ServiceResponse:
"""Handle the send_keypress service call."""
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.keyboard_keypress(
KeyboardKey(key=service_call.data[CONF_KEY])
)
return asdict(response)
async def handle_send_text(service_call: ServiceCall) -> ServiceResponse:
"""Handle the send_keypress service call."""
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.keyboard_text(
KeyboardText(text=service_call.data[CONF_TEXT])
)
return asdict(response)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_GET_PROCESS_BY_ID,
handle_get_process_by_id,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_ID): cv.positive_int,
},
),
supports_response=SupportsResponse.ONLY,
)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_GET_PROCESSES_BY_NAME,
handle_get_processes_by_name,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_NAME): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_OPEN_PATH,
handle_open_path,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_PATH): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_POWER_COMMAND,
handle_power_command,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_COMMAND): vol.In(POWER_COMMAND_MAP),
},
),
supports_response=SupportsResponse.ONLY,
)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_OPEN_URL,
handle_open_url,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_URL): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_SEND_KEYPRESS,
handle_send_keypress,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_KEY): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
description_placeholders={
"syntax_keys_documentation_url": "http://robotjs.io/docs/syntax#keys"
},
)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_SEND_TEXT,
handle_send_text,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_TEXT): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
# Reload entry when its updated.
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
@@ -454,9 +188,7 @@ async def async_unload_entry(
hass: HomeAssistant, entry: SystemBridgeConfigEntry
) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
coordinator = entry.runtime_data
@@ -17,15 +17,51 @@
"boot_time": {
"default": "mdi:av-timer"
},
"cpu_power_core": {
"default": "mdi:chip"
},
"cpu_power_package": {
"default": "mdi:chip"
},
"cpu_speed": {
"default": "mdi:speedometer"
},
"display_refresh_rate": {
"default": "mdi:monitor"
},
"display_resolution_x": {
"default": "mdi:monitor"
},
"display_resolution_y": {
"default": "mdi:monitor"
},
"displays_connected": {
"default": "mdi:monitor"
},
"gpu_core_clock_speed": {
"default": "mdi:speedometer"
},
"gpu_fan_speed": {
"default": "mdi:fan"
},
"gpu_memory_clock_speed": {
"default": "mdi:speedometer"
},
"gpu_memory_free": {
"default": "mdi:memory"
},
"gpu_memory_used": {
"default": "mdi:memory"
},
"gpu_memory_used_percentage": {
"default": "mdi:memory"
},
"gpu_power_usage": {
"default": "mdi:lightning-bolt"
},
"gpu_usage_percentage": {
"default": "mdi:percent"
},
"kernel": {
"default": "mdi:devices"
},
@@ -38,6 +74,9 @@
"memory_used": {
"default": "mdi:memory"
},
"memory_used_percentage": {
"default": "mdi:memory"
},
"os": {
"default": "mdi:devices"
},
@@ -47,6 +86,12 @@
"processes": {
"default": "mdi:counter"
},
"processes_load_cpu": {
"default": "mdi:percent"
},
"space_used": {
"default": "mdi:harddisk"
},
"version": {
"default": "mdi:counter"
},
@@ -27,7 +27,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import UNDEFINED, StateType
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
@@ -284,10 +284,10 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
),
SystemBridgeSensorEntityDescription(
key="memory_used_percentage",
translation_key="memory_used_percentage",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
icon="mdi:memory",
value=lambda data: data.memory.virtual.percent,
),
SystemBridgeSensorEntityDescription(
@@ -380,11 +380,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"filesystem_{partition.mount_point.replace(':', '')}",
name=f"{partition.mount_point} space used",
translation_key="space_used",
translation_placeholders={"partition": partition.mount_point},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
icon="mdi:harddisk",
value=(
lambda data, dk=index_device, pk=index_partition: (
partition_usage(data, dk, pk)
@@ -427,10 +427,10 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"display_{display.id}_resolution_x",
name=f"Display {display.id} resolution x",
translation_key="display_resolution_x",
translation_placeholders={"display_id": display.id},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PIXELS,
icon="mdi:monitor",
value=lambda data, k=index: display_resolution_horizontal(
data, k
),
@@ -441,10 +441,10 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"display_{display.id}_resolution_y",
name=f"Display {display.id} resolution y",
translation_key="display_resolution_y",
translation_placeholders={"display_id": display.id},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PIXELS,
icon="mdi:monitor",
value=lambda data, k=index: display_resolution_vertical(
data, k
),
@@ -455,12 +455,12 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"display_{display.id}_refresh_rate",
name=f"Display {display.id} refresh rate",
translation_key="display_refresh_rate",
translation_placeholders={"display_id": display.id},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=0,
icon="mdi:monitor",
value=lambda data, k=index: display_refresh_rate(data, k),
),
entry.data[CONF_PORT],
@@ -474,13 +474,13 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_core_clock_speed",
name=f"{gpu.name} clock speed",
translation_key="gpu_core_clock_speed",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=0,
icon="mdi:speedometer",
value=lambda data, k=index: gpu_core_clock_speed(data, k),
),
entry.data[CONF_PORT],
@@ -489,13 +489,13 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_memory_clock_speed",
name=f"{gpu.name} memory clock speed",
translation_key="gpu_memory_clock_speed",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=0,
icon="mdi:speedometer",
value=lambda data, k=index: gpu_memory_clock_speed(data, k),
),
entry.data[CONF_PORT],
@@ -504,12 +504,12 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_memory_free",
name=f"{gpu.name} memory free",
translation_key="gpu_memory_free",
translation_placeholders={"gpu_name": gpu.name},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfInformation.MEGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
suggested_display_precision=0,
icon="mdi:memory",
value=lambda data, k=index: gpu_memory_free(data, k),
),
entry.data[CONF_PORT],
@@ -518,11 +518,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_memory_used_percentage",
name=f"{gpu.name} memory used %",
translation_key="gpu_memory_used_percentage",
translation_placeholders={"gpu_name": gpu.name},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
icon="mdi:memory",
value=lambda data, k=index: gpu_memory_used_percentage(data, k),
),
entry.data[CONF_PORT],
@@ -531,13 +531,13 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_memory_used",
name=f"{gpu.name} memory used",
translation_key="gpu_memory_used",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfInformation.MEGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
suggested_display_precision=0,
icon="mdi:memory",
value=lambda data, k=index: gpu_memory_used(data, k),
),
entry.data[CONF_PORT],
@@ -546,11 +546,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_fan_speed",
name=f"{gpu.name} fan speed",
translation_key="gpu_fan_speed",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
icon="mdi:fan",
value=lambda data, k=index: gpu_fan_speed(data, k),
),
entry.data[CONF_PORT],
@@ -559,7 +559,8 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_power_usage",
name=f"{gpu.name} power usage",
translation_key="gpu_power_usage",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
@@ -571,7 +572,8 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_temperature",
name=f"{gpu.name} temperature",
translation_key="gpu_temperature",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
@@ -585,11 +587,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_usage_percentage",
name=f"{gpu.name} usage %",
translation_key="gpu_usage_percentage",
translation_placeholders={"gpu_name": gpu.name},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
icon="mdi:percent",
value=lambda data, k=index: gpu_usage_percentage(data, k),
),
entry.data[CONF_PORT],
@@ -605,11 +607,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"processes_load_cpu_{cpu.id}",
name=f"Load CPU {cpu.id}",
translation_key="processes_load_cpu",
translation_placeholders={"cpu_id": str(cpu.id)},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
icon="mdi:percent",
suggested_display_precision=2,
value=lambda data, k=cpu.id: cpu_usage_per_cpu(data, k),
),
@@ -619,11 +621,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"cpu_power_core_{cpu.id}",
name=f"CPU Core {cpu.id} Power",
translation_key="cpu_power_core",
translation_placeholders={"cpu_id": str(cpu.id)},
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:chip",
suggested_display_precision=2,
value=lambda data, k=cpu.id: cpu_power_per_cpu(data, k),
),
@@ -653,8 +655,6 @@ class SystemBridgeSensor(SystemBridgeEntity, SensorEntity):
description.key,
)
self.entity_description = description
if description.name is not UNDEFINED:
self._attr_has_entity_name = False
@property
def native_value(self) -> StateType:
@@ -0,0 +1,269 @@
"""Service registration for System Bridge integration."""
from dataclasses import asdict
import logging
from typing import Any
from systembridgeconnector.models.keyboard_key import KeyboardKey
from systembridgeconnector.models.keyboard_text import KeyboardText
from systembridgeconnector.models.modules.processes import Process
from systembridgeconnector.models.open_path import OpenPath
from systembridgeconnector.models.open_url import OpenUrl
import voluptuous as vol
from homeassistant.const import CONF_COMMAND, CONF_ID, CONF_NAME, CONF_PATH, CONF_URL
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
service,
)
from .const import DOMAIN
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
CONF_BRIDGE = "bridge"
CONF_KEY = "key"
CONF_TEXT = "text"
POWER_COMMAND_MAP = {
"hibernate": "power_hibernate",
"lock": "power_lock",
"logout": "power_logout",
"restart": "power_restart",
"shutdown": "power_shutdown",
"sleep": "power_sleep",
}
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services for System Bridge integration."""
hass.services.async_register(
DOMAIN,
"get_process_by_id",
handle_get_process_by_id,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_ID): cv.positive_int,
},
),
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
"get_processes_by_name",
handle_get_processes_by_name,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_NAME): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
"open_path",
handle_open_path,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_PATH): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
"power_command",
handle_power_command,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_COMMAND): vol.In(POWER_COMMAND_MAP),
},
),
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
"open_url",
handle_open_url,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_URL): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
"send_keypress",
handle_send_keypress,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_KEY): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
description_placeholders={
"syntax_keys_documentation_url": "https://robotjs.dev/docs/syntax#keys"
},
)
hass.services.async_register(
DOMAIN,
"send_text",
handle_send_text,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_TEXT): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
def _get_coordinator(
hass: HomeAssistant, device_id: str
) -> SystemBridgeDataUpdateCoordinator:
"""Return the coordinator for a device id."""
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get(device_id)
if device_entry is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device": device_id},
)
try:
entry_id = next(
entry.entry_id
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.entry_id in device_entry.config_entries
)
except StopIteration as e:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device": device_id},
) from e
entry: SystemBridgeConfigEntry = service.async_get_config_entry(
hass, DOMAIN, entry_id
)
return entry.runtime_data
async def handle_get_process_by_id(service_call: ServiceCall) -> ServiceResponse:
"""Handle the get process by id service call."""
_LOGGER.debug("Get process by id: %s", service_call.data)
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
processes: list[Process] = coordinator.data.processes
# Find process.id from list, raise ServiceValidationError if not found
try:
return asdict(
next(
process
for process in processes
if process.id == service_call.data[CONF_ID]
)
)
except StopIteration as e:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="process_not_found",
translation_placeholders={"id": service_call.data[CONF_ID]},
) from e
async def handle_get_processes_by_name(
service_call: ServiceCall,
) -> ServiceResponse:
"""Handle the get process by name service call."""
_LOGGER.debug("Get process by name: %s", service_call.data)
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
# Find processes from list
items: list[dict[str, Any]] = [
asdict(process)
for process in coordinator.data.processes
if process.name is not None
and service_call.data[CONF_NAME].lower() in process.name.lower()
]
return {
"count": len(items),
"processes": list(items),
}
async def handle_open_path(service_call: ServiceCall) -> ServiceResponse:
"""Handle the open path service call."""
_LOGGER.debug("Open path: %s", service_call.data)
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.open_path(
OpenPath(path=service_call.data[CONF_PATH])
)
return asdict(response)
async def handle_power_command(service_call: ServiceCall) -> ServiceResponse:
"""Handle the power command service call."""
_LOGGER.debug("Power command: %s", service_call.data)
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
response = await getattr(
coordinator.websocket_client,
POWER_COMMAND_MAP[service_call.data[CONF_COMMAND]],
)()
return asdict(response)
async def handle_open_url(service_call: ServiceCall) -> ServiceResponse:
"""Handle the open url service call."""
_LOGGER.debug("Open URL: %s", service_call.data)
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.open_url(
OpenUrl(url=service_call.data[CONF_URL])
)
return asdict(response)
async def handle_send_keypress(service_call: ServiceCall) -> ServiceResponse:
"""Handle the send_keypress service call."""
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.keyboard_keypress(
KeyboardKey(key=service_call.data[CONF_KEY])
)
return asdict(response)
async def handle_send_text(service_call: ServiceCall) -> ServiceResponse:
"""Handle the send_text service call."""
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.keyboard_text(
KeyboardText(text=service_call.data[CONF_TEXT])
)
return asdict(response)
@@ -89,3 +89,4 @@ power_command:
- "restart"
- "shutdown"
- "sleep"
translation_key: "power_command"
@@ -54,6 +54,9 @@
"boot_time": {
"name": "Boot time"
},
"cpu_power_core": {
"name": "CPU core {cpu_id} power"
},
"cpu_power_package": {
"name": "CPU package power"
},
@@ -66,9 +69,45 @@
"cpu_voltage": {
"name": "CPU voltage"
},
"display_refresh_rate": {
"name": "Display {display_id} refresh rate"
},
"display_resolution_x": {
"name": "Display {display_id} resolution x"
},
"display_resolution_y": {
"name": "Display {display_id} resolution y"
},
"displays_connected": {
"name": "Displays connected"
},
"gpu_core_clock_speed": {
"name": "{gpu_name} clock speed"
},
"gpu_fan_speed": {
"name": "{gpu_name} fan speed"
},
"gpu_memory_clock_speed": {
"name": "{gpu_name} memory clock speed"
},
"gpu_memory_free": {
"name": "{gpu_name} memory free"
},
"gpu_memory_used": {
"name": "{gpu_name} memory used"
},
"gpu_memory_used_percentage": {
"name": "{gpu_name} memory used %"
},
"gpu_power_usage": {
"name": "{gpu_name} power usage"
},
"gpu_temperature": {
"name": "{gpu_name} temperature"
},
"gpu_usage_percentage": {
"name": "{gpu_name} usage %"
},
"kernel": {
"name": "Kernel"
},
@@ -81,6 +120,9 @@
"memory_used": {
"name": "Memory used"
},
"memory_used_percentage": {
"name": "Memory used %"
},
"os": {
"name": "Operating system"
},
@@ -90,6 +132,12 @@
"processes": {
"name": "Processes"
},
"processes_load_cpu": {
"name": "Load CPU {cpu_id}"
},
"space_used": {
"name": "{partition} space used"
},
"version": {
"name": "Version"
},
@@ -130,6 +178,18 @@
"title": "System Bridge upgrade required"
}
},
"selector": {
"power_command": {
"options": {
"hibernate": "Hibernate",
"lock": "Lock",
"logout": "Logout",
"restart": "[%key:common::action::restart%]",
"shutdown": "Shutdown",
"sleep": "Sleep"
}
}
},
"services": {
"get_process_by_id": {
"description": "Gets a process by the ID.",
@@ -32,6 +32,7 @@ class SystemBridgeUpdateEntity(SystemBridgeEntity, UpdateEntity):
_attr_has_entity_name = True
_attr_title = "System Bridge"
_attr_name = None
def __init__(
self,
@@ -44,7 +45,6 @@ class SystemBridgeUpdateEntity(SystemBridgeEntity, UpdateEntity):
api_port,
"update",
)
self._attr_name = coordinator.data.system.hostname
@property
def installed_version(self) -> str | None:
@@ -58,7 +58,7 @@ send_message:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -101,7 +101,7 @@ send_chat_action:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -195,7 +195,7 @@ send_photo:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -287,7 +287,7 @@ send_media_group:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -372,7 +372,7 @@ send_sticker:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -466,7 +466,7 @@ send_animation:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -560,7 +560,7 @@ send_video:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -645,7 +645,7 @@ send_voice:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -739,7 +739,7 @@ send_document:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -804,7 +804,7 @@ send_location:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -861,7 +861,7 @@ send_poll:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -913,7 +913,7 @@ edit_message:
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
selector:
object:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -991,7 +991,7 @@ edit_message_media:
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
selector:
object:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1028,7 +1028,7 @@ edit_caption:
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
selector:
object:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1061,7 +1061,7 @@ edit_replymarkup:
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
selector:
object:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1108,7 +1108,7 @@ delete_message:
example: "{{ trigger.event.data.message.message_id }}"
selector:
text:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1129,7 +1129,7 @@ leave_chat:
filter:
domain: notify
integration: telegram_bot
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1164,7 +1164,7 @@ set_message_reaction:
required: false
selector:
boolean:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1233,7 +1233,7 @@ send_message_draft:
- "markdownv2"
- "plain_text"
translation_key: "parse_mode"
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -367,8 +367,8 @@
},
"name": "Delete message",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -425,8 +425,8 @@
},
"name": "Edit caption",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -472,8 +472,8 @@
},
"name": "Edit message",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -535,8 +535,8 @@
},
"name": "Edit message media",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -569,8 +569,8 @@
},
"name": "Edit reply markup",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -592,8 +592,8 @@
},
"name": "Leave chat",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -671,8 +671,8 @@
},
"name": "Send animation",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -705,8 +705,8 @@
},
"name": "Send chat action",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -784,8 +784,8 @@
},
"name": "Send document",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -842,8 +842,8 @@
},
"name": "Send location",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -889,8 +889,8 @@
},
"name": "Send media group",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -952,8 +952,8 @@
},
"name": "Send message",
"sections": {
"advanced": {
"name": "Advanced"
"additional_fields": {
"name": "Additional options"
}
}
},
@@ -991,8 +991,8 @@
},
"name": "Send message draft",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -1070,8 +1070,8 @@
},
"name": "Send photo",
"sections": {
"advanced": {
"name": "Advanced"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "URL options"
@@ -1128,8 +1128,8 @@
},
"name": "Send poll",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -1203,8 +1203,8 @@
},
"name": "Send sticker",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -1285,8 +1285,8 @@
},
"name": "Send video",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -1363,8 +1363,8 @@
},
"name": "Send voice",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -1401,8 +1401,8 @@
},
"name": "Set message reaction",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
}
@@ -497,7 +497,7 @@
"default": "mdi:battery-clock"
},
"forward_collision_warning": {
"default": "mdi:car-crash",
"default": "mdi:car-emergency",
"state": {
"average": "mdi:alert-circle",
"early": "mdi:alert-octagon",
@@ -634,7 +634,7 @@
"default": "mdi:key"
},
"pedal_position": {
"default": "mdi:pedestal"
"default": "mdi:gauge"
},
"powershare_hours_left": {
"default": "mdi:clock-time-eight-outline"
@@ -794,7 +794,7 @@
"service": "mdi:calendar-plus"
},
"add_precondition_schedule": {
"service": "mdi:hvac-outline"
"service": "mdi:hvac"
},
"navigation_gps_request": {
"service": "mdi:crosshairs-gps"
@@ -803,7 +803,7 @@
"service": "mdi:calendar-minus"
},
"remove_precondition_schedule": {
"service": "mdi:hvac-off-outline"
"service": "mdi:hvac-off"
},
"set_scheduled_charging": {
"service": "mdi:timeline-clock-outline"
@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["uiprotect"],
"quality_scale": "platinum",
"requirements": ["uiprotect==10.4.1"]
"requirements": ["uiprotect==10.5.0"]
}
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import PLATFORMS
from .const import DOMAIN, PLATFORMS
from .coordinator import UptimeRobotConfigEntry, UptimeRobotDataUpdateCoordinator
@@ -15,9 +15,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: UptimeRobotConfigEntry)
"""Set up UptimeRobot from a config entry."""
key: str = entry.data[CONF_API_KEY]
if key.startswith(("ur", "m")):
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed(
"Wrong API key type detected, use the 'main' API key"
translation_domain=DOMAIN,
translation_key="api_key_wrong_type",
)
uptime_robot_api = UptimeRobot(key, async_get_clientsession(hass))
@@ -48,11 +48,16 @@ class UptimeRobotDataUpdateCoordinator(
try:
response = await self.api.async_get_monitors()
except UptimeRobotAuthenticationException as exception:
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed(exception) from exception
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="api_authentication_exception",
) from exception
except UptimeRobotException as exception:
# pylint: disable-next=home-assistant-exception-not-translated
raise UpdateFailed(exception) from exception
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="api_generic_exception",
translation_placeholders={"error": "Generic UptimeRobot exception"},
) from exception
if TYPE_CHECKING:
assert isinstance(response.data, list)
@@ -57,7 +57,16 @@
}
},
"exceptions": {
"api_exception": {
"api_authentication_exception": {
"message": "API authentication failed, please check your API key"
},
"api_generic_exception": {
"message": "API error: {error}"
},
"api_key_wrong_type": {
"message": "Wrong API key type detected, use the 'main' API key"
},
"api_switch_exception": {
"message": "Could not turn on/off monitoring: {error}"
}
}
@@ -33,7 +33,7 @@ def uptimerobot_api_call[_T: UptimeRobotEntity, **_P](
except UptimeRobotException as exception:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_exception",
translation_key="api_switch_exception",
translation_placeholders={"error": "Generic UptimeRobot exception"},
) from exception
@@ -68,7 +68,7 @@
"state": {
"lightning": "mdi:weather-lightning-rainy",
"rain": "mdi:weather-rainy",
"rain_snow": "mdi:weather-snoy-rainy",
"rain_snow": "mdi:weather-snowy-rainy",
"snow": "mdi:weather-snowy"
}
},
+4 -2
View File
@@ -46,8 +46,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: WebOsTvConfigEntry) -> b
try:
await client.connect()
except WebOsTvPairError as err:
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed(err) from err
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_failed",
) from err
# If pairing request accepted there will be no error
# Update the stored key without triggering reauth
@@ -6,6 +6,7 @@ from homeassistant.components.device_automation import (
DEVICE_TRIGGER_BASE_SCHEMA,
InvalidDeviceAutomationConfig,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_DEVICE_ID, CONF_PLATFORM, CONF_TYPE
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -13,10 +14,7 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from . import DOMAIN, trigger
from .helpers import (
async_get_client_by_device_entry,
async_get_device_entry_by_device_id,
)
from .helpers import async_get_device_entry_by_device_id
from .triggers.turn_on import (
PLATFORM_TYPE as TURN_ON_PLATFORM_TYPE,
async_get_turn_on_trigger,
@@ -40,10 +38,31 @@ async def async_validate_trigger_config(
device_id = config[CONF_DEVICE_ID]
try:
device = async_get_device_entry_by_device_id(hass, device_id)
async_get_client_by_device_entry(hass, device)
except ValueError as err:
# pylint: disable-next=home-assistant-exception-not-translated
raise InvalidDeviceAutomationConfig(err) from err
raise InvalidDeviceAutomationConfig(
translation_domain=DOMAIN,
translation_key="device_not_valid",
translation_placeholders={"device_id": device_id},
) from err
for config_entry_id in device.config_entries:
if (
entry := hass.config_entries.async_get_entry(config_entry_id)
) and entry.domain == DOMAIN:
if entry.state is ConfigEntryState.LOADED:
break
raise InvalidDeviceAutomationConfig(
translation_domain=DOMAIN,
translation_key="device_config_entry_not_loaded",
translation_placeholders={"device_id": device.id},
)
else:
raise InvalidDeviceAutomationConfig(
translation_domain=DOMAIN,
translation_key="device_not_valid",
translation_placeholders={"device_id": device.id},
)
return config
+1 -26
View File
@@ -4,7 +4,7 @@ import logging
from aiowebostv import WebOsClient, WebOsTvState
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -56,31 +56,6 @@ def async_get_device_id_from_entity_id(hass: HomeAssistant, entity_id: str) -> s
return entity_entry.device_id
@callback
def async_get_client_by_device_entry(
hass: HomeAssistant, device: DeviceEntry
) -> WebOsClient:
"""Get WebOsClient from Device Registry by device entry.
Raises ValueError if client is not found.
"""
for config_entry_id in device.config_entries:
entry: WebOsTvConfigEntry | None = hass.config_entries.async_get_entry(
config_entry_id
)
if entry and entry.domain == DOMAIN:
if entry.state is ConfigEntryState.LOADED:
return entry.runtime_data
raise ValueError(
f"Device {device.id} is not from a loaded {DOMAIN} config entry"
)
raise ValueError(
f"Device {device.id} is not from an existing {DOMAIN} config entry"
)
def get_sources(tv_state: WebOsTvState) -> list[str]:
"""Construct sources list."""
sources = []
@@ -46,9 +46,18 @@
}
},
"exceptions": {
"auth_failed": {
"message": "Pairing failed, make sure to accept the pairing request on your TV."
},
"communication_error": {
"message": "Communication error while calling {func} for device {name}: {error}"
},
"device_config_entry_not_loaded": {
"message": "The LG webOS TV integration for device {device_id} is not loaded."
},
"device_not_valid": {
"message": "Device {device_id} is not a valid LG webOS TV device."
},
"device_off": {
"message": "Error calling {func} for device {name}: Device is off and cannot be controlled."
},
@@ -28,7 +28,7 @@ from miio.integrations.humidifier.zhimi.airhumidifier_miot import (
)
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import CONF_DEVICE, CONF_MODEL, EntityCategory
from homeassistant.const import ATTR_MODE, CONF_DEVICE, CONF_MODEL, EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@@ -65,8 +65,6 @@ from .typing import XiaomiMiioConfigEntry
ATTR_DISPLAY_ORIENTATION = "display_orientation"
ATTR_LED_BRIGHTNESS = "led_brightness"
ATTR_PTC_LEVEL = "ptc_level"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODE = "mode"
_LOGGER = logging.getLogger(__name__)
@@ -25,6 +25,7 @@ from homeassistant.components.switch import (
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
ATTR_MODEL,
ATTR_TEMPERATURE,
CONF_DEVICE,
CONF_HOST,
@@ -149,8 +150,6 @@ ATTR_LED = "led"
ATTR_IONIZER = "ionizer"
ATTR_ANION = "anion"
ATTR_LOAD_POWER = "load_power"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODEL = "model"
ATTR_POWER = "power"
ATTR_POWER_MODE = "power_mode"
ATTR_POWER_PRICE = "power_price"
@@ -21,4 +21,4 @@ CONF_INSTANCE_ID = "instance_id"
# Polling interval (seconds)
DEFAULT_SCAN_INTERVAL = 1800
PLATFORMS: list[Platform] = [Platform.LIGHT, Platform.SWITCH]
PLATFORMS: list[Platform] = [Platform.LIGHT, Platform.LOCK, Platform.SWITCH]
@@ -0,0 +1,47 @@
"""Lock platform for Xthings Cloud."""
from typing import Any
from homeassistant.components.lock import LockEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import XthingsCloudConfigEntry
from .entity import XthingsCloudEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: XthingsCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up lock platform."""
coordinator = entry.runtime_data
entities = [
XthingsCloudLock(coordinator, device_id, device_data)
for device_id, device_data in coordinator.data.items()
if device_data["type"] == "lock"
]
async_add_entities(entities)
class XthingsCloudLock(XthingsCloudEntity, LockEntity):
"""Xthings Cloud lock entity."""
@property
def is_locked(self) -> bool | None:
"""Return true if lock is locked."""
return self.device_data["status"].get("locked")
@property
def is_jammed(self) -> bool | None:
"""Return true if lock is jammed."""
return self.device_data["status"].get("jammed")
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
await self.coordinator.client.async_lock_lock(self._device_id)
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
await self.coordinator.client.async_lock_unlock(self._device_id)

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