mirror of
https://github.com/home-assistant/core.git
synced 2026-05-24 09:45:13 +02:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21664b933a | |||
| 814ec697cf | |||
| 120f1446d4 | |||
| 170af75b7d | |||
| 5432d29489 | |||
| 8098f4f6bc | |||
| 6a70077687 | |||
| 5dbb0464ba | |||
| 1df165ea02 | |||
| 62542eb911 | |||
| a842cac34c | |||
| 2460f688e3 | |||
| a868ea443c | |||
| 1d8565483b | |||
| 1ef3301253 | |||
| 525952f016 | |||
| 3257275c5a | |||
| cb54fd4921 | |||
| b391fc61ea | |||
| fcd4e4939c | |||
| deb8b5da05 | |||
| c7754a6ce9 | |||
| 242724bd50 | |||
| 42454563db | |||
| bf03d0c216 | |||
| 568107e06b | |||
| 7da44428b6 | |||
| 0a27f31949 | |||
| 905b868c82 | |||
| 3187289913 | |||
| 87cecd4a44 | |||
| fed38b0e38 | |||
| 6a36d1260b | |||
| 49fc1b413d | |||
| bffb0417cc | |||
| 8b8c687fc3 |
@@ -1,52 +0,0 @@
|
||||
name: Cache and install APT packages
|
||||
description: >-
|
||||
Wraps awalsh128/cache-apt-pkgs-action with the workarounds Home Assistant CI
|
||||
needs. Removes the conflicting Microsoft apt source before any apt run, and
|
||||
points the dynamic linker at the host's multiarch lib subdirectories so
|
||||
shared libraries that rely on update-alternatives or postinst-managed paths
|
||||
(eg libblas, liblapack pulled in by ffmpeg) stay reachable since the upstream
|
||||
action does not execute postinst scripts on cache restore.
|
||||
|
||||
inputs:
|
||||
packages:
|
||||
description: Space-delimited list of apt packages to install.
|
||||
required: true
|
||||
version:
|
||||
description: Cache version. Bump to invalidate the cache.
|
||||
required: false
|
||||
default: "1"
|
||||
execute_install_scripts:
|
||||
description: >-
|
||||
Pass-through to awalsh128/cache-apt-pkgs-action. Postinst scripts are not
|
||||
actually cached by the upstream action, so this is largely a no-op today.
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Remove conflicting Microsoft apt source
|
||||
shell: bash
|
||||
run: sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
|
||||
- name: Install apt packages via cache
|
||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
|
||||
with:
|
||||
packages: ${{ inputs.packages }}
|
||||
version: ${{ inputs.version }}
|
||||
execute_install_scripts: ${{ inputs.execute_install_scripts }}
|
||||
- name: Refresh dynamic linker cache
|
||||
shell: bash
|
||||
run: |
|
||||
# awalsh128/cache-apt-pkgs-action does not run postinst scripts on
|
||||
# cache restore, so update-alternatives symlinks (eg the one libblas
|
||||
# creates at /usr/lib/<multiarch>/libblas.so.3) are never produced.
|
||||
# Add every /usr/lib/<multiarch> subdirectory that holds shared
|
||||
# libraries to the ldconfig search path so the dynamic linker still
|
||||
# finds them. Use dpkg-architecture to derive the host's multiarch
|
||||
# tuple so this works on non-x86_64 runners too.
|
||||
multiarch="$(dpkg-architecture -qDEB_HOST_MULTIARCH)"
|
||||
find "/usr/lib/${multiarch}" -mindepth 2 -maxdepth 2 \
|
||||
-name '*.so.*' -printf '%h\n' \
|
||||
| sort -u \
|
||||
| sudo tee /etc/ld.so.conf.d/zzz-cache-apt-extras.conf > /dev/null
|
||||
sudo ldconfig
|
||||
@@ -1,42 +0,0 @@
|
||||
name: Set up uv and managed Python
|
||||
description: >-
|
||||
Pins uv (avoids the raw.githubusercontent.com manifest fetch on cache miss)
|
||||
and proactively installs the requested Python so cached venvs created with
|
||||
`uv venv` resolve their interpreter symlinks in jobs that only restore the
|
||||
venv. setup-uv alone only sets UV_PYTHON, it does not actually fetch the
|
||||
interpreter until uv first uses it, so jobs that just activate the venv
|
||||
blow up with broken symlinks on cache hit.
|
||||
|
||||
inputs:
|
||||
python-version:
|
||||
description: The Python version uv should install and use.
|
||||
required: true
|
||||
uv-version:
|
||||
description: The uv version setup-uv should install.
|
||||
required: true
|
||||
|
||||
outputs:
|
||||
python-version:
|
||||
description: The Python version uv reports as installed.
|
||||
value: ${{ steps.uv.outputs.python-version }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set up uv
|
||||
id: uv
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
version: ${{ inputs.uv-version }}
|
||||
python-version: ${{ inputs.python-version }}
|
||||
# Persist astral's managed Python across jobs so 'uv venv' below is
|
||||
# fast on the second job onwards.
|
||||
cache-python: true
|
||||
# Lint-only and codegen jobs touch no Python deps, so the post-step
|
||||
# cache save would otherwise abort the job.
|
||||
ignore-nothing-to-cache: true
|
||||
- name: Install Python interpreter
|
||||
shell: bash
|
||||
env:
|
||||
PYTHON_VERSION: ${{ inputs.python-version }}
|
||||
run: uv python install "${PYTHON_VERSION}"
|
||||
@@ -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
|
||||
|
||||
|
||||
+243
-135
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 4
|
||||
CACHE_VERSION: 3
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.6"
|
||||
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
||||
@@ -60,7 +60,9 @@ env:
|
||||
# - 15.2 is the latest (as of 9 Feb 2023)
|
||||
POSTGRESQL_VERSIONS: "['postgres:12.14','postgres:15.2']"
|
||||
UV_CACHE_DIR: /tmp/uv-cache
|
||||
APT_CACHE_VERSION: 1
|
||||
APT_CACHE_BASE: /home/runner/work/apt
|
||||
APT_CACHE_DIR: /home/runner/work/apt/cache
|
||||
APT_LIST_CACHE_DIR: /home/runner/work/apt/lists
|
||||
SQLALCHEMY_WARN_20: 1
|
||||
PYTHONASYNCIODEBUG: 1
|
||||
HASS_CI: 1
|
||||
@@ -84,13 +86,12 @@ jobs:
|
||||
core: ${{ steps.core.outputs.changes }}
|
||||
integrations_glob: ${{ steps.info.outputs.integrations_glob }}
|
||||
integrations: ${{ steps.integrations.outputs.changes }}
|
||||
apt_cache_key: ${{ steps.generate_apt_cache_key.outputs.key }}
|
||||
python_cache_key: ${{ steps.generate_python_cache_key.outputs.key }}
|
||||
requirements: ${{ steps.core.outputs.requirements }}
|
||||
mariadb_groups: ${{ steps.info.outputs.mariadb_groups }}
|
||||
postgresql_groups: ${{ steps.info.outputs.postgresql_groups }}
|
||||
python_versions: ${{ steps.info.outputs.python_versions }}
|
||||
default_python: ${{ steps.info.outputs.default_python }}
|
||||
uv_version: ${{ steps.info.outputs.uv_version }}
|
||||
test_full_suite: ${{ steps.info.outputs.test_full_suite }}
|
||||
test_group_count: ${{ steps.info.outputs.test_group_count }}
|
||||
test_groups: ${{ steps.info.outputs.test_groups }}
|
||||
@@ -115,6 +116,10 @@ jobs:
|
||||
# Include HA_SHORT_VERSION to force the immediate creation
|
||||
# of a new uv cache entry after a version bump.
|
||||
echo "key=venv-${CACHE_VERSION}-${HA_SHORT_VERSION}-${HASH_REQUIREMENTS_TEST}-${HASH_REQUIREMENTS}-${HASH_REQUIREMENTS_ALL}-${HASH_PACKAGE_CONSTRAINTS}-${HASH_GEN_REQUIREMENTS}" >> $GITHUB_OUTPUT
|
||||
- name: Generate partial apt restore key
|
||||
id: generate_apt_cache_key
|
||||
run: |
|
||||
echo "key=$(lsb_release -rs)-apt-${CACHE_VERSION}-${HA_SHORT_VERSION}" >> $GITHUB_OUTPUT
|
||||
- name: Filter for core changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: core
|
||||
@@ -237,11 +242,6 @@ jobs:
|
||||
echo "postgresql_groups=${postgresql_groups}" >> $GITHUB_OUTPUT
|
||||
echo "python_versions: ${all_python_versions}"
|
||||
echo "python_versions=${all_python_versions}" >> $GITHUB_OUTPUT
|
||||
echo "default_python: ${default_python}"
|
||||
echo "default_python=${default_python}" >> $GITHUB_OUTPUT
|
||||
uv_version=$(grep '^uv==' requirements.txt | cut -d'=' -f3)
|
||||
echo "uv_version: ${uv_version}"
|
||||
echo "uv_version=${uv_version}" >> $GITHUB_OUTPUT
|
||||
echo "test_full_suite: ${test_full_suite}"
|
||||
echo "test_full_suite=${test_full_suite}" >> $GITHUB_OUTPUT
|
||||
echo "integrations_glob: ${integrations_glob}"
|
||||
@@ -281,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
|
||||
@@ -302,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
|
||||
|
||||
@@ -351,12 +351,12 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up uv and Python ${{ matrix.python-version }}
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -384,41 +384,80 @@ jobs:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: ${{ steps.generate-uv-key.outputs.full_key }}
|
||||
restore-keys: ${{ steps.generate-uv-key.outputs.partial_key }}
|
||||
- name: Install additional OS dependencies
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/cache-apt-packages
|
||||
- name: Check if apt cache exists
|
||||
id: cache-apt-check
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
packages: >-
|
||||
bluez
|
||||
ffmpeg
|
||||
libturbojpeg
|
||||
libxml2-utils
|
||||
libavcodec-dev
|
||||
libavdevice-dev
|
||||
libavfilter-dev
|
||||
libavformat-dev
|
||||
libavutil-dev
|
||||
libswresample-dev
|
||||
libswscale-dev
|
||||
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
${{ env.APT_LIST_CACHE_DIR }}
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
|
||||
- name: Install additional OS dependencies
|
||||
if: |
|
||||
steps.cache-venv.outputs.cache-hit != 'true'
|
||||
|| steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
id: install-os-deps
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
APT_CACHE_HIT: ${{ steps.cache-apt-check.outputs.cache-hit }}
|
||||
run: |
|
||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||
if [[ "${APT_CACHE_HIT}" != 'true' ]]; then
|
||||
mkdir -p ${APT_CACHE_DIR}
|
||||
mkdir -p ${APT_LIST_CACHE_DIR}
|
||||
fi
|
||||
|
||||
sudo apt-get update \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
|
||||
sudo apt-get -y install \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libxml2-utils \
|
||||
libavcodec-dev \
|
||||
libavdevice-dev \
|
||||
libavfilter-dev \
|
||||
libavformat-dev \
|
||||
libavutil-dev \
|
||||
libswresample-dev \
|
||||
libswscale-dev \
|
||||
libudev-dev
|
||||
version: ${{ env.APT_CACHE_VERSION }}
|
||||
execute_install_scripts: true
|
||||
|
||||
if [[ "${APT_CACHE_HIT}" != 'true' ]]; then
|
||||
sudo chmod -R 755 ${APT_CACHE_BASE}
|
||||
fi
|
||||
- name: Save apt cache
|
||||
if: |
|
||||
always()
|
||||
&& steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
&& steps.install-os-deps.outcome == 'success'
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
${{ env.APT_LIST_CACHE_DIR }}
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
id: create-venv
|
||||
env:
|
||||
PYTHON_VERSION: ${{ steps.python.outputs.python-version }}
|
||||
run: |
|
||||
uv venv venv --python "${PYTHON_VERSION}"
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pip install "$(grep '^uv' < requirements.txt)"
|
||||
uv pip install -U "pip>=25.2"
|
||||
uv pip install -r requirements.txt
|
||||
uv pip install -r requirements_all.txt -r requirements_test.txt
|
||||
uv pip install -e . --config-settings editable_mode=compat
|
||||
- name: Dump pip freeze
|
||||
run: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
uv pip freeze >> pip_freeze.txt
|
||||
@@ -467,22 +506,36 @@ jobs:
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
${{ env.APT_LIST_CACHE_DIR }}
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
|
||||
- name: Install additional OS dependencies
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||
sudo apt-get update \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
|
||||
sudo apt-get -y install \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
|
||||
libturbojpeg
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install additional OS dependencies
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/cache-apt-packages
|
||||
with:
|
||||
packages: libturbojpeg
|
||||
version: ${{ env.APT_CACHE_VERSION }}
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -516,10 +569,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -552,10 +605,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Run gen_copilot_instructions.py
|
||||
run: |
|
||||
python -m script.gen_copilot_instructions validate
|
||||
@@ -607,10 +660,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -658,10 +711,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -711,10 +764,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -762,10 +815,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Generate partial mypy restore key
|
||||
id: generate-mypy-key
|
||||
run: |
|
||||
@@ -823,26 +876,38 @@ jobs:
|
||||
- info
|
||||
- base
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
${{ env.APT_LIST_CACHE_DIR }}
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
|
||||
- name: Install additional OS dependencies
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||
sudo apt-get update \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
|
||||
sudo apt-get -y install \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install additional OS dependencies
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/cache-apt-packages
|
||||
with:
|
||||
packages: >-
|
||||
bluez
|
||||
ffmpeg
|
||||
libturbojpeg
|
||||
version: ${{ env.APT_CACHE_VERSION }}
|
||||
execute_install_scripts: true
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -887,27 +952,39 @@ jobs:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
${{ env.APT_LIST_CACHE_DIR }}
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
|
||||
- name: Install additional OS dependencies
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||
sudo apt-get update \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
|
||||
sudo apt-get -y install \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libxml2-utils
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install additional OS dependencies
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/cache-apt-packages
|
||||
with:
|
||||
packages: >-
|
||||
bluez
|
||||
ffmpeg
|
||||
libturbojpeg
|
||||
libxml2-utils
|
||||
version: ${{ env.APT_CACHE_VERSION }}
|
||||
execute_install_scripts: true
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -1028,28 +1105,40 @@ jobs:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
${{ env.APT_LIST_CACHE_DIR }}
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
|
||||
- name: Install additional OS dependencies
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||
sudo apt-get update \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
|
||||
sudo apt-get -y install \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libmariadb-dev-compat \
|
||||
libxml2-utils
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install additional OS dependencies
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/cache-apt-packages
|
||||
with:
|
||||
packages: >-
|
||||
bluez
|
||||
ffmpeg
|
||||
libturbojpeg
|
||||
libmariadb-dev-compat
|
||||
libxml2-utils
|
||||
version: ${{ env.APT_CACHE_VERSION }}
|
||||
execute_install_scripts: true
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -1177,35 +1266,42 @@ jobs:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
${{ env.APT_LIST_CACHE_DIR }}
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
|
||||
- name: Install additional OS dependencies
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||
sudo apt-get update \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
|
||||
sudo apt-get -y install \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libxml2-utils
|
||||
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
|
||||
sudo apt-get -y install \
|
||||
postgresql-server-dev-14
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install additional OS dependencies
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/cache-apt-packages
|
||||
with:
|
||||
packages: >-
|
||||
bluez
|
||||
ffmpeg
|
||||
libturbojpeg
|
||||
libxml2-utils
|
||||
version: ${{ env.APT_CACHE_VERSION }}
|
||||
execute_install_scripts: true
|
||||
- name: Set up PostgreSQL apt repository
|
||||
run: sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
|
||||
- name: Cache PostgreSQL development headers
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/cache-apt-packages
|
||||
with:
|
||||
packages: postgresql-server-dev-14
|
||||
version: ${{ env.APT_CACHE_VERSION }}
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -1353,27 +1449,39 @@ jobs:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
${{ env.APT_LIST_CACHE_DIR }}
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
|
||||
- name: Install additional OS dependencies
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||
sudo apt-get update \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR}
|
||||
sudo apt-get -y install \
|
||||
-o Dir::Cache=${APT_CACHE_DIR} \
|
||||
-o Dir::State::Lists=${APT_LIST_CACHE_DIR} \
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libxml2-utils
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install additional OS dependencies
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/cache-apt-packages
|
||||
with:
|
||||
packages: >-
|
||||
bluez
|
||||
ffmpeg
|
||||
libturbojpeg
|
||||
libxml2-utils
|
||||
version: ${{ env.APT_CACHE_VERSION }}
|
||||
execute_install_scripts: true
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"service": "mdi:refresh"
|
||||
},
|
||||
"set_dhw_override": {
|
||||
"service": "mdi:water-heater"
|
||||
"service": "mdi:water-boiler"
|
||||
},
|
||||
"set_system_mode": {
|
||||
"service": "mdi:pencil"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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" }
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"service": "mdi:lock-open"
|
||||
},
|
||||
"disable": {
|
||||
"service": "mdi:fash-off"
|
||||
"service": "mdi:flash-off"
|
||||
},
|
||||
"enable": {
|
||||
"service": "mdi:flash"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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}}"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from rf_protocols.codes.novy.cooker_hood import get_codes_for_code
|
||||
from rf_protocols.commands.novy import NovyCookerHoodCommand
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.radio_frequency import (
|
||||
@@ -128,10 +128,8 @@ class NovyCookerHoodConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Toggle the hood light on then off so it ends in its starting state."""
|
||||
assert self._transmitter_entity_id is not None
|
||||
command = NovyCookerHoodCommand(channel=self._code, key=COMMAND_LIGHT)
|
||||
try:
|
||||
command = await get_codes_for_code(self._code).async_load_command(
|
||||
COMMAND_LIGHT
|
||||
)
|
||||
await async_send_command(self.hass, self._transmitter_entity_id, command)
|
||||
await asyncio.sleep(_TOGGLE_GAP)
|
||||
await async_send_command(self.hass, self._transmitter_entity_id, command)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Fan platform for the Novy Cooker Hood (calibrated speed control)."""
|
||||
|
||||
import asyncio
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from rf_protocols.codes.novy.cooker_hood import get_codes_for_code
|
||||
from rf_protocols.commands.novy import NovyCookerHoodCommand
|
||||
|
||||
from homeassistant.components.fan import ATTR_PERCENTAGE, FanEntity, FanEntityFeature
|
||||
from homeassistant.components.radio_frequency import async_send_command
|
||||
@@ -25,6 +26,9 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
_SPEED_RANGE = (1, SPEED_COUNT)
|
||||
|
||||
# Novy hood expects at least 150ms between RF commands
|
||||
_COMMAND_DELAY = 0.2
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -49,7 +53,7 @@ class NovyCookerHoodFan(NovyCookerHoodEntity, FanEntity, RestoreEntity):
|
||||
def __init__(self, entry: ConfigEntry) -> None:
|
||||
"""Initialize the fan."""
|
||||
super().__init__(entry)
|
||||
self._codes = get_codes_for_code(entry.data[CONF_CODE])
|
||||
self._code: int = entry.data[CONF_CODE]
|
||||
self._level = 0
|
||||
self._attr_unique_id = entry.entry_id
|
||||
|
||||
@@ -103,18 +107,16 @@ class NovyCookerHoodFan(NovyCookerHoodEntity, FanEntity, RestoreEntity):
|
||||
async def async_increase_speed(self, percentage_step: int | None = None) -> None:
|
||||
"""Bump speed up by N hardware levels (no recalibration)."""
|
||||
steps = self._steps_from_percentage(percentage_step)
|
||||
plus = await self._codes.async_load_command(COMMAND_PLUS)
|
||||
for _ in range(steps):
|
||||
await self._async_send(plus)
|
||||
plus = NovyCookerHoodCommand(channel=self._code, key=COMMAND_PLUS)
|
||||
await self._async_send_repeated(plus, steps)
|
||||
self._level = min(SPEED_COUNT, self._level + steps)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_decrease_speed(self, percentage_step: int | None = None) -> None:
|
||||
"""Bump speed down by N hardware levels (no recalibration)."""
|
||||
steps = self._steps_from_percentage(percentage_step)
|
||||
minus = await self._codes.async_load_command(COMMAND_MINUS)
|
||||
for _ in range(steps):
|
||||
await self._async_send(minus)
|
||||
minus = NovyCookerHoodCommand(channel=self._code, key=COMMAND_MINUS)
|
||||
await self._async_send_repeated(minus, steps)
|
||||
self._level = max(0, self._level - steps)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -127,17 +129,25 @@ class NovyCookerHoodFan(NovyCookerHoodEntity, FanEntity, RestoreEntity):
|
||||
|
||||
async def _async_set_level(self, level: int) -> None:
|
||||
"""Reset to off with `SPEED_COUNT` minus presses, then climb to level."""
|
||||
minus = await self._codes.async_load_command(COMMAND_MINUS)
|
||||
for _ in range(SPEED_COUNT):
|
||||
await self._async_send(minus)
|
||||
minus = NovyCookerHoodCommand(channel=self._code, key=COMMAND_MINUS)
|
||||
await self._async_send_repeated(minus, SPEED_COUNT)
|
||||
if level > 0:
|
||||
plus = await self._codes.async_load_command(COMMAND_PLUS)
|
||||
for _ in range(level):
|
||||
await self._async_send(plus)
|
||||
await asyncio.sleep(_COMMAND_DELAY)
|
||||
plus = NovyCookerHoodCommand(channel=self._code, key=COMMAND_PLUS)
|
||||
await self._async_send_repeated(plus, level)
|
||||
self._level = level
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _async_send(self, command: Any) -> None:
|
||||
async def _async_send_repeated(
|
||||
self, command: NovyCookerHoodCommand, count: int
|
||||
) -> None:
|
||||
"""Send the same RF command N times, pausing between presses."""
|
||||
for i in range(count):
|
||||
if i > 0:
|
||||
await asyncio.sleep(_COMMAND_DELAY)
|
||||
await self._async_send(command)
|
||||
|
||||
async def _async_send(self, command: NovyCookerHoodCommand) -> None:
|
||||
"""Send a single RF command via the configured transmitter."""
|
||||
await async_send_command(
|
||||
self.hass, self._transmitter, command, context=self._context
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from rf_protocols.codes.novy.cooker_hood import get_codes_for_code
|
||||
from rf_protocols.commands.novy import NovyCookerHoodCommand
|
||||
|
||||
from homeassistant.components.light import ColorMode, LightEntity
|
||||
from homeassistant.components.radio_frequency import async_send_command
|
||||
@@ -37,7 +37,7 @@ class NovyCookerHoodLight(NovyCookerHoodEntity, LightEntity, RestoreEntity):
|
||||
def __init__(self, entry: ConfigEntry) -> None:
|
||||
"""Initialize the light."""
|
||||
super().__init__(entry)
|
||||
self._codes = get_codes_for_code(entry.data[CONF_CODE])
|
||||
self._code = entry.data[CONF_CODE]
|
||||
self._attr_unique_id = entry.entry_id
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@@ -58,9 +58,9 @@ class NovyCookerHoodLight(NovyCookerHoodEntity, LightEntity, RestoreEntity):
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _async_send_command(self, name: str) -> None:
|
||||
"""Load the named command and send it via the configured transmitter."""
|
||||
command = await self._codes.async_load_command(name)
|
||||
async def _async_send_command(self, key: str) -> None:
|
||||
"""Build the named command and send it via the configured transmitter."""
|
||||
command = NovyCookerHoodCommand(channel=self._code, key=key)
|
||||
await async_send_command(
|
||||
self.hass, self._transmitter, command, context=self._context
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Automatically generated by gen_requirements_all.py, do not edit
|
||||
|
||||
aiodhcpwatcher==1.2.1
|
||||
aiodiscover==3.2.0
|
||||
aiodiscover==3.2.3
|
||||
aiodns==4.0.4
|
||||
aiogithubapi==26.0.0
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
@@ -30,12 +30,12 @@ certifi>=2021.5.30
|
||||
ciso8601==2.3.3
|
||||
cronsim==2.7
|
||||
cryptography==48.0.0
|
||||
dbus-fast==5.0.0
|
||||
dbus-fast==5.0.3
|
||||
file-read-backwards==2.0.0
|
||||
fnv-hash-fast==2.0.2
|
||||
go2rtc-client==0.4.0
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==6.1.0
|
||||
habluetooth==6.2.0
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
@@ -133,7 +133,7 @@ multidict>=6.0.2
|
||||
Brotli>=1.2.0
|
||||
|
||||
# ensure pydantic version does not float since it might have breaking changes
|
||||
pydantic==2.13.2
|
||||
pydantic==2.13.4
|
||||
|
||||
# Required for Python 3.14.0 compatibility (#119223).
|
||||
mashumaro>=3.17.0
|
||||
|
||||
Generated
+6
-6
@@ -233,7 +233,7 @@ aiocomelit==2.0.3
|
||||
aiodhcpwatcher==1.2.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==3.2.0
|
||||
aiodiscover==3.2.3
|
||||
|
||||
# homeassistant.components.dnsip
|
||||
aiodns==4.0.4
|
||||
@@ -794,7 +794,7 @@ datadog==0.52.0
|
||||
datapoint==0.12.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
dbus-fast==5.0.0
|
||||
dbus-fast==5.0.3
|
||||
|
||||
# homeassistant.components.debugpy
|
||||
debugpy==1.8.17
|
||||
@@ -1183,7 +1183,7 @@ growattServer==2.1.0
|
||||
gspread==5.5.0
|
||||
|
||||
# homeassistant.components.guntamatic
|
||||
guntamatic==1.8.0
|
||||
guntamatic==1.9.0
|
||||
|
||||
# homeassistant.components.profiler
|
||||
guppy3==3.1.6
|
||||
@@ -1210,7 +1210,7 @@ ha-xthings-cloud==1.0.5
|
||||
habiticalib==0.4.7
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==6.1.0
|
||||
habluetooth==6.2.0
|
||||
|
||||
# homeassistant.components.hanna
|
||||
hanna-cloud==0.0.7
|
||||
@@ -1923,7 +1923,7 @@ py-nightscout==1.2.2
|
||||
py-nymta==0.4.0
|
||||
|
||||
# homeassistant.components.opendisplay
|
||||
py-opendisplay==5.9.0
|
||||
py-opendisplay==7.2.3
|
||||
|
||||
# homeassistant.components.schluter
|
||||
py-schluter==0.1.7
|
||||
@@ -3224,7 +3224,7 @@ uasiren==0.0.1
|
||||
uhooapi==1.2.8
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==10.4.1
|
||||
uiprotect==10.5.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
|
||||
@@ -18,7 +18,7 @@ license-expression==30.4.3
|
||||
mock-open==1.4.0
|
||||
mypy==2.1.0
|
||||
prek==0.2.28
|
||||
pydantic==2.13.2
|
||||
pydantic==2.13.4
|
||||
pylint==4.0.5
|
||||
pylint-per-file-ignores==3.2.1
|
||||
pipdeptree==2.26.1
|
||||
|
||||
@@ -117,7 +117,7 @@ multidict>=6.0.2
|
||||
Brotli>=1.2.0
|
||||
|
||||
# ensure pydantic version does not float since it might have breaking changes
|
||||
pydantic==2.13.2
|
||||
pydantic==2.13.4
|
||||
|
||||
# Required for Python 3.14.0 compatibility (#119223).
|
||||
mashumaro>=3.17.0
|
||||
|
||||
+75
-16
@@ -2,13 +2,19 @@
|
||||
"""Helper script to split test into n buckets."""
|
||||
|
||||
import argparse
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from dataclasses import dataclass, field
|
||||
from math import ceil
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Final
|
||||
|
||||
# tests/components has ~1000 sub-directories, which makes it the natural
|
||||
# place to subdivide to keep each pytest invocation roughly equal in size.
|
||||
_FAN_OUT_DIRS: Final = frozenset({"components"})
|
||||
|
||||
|
||||
class Bucket:
|
||||
"""Class to hold bucket."""
|
||||
@@ -164,33 +170,86 @@ class TestFolder:
|
||||
return result
|
||||
|
||||
|
||||
def collect_tests(path: Path) -> TestFolder:
|
||||
"""Collect all tests."""
|
||||
def _collect_batch(paths: list[Path]) -> tuple[str, str, int]:
|
||||
"""Run pytest --collect-only on a batch of paths."""
|
||||
result = subprocess.run(
|
||||
["pytest", "--collect-only", "-qq", "-p", "no:warnings", path],
|
||||
["pytest", "--collect-only", "-qq", "-p", "no:warnings", *map(str, paths)],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout, result.stderr, result.returncode
|
||||
|
||||
if result.returncode != 0:
|
||||
print("Failed to collect tests:")
|
||||
print(result.stderr)
|
||||
print(result.stdout)
|
||||
|
||||
def _iter_eligible_children(path: Path) -> list[Path]:
|
||||
"""Return immediate children of ``path`` that pytest should collect.
|
||||
|
||||
Filters out hidden/dunder entries, non-``test_*.py`` files (so helper
|
||||
modules like ``conftest.py`` and ``common.py`` are not passed as
|
||||
explicit collection targets), and pycache-style directories.
|
||||
"""
|
||||
children: list[Path] = []
|
||||
for entry in sorted(path.iterdir()):
|
||||
if entry.name.startswith((".", "_")):
|
||||
continue
|
||||
if entry.is_dir() or (entry.suffix == ".py" and entry.name.startswith("test_")):
|
||||
children.append(entry)
|
||||
return children
|
||||
|
||||
|
||||
def _enumerate_batch_paths(path: Path) -> list[Path]:
|
||||
"""Return the child paths to run pytest --collect-only over.
|
||||
|
||||
Files are returned as-is. Directories are expanded one level deep, with
|
||||
a second level of expansion for entries named in ``_FAN_OUT_DIRS`` so the
|
||||
enormous ``tests/components`` tree fans out into per-integration paths.
|
||||
"""
|
||||
if path.is_file():
|
||||
return [path]
|
||||
|
||||
paths: list[Path] = []
|
||||
for entry in _iter_eligible_children(path):
|
||||
if entry.is_dir() and entry.name in _FAN_OUT_DIRS:
|
||||
paths.extend(_iter_eligible_children(entry))
|
||||
else:
|
||||
paths.append(entry)
|
||||
return paths
|
||||
|
||||
|
||||
def collect_tests(path: Path) -> TestFolder:
|
||||
"""Collect all tests."""
|
||||
batch_paths = _enumerate_batch_paths(path)
|
||||
if not batch_paths:
|
||||
print(f"No eligible test paths found under {path}")
|
||||
sys.exit(1)
|
||||
workers = min(len(batch_paths), os.cpu_count() or 1) or 1
|
||||
# Round-robin chunking keeps batches roughly balanced when path
|
||||
# ordering correlates with test size.
|
||||
batches = [batch_paths[i::workers] for i in range(workers)]
|
||||
|
||||
if workers == 1:
|
||||
results = [_collect_batch(batches[0])]
|
||||
else:
|
||||
with ProcessPoolExecutor(max_workers=workers) as executor:
|
||||
results = list(executor.map(_collect_batch, batches))
|
||||
|
||||
folder = TestFolder(path)
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
file_path, _, total_tests = line.partition(": ")
|
||||
if not path or not total_tests:
|
||||
print(f"Unexpected line: {line}")
|
||||
for stdout, stderr, returncode in results:
|
||||
if returncode != 0:
|
||||
print("Failed to collect tests:")
|
||||
print(stderr)
|
||||
print(stdout)
|
||||
sys.exit(1)
|
||||
for line in stdout.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
file_path, _, total_tests = line.partition(": ")
|
||||
if not file_path or not total_tests:
|
||||
print(f"Unexpected line: {line}")
|
||||
sys.exit(1)
|
||||
|
||||
file = TestFile(int(total_tests), Path(file_path))
|
||||
folder.add_test_file(file)
|
||||
file = TestFile(int(total_tests), Path(file_path))
|
||||
folder.add_test_file(file)
|
||||
|
||||
return folder
|
||||
|
||||
|
||||
@@ -102,37 +102,37 @@ async def test_login(hass: HomeAssistant) -> None:
|
||||
|
||||
provider = hass.auth.auth_providers[0]
|
||||
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "incorrect-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "incorrect-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["data_schema"].schema.get("pin") is str
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"pin": "invalid-code"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_code"
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"pin": "123456"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"].id == "mock-id"
|
||||
|
||||
|
||||
@@ -149,9 +149,9 @@ async def test_setup_flow(hass: HomeAssistant) -> None:
|
||||
flow = await auth_module.async_setup_flow("new-user")
|
||||
|
||||
result = await flow.async_step_init()
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await flow.async_step_init({"pin": "abcdefg"})
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert auth_module._data[1]["user_id"] == "new-user"
|
||||
assert auth_module._data[1]["pin"] == "abcdefg"
|
||||
|
||||
@@ -137,25 +137,25 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
provider = hass.auth.auth_providers[0]
|
||||
|
||||
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "incorrect-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "incorrect-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
with patch("pyotp.HOTP.at", return_value=MOCK_CODE):
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["data_schema"].schema.get("code") is str
|
||||
|
||||
@@ -173,7 +173,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"code": "invalid-code"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["errors"]["base"] == "invalid_code"
|
||||
|
||||
@@ -191,7 +191,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"code": "invalid-code"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["errors"]["base"] == "invalid_code"
|
||||
|
||||
@@ -199,7 +199,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"code": "invalid-code"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert result["type"] is data_entry_flow.FlowResultType.ABORT
|
||||
assert result["reason"] == "too_many_retry"
|
||||
|
||||
# wait service call finished
|
||||
@@ -207,13 +207,13 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
|
||||
# restart login
|
||||
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
with patch("pyotp.HOTP.at", return_value=MOCK_CODE):
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["data_schema"].schema.get("code") is str
|
||||
|
||||
@@ -231,7 +231,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"code": MOCK_CODE}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"].id == "mock-id"
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ async def test_setup_user_notify_service(hass: HomeAssistant) -> None:
|
||||
|
||||
flow = await notify_auth_module.async_setup_flow("test-user")
|
||||
step = await flow.async_step_init()
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "init"
|
||||
schema = step["data_schema"]
|
||||
schema({"notify_service": "test2"})
|
||||
@@ -277,7 +277,7 @@ async def test_setup_user_notify_service(hass: HomeAssistant) -> None:
|
||||
|
||||
with patch("pyotp.HOTP.at", return_value=MOCK_CODE):
|
||||
step = await flow.async_step_init({"notify_service": "test1"})
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "setup"
|
||||
|
||||
# wait service call finished
|
||||
@@ -357,7 +357,7 @@ async def test_setup_user_no_notify_service(hass: HomeAssistant) -> None:
|
||||
|
||||
flow = await notify_auth_module.async_setup_flow("test-user")
|
||||
step = await flow.async_step_init()
|
||||
assert step["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert step["type"] is data_entry_flow.FlowResultType.ABORT
|
||||
assert step["reason"] == "no_available_service"
|
||||
|
||||
|
||||
@@ -394,13 +394,13 @@ async def test_not_raise_exception_when_service_not_exist(hass: HomeAssistant) -
|
||||
provider = hass.auth.auth_providers[0]
|
||||
|
||||
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
with patch("pyotp.HOTP.at", return_value=MOCK_CODE):
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert result["type"] is data_entry_flow.FlowResultType.ABORT
|
||||
assert result["reason"] == "unknown_error"
|
||||
|
||||
# wait service call finished
|
||||
|
||||
@@ -95,24 +95,24 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
provider = hass.auth.auth_providers[0]
|
||||
|
||||
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "incorrect-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "incorrect-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["data_schema"].schema.get("code") is str
|
||||
|
||||
@@ -120,7 +120,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"code": "invalid-code"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "mfa"
|
||||
assert result["errors"]["base"] == "invalid_code"
|
||||
|
||||
@@ -128,7 +128,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result["flow_id"], {"code": MOCK_CODE}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"].id == "mock-id"
|
||||
|
||||
|
||||
|
||||
@@ -139,18 +139,18 @@ async def test_login_flow_validates(
|
||||
"""Test login flow."""
|
||||
flow = await provider.async_login_flow({})
|
||||
result = await flow.async_step_init()
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "bad-user", "password": "bad-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "good-user", "password": "good-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"]["username"] == "good-user"
|
||||
|
||||
|
||||
@@ -160,5 +160,5 @@ async def test_strip_username(provider: command_line.CommandLineAuthProvider) ->
|
||||
result = await flow.async_step_init(
|
||||
{"username": "\t\ngood-user ", "password": "good-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"]["username"] == "good-user"
|
||||
|
||||
@@ -161,24 +161,24 @@ async def test_login_flow_validates(data: hass_auth.Data, hass: HomeAssistant) -
|
||||
)
|
||||
flow = await provider.async_login_flow({})
|
||||
result = await flow.async_step_init()
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "incorrect-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "TEST-user ", "password": "incorrect-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "test-USER", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"]["username"] == "test-USER"
|
||||
|
||||
|
||||
@@ -260,24 +260,24 @@ async def test_legacy_login_flow_validates(
|
||||
)
|
||||
flow = await provider.async_login_flow({})
|
||||
result = await flow.async_step_init()
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "incorrect-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "test-user", "password": "incorrect-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
result = await flow.async_step_init(
|
||||
{"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"]["username"] == "test-user"
|
||||
|
||||
|
||||
|
||||
+15
-15
@@ -172,12 +172,12 @@ async def test_create_new_user(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
step = await manager.login_flow.async_init(("insecure_example", None))
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
step["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert step["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
credential = step["result"]
|
||||
assert credential is not None
|
||||
|
||||
@@ -241,12 +241,12 @@ async def test_login_as_existing_user(mock_hass) -> None:
|
||||
)
|
||||
|
||||
step = await manager.login_flow.async_init(("insecure_example", None))
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
step["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert step["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
|
||||
credential = step["result"]
|
||||
user = await manager.async_get_user_by_credentials(credential)
|
||||
@@ -840,14 +840,14 @@ async def test_login_with_auth_module(mock_hass) -> None:
|
||||
)
|
||||
|
||||
step = await manager.login_flow.async_init(("insecure_example", None))
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
step["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
|
||||
# After auth_provider validated, request auth module input form
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "mfa"
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
@@ -855,7 +855,7 @@ async def test_login_with_auth_module(mock_hass) -> None:
|
||||
)
|
||||
|
||||
# Invalid code error
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "mfa"
|
||||
assert step["errors"] == {"base": "invalid_code"}
|
||||
|
||||
@@ -864,7 +864,7 @@ async def test_login_with_auth_module(mock_hass) -> None:
|
||||
)
|
||||
|
||||
# Finally passed, get credential
|
||||
assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert step["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert step["result"]
|
||||
assert step["result"].id == "mock-id"
|
||||
|
||||
@@ -915,21 +915,21 @@ async def test_login_with_multi_auth_module(mock_hass) -> None:
|
||||
)
|
||||
|
||||
step = await manager.login_flow.async_init(("insecure_example", None))
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
step["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
|
||||
# After auth_provider validated, request select auth module
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "select_mfa_module"
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
step["flow_id"], {"multi_factor_auth_module": "module2"}
|
||||
)
|
||||
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "mfa"
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
@@ -937,7 +937,7 @@ async def test_login_with_multi_auth_module(mock_hass) -> None:
|
||||
)
|
||||
|
||||
# Finally passed, get credential
|
||||
assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert step["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert step["result"]
|
||||
assert step["result"].id == "mock-id"
|
||||
|
||||
@@ -983,13 +983,13 @@ async def test_auth_module_expired_session(mock_hass) -> None:
|
||||
)
|
||||
|
||||
step = await manager.login_flow.async_init(("insecure_example", None))
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
step = await manager.login_flow.async_configure(
|
||||
step["flow_id"], {"username": "test-user", "password": "test-pass"}
|
||||
)
|
||||
|
||||
assert step["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert step["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert step["step_id"] == "mfa"
|
||||
|
||||
with freeze_time(dt_util.utcnow() + MFA_SESSION_EXPIRATION):
|
||||
@@ -997,7 +997,7 @@ async def test_auth_module_expired_session(mock_hass) -> None:
|
||||
step["flow_id"], {"pin": "test-pin"}
|
||||
)
|
||||
# login flow abort due session timeout
|
||||
assert step["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert step["type"] is data_entry_flow.FlowResultType.ABORT
|
||||
assert step["reason"] == "login_expired"
|
||||
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ async def test_reauth_flow_scenario(
|
||||
data=mock_config_entry.data,
|
||||
)
|
||||
|
||||
assert flow["type"] == FlowResultType.FORM
|
||||
assert flow["type"] is FlowResultType.FORM
|
||||
assert flow["step_id"] == REAUTH_STEP
|
||||
|
||||
fw_major = int(ap_status_fixture.host.fwversion.lstrip("v").split(".", 1)[0])
|
||||
@@ -305,7 +305,7 @@ async def test_reauth_flow_scenarios(
|
||||
data=mock_config_entry.data,
|
||||
)
|
||||
|
||||
assert flow["type"] == FlowResultType.FORM
|
||||
assert flow["type"] is FlowResultType.FORM
|
||||
assert flow["step_id"] == REAUTH_STEP
|
||||
|
||||
with patch(
|
||||
@@ -337,7 +337,7 @@ async def test_reauth_flow_scenarios(
|
||||
user_input={CONF_PASSWORD: NEW_PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
|
||||
@@ -284,7 +284,7 @@ async def test_setup_entry_failure(
|
||||
|
||||
result = await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
assert result is False
|
||||
assert mock_config_entry.state == state
|
||||
assert mock_config_entry.state is state
|
||||
|
||||
|
||||
async def test_fetch_airos_data_auth_error(mock_airos_client: MagicMock) -> None:
|
||||
|
||||
@@ -138,4 +138,4 @@ async def test_migrate_future_version_returns_false(
|
||||
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.MIGRATION_ERROR
|
||||
assert config_entry.state is ConfigEntryState.MIGRATION_ERROR
|
||||
|
||||
@@ -164,7 +164,7 @@ async def test_error_handling(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown", result
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ async def test_template_error(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown", result
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ async def test_template_variables(
|
||||
hass, "hello", None, context, agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "Okay, let me take care of that for you."
|
||||
@@ -382,7 +382,7 @@ async def test_function_call(
|
||||
system_text = " ".join(block["text"] for block in system if "text" in block)
|
||||
assert "You are a voice assistant for Home Assistant." in system_text
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "I have successfully called the function"
|
||||
@@ -457,7 +457,7 @@ async def test_function_exception(
|
||||
agent_id=agent_id,
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "There was an error calling the function"
|
||||
@@ -638,7 +638,7 @@ async def test_refusal(
|
||||
agent_id="conversation.claude_conversation",
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown"
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
@@ -670,7 +670,7 @@ async def test_stream_wrong_type(
|
||||
agent_id="conversation.claude_conversation",
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown"
|
||||
assert result.response.speech["plain"]["speech"] == "Expected a stream of messages"
|
||||
|
||||
@@ -700,7 +700,7 @@ async def test_double_system_messages(
|
||||
agent_id="conversation.claude_conversation",
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown"
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
|
||||
@@ -42,7 +42,7 @@ async def test_auth_error_handling(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown", result
|
||||
|
||||
await hass.async_block_till_done()
|
||||
@@ -86,7 +86,7 @@ async def test_connection_error_handling(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown", result
|
||||
|
||||
# Check new state
|
||||
|
||||
@@ -190,7 +190,7 @@ async def test_device_trigger_reauth_flow(
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
mock_flow_init.assert_called_once()
|
||||
assert config_entry.state == ConfigEntryState.SETUP_ERROR
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_shutdown(config_entry_data: MappingProxyType[str, Any]) -> None:
|
||||
@@ -235,4 +235,4 @@ async def test_get_axis_api_errors(
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state == state
|
||||
assert config_entry.state is state
|
||||
|
||||
@@ -26,7 +26,7 @@ async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) ->
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
@@ -34,7 +34,7 @@ async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) ->
|
||||
BASE_CONFIG.copy(),
|
||||
)
|
||||
|
||||
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result2["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert (
|
||||
result2["title"]
|
||||
== "cluster.region.kusto.windows.net / test-database-name (test-table-name)"
|
||||
@@ -61,7 +61,7 @@ async def test_config_flow_errors(
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data=None,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
# Test error handling with error
|
||||
@@ -71,7 +71,7 @@ async def test_config_flow_errors(
|
||||
result["flow_id"],
|
||||
BASE_CONFIG.copy(),
|
||||
)
|
||||
assert result2["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result2["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": expected}
|
||||
|
||||
schema = result2["data_schema"]
|
||||
@@ -99,7 +99,7 @@ async def test_config_flow_errors(
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result2["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
# Retest error handling if error is corrected and connection is successful
|
||||
|
||||
@@ -112,4 +112,4 @@ async def test_config_flow_errors(
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result3["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
|
||||
@@ -70,7 +70,7 @@ async def test_config_flow_step_user(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result1["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result1["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result1["result"].title == "Office occupied"
|
||||
assert result1["next_flow"][0] == FlowType.CONFIG_SUBENTRIES_FLOW
|
||||
|
||||
@@ -260,7 +260,7 @@ async def test_single_state_observation(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
entry_id = result["result"].entry_id
|
||||
sub_flow_id = result["next_flow"][1]
|
||||
|
||||
@@ -287,7 +287,7 @@ async def test_single_state_observation(hass: HomeAssistant) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
await hass.async_block_till_done()
|
||||
|
||||
config_entry = hass.config_entries.async_get_entry(entry_id)
|
||||
@@ -337,7 +337,7 @@ async def test_single_numeric_state_observation(hass: HomeAssistant) -> None:
|
||||
CONF_PRIOR: 20,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
config_entry = result["result"]
|
||||
sub_flow_id = result["next_flow"][1]
|
||||
await hass.async_block_till_done()
|
||||
@@ -408,7 +408,7 @@ async def test_multi_numeric_state_observation(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
config_entry = result["result"]
|
||||
sub_flow_id = result["next_flow"][1]
|
||||
|
||||
@@ -546,7 +546,7 @@ async def test_single_template_observation(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
config_entry = result["result"]
|
||||
sub_flow_id = result["next_flow"][1]
|
||||
|
||||
@@ -1086,7 +1086,7 @@ async def test_invalid_configs(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
assert result.get("errors") is None
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
config_entry = result["result"]
|
||||
sub_flow_id = result["next_flow"][1]
|
||||
|
||||
|
||||
@@ -182,8 +182,13 @@ async def test_diagnostics(
|
||||
"scanners": [
|
||||
{
|
||||
"adapter": "hci0",
|
||||
"connect_completed_total": 0,
|
||||
"connect_failed_total": 0,
|
||||
"connect_failures": {},
|
||||
"connect_in_progress": {},
|
||||
"connectable": True,
|
||||
"discovered_devices_and_advertisement_data": [],
|
||||
"last_connect_completed_time": 0.0,
|
||||
"last_detection": ANY,
|
||||
"monotonic_time": ANY,
|
||||
"name": "hci0 (00:00:00:00:00:01)",
|
||||
@@ -202,6 +207,11 @@ async def test_diagnostics(
|
||||
},
|
||||
{
|
||||
"adapter": "hci1",
|
||||
"connect_completed_total": 0,
|
||||
"connect_failed_total": 0,
|
||||
"connect_failures": {},
|
||||
"connect_in_progress": {},
|
||||
"last_connect_completed_time": 0.0,
|
||||
"discovered_devices_and_advertisement_data": [
|
||||
{
|
||||
"address": "44:44:33:11:23:45",
|
||||
@@ -397,7 +407,12 @@ async def test_diagnostics_macos(
|
||||
"scanners": [
|
||||
{
|
||||
"adapter": "Core Bluetooth",
|
||||
"connect_completed_total": 0,
|
||||
"connect_failed_total": 0,
|
||||
"connect_failures": {},
|
||||
"connect_in_progress": {},
|
||||
"connectable": True,
|
||||
"last_connect_completed_time": 0.0,
|
||||
"discovered_devices_and_advertisement_data": [
|
||||
{
|
||||
"address": "44:44:33:11:23:45",
|
||||
@@ -602,8 +617,13 @@ async def test_diagnostics_remote_adapter(
|
||||
"scanners": [
|
||||
{
|
||||
"adapter": "hci0",
|
||||
"connect_completed_total": 0,
|
||||
"connect_failed_total": 0,
|
||||
"connect_failures": {},
|
||||
"connect_in_progress": {},
|
||||
"connectable": True,
|
||||
"discovered_devices_and_advertisement_data": [],
|
||||
"last_connect_completed_time": 0.0,
|
||||
"last_detection": ANY,
|
||||
"monotonic_time": ANY,
|
||||
"name": "hci0 (00:00:00:00:00:01)",
|
||||
@@ -621,9 +641,14 @@ async def test_diagnostics_remote_adapter(
|
||||
},
|
||||
},
|
||||
{
|
||||
"connect_completed_total": 0,
|
||||
"connect_failed_total": 0,
|
||||
"connect_failures": {},
|
||||
"connect_in_progress": {},
|
||||
"connectable": True,
|
||||
"current_mode": None,
|
||||
"requested_mode": None,
|
||||
"last_connect_completed_time": 0.0,
|
||||
"discovered_device_timestamps": {"44:44:33:11:23:45": ANY},
|
||||
"discovered_devices_and_advertisement_data": [
|
||||
{
|
||||
|
||||
@@ -138,8 +138,10 @@ async def test_setup_and_stop_passive(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert init_kwargs == {
|
||||
"adapter": "hci0",
|
||||
"bluez": scanner.PASSIVE_SCANNER_ARGS, # pylint: disable=c-extension-no-member
|
||||
"bluez": {
|
||||
**scanner.PASSIVE_SCANNER_ARGS, # pylint: disable=c-extension-no-member
|
||||
"adapter": "hci0",
|
||||
},
|
||||
"scanning_mode": "passive",
|
||||
}
|
||||
|
||||
@@ -188,7 +190,7 @@ async def test_setup_and_stop_old_bluez(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert init_kwargs == {
|
||||
"adapter": "hci0",
|
||||
"bluez": {"adapter": "hci0"},
|
||||
"scanning_mode": "active",
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ async def test_init_failure(
|
||||
"""Test an initialization error on integration load."""
|
||||
mock_bring_client.login.side_effect = exception
|
||||
await setup_integration(hass, bring_config_entry)
|
||||
assert bring_config_entry.state == status
|
||||
assert bring_config_entry.state is status
|
||||
|
||||
assert (
|
||||
any(
|
||||
|
||||
@@ -67,7 +67,7 @@ async def test_client_failure(
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state == expected_state
|
||||
assert config_entry.state is expected_state
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert [flow.get("step_id") for flow in flows] == expected_flows
|
||||
|
||||
@@ -56,7 +56,7 @@ async def test_async_unload_entry(
|
||||
result = await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
|
||||
assert result is True
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_device_info(
|
||||
|
||||
@@ -201,7 +201,7 @@ async def test_set_temperature(
|
||||
{"temperature": {"value": 20}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert err.value.result.no_match_reason == intent.MatchFailedReason.MULTIPLE_TARGETS
|
||||
assert err.value.result.no_match_reason is intent.MatchFailedReason.MULTIPLE_TARGETS
|
||||
|
||||
# Select by area explicitly (climate_2)
|
||||
response = await intent.async_handle(
|
||||
@@ -211,7 +211,7 @@ async def test_set_temperature(
|
||||
{"area": {"value": bedroom_area.name}, "temperature": {"value": 20.1}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert len(response.matched_states) == 1
|
||||
assert response.matched_states[0].entity_id == climate_2.entity_id
|
||||
state = hass.states.get(climate_2.entity_id)
|
||||
@@ -228,7 +228,7 @@ async def test_set_temperature(
|
||||
},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.matched_states
|
||||
assert response.matched_states[0].entity_id == climate_2.entity_id
|
||||
state = hass.states.get(climate_2.entity_id)
|
||||
@@ -242,7 +242,7 @@ async def test_set_temperature(
|
||||
{"floor": {"value": second_floor.name}, "temperature": {"value": 20.3}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.matched_states
|
||||
assert response.matched_states[0].entity_id == climate_2.entity_id
|
||||
state = hass.states.get(climate_2.entity_id)
|
||||
@@ -259,7 +259,7 @@ async def test_set_temperature(
|
||||
},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert response.matched_states
|
||||
assert response.matched_states[0].entity_id == climate_2.entity_id
|
||||
state = hass.states.get(climate_2.entity_id)
|
||||
@@ -273,7 +273,7 @@ async def test_set_temperature(
|
||||
{"name": {"value": "Climate 2"}, "temperature": {"value": 20.5}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert response.response_type is intent.IntentResponseType.ACTION_DONE
|
||||
assert len(response.matched_states) == 1
|
||||
assert response.matched_states[0].entity_id == climate_2.entity_id
|
||||
state = hass.states.get(climate_2.entity_id)
|
||||
@@ -291,7 +291,7 @@ async def test_set_temperature(
|
||||
|
||||
# Exception should contain details of what we tried to match
|
||||
assert isinstance(error.value, intent.MatchFailedError)
|
||||
assert error.value.result.no_match_reason == intent.MatchFailedReason.AREA
|
||||
assert error.value.result.no_match_reason is intent.MatchFailedReason.AREA
|
||||
constraints = error.value.constraints
|
||||
assert constraints.name is None
|
||||
assert constraints.area_name == office_area.name
|
||||
@@ -310,7 +310,7 @@ async def test_set_temperature(
|
||||
},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert err.value.result.no_match_reason == intent.MatchFailedReason.MULTIPLE_TARGETS
|
||||
assert err.value.result.no_match_reason is intent.MatchFailedReason.MULTIPLE_TARGETS
|
||||
|
||||
|
||||
async def test_set_temperature_no_entities(
|
||||
@@ -330,7 +330,7 @@ async def test_set_temperature_no_entities(
|
||||
{"temperature": {"value": 20}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert err.value.result.no_match_reason == intent.MatchFailedReason.DOMAIN
|
||||
assert err.value.result.no_match_reason is intent.MatchFailedReason.DOMAIN
|
||||
|
||||
|
||||
async def test_set_temperature_not_supported(hass: HomeAssistant) -> None:
|
||||
@@ -357,4 +357,4 @@ async def test_set_temperature_not_supported(hass: HomeAssistant) -> None:
|
||||
|
||||
# Exception should contain details of what we tried to match
|
||||
assert isinstance(error.value, intent.MatchFailedError)
|
||||
assert error.value.result.no_match_reason == intent.MatchFailedReason.FEATURE
|
||||
assert error.value.result.no_match_reason is intent.MatchFailedReason.FEATURE
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user