mirror of
https://github.com/home-assistant/core.git
synced 2026-03-30 20:40:25 +02:00
Compare commits
34 Commits
python-3.1
...
setpoint_c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a8a1ff345 | ||
|
|
43b2e26993 | ||
|
|
68bea745d4 | ||
|
|
9f876757f6 | ||
|
|
28f70fab8d | ||
|
|
289490faa3 | ||
|
|
dea46f7b2e | ||
|
|
82e3221126 | ||
|
|
47e8fbc1ed | ||
|
|
0428d0b97f | ||
|
|
45344c04c1 | ||
|
|
c472b6ac5e | ||
|
|
58f533feb6 | ||
|
|
0af8c8fd8c | ||
|
|
b9d6c3b9fe | ||
|
|
b700940bb9 | ||
|
|
3b73f6d37e | ||
|
|
2812bb21da | ||
|
|
5d474675e8 | ||
|
|
ea7bcf6cda | ||
|
|
725bd3d671 | ||
|
|
cfc4fa6342 | ||
|
|
b650e71660 | ||
|
|
9ddf15e348 | ||
|
|
15082f9111 | ||
|
|
12f16611ff | ||
|
|
8041be3d08 | ||
|
|
40b021e755 | ||
|
|
aab57eda96 | ||
|
|
f0dd37caa5 | ||
|
|
662b178495 | ||
|
|
cb3d30884a | ||
|
|
49e6f20372 | ||
|
|
75d02661eb |
6
.github/copilot-instructions.md
vendored
6
.github/copilot-instructions.md
vendored
@@ -1,12 +1,6 @@
|
|||||||
<!-- Automatically generated by gen_copilot_instructions.py, do not edit -->
|
<!-- Automatically generated by gen_copilot_instructions.py, do not edit -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Copilot code review instructions
|
|
||||||
|
|
||||||
- Start review comments with a short, one-sentence summary of the suggested fix.
|
|
||||||
- Do not add comments about code style, formatting or linting issues.
|
|
||||||
|
|
||||||
# GitHub Copilot & Claude Code Instructions
|
# GitHub Copilot & Claude Code Instructions
|
||||||
|
|
||||||
This repository contains the core of Home Assistant, a Python 3 based home automation application.
|
This repository contains the core of Home Assistant, a Python 3 based home automation application.
|
||||||
|
|||||||
11
.github/workflows/builder.yml
vendored
11
.github/workflows/builder.yml
vendored
@@ -14,7 +14,7 @@ env:
|
|||||||
UV_HTTP_TIMEOUT: 60
|
UV_HTTP_TIMEOUT: 60
|
||||||
UV_SYSTEM_PYTHON: "true"
|
UV_SYSTEM_PYTHON: "true"
|
||||||
# Base image version from https://github.com/home-assistant/docker
|
# Base image version from https://github.com/home-assistant/docker
|
||||||
BASE_IMAGE_VERSION: "2026.02.0"
|
BASE_IMAGE_VERSION: "2026.01.0"
|
||||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
|
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
repo: home-assistant/frontend
|
repo: home-assistant/frontend
|
||||||
@@ -123,7 +123,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download nightly wheels of intents
|
- name: Download nightly wheels of intents
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
|
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
repo: OHF-Voice/intents-package
|
repo: OHF-Voice/intents-package
|
||||||
@@ -224,6 +224,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
machine:
|
machine:
|
||||||
- generic-x86-64
|
- generic-x86-64
|
||||||
|
- intel-nuc
|
||||||
- khadas-vim3
|
- khadas-vim3
|
||||||
- odroid-c2
|
- odroid-c2
|
||||||
- odroid-c4
|
- odroid-c4
|
||||||
@@ -247,6 +248,10 @@ jobs:
|
|||||||
- machine: qemux86-64
|
- machine: qemux86-64
|
||||||
arch: amd64
|
arch: amd64
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
# TODO: remove, intel-nuc is a legacy name for x86-64, renamed in 2021
|
||||||
|
- machine: intel-nuc
|
||||||
|
arch: amd64
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|||||||
60
.github/workflows/ci.yaml
vendored
60
.github/workflows/ci.yaml
vendored
@@ -40,7 +40,7 @@ env:
|
|||||||
CACHE_VERSION: 3
|
CACHE_VERSION: 3
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2026.5"
|
HA_SHORT_VERSION: "2026.4"
|
||||||
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "key=$(lsb_release -rs)-apt-${CACHE_VERSION}-${HA_SHORT_VERSION}" >> $GITHUB_OUTPUT
|
echo "key=$(lsb_release -rs)-apt-${CACHE_VERSION}-${HA_SHORT_VERSION}" >> $GITHUB_OUTPUT
|
||||||
- name: Filter for core changes
|
- name: Filter for core changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: core
|
id: core
|
||||||
with:
|
with:
|
||||||
filters: .core_files.yaml
|
filters: .core_files.yaml
|
||||||
@@ -135,7 +135,7 @@ jobs:
|
|||||||
echo "Result:"
|
echo "Result:"
|
||||||
cat .integration_paths.yaml
|
cat .integration_paths.yaml
|
||||||
- name: Filter for integration changes
|
- name: Filter for integration changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: integrations
|
id: integrations
|
||||||
with:
|
with:
|
||||||
filters: .integration_paths.yaml
|
filters: .integration_paths.yaml
|
||||||
@@ -280,7 +280,7 @@ jobs:
|
|||||||
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
||||||
echo "::add-matcher::.github/workflows/matchers/codespell.json"
|
echo "::add-matcher::.github/workflows/matchers/codespell.json"
|
||||||
- name: Run prek
|
- name: Run prek
|
||||||
uses: j178/prek-action@79f765515bd648eb4d6bb1b17277b7cb22cb6468 # v2.0.0
|
uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1.1.1
|
||||||
env:
|
env:
|
||||||
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
|
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
|
||||||
RUFF_OUTPUT_FORMAT: github
|
RUFF_OUTPUT_FORMAT: github
|
||||||
@@ -301,7 +301,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: j178/prek-action@79f765515bd648eb4d6bb1b17277b7cb22cb6468 # v2.0.0
|
uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1.1.1
|
||||||
with:
|
with:
|
||||||
extra-args: --all-files zizmor
|
extra-args: --all-files zizmor
|
||||||
|
|
||||||
@@ -364,7 +364,7 @@ jobs:
|
|||||||
echo "key=uv-${UV_CACHE_VERSION}-${uv_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
echo "key=uv-${UV_CACHE_VERSION}-${uv_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: >-
|
key: >-
|
||||||
@@ -372,7 +372,7 @@ jobs:
|
|||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Restore uv wheel cache
|
- name: Restore uv wheel cache
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.UV_CACHE_DIR }}
|
path: ${{ env.UV_CACHE_DIR }}
|
||||||
key: >-
|
key: >-
|
||||||
@@ -384,7 +384,7 @@ jobs:
|
|||||||
env.HA_SHORT_VERSION }}-
|
env.HA_SHORT_VERSION }}-
|
||||||
- name: Check if apt cache exists
|
- name: Check if apt cache exists
|
||||||
id: cache-apt-check
|
id: cache-apt-check
|
||||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
|
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
|
||||||
path: |
|
path: |
|
||||||
@@ -430,7 +430,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Save apt cache
|
- name: Save apt cache
|
||||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -484,7 +484,7 @@ jobs:
|
|||||||
&& github.event.inputs.audit-licenses-only != 'true'
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -515,7 +515,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -552,7 +552,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -643,7 +643,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -694,7 +694,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -747,7 +747,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -804,7 +804,7 @@ jobs:
|
|||||||
echo "key=mypy-${MYPY_CACHE_VERSION}-${mypy_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
echo "key=mypy-${MYPY_CACHE_VERSION}-${mypy_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||||
- name: Restore full Python virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -812,7 +812,7 @@ jobs:
|
|||||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Restore mypy cache
|
- name: Restore mypy cache
|
||||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: .mypy_cache
|
path: .mypy_cache
|
||||||
key: >-
|
key: >-
|
||||||
@@ -854,7 +854,7 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -887,7 +887,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -930,7 +930,7 @@ jobs:
|
|||||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -964,7 +964,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1080,7 +1080,7 @@ jobs:
|
|||||||
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
|
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -1115,7 +1115,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1238,7 +1238,7 @@ jobs:
|
|||||||
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
|
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -1275,7 +1275,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1392,7 +1392,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'true'
|
if: needs.info.outputs.test_full_suite == 'true'
|
||||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
flags: full-suite
|
flags: full-suite
|
||||||
@@ -1421,7 +1421,7 @@ jobs:
|
|||||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -1455,7 +1455,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1563,7 +1563,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'false'
|
if: needs.info.outputs.test_full_suite == 'false'
|
||||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
||||||
@@ -1591,7 +1591,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
pattern: test-results-*
|
pattern: test-results-*
|
||||||
- name: Upload test results to Codecov
|
- name: Upload test results to Codecov
|
||||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||||
with:
|
with:
|
||||||
report_type: test_results
|
report_type: test_results
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
3.14.3
|
3.14.2
|
||||||
|
|||||||
@@ -137,7 +137,6 @@ homeassistant.components.calendar.*
|
|||||||
homeassistant.components.cambridge_audio.*
|
homeassistant.components.cambridge_audio.*
|
||||||
homeassistant.components.camera.*
|
homeassistant.components.camera.*
|
||||||
homeassistant.components.canary.*
|
homeassistant.components.canary.*
|
||||||
homeassistant.components.casper_glow.*
|
|
||||||
homeassistant.components.cert_expiry.*
|
homeassistant.components.cert_expiry.*
|
||||||
homeassistant.components.clickatell.*
|
homeassistant.components.clickatell.*
|
||||||
homeassistant.components.clicksend.*
|
homeassistant.components.clicksend.*
|
||||||
@@ -274,12 +273,10 @@ homeassistant.components.homekit_controller.storage
|
|||||||
homeassistant.components.homekit_controller.utils
|
homeassistant.components.homekit_controller.utils
|
||||||
homeassistant.components.homewizard.*
|
homeassistant.components.homewizard.*
|
||||||
homeassistant.components.homeworks.*
|
homeassistant.components.homeworks.*
|
||||||
homeassistant.components.hr_energy_qube.*
|
|
||||||
homeassistant.components.http.*
|
homeassistant.components.http.*
|
||||||
homeassistant.components.huawei_lte.*
|
homeassistant.components.huawei_lte.*
|
||||||
homeassistant.components.humidifier.*
|
homeassistant.components.humidifier.*
|
||||||
homeassistant.components.husqvarna_automower.*
|
homeassistant.components.husqvarna_automower.*
|
||||||
homeassistant.components.huum.*
|
|
||||||
homeassistant.components.hydrawise.*
|
homeassistant.components.hydrawise.*
|
||||||
homeassistant.components.hyperion.*
|
homeassistant.components.hyperion.*
|
||||||
homeassistant.components.hypontech.*
|
homeassistant.components.hypontech.*
|
||||||
@@ -329,7 +326,6 @@ homeassistant.components.ld2410_ble.*
|
|||||||
homeassistant.components.led_ble.*
|
homeassistant.components.led_ble.*
|
||||||
homeassistant.components.lektrico.*
|
homeassistant.components.lektrico.*
|
||||||
homeassistant.components.letpot.*
|
homeassistant.components.letpot.*
|
||||||
homeassistant.components.lg_infrared.*
|
|
||||||
homeassistant.components.libre_hardware_monitor.*
|
homeassistant.components.libre_hardware_monitor.*
|
||||||
homeassistant.components.lidarr.*
|
homeassistant.components.lidarr.*
|
||||||
homeassistant.components.lifx.*
|
homeassistant.components.lifx.*
|
||||||
|
|||||||
26
CODEOWNERS
generated
26
CODEOWNERS
generated
@@ -214,8 +214,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/balboa/ @garbled1 @natekspencer
|
/tests/components/balboa/ @garbled1 @natekspencer
|
||||||
/homeassistant/components/bang_olufsen/ @mj23000
|
/homeassistant/components/bang_olufsen/ @mj23000
|
||||||
/tests/components/bang_olufsen/ @mj23000
|
/tests/components/bang_olufsen/ @mj23000
|
||||||
/homeassistant/components/battery/ @home-assistant/core
|
|
||||||
/tests/components/battery/ @home-assistant/core
|
|
||||||
/homeassistant/components/bayesian/ @HarvsG
|
/homeassistant/components/bayesian/ @HarvsG
|
||||||
/tests/components/bayesian/ @HarvsG
|
/tests/components/bayesian/ @HarvsG
|
||||||
/homeassistant/components/beewi_smartclim/ @alemuro
|
/homeassistant/components/beewi_smartclim/ @alemuro
|
||||||
@@ -275,8 +273,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/cambridge_audio/ @noahhusby
|
/tests/components/cambridge_audio/ @noahhusby
|
||||||
/homeassistant/components/camera/ @home-assistant/core
|
/homeassistant/components/camera/ @home-assistant/core
|
||||||
/tests/components/camera/ @home-assistant/core
|
/tests/components/camera/ @home-assistant/core
|
||||||
/homeassistant/components/casper_glow/ @mikeodr
|
|
||||||
/tests/components/casper_glow/ @mikeodr
|
|
||||||
/homeassistant/components/cast/ @emontnemery
|
/homeassistant/components/cast/ @emontnemery
|
||||||
/tests/components/cast/ @emontnemery
|
/tests/components/cast/ @emontnemery
|
||||||
/homeassistant/components/ccm15/ @ocalvo
|
/homeassistant/components/ccm15/ @ocalvo
|
||||||
@@ -739,8 +735,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/homewizard/ @DCSBL
|
/tests/components/homewizard/ @DCSBL
|
||||||
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
||||||
/tests/components/honeywell/ @rdfurman @mkmer
|
/tests/components/honeywell/ @rdfurman @mkmer
|
||||||
/homeassistant/components/hr_energy_qube/ @MattieGit
|
|
||||||
/tests/components/hr_energy_qube/ @MattieGit
|
|
||||||
/homeassistant/components/html5/ @alexyao2015
|
/homeassistant/components/html5/ @alexyao2015
|
||||||
/tests/components/html5/ @alexyao2015
|
/tests/components/html5/ @alexyao2015
|
||||||
/homeassistant/components/http/ @home-assistant/core
|
/homeassistant/components/http/ @home-assistant/core
|
||||||
@@ -788,8 +782,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/igloohome/ @keithle888
|
/tests/components/igloohome/ @keithle888
|
||||||
/homeassistant/components/ign_sismologia/ @exxamalte
|
/homeassistant/components/ign_sismologia/ @exxamalte
|
||||||
/tests/components/ign_sismologia/ @exxamalte
|
/tests/components/ign_sismologia/ @exxamalte
|
||||||
/homeassistant/components/illuminance/ @home-assistant/core
|
|
||||||
/tests/components/illuminance/ @home-assistant/core
|
|
||||||
/homeassistant/components/image/ @home-assistant/core
|
/homeassistant/components/image/ @home-assistant/core
|
||||||
/tests/components/image/ @home-assistant/core
|
/tests/components/image/ @home-assistant/core
|
||||||
/homeassistant/components/image_processing/ @home-assistant/core
|
/homeassistant/components/image_processing/ @home-assistant/core
|
||||||
@@ -949,16 +941,12 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/lektrico/ @lektrico
|
/tests/components/lektrico/ @lektrico
|
||||||
/homeassistant/components/letpot/ @jpelgrom
|
/homeassistant/components/letpot/ @jpelgrom
|
||||||
/tests/components/letpot/ @jpelgrom
|
/tests/components/letpot/ @jpelgrom
|
||||||
/homeassistant/components/lg_infrared/ @home-assistant/core
|
|
||||||
/tests/components/lg_infrared/ @home-assistant/core
|
|
||||||
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
||||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||||
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
/homeassistant/components/libre_hardware_monitor/ @Sab44
|
/homeassistant/components/libre_hardware_monitor/ @Sab44
|
||||||
/tests/components/libre_hardware_monitor/ @Sab44
|
/tests/components/libre_hardware_monitor/ @Sab44
|
||||||
/homeassistant/components/lichess/ @aryanhasgithub
|
|
||||||
/tests/components/lichess/ @aryanhasgithub
|
|
||||||
/homeassistant/components/lidarr/ @tkdrob
|
/homeassistant/components/lidarr/ @tkdrob
|
||||||
/tests/components/lidarr/ @tkdrob
|
/tests/components/lidarr/ @tkdrob
|
||||||
/homeassistant/components/liebherr/ @mettolen
|
/homeassistant/components/liebherr/ @mettolen
|
||||||
@@ -1079,8 +1067,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/modern_forms/ @wonderslug
|
/tests/components/modern_forms/ @wonderslug
|
||||||
/homeassistant/components/moehlenhoff_alpha2/ @j-a-n
|
/homeassistant/components/moehlenhoff_alpha2/ @j-a-n
|
||||||
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
||||||
/homeassistant/components/moisture/ @home-assistant/core
|
|
||||||
/tests/components/moisture/ @home-assistant/core
|
|
||||||
/homeassistant/components/monarch_money/ @jeeftor
|
/homeassistant/components/monarch_money/ @jeeftor
|
||||||
/tests/components/monarch_money/ @jeeftor
|
/tests/components/monarch_money/ @jeeftor
|
||||||
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
||||||
@@ -1228,8 +1214,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/onewire/ @garbled1 @epenet
|
/tests/components/onewire/ @garbled1 @epenet
|
||||||
/homeassistant/components/onkyo/ @arturpragacz @eclair4151
|
/homeassistant/components/onkyo/ @arturpragacz @eclair4151
|
||||||
/tests/components/onkyo/ @arturpragacz @eclair4151
|
/tests/components/onkyo/ @arturpragacz @eclair4151
|
||||||
/homeassistant/components/onvif/ @jterrace
|
/homeassistant/components/onvif/ @hunterjm @jterrace
|
||||||
/tests/components/onvif/ @jterrace
|
/tests/components/onvif/ @hunterjm @jterrace
|
||||||
/homeassistant/components/open_meteo/ @frenck
|
/homeassistant/components/open_meteo/ @frenck
|
||||||
/tests/components/open_meteo/ @frenck
|
/tests/components/open_meteo/ @frenck
|
||||||
/homeassistant/components/open_router/ @joostlek
|
/homeassistant/components/open_router/ @joostlek
|
||||||
@@ -1319,8 +1305,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/poolsense/ @haemishkyd
|
/tests/components/poolsense/ @haemishkyd
|
||||||
/homeassistant/components/portainer/ @erwindouna
|
/homeassistant/components/portainer/ @erwindouna
|
||||||
/tests/components/portainer/ @erwindouna
|
/tests/components/portainer/ @erwindouna
|
||||||
/homeassistant/components/power/ @home-assistant/core
|
|
||||||
/tests/components/power/ @home-assistant/core
|
|
||||||
/homeassistant/components/powerfox/ @klaasnicolaas
|
/homeassistant/components/powerfox/ @klaasnicolaas
|
||||||
/tests/components/powerfox/ @klaasnicolaas
|
/tests/components/powerfox/ @klaasnicolaas
|
||||||
/homeassistant/components/powerfox_local/ @klaasnicolaas
|
/homeassistant/components/powerfox_local/ @klaasnicolaas
|
||||||
@@ -1606,8 +1590,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
||||||
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
|
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
|
||||||
/tests/components/solarlog/ @Ernst79 @dontinelli
|
/tests/components/solarlog/ @Ernst79 @dontinelli
|
||||||
/homeassistant/components/solarman/ @solarmanpv
|
|
||||||
/tests/components/solarman/ @solarmanpv
|
|
||||||
/homeassistant/components/solax/ @squishykid @Darsstar
|
/homeassistant/components/solax/ @squishykid @Darsstar
|
||||||
/tests/components/solax/ @squishykid @Darsstar
|
/tests/components/solax/ @squishykid @Darsstar
|
||||||
/homeassistant/components/soma/ @ratsept
|
/homeassistant/components/soma/ @ratsept
|
||||||
@@ -1717,8 +1699,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tellduslive/ @fredrike
|
/tests/components/tellduslive/ @fredrike
|
||||||
/homeassistant/components/teltonika/ @karlbeecken
|
/homeassistant/components/teltonika/ @karlbeecken
|
||||||
/tests/components/teltonika/ @karlbeecken
|
/tests/components/teltonika/ @karlbeecken
|
||||||
/homeassistant/components/temperature/ @home-assistant/core
|
|
||||||
/tests/components/temperature/ @home-assistant/core
|
|
||||||
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
||||||
/tests/components/template/ @Petro31 @home-assistant/core
|
/tests/components/template/ @Petro31 @home-assistant/core
|
||||||
/homeassistant/components/tesla_fleet/ @Bre77
|
/homeassistant/components/tesla_fleet/ @Bre77
|
||||||
@@ -1764,8 +1744,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tomorrowio/ @raman325 @lymanepp
|
/tests/components/tomorrowio/ @raman325 @lymanepp
|
||||||
/homeassistant/components/totalconnect/ @austinmroczek
|
/homeassistant/components/totalconnect/ @austinmroczek
|
||||||
/tests/components/totalconnect/ @austinmroczek
|
/tests/components/totalconnect/ @austinmroczek
|
||||||
/homeassistant/components/touchline/ @mnordseth
|
|
||||||
/tests/components/touchline/ @mnordseth
|
|
||||||
/homeassistant/components/touchline_sl/ @jnsgruk
|
/homeassistant/components/touchline_sl/ @jnsgruk
|
||||||
/tests/components/touchline_sl/ @jnsgruk
|
/tests/components/touchline_sl/ @jnsgruk
|
||||||
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
||||||
|
|||||||
2
Dockerfile
generated
2
Dockerfile
generated
@@ -29,7 +29,7 @@ RUN \
|
|||||||
# Verify go2rtc can be executed
|
# Verify go2rtc can be executed
|
||||||
go2rtc --version \
|
go2rtc --version \
|
||||||
# Install uv
|
# Install uv
|
||||||
&& pip3 install uv==0.11.1
|
&& pip3 install uv==0.10.6
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
|||||||
@@ -241,18 +241,12 @@ DEFAULT_INTEGRATIONS = {
|
|||||||
*BASE_PLATFORMS,
|
*BASE_PLATFORMS,
|
||||||
#
|
#
|
||||||
# Integrations providing triggers and conditions for base platforms:
|
# Integrations providing triggers and conditions for base platforms:
|
||||||
"air_quality",
|
|
||||||
"battery",
|
|
||||||
"door",
|
"door",
|
||||||
"garage_door",
|
"garage_door",
|
||||||
"gate",
|
"gate",
|
||||||
"humidity",
|
"humidity",
|
||||||
"illuminance",
|
|
||||||
"moisture",
|
|
||||||
"motion",
|
"motion",
|
||||||
"occupancy",
|
"occupancy",
|
||||||
"power",
|
|
||||||
"temperature",
|
|
||||||
"window",
|
"window",
|
||||||
}
|
}
|
||||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||||
@@ -468,7 +462,6 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> bool:
|
|||||||
translation.async_setup(hass)
|
translation.async_setup(hass)
|
||||||
|
|
||||||
recovery = hass.config.recovery_mode
|
recovery = hass.config.recovery_mode
|
||||||
device_registry.async_setup(hass)
|
|
||||||
try:
|
try:
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "lg",
|
"domain": "lg",
|
||||||
"name": "LG",
|
"name": "LG",
|
||||||
"integrations": [
|
"integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
|
||||||
"lg_infrared",
|
|
||||||
"lg_netcast",
|
|
||||||
"lg_soundbar",
|
|
||||||
"lg_thinq",
|
|
||||||
"webostv"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
},
|
},
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["jaraco.abode", "lomond"],
|
"loggers": ["jaraco.abode", "lomond"],
|
||||||
"requirements": ["jaraco.abode==6.4.0"],
|
"requirements": ["jaraco.abode==6.2.1"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
||||||
@@ -41,7 +40,6 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
|||||||
AbodeSensorDescription(
|
AbodeSensorDescription(
|
||||||
key="temperature",
|
key="temperature",
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
|
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
|
||||||
device.temp_unit
|
device.temp_unit
|
||||||
],
|
],
|
||||||
@@ -50,14 +48,12 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
|||||||
AbodeSensorDescription(
|
AbodeSensorDescription(
|
||||||
key="humidity",
|
key="humidity",
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
|
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
|
||||||
value_fn=lambda device: cast(float, device.humidity),
|
value_fn=lambda device: cast(float, device.humidity),
|
||||||
),
|
),
|
||||||
AbodeSensorDescription(
|
AbodeSensorDescription(
|
||||||
key="lux",
|
key="lux",
|
||||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
|
native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
|
||||||
value_fn=lambda device: cast(float, device.lux),
|
value_fn=lambda device: cast(float, device.lux),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
"""Provides conditions for air quality."""
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
|
||||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
|
||||||
BinarySensorDeviceClass,
|
|
||||||
)
|
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
|
||||||
from homeassistant.const import (
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
|
||||||
CONCENTRATION_PARTS_PER_MILLION,
|
|
||||||
STATE_OFF,
|
|
||||||
STATE_ON,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.automation import DomainSpec
|
|
||||||
from homeassistant.helpers.condition import (
|
|
||||||
Condition,
|
|
||||||
make_entity_numerical_condition,
|
|
||||||
make_entity_numerical_condition_with_unit,
|
|
||||||
make_entity_state_condition,
|
|
||||||
)
|
|
||||||
from homeassistant.util.unit_conversion import (
|
|
||||||
CarbonMonoxideConcentrationConverter,
|
|
||||||
MassVolumeConcentrationConverter,
|
|
||||||
NitrogenDioxideConcentrationConverter,
|
|
||||||
NitrogenMonoxideConcentrationConverter,
|
|
||||||
OzoneConcentrationConverter,
|
|
||||||
SulphurDioxideConcentrationConverter,
|
|
||||||
UnitlessRatioConverter,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _make_detected_condition(
|
|
||||||
device_class: BinarySensorDeviceClass,
|
|
||||||
) -> type[Condition]:
|
|
||||||
"""Create a detected condition for a binary sensor device class."""
|
|
||||||
return make_entity_state_condition(
|
|
||||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _make_cleared_condition(
|
|
||||||
device_class: BinarySensorDeviceClass,
|
|
||||||
) -> type[Condition]:
|
|
||||||
"""Create a cleared condition for a binary sensor device class."""
|
|
||||||
return make_entity_state_condition(
|
|
||||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
CONDITIONS: dict[str, type[Condition]] = {
|
|
||||||
# Binary sensor conditions (detected/cleared)
|
|
||||||
"is_gas_detected": _make_detected_condition(BinarySensorDeviceClass.GAS),
|
|
||||||
"is_gas_cleared": _make_cleared_condition(BinarySensorDeviceClass.GAS),
|
|
||||||
"is_co_detected": _make_detected_condition(BinarySensorDeviceClass.CO),
|
|
||||||
"is_co_cleared": _make_cleared_condition(BinarySensorDeviceClass.CO),
|
|
||||||
"is_smoke_detected": _make_detected_condition(BinarySensorDeviceClass.SMOKE),
|
|
||||||
"is_smoke_cleared": _make_cleared_condition(BinarySensorDeviceClass.SMOKE),
|
|
||||||
# Numerical sensor conditions with unit conversion
|
|
||||||
"is_co_value": make_entity_numerical_condition_with_unit(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
CarbonMonoxideConcentrationConverter,
|
|
||||||
),
|
|
||||||
"is_ozone_value": make_entity_numerical_condition_with_unit(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
OzoneConcentrationConverter,
|
|
||||||
),
|
|
||||||
"is_voc_value": make_entity_numerical_condition_with_unit(
|
|
||||||
{
|
|
||||||
SENSOR_DOMAIN: DomainSpec(
|
|
||||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
|
||||||
)
|
|
||||||
},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
MassVolumeConcentrationConverter,
|
|
||||||
),
|
|
||||||
"is_voc_ratio_value": make_entity_numerical_condition_with_unit(
|
|
||||||
{
|
|
||||||
SENSOR_DOMAIN: DomainSpec(
|
|
||||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
|
||||||
)
|
|
||||||
},
|
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
|
||||||
UnitlessRatioConverter,
|
|
||||||
),
|
|
||||||
"is_no_value": make_entity_numerical_condition_with_unit(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
NitrogenMonoxideConcentrationConverter,
|
|
||||||
),
|
|
||||||
"is_no2_value": make_entity_numerical_condition_with_unit(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
NitrogenDioxideConcentrationConverter,
|
|
||||||
),
|
|
||||||
"is_so2_value": make_entity_numerical_condition_with_unit(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
SulphurDioxideConcentrationConverter,
|
|
||||||
),
|
|
||||||
# Numerical sensor conditions without unit conversion (single-unit device classes)
|
|
||||||
"is_co2_value": make_entity_numerical_condition(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
|
||||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
|
||||||
),
|
|
||||||
"is_pm1_value": make_entity_numerical_condition(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"is_pm25_value": make_entity_numerical_condition(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"is_pm4_value": make_entity_numerical_condition(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"is_pm10_value": make_entity_numerical_condition(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"is_n2o_value": make_entity_numerical_condition(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
|
||||||
"""Return the air quality conditions."""
|
|
||||||
return CONDITIONS
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
# --- Common condition fields ---
|
|
||||||
|
|
||||||
.condition_behavior: &condition_behavior
|
|
||||||
required: true
|
|
||||||
default: any
|
|
||||||
selector:
|
|
||||||
select:
|
|
||||||
translation_key: condition_behavior
|
|
||||||
options:
|
|
||||||
- all
|
|
||||||
- any
|
|
||||||
|
|
||||||
# --- Unit lists for multi-unit pollutants ---
|
|
||||||
|
|
||||||
.co_units: &co_units
|
|
||||||
- "ppb"
|
|
||||||
- "ppm"
|
|
||||||
- "mg/m³"
|
|
||||||
- "μg/m³"
|
|
||||||
|
|
||||||
.ozone_units: &ozone_units
|
|
||||||
- "ppb"
|
|
||||||
- "ppm"
|
|
||||||
- "μg/m³"
|
|
||||||
|
|
||||||
.voc_units: &voc_units
|
|
||||||
- "μg/m³"
|
|
||||||
- "mg/m³"
|
|
||||||
|
|
||||||
.voc_ratio_units: &voc_ratio_units
|
|
||||||
- "ppb"
|
|
||||||
- "ppm"
|
|
||||||
|
|
||||||
.no_units: &no_units
|
|
||||||
- "ppb"
|
|
||||||
- "μg/m³"
|
|
||||||
|
|
||||||
.no2_units: &no2_units
|
|
||||||
- "ppb"
|
|
||||||
- "ppm"
|
|
||||||
- "μg/m³"
|
|
||||||
|
|
||||||
.so2_units: &so2_units
|
|
||||||
- "ppb"
|
|
||||||
- "μg/m³"
|
|
||||||
|
|
||||||
# --- Entity filter anchors ---
|
|
||||||
|
|
||||||
.co_threshold_entity: &co_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *co_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: carbon_monoxide
|
|
||||||
- domain: number
|
|
||||||
device_class: carbon_monoxide
|
|
||||||
|
|
||||||
.co2_threshold_entity: &co2_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "ppm"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: carbon_dioxide
|
|
||||||
- domain: number
|
|
||||||
device_class: carbon_dioxide
|
|
||||||
|
|
||||||
.pm1_threshold_entity: &pm1_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "μg/m³"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm1
|
|
||||||
- domain: number
|
|
||||||
device_class: pm1
|
|
||||||
|
|
||||||
.pm25_threshold_entity: &pm25_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "μg/m³"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm25
|
|
||||||
- domain: number
|
|
||||||
device_class: pm25
|
|
||||||
|
|
||||||
.pm4_threshold_entity: &pm4_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "μg/m³"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm4
|
|
||||||
- domain: number
|
|
||||||
device_class: pm4
|
|
||||||
|
|
||||||
.pm10_threshold_entity: &pm10_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "μg/m³"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm10
|
|
||||||
- domain: number
|
|
||||||
device_class: pm10
|
|
||||||
|
|
||||||
.ozone_threshold_entity: &ozone_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *ozone_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: ozone
|
|
||||||
- domain: number
|
|
||||||
device_class: ozone
|
|
||||||
|
|
||||||
.voc_threshold_entity: &voc_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *voc_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: volatile_organic_compounds
|
|
||||||
- domain: number
|
|
||||||
device_class: volatile_organic_compounds
|
|
||||||
|
|
||||||
.voc_ratio_threshold_entity: &voc_ratio_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *voc_ratio_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: volatile_organic_compounds_parts
|
|
||||||
- domain: number
|
|
||||||
device_class: volatile_organic_compounds_parts
|
|
||||||
|
|
||||||
.no_threshold_entity: &no_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *no_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: nitrogen_monoxide
|
|
||||||
- domain: number
|
|
||||||
device_class: nitrogen_monoxide
|
|
||||||
|
|
||||||
.no2_threshold_entity: &no2_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *no2_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: nitrogen_dioxide
|
|
||||||
- domain: number
|
|
||||||
device_class: nitrogen_dioxide
|
|
||||||
|
|
||||||
.n2o_threshold_entity: &n2o_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "μg/m³"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: nitrous_oxide
|
|
||||||
- domain: number
|
|
||||||
device_class: nitrous_oxide
|
|
||||||
|
|
||||||
.so2_threshold_entity: &so2_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *so2_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: sulphur_dioxide
|
|
||||||
- domain: number
|
|
||||||
device_class: sulphur_dioxide
|
|
||||||
|
|
||||||
# --- Number anchors for single-unit pollutants ---
|
|
||||||
|
|
||||||
.co2_threshold_number: &co2_threshold_number
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: "ppm"
|
|
||||||
|
|
||||||
.ugm3_threshold_number: &ugm3_threshold_number
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: "μg/m³"
|
|
||||||
|
|
||||||
# --- Binary sensor targets ---
|
|
||||||
|
|
||||||
.target_gas: &target_gas
|
|
||||||
entity:
|
|
||||||
- domain: binary_sensor
|
|
||||||
device_class: gas
|
|
||||||
|
|
||||||
.target_co_binary: &target_co_binary
|
|
||||||
entity:
|
|
||||||
- domain: binary_sensor
|
|
||||||
device_class: carbon_monoxide
|
|
||||||
|
|
||||||
.target_smoke: &target_smoke
|
|
||||||
entity:
|
|
||||||
- domain: binary_sensor
|
|
||||||
device_class: smoke
|
|
||||||
|
|
||||||
# --- Sensor targets ---
|
|
||||||
|
|
||||||
.target_co_sensor: &target_co_sensor
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: carbon_monoxide
|
|
||||||
|
|
||||||
.target_co2: &target_co2
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: carbon_dioxide
|
|
||||||
|
|
||||||
.target_pm1: &target_pm1
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm1
|
|
||||||
|
|
||||||
.target_pm25: &target_pm25
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm25
|
|
||||||
|
|
||||||
.target_pm4: &target_pm4
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm4
|
|
||||||
|
|
||||||
.target_pm10: &target_pm10
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm10
|
|
||||||
|
|
||||||
.target_ozone: &target_ozone
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: ozone
|
|
||||||
|
|
||||||
.target_voc: &target_voc
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: volatile_organic_compounds
|
|
||||||
|
|
||||||
.target_voc_ratio: &target_voc_ratio
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: volatile_organic_compounds_parts
|
|
||||||
|
|
||||||
.target_no: &target_no
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: nitrogen_monoxide
|
|
||||||
|
|
||||||
.target_no2: &target_no2
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: nitrogen_dioxide
|
|
||||||
|
|
||||||
.target_n2o: &target_n2o
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: nitrous_oxide
|
|
||||||
|
|
||||||
.target_so2: &target_so2
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: sulphur_dioxide
|
|
||||||
|
|
||||||
# --- Binary sensor conditions ---
|
|
||||||
|
|
||||||
.condition_binary_common: &condition_binary_common
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
|
|
||||||
is_gas_detected:
|
|
||||||
<<: *condition_binary_common
|
|
||||||
target: *target_gas
|
|
||||||
|
|
||||||
is_gas_cleared:
|
|
||||||
<<: *condition_binary_common
|
|
||||||
target: *target_gas
|
|
||||||
|
|
||||||
is_co_detected:
|
|
||||||
<<: *condition_binary_common
|
|
||||||
target: *target_co_binary
|
|
||||||
|
|
||||||
is_co_cleared:
|
|
||||||
<<: *condition_binary_common
|
|
||||||
target: *target_co_binary
|
|
||||||
|
|
||||||
is_smoke_detected:
|
|
||||||
<<: *condition_binary_common
|
|
||||||
target: *target_smoke
|
|
||||||
|
|
||||||
is_smoke_cleared:
|
|
||||||
<<: *condition_binary_common
|
|
||||||
target: *target_smoke
|
|
||||||
|
|
||||||
# --- Numerical sensor conditions with unit conversion ---
|
|
||||||
|
|
||||||
is_co_value:
|
|
||||||
target: *target_co_sensor
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *co_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *co_units
|
|
||||||
|
|
||||||
is_ozone_value:
|
|
||||||
target: *target_ozone
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *ozone_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *ozone_units
|
|
||||||
|
|
||||||
is_voc_value:
|
|
||||||
target: *target_voc
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *voc_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *voc_units
|
|
||||||
|
|
||||||
is_voc_ratio_value:
|
|
||||||
target: *target_voc_ratio
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *voc_ratio_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *voc_ratio_units
|
|
||||||
|
|
||||||
is_no_value:
|
|
||||||
target: *target_no
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *no_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *no_units
|
|
||||||
|
|
||||||
is_no2_value:
|
|
||||||
target: *target_no2
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *no2_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *no2_units
|
|
||||||
|
|
||||||
is_so2_value:
|
|
||||||
target: *target_so2
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *so2_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *so2_units
|
|
||||||
|
|
||||||
# --- Numerical sensor conditions without unit conversion ---
|
|
||||||
|
|
||||||
is_co2_value:
|
|
||||||
target: *target_co2
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *co2_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number: *co2_threshold_number
|
|
||||||
|
|
||||||
is_pm1_value:
|
|
||||||
target: *target_pm1
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *pm1_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
is_pm25_value:
|
|
||||||
target: *target_pm25
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *pm25_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
is_pm4_value:
|
|
||||||
target: *target_pm4
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *pm4_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
is_pm10_value:
|
|
||||||
target: *target_pm10
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *pm10_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
is_n2o_value:
|
|
||||||
target: *target_n2o
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *n2o_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
@@ -1,164 +1,7 @@
|
|||||||
{
|
{
|
||||||
"conditions": {
|
|
||||||
"is_co2_value": {
|
|
||||||
"condition": "mdi:molecule-co2"
|
|
||||||
},
|
|
||||||
"is_co_cleared": {
|
|
||||||
"condition": "mdi:check-circle"
|
|
||||||
},
|
|
||||||
"is_co_detected": {
|
|
||||||
"condition": "mdi:molecule-co"
|
|
||||||
},
|
|
||||||
"is_co_value": {
|
|
||||||
"condition": "mdi:molecule-co"
|
|
||||||
},
|
|
||||||
"is_gas_cleared": {
|
|
||||||
"condition": "mdi:check-circle"
|
|
||||||
},
|
|
||||||
"is_gas_detected": {
|
|
||||||
"condition": "mdi:gas-cylinder"
|
|
||||||
},
|
|
||||||
"is_n2o_value": {
|
|
||||||
"condition": "mdi:factory"
|
|
||||||
},
|
|
||||||
"is_no2_value": {
|
|
||||||
"condition": "mdi:factory"
|
|
||||||
},
|
|
||||||
"is_no_value": {
|
|
||||||
"condition": "mdi:factory"
|
|
||||||
},
|
|
||||||
"is_ozone_value": {
|
|
||||||
"condition": "mdi:weather-sunny-alert"
|
|
||||||
},
|
|
||||||
"is_pm10_value": {
|
|
||||||
"condition": "mdi:blur"
|
|
||||||
},
|
|
||||||
"is_pm1_value": {
|
|
||||||
"condition": "mdi:blur"
|
|
||||||
},
|
|
||||||
"is_pm25_value": {
|
|
||||||
"condition": "mdi:blur"
|
|
||||||
},
|
|
||||||
"is_pm4_value": {
|
|
||||||
"condition": "mdi:blur"
|
|
||||||
},
|
|
||||||
"is_smoke_cleared": {
|
|
||||||
"condition": "mdi:check-circle"
|
|
||||||
},
|
|
||||||
"is_smoke_detected": {
|
|
||||||
"condition": "mdi:smoke-detector-variant"
|
|
||||||
},
|
|
||||||
"is_so2_value": {
|
|
||||||
"condition": "mdi:factory"
|
|
||||||
},
|
|
||||||
"is_voc_ratio_value": {
|
|
||||||
"condition": "mdi:air-filter"
|
|
||||||
},
|
|
||||||
"is_voc_value": {
|
|
||||||
"condition": "mdi:air-filter"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity_component": {
|
"entity_component": {
|
||||||
"_": {
|
"_": {
|
||||||
"default": "mdi:air-filter"
|
"default": "mdi:air-filter"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"triggers": {
|
|
||||||
"co2_changed": {
|
|
||||||
"trigger": "mdi:molecule-co2"
|
|
||||||
},
|
|
||||||
"co2_crossed_threshold": {
|
|
||||||
"trigger": "mdi:molecule-co2"
|
|
||||||
},
|
|
||||||
"co_changed": {
|
|
||||||
"trigger": "mdi:molecule-co"
|
|
||||||
},
|
|
||||||
"co_cleared": {
|
|
||||||
"trigger": "mdi:check-circle"
|
|
||||||
},
|
|
||||||
"co_crossed_threshold": {
|
|
||||||
"trigger": "mdi:molecule-co"
|
|
||||||
},
|
|
||||||
"co_detected": {
|
|
||||||
"trigger": "mdi:molecule-co"
|
|
||||||
},
|
|
||||||
"gas_cleared": {
|
|
||||||
"trigger": "mdi:check-circle"
|
|
||||||
},
|
|
||||||
"gas_detected": {
|
|
||||||
"trigger": "mdi:gas-cylinder"
|
|
||||||
},
|
|
||||||
"n2o_changed": {
|
|
||||||
"trigger": "mdi:factory"
|
|
||||||
},
|
|
||||||
"n2o_crossed_threshold": {
|
|
||||||
"trigger": "mdi:factory"
|
|
||||||
},
|
|
||||||
"no2_changed": {
|
|
||||||
"trigger": "mdi:factory"
|
|
||||||
},
|
|
||||||
"no2_crossed_threshold": {
|
|
||||||
"trigger": "mdi:factory"
|
|
||||||
},
|
|
||||||
"no_changed": {
|
|
||||||
"trigger": "mdi:factory"
|
|
||||||
},
|
|
||||||
"no_crossed_threshold": {
|
|
||||||
"trigger": "mdi:factory"
|
|
||||||
},
|
|
||||||
"ozone_changed": {
|
|
||||||
"trigger": "mdi:weather-sunny-alert"
|
|
||||||
},
|
|
||||||
"ozone_crossed_threshold": {
|
|
||||||
"trigger": "mdi:weather-sunny-alert"
|
|
||||||
},
|
|
||||||
"pm10_changed": {
|
|
||||||
"trigger": "mdi:blur"
|
|
||||||
},
|
|
||||||
"pm10_crossed_threshold": {
|
|
||||||
"trigger": "mdi:blur"
|
|
||||||
},
|
|
||||||
"pm1_changed": {
|
|
||||||
"trigger": "mdi:blur"
|
|
||||||
},
|
|
||||||
"pm1_crossed_threshold": {
|
|
||||||
"trigger": "mdi:blur"
|
|
||||||
},
|
|
||||||
"pm25_changed": {
|
|
||||||
"trigger": "mdi:blur"
|
|
||||||
},
|
|
||||||
"pm25_crossed_threshold": {
|
|
||||||
"trigger": "mdi:blur"
|
|
||||||
},
|
|
||||||
"pm4_changed": {
|
|
||||||
"trigger": "mdi:blur"
|
|
||||||
},
|
|
||||||
"pm4_crossed_threshold": {
|
|
||||||
"trigger": "mdi:blur"
|
|
||||||
},
|
|
||||||
"smoke_cleared": {
|
|
||||||
"trigger": "mdi:check-circle"
|
|
||||||
},
|
|
||||||
"smoke_detected": {
|
|
||||||
"trigger": "mdi:smoke-detector-variant"
|
|
||||||
},
|
|
||||||
"so2_changed": {
|
|
||||||
"trigger": "mdi:factory"
|
|
||||||
},
|
|
||||||
"so2_crossed_threshold": {
|
|
||||||
"trigger": "mdi:factory"
|
|
||||||
},
|
|
||||||
"voc_changed": {
|
|
||||||
"trigger": "mdi:air-filter"
|
|
||||||
},
|
|
||||||
"voc_crossed_threshold": {
|
|
||||||
"trigger": "mdi:air-filter"
|
|
||||||
},
|
|
||||||
"voc_ratio_changed": {
|
|
||||||
"trigger": "mdi:air-filter"
|
|
||||||
},
|
|
||||||
"voc_ratio_crossed_threshold": {
|
|
||||||
"trigger": "mdi:air-filter"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,647 +0,0 @@
|
|||||||
{
|
|
||||||
"common": {
|
|
||||||
"condition_behavior_description": "How the value should match on the targeted entities.",
|
|
||||||
"condition_behavior_name": "Behavior",
|
|
||||||
"condition_threshold_description": "What to test for and threshold values.",
|
|
||||||
"condition_threshold_name": "Threshold configuration",
|
|
||||||
"trigger_behavior_description": "The behavior of the targeted entities to trigger on.",
|
|
||||||
"trigger_behavior_name": "Behavior",
|
|
||||||
"trigger_threshold_changed_description": "Which changes to trigger on and threshold values.",
|
|
||||||
"trigger_threshold_crossed_description": "Which threshold crossing to trigger on and threshold values.",
|
|
||||||
"trigger_threshold_name": "Threshold configuration"
|
|
||||||
},
|
|
||||||
"conditions": {
|
|
||||||
"is_co2_value": {
|
|
||||||
"description": "Tests the carbon dioxide level of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Carbon dioxide value"
|
|
||||||
},
|
|
||||||
"is_co_cleared": {
|
|
||||||
"description": "Tests if one or more carbon monoxide sensors are cleared.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Carbon monoxide cleared"
|
|
||||||
},
|
|
||||||
"is_co_detected": {
|
|
||||||
"description": "Tests if one or more carbon monoxide sensors are detecting carbon monoxide.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Carbon monoxide detected"
|
|
||||||
},
|
|
||||||
"is_co_value": {
|
|
||||||
"description": "Tests the carbon monoxide level of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Carbon monoxide value"
|
|
||||||
},
|
|
||||||
"is_gas_cleared": {
|
|
||||||
"description": "Tests if one or more gas sensors are cleared.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Gas cleared"
|
|
||||||
},
|
|
||||||
"is_gas_detected": {
|
|
||||||
"description": "Tests if one or more gas sensors are detecting gas.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Gas detected"
|
|
||||||
},
|
|
||||||
"is_n2o_value": {
|
|
||||||
"description": "Tests the nitrous oxide level of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Nitrous oxide value"
|
|
||||||
},
|
|
||||||
"is_no2_value": {
|
|
||||||
"description": "Tests the nitrogen dioxide level of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Nitrogen dioxide value"
|
|
||||||
},
|
|
||||||
"is_no_value": {
|
|
||||||
"description": "Tests the nitrogen monoxide level of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Nitrogen monoxide value"
|
|
||||||
},
|
|
||||||
"is_ozone_value": {
|
|
||||||
"description": "Tests the ozone level of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Ozone value"
|
|
||||||
},
|
|
||||||
"is_pm10_value": {
|
|
||||||
"description": "Tests the PM10 level of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "PM10 value"
|
|
||||||
},
|
|
||||||
"is_pm1_value": {
|
|
||||||
"description": "Tests the PM1 level of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "PM1 value"
|
|
||||||
},
|
|
||||||
"is_pm25_value": {
|
|
||||||
"description": "Tests the PM2.5 level of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "PM2.5 value"
|
|
||||||
},
|
|
||||||
"is_pm4_value": {
|
|
||||||
"description": "Tests the PM4 level of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "PM4 value"
|
|
||||||
},
|
|
||||||
"is_smoke_cleared": {
|
|
||||||
"description": "Tests if one or more smoke sensors are cleared.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Smoke cleared"
|
|
||||||
},
|
|
||||||
"is_smoke_detected": {
|
|
||||||
"description": "Tests if one or more smoke sensors are detecting smoke.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Smoke detected"
|
|
||||||
},
|
|
||||||
"is_so2_value": {
|
|
||||||
"description": "Tests the sulphur dioxide level of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Sulphur dioxide value"
|
|
||||||
},
|
|
||||||
"is_voc_ratio_value": {
|
|
||||||
"description": "Tests the volatile organic compounds ratio of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Volatile organic compounds ratio value"
|
|
||||||
},
|
|
||||||
"is_voc_value": {
|
|
||||||
"description": "Tests the volatile organic compounds level of one or more entities.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Volatile organic compounds value"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"selector": {
|
|
||||||
"condition_behavior": {
|
|
||||||
"options": {
|
|
||||||
"all": "All",
|
|
||||||
"any": "Any"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"trigger_behavior": {
|
|
||||||
"options": {
|
|
||||||
"any": "Any",
|
|
||||||
"first": "First",
|
|
||||||
"last": "Last"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Air Quality",
|
|
||||||
"triggers": {
|
|
||||||
"co2_changed": {
|
|
||||||
"description": "Triggers after one or more carbon dioxide levels change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Carbon dioxide level changed"
|
|
||||||
},
|
|
||||||
"co2_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more carbon dioxide levels cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Carbon dioxide level crossed threshold"
|
|
||||||
},
|
|
||||||
"co_changed": {
|
|
||||||
"description": "Triggers after one or more carbon monoxide levels change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Carbon monoxide level changed"
|
|
||||||
},
|
|
||||||
"co_cleared": {
|
|
||||||
"description": "Triggers after one or more carbon monoxide sensors stop detecting carbon monoxide.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Carbon monoxide cleared"
|
|
||||||
},
|
|
||||||
"co_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more carbon monoxide levels cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Carbon monoxide level crossed threshold"
|
|
||||||
},
|
|
||||||
"co_detected": {
|
|
||||||
"description": "Triggers after one or more carbon monoxide sensors start detecting carbon monoxide.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Carbon monoxide detected"
|
|
||||||
},
|
|
||||||
"gas_cleared": {
|
|
||||||
"description": "Triggers after one or more gas sensors stop detecting gas.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Gas cleared"
|
|
||||||
},
|
|
||||||
"gas_detected": {
|
|
||||||
"description": "Triggers after one or more gas sensors start detecting gas.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Gas detected"
|
|
||||||
},
|
|
||||||
"n2o_changed": {
|
|
||||||
"description": "Triggers after one or more nitrous oxide levels change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Nitrous oxide level changed"
|
|
||||||
},
|
|
||||||
"n2o_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more nitrous oxide levels cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Nitrous oxide level crossed threshold"
|
|
||||||
},
|
|
||||||
"no2_changed": {
|
|
||||||
"description": "Triggers after one or more nitrogen dioxide levels change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Nitrogen dioxide level changed"
|
|
||||||
},
|
|
||||||
"no2_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more nitrogen dioxide levels cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Nitrogen dioxide level crossed threshold"
|
|
||||||
},
|
|
||||||
"no_changed": {
|
|
||||||
"description": "Triggers after one or more nitrogen monoxide levels change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Nitrogen monoxide level changed"
|
|
||||||
},
|
|
||||||
"no_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more nitrogen monoxide levels cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Nitrogen monoxide level crossed threshold"
|
|
||||||
},
|
|
||||||
"ozone_changed": {
|
|
||||||
"description": "Triggers after one or more ozone levels change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Ozone level changed"
|
|
||||||
},
|
|
||||||
"ozone_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more ozone levels cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Ozone level crossed threshold"
|
|
||||||
},
|
|
||||||
"pm10_changed": {
|
|
||||||
"description": "Triggers after one or more PM10 levels change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "PM10 level changed"
|
|
||||||
},
|
|
||||||
"pm10_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more PM10 levels cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "PM10 level crossed threshold"
|
|
||||||
},
|
|
||||||
"pm1_changed": {
|
|
||||||
"description": "Triggers after one or more PM1 levels change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "PM1 level changed"
|
|
||||||
},
|
|
||||||
"pm1_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more PM1 levels cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "PM1 level crossed threshold"
|
|
||||||
},
|
|
||||||
"pm25_changed": {
|
|
||||||
"description": "Triggers after one or more PM2.5 levels change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "PM2.5 level changed"
|
|
||||||
},
|
|
||||||
"pm25_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more PM2.5 levels cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "PM2.5 level crossed threshold"
|
|
||||||
},
|
|
||||||
"pm4_changed": {
|
|
||||||
"description": "Triggers after one or more PM4 levels change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "PM4 level changed"
|
|
||||||
},
|
|
||||||
"pm4_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more PM4 levels cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "PM4 level crossed threshold"
|
|
||||||
},
|
|
||||||
"smoke_cleared": {
|
|
||||||
"description": "Triggers after one or more smoke sensors stop detecting smoke.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Smoke cleared"
|
|
||||||
},
|
|
||||||
"smoke_detected": {
|
|
||||||
"description": "Triggers after one or more smoke sensors start detecting smoke.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Smoke detected"
|
|
||||||
},
|
|
||||||
"so2_changed": {
|
|
||||||
"description": "Triggers after one or more sulphur dioxide levels change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Sulphur dioxide level changed"
|
|
||||||
},
|
|
||||||
"so2_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more sulphur dioxide levels cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Sulphur dioxide level crossed threshold"
|
|
||||||
},
|
|
||||||
"voc_changed": {
|
|
||||||
"description": "Triggers after one or more volatile organic compound levels change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Volatile organic compounds level changed"
|
|
||||||
},
|
|
||||||
"voc_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more volatile organic compounds levels cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Volatile organic compounds level crossed threshold"
|
|
||||||
},
|
|
||||||
"voc_ratio_changed": {
|
|
||||||
"description": "Triggers after one or more volatile organic compound ratios change.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Volatile organic compounds ratio changed"
|
|
||||||
},
|
|
||||||
"voc_ratio_crossed_threshold": {
|
|
||||||
"description": "Triggers after one or more volatile organic compounds ratios cross a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::air_quality::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Volatile organic compounds ratio crossed threshold"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
"""Provides triggers for air quality."""
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
|
||||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
|
||||||
BinarySensorDeviceClass,
|
|
||||||
)
|
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
|
||||||
from homeassistant.const import (
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
|
||||||
CONCENTRATION_PARTS_PER_MILLION,
|
|
||||||
STATE_OFF,
|
|
||||||
STATE_ON,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.automation import DomainSpec
|
|
||||||
from homeassistant.helpers.trigger import (
|
|
||||||
EntityTargetStateTriggerBase,
|
|
||||||
Trigger,
|
|
||||||
make_entity_numerical_state_changed_trigger,
|
|
||||||
make_entity_numerical_state_changed_with_unit_trigger,
|
|
||||||
make_entity_numerical_state_crossed_threshold_trigger,
|
|
||||||
make_entity_numerical_state_crossed_threshold_with_unit_trigger,
|
|
||||||
make_entity_target_state_trigger,
|
|
||||||
)
|
|
||||||
from homeassistant.util.unit_conversion import (
|
|
||||||
CarbonMonoxideConcentrationConverter,
|
|
||||||
MassVolumeConcentrationConverter,
|
|
||||||
NitrogenDioxideConcentrationConverter,
|
|
||||||
NitrogenMonoxideConcentrationConverter,
|
|
||||||
OzoneConcentrationConverter,
|
|
||||||
SulphurDioxideConcentrationConverter,
|
|
||||||
UnitlessRatioConverter,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _make_detected_trigger(
|
|
||||||
device_class: BinarySensorDeviceClass,
|
|
||||||
) -> type[EntityTargetStateTriggerBase]:
|
|
||||||
"""Create a detected trigger for a binary sensor device class."""
|
|
||||||
|
|
||||||
return make_entity_target_state_trigger(
|
|
||||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _make_cleared_trigger(
|
|
||||||
device_class: BinarySensorDeviceClass,
|
|
||||||
) -> type[EntityTargetStateTriggerBase]:
|
|
||||||
"""Create a cleared trigger for a binary sensor device class."""
|
|
||||||
|
|
||||||
return make_entity_target_state_trigger(
|
|
||||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
TRIGGERS: dict[str, type[Trigger]] = {
|
|
||||||
# Binary sensor triggers (detected/cleared)
|
|
||||||
"gas_detected": _make_detected_trigger(BinarySensorDeviceClass.GAS),
|
|
||||||
"gas_cleared": _make_cleared_trigger(BinarySensorDeviceClass.GAS),
|
|
||||||
"co_detected": _make_detected_trigger(BinarySensorDeviceClass.CO),
|
|
||||||
"co_cleared": _make_cleared_trigger(BinarySensorDeviceClass.CO),
|
|
||||||
"smoke_detected": _make_detected_trigger(BinarySensorDeviceClass.SMOKE),
|
|
||||||
"smoke_cleared": _make_cleared_trigger(BinarySensorDeviceClass.SMOKE),
|
|
||||||
# Numerical sensor triggers with unit conversion
|
|
||||||
"co_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
CarbonMonoxideConcentrationConverter,
|
|
||||||
),
|
|
||||||
"co_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
CarbonMonoxideConcentrationConverter,
|
|
||||||
),
|
|
||||||
"ozone_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
OzoneConcentrationConverter,
|
|
||||||
),
|
|
||||||
"ozone_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
OzoneConcentrationConverter,
|
|
||||||
),
|
|
||||||
"voc_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
|
||||||
{
|
|
||||||
SENSOR_DOMAIN: DomainSpec(
|
|
||||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
|
||||||
)
|
|
||||||
},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
MassVolumeConcentrationConverter,
|
|
||||||
),
|
|
||||||
"voc_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
|
||||||
{
|
|
||||||
SENSOR_DOMAIN: DomainSpec(
|
|
||||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
|
||||||
)
|
|
||||||
},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
MassVolumeConcentrationConverter,
|
|
||||||
),
|
|
||||||
"voc_ratio_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
|
||||||
{
|
|
||||||
SENSOR_DOMAIN: DomainSpec(
|
|
||||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
|
||||||
)
|
|
||||||
},
|
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
|
||||||
UnitlessRatioConverter,
|
|
||||||
),
|
|
||||||
"voc_ratio_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
|
||||||
{
|
|
||||||
SENSOR_DOMAIN: DomainSpec(
|
|
||||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
|
||||||
)
|
|
||||||
},
|
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
|
||||||
UnitlessRatioConverter,
|
|
||||||
),
|
|
||||||
"no_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
NitrogenMonoxideConcentrationConverter,
|
|
||||||
),
|
|
||||||
"no_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
NitrogenMonoxideConcentrationConverter,
|
|
||||||
),
|
|
||||||
"no2_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
NitrogenDioxideConcentrationConverter,
|
|
||||||
),
|
|
||||||
"no2_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
NitrogenDioxideConcentrationConverter,
|
|
||||||
),
|
|
||||||
"so2_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
SulphurDioxideConcentrationConverter,
|
|
||||||
),
|
|
||||||
"so2_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
SulphurDioxideConcentrationConverter,
|
|
||||||
),
|
|
||||||
# Numerical sensor triggers without unit conversion (single-unit device classes)
|
|
||||||
"co2_changed": make_entity_numerical_state_changed_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
|
||||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
|
||||||
),
|
|
||||||
"co2_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
|
||||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
|
||||||
),
|
|
||||||
"pm1_changed": make_entity_numerical_state_changed_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"pm1_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"pm25_changed": make_entity_numerical_state_changed_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"pm25_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"pm4_changed": make_entity_numerical_state_changed_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"pm4_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"pm10_changed": make_entity_numerical_state_changed_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"pm10_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"n2o_changed": make_entity_numerical_state_changed_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
"n2o_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
|
||||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
|
||||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
|
||||||
"""Return the triggers for air quality."""
|
|
||||||
return TRIGGERS
|
|
||||||
@@ -1,617 +0,0 @@
|
|||||||
.trigger_common_fields:
|
|
||||||
behavior: &trigger_behavior
|
|
||||||
required: true
|
|
||||||
default: any
|
|
||||||
selector:
|
|
||||||
select:
|
|
||||||
translation_key: trigger_behavior
|
|
||||||
options:
|
|
||||||
- first
|
|
||||||
- last
|
|
||||||
- any
|
|
||||||
|
|
||||||
# --- Unit lists for multi-unit pollutants ---
|
|
||||||
|
|
||||||
.co_units: &co_units
|
|
||||||
- "ppb"
|
|
||||||
- "ppm"
|
|
||||||
- "mg/m³"
|
|
||||||
- "μg/m³"
|
|
||||||
|
|
||||||
.ozone_units: &ozone_units
|
|
||||||
- "ppb"
|
|
||||||
- "ppm"
|
|
||||||
- "μg/m³"
|
|
||||||
|
|
||||||
.voc_units: &voc_units
|
|
||||||
- "μg/m³"
|
|
||||||
- "mg/m³"
|
|
||||||
|
|
||||||
.voc_ratio_units: &voc_ratio_units
|
|
||||||
- "ppb"
|
|
||||||
- "ppm"
|
|
||||||
|
|
||||||
.no_units: &no_units
|
|
||||||
- "ppb"
|
|
||||||
- "μg/m³"
|
|
||||||
|
|
||||||
.no2_units: &no2_units
|
|
||||||
- "ppb"
|
|
||||||
- "ppm"
|
|
||||||
- "μg/m³"
|
|
||||||
|
|
||||||
.so2_units: &so2_units
|
|
||||||
- "ppb"
|
|
||||||
- "μg/m³"
|
|
||||||
|
|
||||||
# --- Entity filter anchors ---
|
|
||||||
|
|
||||||
.co_threshold_entity: &co_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *co_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: carbon_monoxide
|
|
||||||
- domain: number
|
|
||||||
device_class: carbon_monoxide
|
|
||||||
|
|
||||||
.co2_threshold_entity: &co2_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "ppm"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: carbon_dioxide
|
|
||||||
- domain: number
|
|
||||||
device_class: carbon_dioxide
|
|
||||||
|
|
||||||
.pm1_threshold_entity: &pm1_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "μg/m³"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm1
|
|
||||||
- domain: number
|
|
||||||
device_class: pm1
|
|
||||||
|
|
||||||
.pm25_threshold_entity: &pm25_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "μg/m³"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm25
|
|
||||||
- domain: number
|
|
||||||
device_class: pm25
|
|
||||||
|
|
||||||
.pm4_threshold_entity: &pm4_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "μg/m³"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm4
|
|
||||||
- domain: number
|
|
||||||
device_class: pm4
|
|
||||||
|
|
||||||
.pm10_threshold_entity: &pm10_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "μg/m³"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm10
|
|
||||||
- domain: number
|
|
||||||
device_class: pm10
|
|
||||||
|
|
||||||
.ozone_threshold_entity: &ozone_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *ozone_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: ozone
|
|
||||||
- domain: number
|
|
||||||
device_class: ozone
|
|
||||||
|
|
||||||
.voc_threshold_entity: &voc_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *voc_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: volatile_organic_compounds
|
|
||||||
- domain: number
|
|
||||||
device_class: volatile_organic_compounds
|
|
||||||
|
|
||||||
.voc_ratio_threshold_entity: &voc_ratio_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *voc_ratio_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: volatile_organic_compounds_parts
|
|
||||||
- domain: number
|
|
||||||
device_class: volatile_organic_compounds_parts
|
|
||||||
|
|
||||||
.no_threshold_entity: &no_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *no_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: nitrogen_monoxide
|
|
||||||
- domain: number
|
|
||||||
device_class: nitrogen_monoxide
|
|
||||||
|
|
||||||
.no2_threshold_entity: &no2_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *no2_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: nitrogen_dioxide
|
|
||||||
- domain: number
|
|
||||||
device_class: nitrogen_dioxide
|
|
||||||
|
|
||||||
.n2o_threshold_entity: &n2o_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "μg/m³"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: nitrous_oxide
|
|
||||||
- domain: number
|
|
||||||
device_class: nitrous_oxide
|
|
||||||
|
|
||||||
.so2_threshold_entity: &so2_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *so2_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: sulphur_dioxide
|
|
||||||
- domain: number
|
|
||||||
device_class: sulphur_dioxide
|
|
||||||
|
|
||||||
# --- Number anchors for single-unit pollutants ---
|
|
||||||
|
|
||||||
.co2_threshold_number: &co2_threshold_number
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: "ppm"
|
|
||||||
|
|
||||||
.ugm3_threshold_number: &ugm3_threshold_number
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: "μg/m³"
|
|
||||||
|
|
||||||
# Binary sensor detected/cleared trigger fields
|
|
||||||
.trigger_binary_fields: &trigger_binary_fields
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
|
|
||||||
# --- Binary sensor targets ---
|
|
||||||
|
|
||||||
.target_gas: &target_gas
|
|
||||||
entity:
|
|
||||||
- domain: binary_sensor
|
|
||||||
device_class: gas
|
|
||||||
|
|
||||||
.target_co_binary: &target_co_binary
|
|
||||||
entity:
|
|
||||||
- domain: binary_sensor
|
|
||||||
device_class: carbon_monoxide
|
|
||||||
|
|
||||||
.target_smoke: &target_smoke
|
|
||||||
entity:
|
|
||||||
- domain: binary_sensor
|
|
||||||
device_class: smoke
|
|
||||||
|
|
||||||
# --- Sensor targets ---
|
|
||||||
|
|
||||||
.target_co_sensor: &target_co_sensor
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: carbon_monoxide
|
|
||||||
|
|
||||||
.target_co2: &target_co2
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: carbon_dioxide
|
|
||||||
|
|
||||||
.target_pm1: &target_pm1
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm1
|
|
||||||
|
|
||||||
.target_pm25: &target_pm25
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm25
|
|
||||||
|
|
||||||
.target_pm4: &target_pm4
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm4
|
|
||||||
|
|
||||||
.target_pm10: &target_pm10
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: pm10
|
|
||||||
|
|
||||||
.target_ozone: &target_ozone
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: ozone
|
|
||||||
|
|
||||||
.target_voc: &target_voc
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: volatile_organic_compounds
|
|
||||||
|
|
||||||
.target_voc_ratio: &target_voc_ratio
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: volatile_organic_compounds_parts
|
|
||||||
|
|
||||||
.target_no: &target_no
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: nitrogen_monoxide
|
|
||||||
|
|
||||||
.target_no2: &target_no2
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: nitrogen_dioxide
|
|
||||||
|
|
||||||
.target_n2o: &target_n2o
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: nitrous_oxide
|
|
||||||
|
|
||||||
.target_so2: &target_so2
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: sulphur_dioxide
|
|
||||||
|
|
||||||
# --- Binary sensor triggers ---
|
|
||||||
|
|
||||||
gas_detected:
|
|
||||||
fields: *trigger_binary_fields
|
|
||||||
target: *target_gas
|
|
||||||
|
|
||||||
gas_cleared:
|
|
||||||
fields: *trigger_binary_fields
|
|
||||||
target: *target_gas
|
|
||||||
|
|
||||||
co_detected:
|
|
||||||
fields: *trigger_binary_fields
|
|
||||||
target: *target_co_binary
|
|
||||||
|
|
||||||
co_cleared:
|
|
||||||
fields: *trigger_binary_fields
|
|
||||||
target: *target_co_binary
|
|
||||||
|
|
||||||
smoke_detected:
|
|
||||||
fields: *trigger_binary_fields
|
|
||||||
target: *target_smoke
|
|
||||||
|
|
||||||
smoke_cleared:
|
|
||||||
fields: *trigger_binary_fields
|
|
||||||
target: *target_smoke
|
|
||||||
|
|
||||||
# --- Numerical sensor triggers ---
|
|
||||||
|
|
||||||
# CO (multi-unit)
|
|
||||||
co_changed:
|
|
||||||
target: *target_co_sensor
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *co_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *co_units
|
|
||||||
|
|
||||||
co_crossed_threshold:
|
|
||||||
target: *target_co_sensor
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *co_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *co_units
|
|
||||||
|
|
||||||
# CO2 (single-unit: ppm)
|
|
||||||
co2_changed:
|
|
||||||
target: *target_co2
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *co2_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number: *co2_threshold_number
|
|
||||||
|
|
||||||
co2_crossed_threshold:
|
|
||||||
target: *target_co2
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *co2_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number: *co2_threshold_number
|
|
||||||
|
|
||||||
# PM1 (single-unit: μg/m³)
|
|
||||||
pm1_changed:
|
|
||||||
target: *target_pm1
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *pm1_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
pm1_crossed_threshold:
|
|
||||||
target: *target_pm1
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *pm1_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
# PM2.5 (single-unit: μg/m³)
|
|
||||||
pm25_changed:
|
|
||||||
target: *target_pm25
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *pm25_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
pm25_crossed_threshold:
|
|
||||||
target: *target_pm25
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *pm25_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
# PM4 (single-unit: μg/m³)
|
|
||||||
pm4_changed:
|
|
||||||
target: *target_pm4
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *pm4_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
pm4_crossed_threshold:
|
|
||||||
target: *target_pm4
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *pm4_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
# PM10 (single-unit: μg/m³)
|
|
||||||
pm10_changed:
|
|
||||||
target: *target_pm10
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *pm10_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
pm10_crossed_threshold:
|
|
||||||
target: *target_pm10
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *pm10_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
# Ozone (multi-unit)
|
|
||||||
ozone_changed:
|
|
||||||
target: *target_ozone
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *ozone_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *ozone_units
|
|
||||||
|
|
||||||
ozone_crossed_threshold:
|
|
||||||
target: *target_ozone
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *ozone_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *ozone_units
|
|
||||||
|
|
||||||
# VOC (multi-unit)
|
|
||||||
voc_changed:
|
|
||||||
target: *target_voc
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *voc_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *voc_units
|
|
||||||
|
|
||||||
voc_crossed_threshold:
|
|
||||||
target: *target_voc
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *voc_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *voc_units
|
|
||||||
|
|
||||||
# VOC ratio (multi-unit)
|
|
||||||
voc_ratio_changed:
|
|
||||||
target: *target_voc_ratio
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *voc_ratio_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *voc_ratio_units
|
|
||||||
|
|
||||||
voc_ratio_crossed_threshold:
|
|
||||||
target: *target_voc_ratio
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *voc_ratio_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *voc_ratio_units
|
|
||||||
|
|
||||||
# NO (multi-unit)
|
|
||||||
no_changed:
|
|
||||||
target: *target_no
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *no_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *no_units
|
|
||||||
|
|
||||||
no_crossed_threshold:
|
|
||||||
target: *target_no
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *no_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *no_units
|
|
||||||
|
|
||||||
# NO2 (multi-unit)
|
|
||||||
no2_changed:
|
|
||||||
target: *target_no2
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *no2_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *no2_units
|
|
||||||
|
|
||||||
no2_crossed_threshold:
|
|
||||||
target: *target_no2
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *no2_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *no2_units
|
|
||||||
|
|
||||||
# N2O (single-unit: μg/m³)
|
|
||||||
n2o_changed:
|
|
||||||
target: *target_n2o
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *n2o_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
n2o_crossed_threshold:
|
|
||||||
target: *target_n2o
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *n2o_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number: *ugm3_threshold_number
|
|
||||||
|
|
||||||
# SO2 (multi-unit)
|
|
||||||
so2_changed:
|
|
||||||
target: *target_so2
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *so2_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *so2_units
|
|
||||||
|
|
||||||
so2_crossed_threshold:
|
|
||||||
target: *target_so2
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *so2_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *so2_units
|
|
||||||
@@ -87,7 +87,7 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.debug("Successfully connected to %s", user_input[CONF_IP_ADDRESS])
|
_LOGGER.debug("Successfully connected to %s", user_input[CONF_IP_ADDRESS])
|
||||||
|
|
||||||
device_info = await airq.fetch_device_info()
|
device_info = await airq.fetch_device_info()
|
||||||
await self.async_set_unique_id(device_info["id"], raise_on_progress=False)
|
await self.async_set_unique_id(device_info["id"])
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
_LOGGER.debug("Creating an entry for %s", device_info["name"])
|
_LOGGER.debug("Creating an entry for %s", device_info["name"])
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
"""Diagnostics support for air-Q."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
|
||||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_UNIQUE_ID
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from . import AirQConfigEntry
|
|
||||||
|
|
||||||
REDACT_CONFIG = {CONF_PASSWORD, CONF_UNIQUE_ID, CONF_IP_ADDRESS, "title"}
|
|
||||||
REDACT_DEVICE_INFO = {"identifiers", "name"}
|
|
||||||
REDACT_COORDINATOR_DATA = {"DeviceID"}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
|
||||||
hass: HomeAssistant, entry: AirQConfigEntry
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Return diagnostics for a config entry."""
|
|
||||||
coordinator = entry.runtime_data
|
|
||||||
|
|
||||||
return {
|
|
||||||
"config_entry": async_redact_data(entry.as_dict(), REDACT_CONFIG),
|
|
||||||
"device_info": async_redact_data(
|
|
||||||
dict(coordinator.device_info), REDACT_DEVICE_INFO
|
|
||||||
),
|
|
||||||
"coordinator_data": async_redact_data(
|
|
||||||
coordinator.data, REDACT_COORDINATOR_DATA
|
|
||||||
),
|
|
||||||
"options": {
|
|
||||||
"clip_negative": coordinator.clip_negative,
|
|
||||||
"return_average": coordinator.return_average,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
|
||||||
"incomplete_discovery": "The discovered air-Q device did not provide a device ID. Ensure the firmware is up to date."
|
"incomplete_discovery": "The discovered air-Q device did not provide a device ID. Ensure the firmware is up to date."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ from homeassistant.helpers import (
|
|||||||
config_entry_oauth2_flow,
|
config_entry_oauth2_flow,
|
||||||
device_registry as dr,
|
device_registry as dr,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
|
||||||
ImplementationUnavailableError,
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import api
|
from . import api
|
||||||
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
|
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
|
||||||
@@ -28,17 +25,11 @@ async def async_setup_entry(
|
|||||||
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set up Aladdin Connect Genie from a config entry."""
|
"""Set up Aladdin Connect Genie from a config entry."""
|
||||||
try:
|
implementation = (
|
||||||
implementation = (
|
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
hass, entry
|
||||||
hass, entry
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
except ImplementationUnavailableError as err:
|
)
|
||||||
raise ConfigEntryNotReady(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="oauth2_implementation_unavailable",
|
|
||||||
) from err
|
|
||||||
|
|
||||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,6 @@
|
|||||||
"close_door_failed": {
|
"close_door_failed": {
|
||||||
"message": "Failed to close the garage door"
|
"message": "Failed to close the garage door"
|
||||||
},
|
},
|
||||||
"oauth2_implementation_unavailable": {
|
|
||||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
|
||||||
},
|
|
||||||
"open_door_failed": {
|
"open_door_failed": {
|
||||||
"message": "Failed to open the garage door"
|
"message": "Failed to open the garage door"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@
|
|||||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Arm alarm away"
|
"name": "Arm away"
|
||||||
},
|
},
|
||||||
"alarm_arm_custom_bypass": {
|
"alarm_arm_custom_bypass": {
|
||||||
"description": "Arms an alarm while allowing to bypass a custom area.",
|
"description": "Arms an alarm while allowing to bypass a custom area.",
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Arm alarm with custom bypass"
|
"name": "Arm with custom bypass"
|
||||||
},
|
},
|
||||||
"alarm_arm_home": {
|
"alarm_arm_home": {
|
||||||
"description": "Arms an alarm in the home mode.",
|
"description": "Arms an alarm in the home mode.",
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Arm alarm home"
|
"name": "Arm home"
|
||||||
},
|
},
|
||||||
"alarm_arm_night": {
|
"alarm_arm_night": {
|
||||||
"description": "Arms an alarm in the night mode.",
|
"description": "Arms an alarm in the night mode.",
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Arm alarm night"
|
"name": "Arm night"
|
||||||
},
|
},
|
||||||
"alarm_arm_vacation": {
|
"alarm_arm_vacation": {
|
||||||
"description": "Arms an alarm in the vacation mode.",
|
"description": "Arms an alarm in the vacation mode.",
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Arm alarm vacation"
|
"name": "Arm vacation"
|
||||||
},
|
},
|
||||||
"alarm_disarm": {
|
"alarm_disarm": {
|
||||||
"description": "Disarms an alarm.",
|
"description": "Disarms an alarm.",
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
"name": "Code"
|
"name": "Code"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Disarm alarm"
|
"name": "Disarm"
|
||||||
},
|
},
|
||||||
"alarm_trigger": {
|
"alarm_trigger": {
|
||||||
"description": "Triggers an alarm manually.",
|
"description": "Triggers an alarm manually.",
|
||||||
@@ -233,7 +233,7 @@
|
|||||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Trigger alarm"
|
"name": "Trigger"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Alarm control panel",
|
"title": "Alarm control panel",
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aioamazondevices==13.3.1"]
|
"requirements": ["aioamazondevices==13.0.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["androidtvremote2"],
|
"loggers": ["androidtvremote2"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["androidtvremote2==0.3.1"],
|
"requirements": ["androidtvremote2==0.2.3"],
|
||||||
"zeroconf": ["_androidtvremote2._tcp.local."]
|
"zeroconf": ["_androidtvremote2._tcp.local."]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyanglianwater"],
|
"loggers": ["pyanglianwater"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pyanglianwater==3.1.2"]
|
"requirements": ["pyanglianwater==3.1.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,21 +45,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
|
|||||||
try:
|
try:
|
||||||
await client.models.list(timeout=10.0)
|
await client.models.list(timeout=10.0)
|
||||||
except anthropic.AuthenticationError as err:
|
except anthropic.AuthenticationError as err:
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryAuthFailed(err) from err
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="api_authentication_error",
|
|
||||||
translation_placeholders={"message": err.message},
|
|
||||||
) from err
|
|
||||||
except anthropic.AnthropicError as err:
|
except anthropic.AnthropicError as err:
|
||||||
raise ConfigEntryNotReady(
|
raise ConfigEntryNotReady(err) from err
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="api_error",
|
|
||||||
translation_placeholders={
|
|
||||||
"message": err.message
|
|
||||||
if isinstance(err, anthropic.APIError)
|
|
||||||
else str(err)
|
|
||||||
},
|
|
||||||
) from err
|
|
||||||
|
|
||||||
entry.runtime_data = client
|
entry.runtime_data = client
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.util.json import json_loads
|
from homeassistant.util.json import json_loads
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .entity import AnthropicBaseLLMEntity
|
from .entity import AnthropicBaseLLMEntity
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -61,7 +60,7 @@ class AnthropicTaskEntity(
|
|||||||
|
|
||||||
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
|
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN, translation_key="response_not_found"
|
"Last content in chat log is not an AssistantContent"
|
||||||
)
|
)
|
||||||
|
|
||||||
text = chat_log.content[-1].content or ""
|
text = chat_log.content[-1].content or ""
|
||||||
@@ -79,9 +78,7 @@ class AnthropicTaskEntity(
|
|||||||
err,
|
err,
|
||||||
text,
|
text,
|
||||||
)
|
)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError("Error with Claude structured response") from err
|
||||||
translation_domain=DOMAIN, translation_key="json_parse_error"
|
|
||||||
) from err
|
|
||||||
|
|
||||||
return ai_task.GenDataTaskResult(
|
return ai_task.GenDataTaskResult(
|
||||||
conversation_id=chat_log.conversation_id,
|
conversation_id=chat_log.conversation_id,
|
||||||
|
|||||||
@@ -401,11 +401,7 @@ def _convert_content(
|
|||||||
messages[-1]["content"] = messages[-1]["content"][0]["text"]
|
messages[-1]["content"] = messages[-1]["content"][0]["text"]
|
||||||
else:
|
else:
|
||||||
# Note: We don't pass SystemContent here as it's passed to the API as the prompt
|
# Note: We don't pass SystemContent here as it's passed to the API as the prompt
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError("Unexpected content type in chat log")
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="unexpected_chat_log_content",
|
|
||||||
translation_placeholders={"type": type(content).__name__},
|
|
||||||
)
|
|
||||||
|
|
||||||
return messages, container_id
|
return messages, container_id
|
||||||
|
|
||||||
@@ -447,9 +443,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
|||||||
Each message could contain multiple blocks of the same type.
|
Each message could contain multiple blocks of the same type.
|
||||||
"""
|
"""
|
||||||
if stream is None or not hasattr(stream, "__aiter__"):
|
if stream is None or not hasattr(stream, "__aiter__"):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError("Expected a stream of messages")
|
||||||
translation_domain=DOMAIN, translation_key="unexpected_stream_object"
|
|
||||||
)
|
|
||||||
|
|
||||||
current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None
|
current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None
|
||||||
current_tool_args: str
|
current_tool_args: str
|
||||||
@@ -611,9 +605,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
|||||||
chat_log.async_trace(_create_token_stats(input_usage, usage))
|
chat_log.async_trace(_create_token_stats(input_usage, usage))
|
||||||
content_details.container = response.delta.container
|
content_details.container = response.delta.container
|
||||||
if response.delta.stop_reason == "refusal":
|
if response.delta.stop_reason == "refusal":
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError("Potential policy violation detected")
|
||||||
translation_domain=DOMAIN, translation_key="api_refusal"
|
|
||||||
)
|
|
||||||
elif isinstance(response, RawMessageStopEvent):
|
elif isinstance(response, RawMessageStopEvent):
|
||||||
if content_details:
|
if content_details:
|
||||||
content_details.delete_empty()
|
content_details.delete_empty()
|
||||||
@@ -672,9 +664,7 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
|
|
||||||
system = chat_log.content[0]
|
system = chat_log.content[0]
|
||||||
if not isinstance(system, conversation.SystemContent):
|
if not isinstance(system, conversation.SystemContent):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError("First message must be a system message")
|
||||||
translation_domain=DOMAIN, translation_key="system_message_not_found"
|
|
||||||
)
|
|
||||||
|
|
||||||
# System prompt with caching enabled
|
# System prompt with caching enabled
|
||||||
system_prompt: list[TextBlockParam] = [
|
system_prompt: list[TextBlockParam] = [
|
||||||
@@ -764,7 +754,7 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
last_message = messages[-1]
|
last_message = messages[-1]
|
||||||
if last_message["role"] != "user":
|
if last_message["role"] != "user":
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN, translation_key="user_message_not_found"
|
"Last message must be a user message to add attachments"
|
||||||
)
|
)
|
||||||
if isinstance(last_message["content"], str):
|
if isinstance(last_message["content"], str):
|
||||||
last_message["content"] = [
|
last_message["content"] = [
|
||||||
@@ -869,19 +859,11 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
except anthropic.AuthenticationError as err:
|
except anthropic.AuthenticationError as err:
|
||||||
self.entry.async_start_reauth(self.hass)
|
self.entry.async_start_reauth(self.hass)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
"Authentication error with Anthropic API, reauthentication required"
|
||||||
translation_key="api_authentication_error",
|
|
||||||
translation_placeholders={"message": err.message},
|
|
||||||
) from err
|
) from err
|
||||||
except anthropic.AnthropicError as err:
|
except anthropic.AnthropicError as err:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
f"Sorry, I had a problem talking to Anthropic: {err}"
|
||||||
translation_key="api_error",
|
|
||||||
translation_placeholders={
|
|
||||||
"message": err.message
|
|
||||||
if isinstance(err, anthropic.APIError)
|
|
||||||
else str(err)
|
|
||||||
},
|
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
if not chat_log.unresponded_tool_results:
|
if not chat_log.unresponded_tool_results:
|
||||||
@@ -901,23 +883,15 @@ async def async_prepare_files_for_prompt(
|
|||||||
|
|
||||||
for file_path, mime_type in files:
|
for file_path, mime_type in files:
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(f"`{file_path}` does not exist")
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="wrong_file_path",
|
|
||||||
translation_placeholders={"file_path": file_path.as_posix()},
|
|
||||||
)
|
|
||||||
|
|
||||||
if mime_type is None:
|
if mime_type is None:
|
||||||
mime_type = guess_file_type(file_path)[0]
|
mime_type = guess_file_type(file_path)[0]
|
||||||
|
|
||||||
if not mime_type or not mime_type.startswith(("image/", "application/pdf")):
|
if not mime_type or not mime_type.startswith(("image/", "application/pdf")):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
"Only images and PDF are supported by the Anthropic API,"
|
||||||
translation_key="wrong_file_type",
|
f"`{file_path}` is not an image file or PDF"
|
||||||
translation_placeholders={
|
|
||||||
"file_path": file_path.as_posix(),
|
|
||||||
"mime_type": mime_type or "unknown",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if mime_type == "image/jpg":
|
if mime_type == "image/jpg":
|
||||||
mime_type = "image/jpeg"
|
mime_type = "image/jpeg"
|
||||||
|
|||||||
@@ -59,7 +59,10 @@ rules:
|
|||||||
status: exempt
|
status: exempt
|
||||||
comment: |
|
comment: |
|
||||||
No data updates.
|
No data updates.
|
||||||
docs-examples: done
|
docs-examples:
|
||||||
|
status: todo
|
||||||
|
comment: |
|
||||||
|
To give examples of how people use the integration
|
||||||
docs-known-limitations: done
|
docs-known-limitations: done
|
||||||
docs-supported-devices:
|
docs-supported-devices:
|
||||||
status: todo
|
status: todo
|
||||||
@@ -85,7 +88,7 @@ rules:
|
|||||||
comment: |
|
comment: |
|
||||||
No entities disabled by default.
|
No entities disabled by default.
|
||||||
entity-translations: todo
|
entity-translations: todo
|
||||||
exception-translations: done
|
exception-translations: todo
|
||||||
icon-translations: done
|
icon-translations: done
|
||||||
reconfiguration-flow: done
|
reconfiguration-flow: done
|
||||||
repair-issues: done
|
repair-issues: done
|
||||||
|
|||||||
@@ -161,9 +161,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
|
|||||||
is None
|
is None
|
||||||
or (subentry := entry.subentries.get(self._current_subentry_id)) is None
|
or (subentry := entry.subentries.get(self._current_subentry_id)) is None
|
||||||
):
|
):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError("Subentry not found")
|
||||||
translation_domain=DOMAIN, translation_key="subentry_not_found"
|
|
||||||
)
|
|
||||||
|
|
||||||
updated_data = {
|
updated_data = {
|
||||||
**subentry.data,
|
**subentry.data,
|
||||||
@@ -192,6 +190,4 @@ async def async_create_fix_flow(
|
|||||||
"""Create flow."""
|
"""Create flow."""
|
||||||
if issue_id == "model_deprecated":
|
if issue_id == "model_deprecated":
|
||||||
return ModelDeprecatedRepairFlow()
|
return ModelDeprecatedRepairFlow()
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError("Unknown issue ID")
|
||||||
translation_domain=DOMAIN, translation_key="unknown_issue_id"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -149,47 +149,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
|
||||||
"api_authentication_error": {
|
|
||||||
"message": "Authentication error with Anthropic API: {message}. Reauthentication required."
|
|
||||||
},
|
|
||||||
"api_error": {
|
|
||||||
"message": "Anthropic API error: {message}."
|
|
||||||
},
|
|
||||||
"api_refusal": {
|
|
||||||
"message": "Potential policy violation detected."
|
|
||||||
},
|
|
||||||
"json_parse_error": {
|
|
||||||
"message": "Error with Claude structured response."
|
|
||||||
},
|
|
||||||
"response_not_found": {
|
|
||||||
"message": "Last content in chat log is not an AssistantContent."
|
|
||||||
},
|
|
||||||
"subentry_not_found": {
|
|
||||||
"message": "Subentry not found."
|
|
||||||
},
|
|
||||||
"system_message_not_found": {
|
|
||||||
"message": "First message must be a system message."
|
|
||||||
},
|
|
||||||
"unexpected_chat_log_content": {
|
|
||||||
"message": "Unexpected content type in chat log: {type}."
|
|
||||||
},
|
|
||||||
"unexpected_stream_object": {
|
|
||||||
"message": "Expected a stream of messages."
|
|
||||||
},
|
|
||||||
"unknown_issue_id": {
|
|
||||||
"message": "Unknown issue ID."
|
|
||||||
},
|
|
||||||
"user_message_not_found": {
|
|
||||||
"message": "Last message must be a user message to add attachments."
|
|
||||||
},
|
|
||||||
"wrong_file_path": {
|
|
||||||
"message": "`{file_path}` does not exist."
|
|
||||||
},
|
|
||||||
"wrong_file_type": {
|
|
||||||
"message": "Only images and PDF are supported by the Anthropic API, `{file_path}` ({mime_type}) is not an image file or PDF."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"issues": {
|
"issues": {
|
||||||
"model_deprecated": {
|
"model_deprecated": {
|
||||||
"fix_flow": {
|
"fix_flow": {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from asyncio import timeout
|
from asyncio import timeout
|
||||||
from contextlib import AsyncExitStack
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from arcam.fmj import ConnectionFailed
|
from arcam.fmj import ConnectionFailed
|
||||||
from arcam.fmj.client import Client
|
from arcam.fmj.client import Client
|
||||||
@@ -54,31 +54,36 @@ async def _run_client(
|
|||||||
client = runtime_data.client
|
client = runtime_data.client
|
||||||
coordinators = runtime_data.coordinators
|
coordinators = runtime_data.coordinators
|
||||||
|
|
||||||
|
def _listen(_: Any) -> None:
|
||||||
|
for coordinator in coordinators.values():
|
||||||
|
coordinator.async_notify_data_updated()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async with AsyncExitStack() as stack:
|
async with timeout(interval):
|
||||||
async with timeout(interval):
|
await client.start()
|
||||||
await client.start()
|
|
||||||
stack.push_async_callback(client.stop)
|
|
||||||
|
|
||||||
_LOGGER.debug("Client connected %s", client.host)
|
_LOGGER.debug("Client connected %s", client.host)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
for coordinator in coordinators.values():
|
||||||
|
await coordinator.state.start()
|
||||||
|
|
||||||
|
with client.listen(_listen):
|
||||||
for coordinator in coordinators.values():
|
for coordinator in coordinators.values():
|
||||||
await stack.enter_async_context(
|
coordinator.async_notify_connected()
|
||||||
coordinator.async_monitor_client()
|
|
||||||
)
|
|
||||||
|
|
||||||
await client.process()
|
await client.process()
|
||||||
finally:
|
finally:
|
||||||
_LOGGER.debug("Client disconnected %s", client.host)
|
await client.stop()
|
||||||
|
|
||||||
|
_LOGGER.debug("Client disconnected %s", client.host)
|
||||||
|
for coordinator in coordinators.values():
|
||||||
|
coordinator.async_notify_disconnected()
|
||||||
|
|
||||||
except ConnectionFailed:
|
except ConnectionFailed:
|
||||||
pass
|
await asyncio.sleep(interval)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected exception, aborting arcam client")
|
_LOGGER.exception("Unexpected exception, aborting arcam client")
|
||||||
return
|
return
|
||||||
|
|
||||||
await asyncio.sleep(interval)
|
|
||||||
|
|||||||
@@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import AsyncGenerator
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from arcam.fmj import ConnectionFailed
|
from arcam.fmj import ConnectionFailed
|
||||||
from arcam.fmj.client import AmxDuetResponse, Client, ResponsePacket
|
from arcam.fmj.client import Client
|
||||||
from arcam.fmj.state import State
|
from arcam.fmj.state import State
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@@ -53,7 +51,7 @@ class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
|
|||||||
)
|
)
|
||||||
self.client = client
|
self.client = client
|
||||||
self.state = State(client, zone)
|
self.state = State(client, zone)
|
||||||
self.update_in_progress = False
|
self.last_update_success = False
|
||||||
|
|
||||||
name = config_entry.title
|
name = config_entry.title
|
||||||
unique_id = config_entry.unique_id or config_entry.entry_id
|
unique_id = config_entry.unique_id or config_entry.entry_id
|
||||||
@@ -76,34 +74,24 @@ class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
|
|||||||
async def _async_update_data(self) -> None:
|
async def _async_update_data(self) -> None:
|
||||||
"""Fetch data for manual refresh."""
|
"""Fetch data for manual refresh."""
|
||||||
try:
|
try:
|
||||||
self.update_in_progress = True
|
|
||||||
await self.state.update()
|
await self.state.update()
|
||||||
except ConnectionFailed as err:
|
except ConnectionFailed as err:
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
f"Connection failed during update for zone {self.state.zn}"
|
f"Connection failed during update for zone {self.state.zn}"
|
||||||
) from err
|
) from err
|
||||||
finally:
|
|
||||||
self.update_in_progress = False
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_notify_packet(self, packet: ResponsePacket | AmxDuetResponse) -> None:
|
def async_notify_data_updated(self) -> None:
|
||||||
"""Packet callback to detect changes to state."""
|
"""Notify that new data has been received from the device."""
|
||||||
if (
|
self.async_set_updated_data(None)
|
||||||
not isinstance(packet, ResponsePacket)
|
|
||||||
or packet.zn != self.state.zn
|
|
||||||
or self.update_in_progress
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_notify_connected(self) -> None:
|
||||||
|
"""Handle client connected."""
|
||||||
|
self.hass.async_create_task(self.async_refresh())
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_notify_disconnected(self) -> None:
|
||||||
|
"""Handle client disconnected."""
|
||||||
|
self.last_update_success = False
|
||||||
self.async_update_listeners()
|
self.async_update_listeners()
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def async_monitor_client(self) -> AsyncGenerator[None]:
|
|
||||||
"""Monitor a client and state for changes while connected."""
|
|
||||||
async with self.state:
|
|
||||||
self.hass.async_create_task(self.async_refresh())
|
|
||||||
try:
|
|
||||||
with self.client.listen(self._async_notify_packet):
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
self.hass.async_create_task(self.async_refresh())
|
|
||||||
|
|||||||
@@ -26,8 +26,3 @@ class ArcamFmjEntity(CoordinatorEntity[ArcamFmjCoordinator]):
|
|||||||
if description is not None:
|
if description is not None:
|
||||||
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
|
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return super().available and self.coordinator.client.connected
|
|
||||||
|
|||||||
@@ -137,4 +137,5 @@ async def async_pipeline_from_audio_stream(
|
|||||||
audio_settings=audio_settings or AudioSettings(),
|
audio_settings=audio_settings or AudioSettings(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await pipeline_input.execute(validate=True)
|
await pipeline_input.validate()
|
||||||
|
await pipeline_input.execute()
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
"""Assist pipeline errors."""
|
"""Assist pipeline errors."""
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .pipeline import PipelineStage
|
|
||||||
|
|
||||||
|
|
||||||
class PipelineError(HomeAssistantError):
|
class PipelineError(HomeAssistantError):
|
||||||
"""Base class for pipeline errors."""
|
"""Base class for pipeline errors."""
|
||||||
@@ -62,25 +55,3 @@ class IntentRecognitionError(PipelineError):
|
|||||||
|
|
||||||
class TextToSpeechError(PipelineError):
|
class TextToSpeechError(PipelineError):
|
||||||
"""Error in text-to-speech portion of pipeline."""
|
"""Error in text-to-speech portion of pipeline."""
|
||||||
|
|
||||||
|
|
||||||
class PipelineRunValidationError(PipelineError):
|
|
||||||
"""Error when a pipeline run is not valid."""
|
|
||||||
|
|
||||||
def __init__(self, message: str) -> None:
|
|
||||||
"""Set error message."""
|
|
||||||
super().__init__("validation-error", message)
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidPipelineStagesError(PipelineRunValidationError):
|
|
||||||
"""Error when given an invalid combination of start/end stages."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
start_stage: PipelineStage,
|
|
||||||
end_stage: PipelineStage,
|
|
||||||
) -> None:
|
|
||||||
"""Set error message."""
|
|
||||||
super().__init__(
|
|
||||||
f"Invalid stage combination: start={start_stage}, end={end_stage}"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -73,10 +73,8 @@ from .const import (
|
|||||||
from .error import (
|
from .error import (
|
||||||
DuplicateWakeUpDetectedError,
|
DuplicateWakeUpDetectedError,
|
||||||
IntentRecognitionError,
|
IntentRecognitionError,
|
||||||
InvalidPipelineStagesError,
|
|
||||||
PipelineError,
|
PipelineError,
|
||||||
PipelineNotFound,
|
PipelineNotFound,
|
||||||
PipelineRunValidationError,
|
|
||||||
SpeechToTextError,
|
SpeechToTextError,
|
||||||
TextToSpeechError,
|
TextToSpeechError,
|
||||||
WakeWordDetectionAborted,
|
WakeWordDetectionAborted,
|
||||||
@@ -494,6 +492,24 @@ PIPELINE_STAGE_ORDER = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PipelineRunValidationError(Exception):
|
||||||
|
"""Error when a pipeline run is not valid."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPipelineStagesError(PipelineRunValidationError):
|
||||||
|
"""Error when given an invalid combination of start/end stages."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
start_stage: PipelineStage,
|
||||||
|
end_stage: PipelineStage,
|
||||||
|
) -> None:
|
||||||
|
"""Set error message."""
|
||||||
|
super().__init__(
|
||||||
|
f"Invalid stage combination: start={start_stage}, end={end_stage}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class WakeWordSettings:
|
class WakeWordSettings:
|
||||||
"""Settings for wake word detection."""
|
"""Settings for wake word detection."""
|
||||||
@@ -646,8 +662,7 @@ class PipelineRun:
|
|||||||
"""Emit run start event."""
|
"""Emit run start event."""
|
||||||
self._device_id = device_id
|
self._device_id = device_id
|
||||||
self._satellite_id = satellite_id
|
self._satellite_id = satellite_id
|
||||||
if self.start_stage in (PipelineStage.WAKE_WORD, PipelineStage.STT):
|
self._start_debug_recording_thread()
|
||||||
self._start_debug_recording_thread()
|
|
||||||
|
|
||||||
data: dict[str, Any] = {
|
data: dict[str, Any] = {
|
||||||
"pipeline": self.pipeline.id,
|
"pipeline": self.pipeline.id,
|
||||||
@@ -1489,7 +1504,9 @@ class PipelineRun:
|
|||||||
|
|
||||||
def _start_debug_recording_thread(self) -> None:
|
def _start_debug_recording_thread(self) -> None:
|
||||||
"""Start thread to record wake/stt audio if debug_recording_dir is set."""
|
"""Start thread to record wake/stt audio if debug_recording_dir is set."""
|
||||||
assert self.debug_recording_thread is None
|
if self.debug_recording_thread is not None:
|
||||||
|
# Already started
|
||||||
|
return
|
||||||
|
|
||||||
# Directory to save audio for each pipeline run.
|
# Directory to save audio for each pipeline run.
|
||||||
# Configured in YAML for assist_pipeline.
|
# Configured in YAML for assist_pipeline.
|
||||||
@@ -1664,39 +1681,26 @@ class PipelineInput:
|
|||||||
satellite_id: str | None = None
|
satellite_id: str | None = None
|
||||||
"""Identifier of the satellite that is processing the input/output of the pipeline."""
|
"""Identifier of the satellite that is processing the input/output of the pipeline."""
|
||||||
|
|
||||||
async def execute(self, validate: bool = False) -> None:
|
async def execute(self) -> None:
|
||||||
"""Run pipeline."""
|
"""Run pipeline."""
|
||||||
validation_error: PipelineError | None = None
|
|
||||||
if validate:
|
|
||||||
try:
|
|
||||||
await self.validate()
|
|
||||||
except PipelineError as err:
|
|
||||||
validation_error = err
|
|
||||||
|
|
||||||
self.run.start(
|
self.run.start(
|
||||||
conversation_id=self.session.conversation_id,
|
conversation_id=self.session.conversation_id,
|
||||||
device_id=self.device_id,
|
device_id=self.device_id,
|
||||||
satellite_id=self.satellite_id,
|
satellite_id=self.satellite_id,
|
||||||
)
|
)
|
||||||
current_stage: PipelineStage | None = self.run.start_stage
|
current_stage: PipelineStage | None = self.run.start_stage
|
||||||
|
stt_audio_buffer: list[EnhancedAudioChunk] = []
|
||||||
|
stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None
|
||||||
|
|
||||||
|
if self.stt_stream is not None:
|
||||||
|
if self.run.audio_settings.needs_processor:
|
||||||
|
# VAD/noise suppression/auto gain/volume
|
||||||
|
stt_processed_stream = self.run.process_enhance_audio(self.stt_stream)
|
||||||
|
else:
|
||||||
|
# Volume multiplier only
|
||||||
|
stt_processed_stream = self.run.process_volume_only(self.stt_stream)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if validation_error is not None:
|
|
||||||
raise validation_error
|
|
||||||
|
|
||||||
stt_audio_buffer: list[EnhancedAudioChunk] = []
|
|
||||||
stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None
|
|
||||||
|
|
||||||
if self.stt_stream is not None:
|
|
||||||
if self.run.audio_settings.needs_processor:
|
|
||||||
# VAD/noise suppression/auto gain/volume
|
|
||||||
stt_processed_stream = self.run.process_enhance_audio(
|
|
||||||
self.stt_stream
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Volume multiplier only
|
|
||||||
stt_processed_stream = self.run.process_volume_only(self.stt_stream)
|
|
||||||
|
|
||||||
if current_stage == PipelineStage.WAKE_WORD:
|
if current_stage == PipelineStage.WAKE_WORD:
|
||||||
# wake-word-detection
|
# wake-word-detection
|
||||||
assert stt_processed_stream is not None
|
assert stt_processed_stream is not None
|
||||||
|
|||||||
@@ -626,7 +626,7 @@ def websocket_delete_all_refresh_tokens(
|
|||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle delete all refresh tokens request."""
|
"""Handle delete all refresh tokens request."""
|
||||||
current_refresh_token: RefreshToken | None = None
|
current_refresh_token: RefreshToken
|
||||||
remove_failed = False
|
remove_failed = False
|
||||||
token_type = msg.get("token_type")
|
token_type = msg.get("token_type")
|
||||||
delete_current_token = msg.get("delete_current_token")
|
delete_current_token = msg.get("delete_current_token")
|
||||||
@@ -654,7 +654,7 @@ def websocket_delete_all_refresh_tokens(
|
|||||||
else:
|
else:
|
||||||
connection.send_result(msg["id"], {})
|
connection.send_result(msg["id"], {})
|
||||||
|
|
||||||
async def _delete_current_token_soon(current_refresh_token: RefreshToken) -> None:
|
async def _delete_current_token_soon() -> None:
|
||||||
"""Delete the current token after a delay.
|
"""Delete the current token after a delay.
|
||||||
|
|
||||||
We do not want to delete the current token immediately as it will
|
We do not want to delete the current token immediately as it will
|
||||||
@@ -675,15 +675,13 @@ def websocket_delete_all_refresh_tokens(
|
|||||||
# the token right away.
|
# the token right away.
|
||||||
hass.auth.async_remove_refresh_token(current_refresh_token)
|
hass.auth.async_remove_refresh_token(current_refresh_token)
|
||||||
|
|
||||||
if (
|
if delete_current_token and (
|
||||||
delete_current_token
|
not limit_token_types or current_refresh_token.token_type == token_type
|
||||||
and current_refresh_token
|
|
||||||
and (not limit_token_types or current_refresh_token.token_type == token_type)
|
|
||||||
):
|
):
|
||||||
# Deleting the token will close the connection so we need
|
# Deleting the token will close the connection so we need
|
||||||
# to do it with a delay in a tracked task to ensure it still
|
# to do it with a delay in a tracked task to ensure it still
|
||||||
# happens if Home Assistant is shutting down.
|
# happens if Home Assistant is shutting down.
|
||||||
hass.async_create_task(_delete_current_token_soon(current_refresh_token))
|
hass.async_create_task(_delete_current_token_soon())
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ def async_setup(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Component to allow users to login."""
|
"""Component to allow users to login."""
|
||||||
hass.http.register_view(WellKnownOAuthInfoView)
|
hass.http.register_view(WellKnownOAuthInfoView)
|
||||||
hass.http.register_view(WellKnownProtectedResourceView)
|
|
||||||
hass.http.register_view(AuthProvidersView)
|
hass.http.register_view(AuthProvidersView)
|
||||||
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow, store_result))
|
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow, store_result))
|
||||||
hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result))
|
hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result))
|
||||||
@@ -142,13 +141,6 @@ class WellKnownOAuthInfoView(HomeAssistantView):
|
|||||||
"authorization_endpoint": f"{url_prefix}/auth/authorize",
|
"authorization_endpoint": f"{url_prefix}/auth/authorize",
|
||||||
"token_endpoint": f"{url_prefix}/auth/token",
|
"token_endpoint": f"{url_prefix}/auth/token",
|
||||||
"revocation_endpoint": f"{url_prefix}/auth/revoke",
|
"revocation_endpoint": f"{url_prefix}/auth/revoke",
|
||||||
# Home Assistant already accepts URL-based client_ids via
|
|
||||||
# IndieAuth without prior registration, which is compatible with
|
|
||||||
# draft-ietf-oauth-client-id-metadata-document. This flag
|
|
||||||
# advertises that support to encourage clients to use it. The
|
|
||||||
# metadata document is not actually fetched as IndieAuth doesn't
|
|
||||||
# require it.
|
|
||||||
"client_id_metadata_document_supported": True,
|
|
||||||
"response_types_supported": ["code"],
|
"response_types_supported": ["code"],
|
||||||
"service_documentation": (
|
"service_documentation": (
|
||||||
"https://developers.home-assistant.io/docs/auth_api"
|
"https://developers.home-assistant.io/docs/auth_api"
|
||||||
@@ -162,32 +154,6 @@ class WellKnownOAuthInfoView(HomeAssistantView):
|
|||||||
return self.json(metadata)
|
return self.json(metadata)
|
||||||
|
|
||||||
|
|
||||||
class WellKnownProtectedResourceView(HomeAssistantView):
|
|
||||||
"""View to host the OAuth2 Protected Resource Metadata per RFC9728."""
|
|
||||||
|
|
||||||
requires_auth = False
|
|
||||||
url = "/.well-known/oauth-protected-resource"
|
|
||||||
name = "well-known/oauth-protected-resource"
|
|
||||||
|
|
||||||
async def get(self, request: web.Request) -> web.Response:
|
|
||||||
"""Return the protected resource metadata."""
|
|
||||||
hass = request.app[KEY_HASS]
|
|
||||||
try:
|
|
||||||
url_prefix = get_url(hass, require_current_request=True)
|
|
||||||
except NoURLAvailableError:
|
|
||||||
return self.json_message("No URL available", HTTPStatus.NOT_FOUND)
|
|
||||||
|
|
||||||
return self.json(
|
|
||||||
{
|
|
||||||
"resource": url_prefix,
|
|
||||||
"authorization_servers": [url_prefix],
|
|
||||||
"resource_documentation": (
|
|
||||||
"https://developers.home-assistant.io/docs/auth_api"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthProvidersView(HomeAssistantView):
|
class AuthProvidersView(HomeAssistantView):
|
||||||
"""View to get available auth providers."""
|
"""View to get available auth providers."""
|
||||||
|
|
||||||
|
|||||||
@@ -118,11 +118,8 @@ SERVICE_TRIGGER = "trigger"
|
|||||||
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
|
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
|
||||||
|
|
||||||
_EXPERIMENTAL_CONDITION_PLATFORMS = {
|
_EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||||
"air_quality",
|
|
||||||
"alarm_control_panel",
|
"alarm_control_panel",
|
||||||
"assist_satellite",
|
"assist_satellite",
|
||||||
"battery",
|
|
||||||
"calendar",
|
|
||||||
"climate",
|
"climate",
|
||||||
"cover",
|
"cover",
|
||||||
"device_tracker",
|
"device_tracker",
|
||||||
@@ -131,69 +128,50 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
|
|||||||
"garage_door",
|
"garage_door",
|
||||||
"gate",
|
"gate",
|
||||||
"humidifier",
|
"humidifier",
|
||||||
"humidity",
|
|
||||||
"illuminance",
|
|
||||||
"lawn_mower",
|
"lawn_mower",
|
||||||
"light",
|
"light",
|
||||||
"lock",
|
"lock",
|
||||||
"media_player",
|
"media_player",
|
||||||
"moisture",
|
|
||||||
"motion",
|
"motion",
|
||||||
"occupancy",
|
"occupancy",
|
||||||
"person",
|
"person",
|
||||||
"power",
|
|
||||||
"schedule",
|
"schedule",
|
||||||
"select",
|
|
||||||
"siren",
|
"siren",
|
||||||
"switch",
|
"switch",
|
||||||
"temperature",
|
|
||||||
"text",
|
|
||||||
"timer",
|
|
||||||
"vacuum",
|
"vacuum",
|
||||||
"valve",
|
|
||||||
"water_heater",
|
|
||||||
"window",
|
"window",
|
||||||
}
|
}
|
||||||
|
|
||||||
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||||
"air_quality",
|
|
||||||
"alarm_control_panel",
|
"alarm_control_panel",
|
||||||
"assist_satellite",
|
"assist_satellite",
|
||||||
"battery",
|
|
||||||
"button",
|
"button",
|
||||||
"climate",
|
"climate",
|
||||||
"counter",
|
|
||||||
"cover",
|
"cover",
|
||||||
"device_tracker",
|
"device_tracker",
|
||||||
"door",
|
"door",
|
||||||
"event",
|
|
||||||
"fan",
|
"fan",
|
||||||
"garage_door",
|
"garage_door",
|
||||||
"gate",
|
"gate",
|
||||||
"humidifier",
|
"humidifier",
|
||||||
"humidity",
|
"humidity",
|
||||||
"illuminance",
|
"input_boolean",
|
||||||
"lawn_mower",
|
"lawn_mower",
|
||||||
"light",
|
"light",
|
||||||
"lock",
|
"lock",
|
||||||
"media_player",
|
"media_player",
|
||||||
"moisture",
|
|
||||||
"motion",
|
"motion",
|
||||||
"occupancy",
|
"occupancy",
|
||||||
"person",
|
"person",
|
||||||
"power",
|
|
||||||
"remote",
|
"remote",
|
||||||
"scene",
|
"scene",
|
||||||
"schedule",
|
"schedule",
|
||||||
"select",
|
"select",
|
||||||
"siren",
|
"siren",
|
||||||
"switch",
|
"switch",
|
||||||
"temperature",
|
|
||||||
"text",
|
"text",
|
||||||
"todo",
|
|
||||||
"update",
|
"update",
|
||||||
"vacuum",
|
"vacuum",
|
||||||
"water_heater",
|
|
||||||
"window",
|
"window",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,11 +78,11 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"reload": {
|
"reload": {
|
||||||
"description": "Reloads the automation configuration.",
|
"description": "Reloads the automation configuration.",
|
||||||
"name": "Reload automations"
|
"name": "[%key:common::action::reload%]"
|
||||||
},
|
},
|
||||||
"toggle": {
|
"toggle": {
|
||||||
"description": "Toggles (enable / disable) an automation.",
|
"description": "Toggles (enable / disable) an automation.",
|
||||||
"name": "Toggle automation"
|
"name": "[%key:common::action::toggle%]"
|
||||||
},
|
},
|
||||||
"trigger": {
|
"trigger": {
|
||||||
"description": "Triggers the actions of an automation.",
|
"description": "Triggers the actions of an automation.",
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
"name": "Skip conditions"
|
"name": "Skip conditions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Trigger automation"
|
"name": "Trigger"
|
||||||
},
|
},
|
||||||
"turn_off": {
|
"turn_off": {
|
||||||
"description": "Disables an automation.",
|
"description": "Disables an automation.",
|
||||||
@@ -102,11 +102,11 @@
|
|||||||
"name": "Stop actions"
|
"name": "Stop actions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Turn off automation"
|
"name": "[%key:common::action::turn_off%]"
|
||||||
},
|
},
|
||||||
"turn_on": {
|
"turn_on": {
|
||||||
"description": "Enables an automation.",
|
"description": "Enables an automation.",
|
||||||
"name": "Turn on automation"
|
"name": "[%key:common::action::turn_on%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Automation"
|
"title": "Automation"
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ class S3BackupAgent(BackupAgent):
|
|||||||
if backup.size < MULTIPART_MIN_PART_SIZE_BYTES:
|
if backup.size < MULTIPART_MIN_PART_SIZE_BYTES:
|
||||||
await self._upload_simple(tar_filename, open_stream)
|
await self._upload_simple(tar_filename, open_stream)
|
||||||
else:
|
else:
|
||||||
await self._upload_multipart(tar_filename, open_stream, on_progress)
|
await self._upload_multipart(tar_filename, open_stream)
|
||||||
|
|
||||||
# Upload the metadata file
|
# Upload the metadata file
|
||||||
metadata_content = json.dumps(backup.as_dict())
|
metadata_content = json.dumps(backup.as_dict())
|
||||||
@@ -188,13 +188,11 @@ class S3BackupAgent(BackupAgent):
|
|||||||
self,
|
self,
|
||||||
tar_filename: str,
|
tar_filename: str,
|
||||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||||
on_progress: OnProgressCallback,
|
):
|
||||||
) -> None:
|
|
||||||
"""Upload a large file using multipart upload.
|
"""Upload a large file using multipart upload.
|
||||||
|
|
||||||
:param tar_filename: The target filename for the backup.
|
:param tar_filename: The target filename for the backup.
|
||||||
:param open_stream: A function returning an async iterator that yields bytes.
|
:param open_stream: A function returning an async iterator that yields bytes.
|
||||||
:param on_progress: A callback to report the number of uploaded bytes.
|
|
||||||
"""
|
"""
|
||||||
_LOGGER.debug("Starting multipart upload for %s", tar_filename)
|
_LOGGER.debug("Starting multipart upload for %s", tar_filename)
|
||||||
multipart_upload = await self._client.create_multipart_upload(
|
multipart_upload = await self._client.create_multipart_upload(
|
||||||
@@ -207,7 +205,6 @@ class S3BackupAgent(BackupAgent):
|
|||||||
part_number = 1
|
part_number = 1
|
||||||
buffer = bytearray() # bytes buffer to store the data
|
buffer = bytearray() # bytes buffer to store the data
|
||||||
offset = 0 # start index of unread data inside buffer
|
offset = 0 # start index of unread data inside buffer
|
||||||
bytes_uploaded = 0
|
|
||||||
|
|
||||||
stream = await open_stream()
|
stream = await open_stream()
|
||||||
async for chunk in stream:
|
async for chunk in stream:
|
||||||
@@ -236,8 +233,6 @@ class S3BackupAgent(BackupAgent):
|
|||||||
Body=part_data.tobytes(),
|
Body=part_data.tobytes(),
|
||||||
)
|
)
|
||||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||||
bytes_uploaded += len(part_data)
|
|
||||||
on_progress(bytes_uploaded=bytes_uploaded)
|
|
||||||
part_number += 1
|
part_number += 1
|
||||||
finally:
|
finally:
|
||||||
view.release()
|
view.release()
|
||||||
@@ -266,8 +261,6 @@ class S3BackupAgent(BackupAgent):
|
|||||||
Body=remaining_data.tobytes(),
|
Body=remaining_data.tobytes(),
|
||||||
)
|
)
|
||||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||||
bytes_uploaded += len(remaining_data)
|
|
||||||
on_progress(bytes_uploaded=bytes_uploaded)
|
|
||||||
|
|
||||||
await cast(Any, self._client).complete_multipart_upload(
|
await cast(Any, self._client).complete_multipart_upload(
|
||||||
Bucket=self._bucket,
|
Bucket=self._bucket,
|
||||||
|
|||||||
@@ -34,4 +34,4 @@ EXCLUDE_DATABASE_FROM_BACKUP = [
|
|||||||
"home-assistant_v2.db-wal",
|
"home-assistant_v2.db-wal",
|
||||||
]
|
]
|
||||||
|
|
||||||
SECURETAR_CREATE_VERSION = 3
|
SECURETAR_CREATE_VERSION = 2
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
"""Integration for battery triggers and conditions."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers import config_validation as cv
|
|
||||||
from homeassistant.helpers.typing import ConfigType
|
|
||||||
|
|
||||||
DOMAIN = "battery"
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
|
||||||
|
|
||||||
__all__ = []
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
||||||
"""Set up the component."""
|
|
||||||
return True
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
"""Provides conditions for batteries."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
|
||||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
|
||||||
BinarySensorDeviceClass,
|
|
||||||
)
|
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
|
||||||
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.automation import DomainSpec
|
|
||||||
from homeassistant.helpers.condition import (
|
|
||||||
Condition,
|
|
||||||
make_entity_numerical_condition,
|
|
||||||
make_entity_state_condition,
|
|
||||||
)
|
|
||||||
|
|
||||||
BATTERY_DOMAIN_SPECS = {
|
|
||||||
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.BATTERY)
|
|
||||||
}
|
|
||||||
BATTERY_CHARGING_DOMAIN_SPECS = {
|
|
||||||
BINARY_SENSOR_DOMAIN: DomainSpec(
|
|
||||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING
|
|
||||||
)
|
|
||||||
}
|
|
||||||
BATTERY_PERCENTAGE_DOMAIN_SPECS = {
|
|
||||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
|
|
||||||
}
|
|
||||||
|
|
||||||
CONDITIONS: dict[str, type[Condition]] = {
|
|
||||||
"is_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_ON),
|
|
||||||
"is_not_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_OFF),
|
|
||||||
"is_charging": make_entity_state_condition(BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON),
|
|
||||||
"is_not_charging": make_entity_state_condition(
|
|
||||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
|
|
||||||
),
|
|
||||||
"is_level": make_entity_numerical_condition(
|
|
||||||
BATTERY_PERCENTAGE_DOMAIN_SPECS, PERCENTAGE
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
|
||||||
"""Return the conditions for batteries."""
|
|
||||||
return CONDITIONS
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
.condition_common: &condition_common
|
|
||||||
target: &target_battery_binary_sensor
|
|
||||||
entity:
|
|
||||||
- domain: binary_sensor
|
|
||||||
device_class: battery
|
|
||||||
fields:
|
|
||||||
behavior: &condition_behavior
|
|
||||||
required: true
|
|
||||||
default: any
|
|
||||||
selector:
|
|
||||||
select:
|
|
||||||
translation_key: condition_behavior
|
|
||||||
options:
|
|
||||||
- all
|
|
||||||
- any
|
|
||||||
|
|
||||||
.battery_threshold_entity: &battery_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "%"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: battery
|
|
||||||
- domain: number
|
|
||||||
device_class: battery
|
|
||||||
|
|
||||||
.battery_threshold_number: &battery_threshold_number
|
|
||||||
min: 0
|
|
||||||
max: 100
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: "%"
|
|
||||||
|
|
||||||
is_low: *condition_common
|
|
||||||
|
|
||||||
is_not_low: *condition_common
|
|
||||||
|
|
||||||
is_charging:
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
- domain: binary_sensor
|
|
||||||
device_class: battery_charging
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
|
|
||||||
is_not_charging:
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
- domain: binary_sensor
|
|
||||||
device_class: battery_charging
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
|
|
||||||
is_level:
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: battery
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *battery_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number: *battery_threshold_number
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"conditions": {
|
|
||||||
"is_charging": {
|
|
||||||
"condition": "mdi:battery-charging"
|
|
||||||
},
|
|
||||||
"is_level": {
|
|
||||||
"condition": "mdi:battery-unknown"
|
|
||||||
},
|
|
||||||
"is_low": {
|
|
||||||
"condition": "mdi:battery-alert"
|
|
||||||
},
|
|
||||||
"is_not_charging": {
|
|
||||||
"condition": "mdi:battery"
|
|
||||||
},
|
|
||||||
"is_not_low": {
|
|
||||||
"condition": "mdi:battery"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"triggers": {
|
|
||||||
"level_changed": {
|
|
||||||
"trigger": "mdi:battery-unknown"
|
|
||||||
},
|
|
||||||
"level_crossed_threshold": {
|
|
||||||
"trigger": "mdi:battery-alert"
|
|
||||||
},
|
|
||||||
"low": {
|
|
||||||
"trigger": "mdi:battery-alert"
|
|
||||||
},
|
|
||||||
"not_low": {
|
|
||||||
"trigger": "mdi:battery"
|
|
||||||
},
|
|
||||||
"started_charging": {
|
|
||||||
"trigger": "mdi:battery-charging"
|
|
||||||
},
|
|
||||||
"stopped_charging": {
|
|
||||||
"trigger": "mdi:battery"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "battery",
|
|
||||||
"name": "Battery",
|
|
||||||
"codeowners": ["@home-assistant/core"],
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/battery",
|
|
||||||
"integration_type": "system",
|
|
||||||
"quality_scale": "internal"
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
{
|
|
||||||
"common": {
|
|
||||||
"condition_behavior_description": "How the state should match on the targeted batteries.",
|
|
||||||
"condition_behavior_name": "Behavior",
|
|
||||||
"condition_threshold_description": "What to test for and threshold values.",
|
|
||||||
"condition_threshold_name": "Threshold configuration",
|
|
||||||
"trigger_behavior_description": "The behavior of the targeted batteries to trigger on.",
|
|
||||||
"trigger_behavior_name": "Behavior",
|
|
||||||
"trigger_threshold_changed_description": "Which changes to trigger on and threshold values.",
|
|
||||||
"trigger_threshold_crossed_description": "Which threshold crossing to trigger on and threshold values.",
|
|
||||||
"trigger_threshold_name": "Threshold configuration"
|
|
||||||
},
|
|
||||||
"conditions": {
|
|
||||||
"is_charging": {
|
|
||||||
"description": "Tests if one or more batteries are charging.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Battery is charging"
|
|
||||||
},
|
|
||||||
"is_level": {
|
|
||||||
"description": "Tests the battery level of one or more batteries.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::battery::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::battery::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Battery level"
|
|
||||||
},
|
|
||||||
"is_low": {
|
|
||||||
"description": "Tests if one or more batteries are low.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Battery is low"
|
|
||||||
},
|
|
||||||
"is_not_charging": {
|
|
||||||
"description": "Tests if one or more batteries are not charging.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Battery is not charging"
|
|
||||||
},
|
|
||||||
"is_not_low": {
|
|
||||||
"description": "Tests if one or more batteries are not low.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Battery is not low"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"selector": {
|
|
||||||
"condition_behavior": {
|
|
||||||
"options": {
|
|
||||||
"all": "All",
|
|
||||||
"any": "Any"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"trigger_behavior": {
|
|
||||||
"options": {
|
|
||||||
"any": "Any",
|
|
||||||
"first": "First",
|
|
||||||
"last": "Last"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Battery",
|
|
||||||
"triggers": {
|
|
||||||
"level_changed": {
|
|
||||||
"description": "Triggers after the battery level of one or more batteries changes.",
|
|
||||||
"fields": {
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::battery::common::trigger_threshold_changed_description%]",
|
|
||||||
"name": "[%key:component::battery::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Battery level changed"
|
|
||||||
},
|
|
||||||
"level_crossed_threshold": {
|
|
||||||
"description": "Triggers after the battery level of one or more batteries crosses a threshold.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::battery::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::battery::common::trigger_threshold_crossed_description%]",
|
|
||||||
"name": "[%key:component::battery::common::trigger_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Battery level crossed threshold"
|
|
||||||
},
|
|
||||||
"low": {
|
|
||||||
"description": "Triggers after one or more batteries become low.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::battery::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Battery low"
|
|
||||||
},
|
|
||||||
"not_low": {
|
|
||||||
"description": "Triggers after one or more batteries are no longer low.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::battery::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Battery not low"
|
|
||||||
},
|
|
||||||
"started_charging": {
|
|
||||||
"description": "Triggers after one or more batteries start charging.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::battery::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Battery started charging"
|
|
||||||
},
|
|
||||||
"stopped_charging": {
|
|
||||||
"description": "Triggers after one or more batteries stop charging.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::battery::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Battery stopped charging"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"""Provides triggers for batteries."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
|
||||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
|
||||||
BinarySensorDeviceClass,
|
|
||||||
)
|
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.automation import DomainSpec
|
|
||||||
from homeassistant.helpers.trigger import (
|
|
||||||
Trigger,
|
|
||||||
make_entity_numerical_state_changed_trigger,
|
|
||||||
make_entity_numerical_state_crossed_threshold_trigger,
|
|
||||||
make_entity_target_state_trigger,
|
|
||||||
)
|
|
||||||
|
|
||||||
BATTERY_LOW_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
|
||||||
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.BATTERY),
|
|
||||||
}
|
|
||||||
|
|
||||||
BATTERY_CHARGING_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
|
||||||
BINARY_SENSOR_DOMAIN: DomainSpec(
|
|
||||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
BATTERY_PERCENTAGE_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
|
||||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
|
|
||||||
}
|
|
||||||
|
|
||||||
TRIGGERS: dict[str, type[Trigger]] = {
|
|
||||||
"low": make_entity_target_state_trigger(BATTERY_LOW_DOMAIN_SPECS, STATE_ON),
|
|
||||||
"not_low": make_entity_target_state_trigger(BATTERY_LOW_DOMAIN_SPECS, STATE_OFF),
|
|
||||||
"started_charging": make_entity_target_state_trigger(
|
|
||||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON
|
|
||||||
),
|
|
||||||
"stopped_charging": make_entity_target_state_trigger(
|
|
||||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
|
|
||||||
),
|
|
||||||
"level_changed": make_entity_numerical_state_changed_trigger(
|
|
||||||
BATTERY_PERCENTAGE_DOMAIN_SPECS, valid_unit="%"
|
|
||||||
),
|
|
||||||
"level_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
|
||||||
BATTERY_PERCENTAGE_DOMAIN_SPECS, valid_unit="%"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
|
||||||
"""Return the triggers for batteries."""
|
|
||||||
return TRIGGERS
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
.trigger_common_fields:
|
|
||||||
behavior: &trigger_behavior
|
|
||||||
required: true
|
|
||||||
default: any
|
|
||||||
selector:
|
|
||||||
select:
|
|
||||||
translation_key: trigger_behavior
|
|
||||||
options:
|
|
||||||
- first
|
|
||||||
- last
|
|
||||||
- any
|
|
||||||
|
|
||||||
.battery_threshold_entity: &battery_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "%"
|
|
||||||
- domain: number
|
|
||||||
device_class: battery
|
|
||||||
- domain: sensor
|
|
||||||
device_class: battery
|
|
||||||
|
|
||||||
.battery_threshold_number: &battery_threshold_number
|
|
||||||
min: 0
|
|
||||||
max: 100
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: "%"
|
|
||||||
|
|
||||||
.trigger_target_battery: &trigger_target_battery
|
|
||||||
entity:
|
|
||||||
- domain: binary_sensor
|
|
||||||
device_class: battery
|
|
||||||
|
|
||||||
.trigger_target_charging: &trigger_target_charging
|
|
||||||
entity:
|
|
||||||
- domain: binary_sensor
|
|
||||||
device_class: battery_charging
|
|
||||||
|
|
||||||
.trigger_target_percentage: &trigger_target_percentage
|
|
||||||
entity:
|
|
||||||
- domain: sensor
|
|
||||||
device_class: battery
|
|
||||||
|
|
||||||
low:
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
target: *trigger_target_battery
|
|
||||||
|
|
||||||
not_low:
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
target: *trigger_target_battery
|
|
||||||
|
|
||||||
started_charging:
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
target: *trigger_target_charging
|
|
||||||
|
|
||||||
stopped_charging:
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
target: *trigger_target_charging
|
|
||||||
|
|
||||||
level_changed:
|
|
||||||
target: *trigger_target_percentage
|
|
||||||
fields:
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *battery_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number: *battery_threshold_number
|
|
||||||
|
|
||||||
level_crossed_threshold:
|
|
||||||
target: *trigger_target_percentage
|
|
||||||
fields:
|
|
||||||
behavior: *trigger_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *battery_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number: *battery_threshold_number
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["pyblu==2.0.6"],
|
"requirements": ["pyblu==2.0.5"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_musc._tcp.local."
|
"type": "_musc._tcp.local."
|
||||||
|
|||||||
@@ -21,6 +21,6 @@
|
|||||||
"bluetooth-auto-recovery==1.5.3",
|
"bluetooth-auto-recovery==1.5.3",
|
||||||
"bluetooth-data-tools==1.28.4",
|
"bluetooth-data-tools==1.28.4",
|
||||||
"dbus-fast==3.1.2",
|
"dbus-fast==3.1.2",
|
||||||
"habluetooth==5.11.1"
|
"habluetooth==5.10.2"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["bsblan"],
|
"loggers": ["bsblan"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["python-bsblan==5.1.3"],
|
"requirements": ["python-bsblan==5.1.2"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"name": "bsb-lan*",
|
"name": "bsb-lan*",
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"press": {
|
"press": {
|
||||||
"description": "Presses a button.",
|
"description": "Presses a button entity.",
|
||||||
"name": "Press button"
|
"name": "Press"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Button",
|
"title": "Button",
|
||||||
|
|||||||
@@ -578,13 +578,13 @@ class CalendarEntity(Entity):
|
|||||||
return STATE_OFF
|
return STATE_OFF
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_write_ha_state(self) -> None:
|
def async_write_ha_state(self) -> None:
|
||||||
"""Write the state to the state machine.
|
"""Write the state to the state machine.
|
||||||
|
|
||||||
This sets up listeners to handle state transitions for start or end of
|
This sets up listeners to handle state transitions for start or end of
|
||||||
the current or upcoming event.
|
the current or upcoming event.
|
||||||
"""
|
"""
|
||||||
super()._async_write_ha_state()
|
super().async_write_ha_state()
|
||||||
if self._alarm_unsubs is None:
|
if self._alarm_unsubs is None:
|
||||||
self._alarm_unsubs = []
|
self._alarm_unsubs = []
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
"""Provides conditions for calendars."""
|
|
||||||
|
|
||||||
from homeassistant.const import STATE_ON
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
CONDITIONS: dict[str, type[Condition]] = {
|
|
||||||
"is_event_active": make_entity_state_condition(DOMAIN, STATE_ON),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
|
||||||
"""Return the calendar conditions."""
|
|
||||||
return CONDITIONS
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
is_event_active:
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
- domain: calendar
|
|
||||||
fields:
|
|
||||||
behavior:
|
|
||||||
required: true
|
|
||||||
default: any
|
|
||||||
selector:
|
|
||||||
select:
|
|
||||||
translation_key: condition_behavior
|
|
||||||
options:
|
|
||||||
- all
|
|
||||||
- any
|
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
{
|
{
|
||||||
"conditions": {
|
|
||||||
"is_event_active": {
|
|
||||||
"condition": "mdi:calendar-check"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity_component": {
|
"entity_component": {
|
||||||
"_": {
|
"_": {
|
||||||
"default": "mdi:calendar",
|
"default": "mdi:calendar",
|
||||||
|
|||||||
@@ -1,20 +1,4 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
|
||||||
"condition_behavior_description": "How the state should match on the targeted calendars.",
|
|
||||||
"condition_behavior_name": "Behavior"
|
|
||||||
},
|
|
||||||
"conditions": {
|
|
||||||
"is_event_active": {
|
|
||||||
"description": "Tests if one or more calendars have an active event.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::calendar::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::calendar::common::condition_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Calendar event is active"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity_component": {
|
"entity_component": {
|
||||||
"_": {
|
"_": {
|
||||||
"name": "[%key:component::calendar::title%]",
|
"name": "[%key:component::calendar::title%]",
|
||||||
@@ -62,12 +46,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"selector": {
|
"selector": {
|
||||||
"condition_behavior": {
|
|
||||||
"options": {
|
|
||||||
"all": "All",
|
|
||||||
"any": "Any"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"trigger_offset_type": {
|
"trigger_offset_type": {
|
||||||
"options": {
|
"options": {
|
||||||
"after": "After",
|
"after": "After",
|
||||||
@@ -112,7 +90,7 @@
|
|||||||
"name": "Summary"
|
"name": "Summary"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Create calendar event"
|
"name": "Create event"
|
||||||
},
|
},
|
||||||
"get_events": {
|
"get_events": {
|
||||||
"description": "Retrieves events on a calendar within a time range.",
|
"description": "Retrieves events on a calendar within a time range.",
|
||||||
@@ -130,7 +108,7 @@
|
|||||||
"name": "Start time"
|
"name": "Start time"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Get calendar events"
|
"name": "Get events"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Calendar",
|
"title": "Calendar",
|
||||||
|
|||||||
@@ -432,7 +432,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Entity Properties
|
# Entity Properties
|
||||||
entity_description: CameraEntityDescription
|
|
||||||
_attr_brand: str | None = None
|
_attr_brand: str | None = None
|
||||||
_attr_frame_interval: float = MIN_STREAM_INTERVAL
|
_attr_frame_interval: float = MIN_STREAM_INTERVAL
|
||||||
_attr_is_on: bool = True
|
_attr_is_on: bool = True
|
||||||
@@ -760,12 +759,12 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
return CameraCapabilities(frontend_stream_types)
|
return CameraCapabilities(frontend_stream_types)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_write_ha_state(self) -> None:
|
def async_write_ha_state(self) -> None:
|
||||||
"""Write the state to the state machine.
|
"""Write the state to the state machine.
|
||||||
|
|
||||||
Schedules async_refresh_providers if support of streams have changed.
|
Schedules async_refresh_providers if support of streams have changed.
|
||||||
"""
|
"""
|
||||||
super()._async_write_ha_state()
|
super().async_write_ha_state()
|
||||||
if self.__supports_stream != (
|
if self.__supports_stream != (
|
||||||
supports_stream := self.supported_features & CameraEntityFeature.STREAM
|
supports_stream := self.supported_features & CameraEntityFeature.STREAM
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -51,11 +51,11 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"disable_motion_detection": {
|
"disable_motion_detection": {
|
||||||
"description": "Disables the motion detection of a camera.",
|
"description": "Disables the motion detection of a camera.",
|
||||||
"name": "Disable camera motion detection"
|
"name": "Disable motion detection"
|
||||||
},
|
},
|
||||||
"enable_motion_detection": {
|
"enable_motion_detection": {
|
||||||
"description": "Enables the motion detection of a camera.",
|
"description": "Enables the motion detection of a camera.",
|
||||||
"name": "Enable camera motion detection"
|
"name": "Enable motion detection"
|
||||||
},
|
},
|
||||||
"play_stream": {
|
"play_stream": {
|
||||||
"description": "Plays a camera stream on a supported media player.",
|
"description": "Plays a camera stream on a supported media player.",
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"name": "Media player"
|
"name": "Media player"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Play camera stream"
|
"name": "Play stream"
|
||||||
},
|
},
|
||||||
"record": {
|
"record": {
|
||||||
"description": "Creates a recording of a live camera feed.",
|
"description": "Creates a recording of a live camera feed.",
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"name": "Lookback"
|
"name": "Lookback"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Record camera feed"
|
"name": "Record"
|
||||||
},
|
},
|
||||||
"snapshot": {
|
"snapshot": {
|
||||||
"description": "Takes a snapshot from a camera.",
|
"description": "Takes a snapshot from a camera.",
|
||||||
@@ -97,15 +97,15 @@
|
|||||||
"name": "Filename"
|
"name": "Filename"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Take camera snapshot"
|
"name": "Take snapshot"
|
||||||
},
|
},
|
||||||
"turn_off": {
|
"turn_off": {
|
||||||
"description": "Turns off a camera.",
|
"description": "Turns off a camera.",
|
||||||
"name": "Turn off camera"
|
"name": "[%key:common::action::turn_off%]"
|
||||||
},
|
},
|
||||||
"turn_on": {
|
"turn_on": {
|
||||||
"description": "Turns on a camera.",
|
"description": "Turns on a camera.",
|
||||||
"name": "Turn on camera"
|
"name": "[%key:common::action::turn_on%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Camera"
|
"title": "Camera"
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
"""The Casper Glow integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pycasperglow import CasperGlow
|
|
||||||
|
|
||||||
from homeassistant.components import bluetooth
|
|
||||||
from homeassistant.const import CONF_ADDRESS, Platform
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
|
||||||
|
|
||||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [
|
|
||||||
Platform.BINARY_SENSOR,
|
|
||||||
Platform.BUTTON,
|
|
||||||
Platform.LIGHT,
|
|
||||||
Platform.SELECT,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:
|
|
||||||
"""Set up Casper Glow from a config entry."""
|
|
||||||
address: str = entry.data[CONF_ADDRESS]
|
|
||||||
ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True)
|
|
||||||
if not ble_device:
|
|
||||||
raise ConfigEntryNotReady(
|
|
||||||
f"Could not find Casper Glow device with address {address}"
|
|
||||||
)
|
|
||||||
|
|
||||||
glow = CasperGlow(ble_device)
|
|
||||||
coordinator = CasperGlowCoordinator(hass, glow, entry.title)
|
|
||||||
entry.runtime_data = coordinator
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
||||||
|
|
||||||
entry.async_on_unload(coordinator.async_start())
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:
|
|
||||||
"""Unload a config entry."""
|
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
"""Casper Glow integration binary sensor platform."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pycasperglow import GlowState
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
|
||||||
from .entity import CasperGlowEntity
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: CasperGlowConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the binary sensor platform for Casper Glow."""
|
|
||||||
async_add_entities([CasperGlowPausedBinarySensor(entry.runtime_data)])
|
|
||||||
|
|
||||||
|
|
||||||
class CasperGlowPausedBinarySensor(CasperGlowEntity, BinarySensorEntity):
|
|
||||||
"""Binary sensor indicating whether the Casper Glow dimming is paused."""
|
|
||||||
|
|
||||||
_attr_translation_key = "paused"
|
|
||||||
|
|
||||||
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
|
|
||||||
"""Initialize the paused binary sensor."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_paused"
|
|
||||||
if coordinator.device.state.is_paused is not None:
|
|
||||||
self._attr_is_on = coordinator.device.state.is_paused
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Register state update callback when entity is added."""
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
self.async_on_remove(
|
|
||||||
self._device.register_callback(self._async_handle_state_update)
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_handle_state_update(self, state: GlowState) -> None:
|
|
||||||
"""Handle a state update from the device."""
|
|
||||||
if state.is_paused is not None:
|
|
||||||
self._attr_is_on = state.is_paused
|
|
||||||
self.async_write_ha_state()
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
"""Casper Glow integration button platform."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from pycasperglow import CasperGlow
|
|
||||||
|
|
||||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
|
||||||
from .entity import CasperGlowEntity
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class CasperGlowButtonEntityDescription(ButtonEntityDescription):
|
|
||||||
"""Describe a Casper Glow button entity."""
|
|
||||||
|
|
||||||
press_fn: Callable[[CasperGlow], Awaitable[None]]
|
|
||||||
|
|
||||||
|
|
||||||
BUTTON_DESCRIPTIONS: tuple[CasperGlowButtonEntityDescription, ...] = (
|
|
||||||
CasperGlowButtonEntityDescription(
|
|
||||||
key="pause",
|
|
||||||
translation_key="pause",
|
|
||||||
press_fn=lambda device: device.pause(),
|
|
||||||
),
|
|
||||||
CasperGlowButtonEntityDescription(
|
|
||||||
key="resume",
|
|
||||||
translation_key="resume",
|
|
||||||
press_fn=lambda device: device.resume(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: CasperGlowConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the button platform for Casper Glow."""
|
|
||||||
async_add_entities(
|
|
||||||
CasperGlowButton(entry.runtime_data, description)
|
|
||||||
for description in BUTTON_DESCRIPTIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CasperGlowButton(CasperGlowEntity, ButtonEntity):
|
|
||||||
"""A Casper Glow button entity."""
|
|
||||||
|
|
||||||
entity_description: CasperGlowButtonEntityDescription
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: CasperGlowCoordinator,
|
|
||||||
description: CasperGlowButtonEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize a Casper Glow button."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self.entity_description = description
|
|
||||||
self._attr_unique_id = (
|
|
||||||
f"{format_mac(coordinator.device.address)}_{description.key}"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_press(self) -> None:
|
|
||||||
"""Press the button."""
|
|
||||||
await self._async_command(self.entity_description.press_fn(self._device))
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
"""Config flow for Casper Glow integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from bluetooth_data_tools import human_readable_name
|
|
||||||
from pycasperglow import CasperGlow, CasperGlowError
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.bluetooth import (
|
|
||||||
BluetoothServiceInfoBleak,
|
|
||||||
async_discovered_service_info,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
|
||||||
from homeassistant.const import CONF_ADDRESS
|
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
|
||||||
|
|
||||||
from .const import DOMAIN, LOCAL_NAMES
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CasperGlowConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
||||||
"""Handle a config flow for Casper Glow."""
|
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
MINOR_VERSION = 1
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""Initialize the config flow."""
|
|
||||||
self._discovery_info: BluetoothServiceInfoBleak | None = None
|
|
||||||
self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {}
|
|
||||||
|
|
||||||
async def async_step_bluetooth(
|
|
||||||
self, discovery_info: BluetoothServiceInfoBleak
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle the bluetooth discovery step."""
|
|
||||||
await self.async_set_unique_id(format_mac(discovery_info.address))
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
self._discovery_info = discovery_info
|
|
||||||
self.context["title_placeholders"] = {
|
|
||||||
"name": human_readable_name(
|
|
||||||
None, discovery_info.name, discovery_info.address
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return await self.async_step_bluetooth_confirm()
|
|
||||||
|
|
||||||
async def async_step_bluetooth_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Confirm a discovered Casper Glow device."""
|
|
||||||
assert self._discovery_info is not None
|
|
||||||
if user_input is not None:
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=self.context["title_placeholders"]["name"],
|
|
||||||
data={CONF_ADDRESS: self._discovery_info.address},
|
|
||||||
)
|
|
||||||
glow = CasperGlow(self._discovery_info.device)
|
|
||||||
try:
|
|
||||||
await glow.handshake()
|
|
||||||
except CasperGlowError:
|
|
||||||
return self.async_abort(reason="cannot_connect")
|
|
||||||
except Exception:
|
|
||||||
_LOGGER.exception(
|
|
||||||
"Unexpected error during Casper Glow config flow "
|
|
||||||
"(step=bluetooth_confirm, address=%s)",
|
|
||||||
self._discovery_info.address,
|
|
||||||
)
|
|
||||||
return self.async_abort(reason="unknown")
|
|
||||||
self._set_confirm_only()
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="bluetooth_confirm",
|
|
||||||
description_placeholders=self.context["title_placeholders"],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_user(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle the user step to pick discovered device."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
address = user_input[CONF_ADDRESS]
|
|
||||||
discovery_info = self._discovered_devices[address]
|
|
||||||
await self.async_set_unique_id(
|
|
||||||
format_mac(discovery_info.address), raise_on_progress=False
|
|
||||||
)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
glow = CasperGlow(discovery_info.device)
|
|
||||||
try:
|
|
||||||
await glow.handshake()
|
|
||||||
except CasperGlowError:
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
except Exception:
|
|
||||||
_LOGGER.exception(
|
|
||||||
"Unexpected error during Casper Glow config flow "
|
|
||||||
"(step=user, address=%s)",
|
|
||||||
discovery_info.address,
|
|
||||||
)
|
|
||||||
errors["base"] = "unknown"
|
|
||||||
else:
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=human_readable_name(
|
|
||||||
None, discovery_info.name, discovery_info.address
|
|
||||||
),
|
|
||||||
data={
|
|
||||||
CONF_ADDRESS: discovery_info.address,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if discovery := self._discovery_info:
|
|
||||||
self._discovered_devices[discovery.address] = discovery
|
|
||||||
else:
|
|
||||||
current_addresses = self._async_current_ids(include_ignore=False)
|
|
||||||
for discovery in async_discovered_service_info(self.hass):
|
|
||||||
if (
|
|
||||||
format_mac(discovery.address) in current_addresses
|
|
||||||
or discovery.address in self._discovered_devices
|
|
||||||
or not (
|
|
||||||
discovery.name
|
|
||||||
and any(
|
|
||||||
discovery.name.startswith(local_name)
|
|
||||||
for local_name in LOCAL_NAMES
|
|
||||||
)
|
|
||||||
)
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
self._discovered_devices[discovery.address] = discovery
|
|
||||||
|
|
||||||
if not self._discovered_devices:
|
|
||||||
return self.async_abort(reason="no_devices_found")
|
|
||||||
|
|
||||||
data_schema = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_ADDRESS): vol.In(
|
|
||||||
{
|
|
||||||
service_info.address: human_readable_name(
|
|
||||||
None, service_info.name, service_info.address
|
|
||||||
)
|
|
||||||
for service_info in self._discovered_devices.values()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=data_schema,
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
"""Constants for the Casper Glow integration."""
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from pycasperglow import BRIGHTNESS_LEVELS, DEVICE_NAME_PREFIX, DIMMING_TIME_MINUTES
|
|
||||||
|
|
||||||
DOMAIN = "casper_glow"
|
|
||||||
|
|
||||||
LOCAL_NAMES = {DEVICE_NAME_PREFIX}
|
|
||||||
|
|
||||||
SORTED_BRIGHTNESS_LEVELS = sorted(BRIGHTNESS_LEVELS)
|
|
||||||
|
|
||||||
DEFAULT_DIMMING_TIME_MINUTES: int = DIMMING_TIME_MINUTES[0]
|
|
||||||
|
|
||||||
DIMMING_TIME_OPTIONS: tuple[str, ...] = tuple(str(m) for m in DIMMING_TIME_MINUTES)
|
|
||||||
|
|
||||||
# Interval between periodic state polls to catch externally-triggered changes.
|
|
||||||
STATE_POLL_INTERVAL = timedelta(seconds=30)
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
"""Coordinator for the Casper Glow integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from bleak import BleakError
|
|
||||||
from bluetooth_data_tools import monotonic_time_coarse
|
|
||||||
from pycasperglow import CasperGlow
|
|
||||||
|
|
||||||
from homeassistant.components.bluetooth import (
|
|
||||||
BluetoothChange,
|
|
||||||
BluetoothScanningMode,
|
|
||||||
BluetoothServiceInfoBleak,
|
|
||||||
)
|
|
||||||
from homeassistant.components.bluetooth.active_update_coordinator import (
|
|
||||||
ActiveBluetoothDataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
|
||||||
|
|
||||||
from .const import SORTED_BRIGHTNESS_LEVELS, STATE_POLL_INTERVAL
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
type CasperGlowConfigEntry = ConfigEntry[CasperGlowCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
class CasperGlowCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
|
|
||||||
"""Coordinator for Casper Glow BLE devices."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
device: CasperGlow,
|
|
||||||
title: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the coordinator."""
|
|
||||||
super().__init__(
|
|
||||||
hass=hass,
|
|
||||||
logger=_LOGGER,
|
|
||||||
address=device.address,
|
|
||||||
mode=BluetoothScanningMode.PASSIVE,
|
|
||||||
needs_poll_method=self._needs_poll,
|
|
||||||
poll_method=self._async_update,
|
|
||||||
connectable=True,
|
|
||||||
)
|
|
||||||
self.device = device
|
|
||||||
self.last_dimming_time_minutes: int | None = (
|
|
||||||
device.state.configured_dimming_time_minutes
|
|
||||||
)
|
|
||||||
self.title = title
|
|
||||||
|
|
||||||
# The device API couples brightness and dimming time into a
|
|
||||||
# single command (set_brightness_and_dimming_time), so both
|
|
||||||
# values must be tracked here for cross-entity use.
|
|
||||||
self.last_brightness_pct: int = (
|
|
||||||
device.state.brightness_level
|
|
||||||
if device.state.brightness_level is not None
|
|
||||||
else SORTED_BRIGHTNESS_LEVELS[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _needs_poll(
|
|
||||||
self,
|
|
||||||
service_info: BluetoothServiceInfoBleak,
|
|
||||||
seconds_since_last_poll: float | None,
|
|
||||||
) -> bool:
|
|
||||||
"""Return True if a poll is needed."""
|
|
||||||
return (
|
|
||||||
seconds_since_last_poll is None
|
|
||||||
or seconds_since_last_poll >= STATE_POLL_INTERVAL.total_seconds()
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _async_update(self, service_info: BluetoothServiceInfoBleak) -> None:
|
|
||||||
"""Poll device state."""
|
|
||||||
await self.device.query_state()
|
|
||||||
|
|
||||||
async def _async_poll(self) -> None:
|
|
||||||
"""Poll the device and log availability changes."""
|
|
||||||
assert self._last_service_info
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self._async_poll_data(self._last_service_info)
|
|
||||||
except BleakError as exc:
|
|
||||||
if self.last_poll_successful:
|
|
||||||
_LOGGER.info("%s is unavailable: %s", self.title, exc)
|
|
||||||
self.last_poll_successful = False
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
if self.last_poll_successful:
|
|
||||||
_LOGGER.exception("%s: unexpected error while polling", self.title)
|
|
||||||
self.last_poll_successful = False
|
|
||||||
return
|
|
||||||
finally:
|
|
||||||
self._last_poll = monotonic_time_coarse()
|
|
||||||
|
|
||||||
if not self.last_poll_successful:
|
|
||||||
_LOGGER.info("%s is back online", self.title)
|
|
||||||
self.last_poll_successful = True
|
|
||||||
|
|
||||||
self._async_handle_bluetooth_poll()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_handle_bluetooth_event(
|
|
||||||
self,
|
|
||||||
service_info: BluetoothServiceInfoBleak,
|
|
||||||
change: BluetoothChange,
|
|
||||||
) -> None:
|
|
||||||
"""Update BLE device reference on each advertisement."""
|
|
||||||
self.device.set_ble_device(service_info.device)
|
|
||||||
super()._async_handle_bluetooth_event(service_info, change)
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
"""Base entity for the Casper Glow integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Awaitable
|
|
||||||
|
|
||||||
from pycasperglow import CasperGlowError
|
|
||||||
|
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
|
||||||
PassiveBluetoothCoordinatorEntity,
|
|
||||||
)
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers import device_registry as dr
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .coordinator import CasperGlowCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
class CasperGlowEntity(PassiveBluetoothCoordinatorEntity[CasperGlowCoordinator]):
|
|
||||||
"""Base class for Casper Glow entities."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
|
|
||||||
"""Initialize a Casper Glow entity."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._device = coordinator.device
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
manufacturer="Casper",
|
|
||||||
model="Glow",
|
|
||||||
model_id="G01",
|
|
||||||
connections={
|
|
||||||
(dr.CONNECTION_BLUETOOTH, format_mac(coordinator.device.address))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _async_command(self, coro: Awaitable[None]) -> None:
|
|
||||||
"""Execute a device command."""
|
|
||||||
try:
|
|
||||||
await coro
|
|
||||||
except CasperGlowError as err:
|
|
||||||
raise HomeAssistantError(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="communication_error",
|
|
||||||
translation_placeholders={"error": str(err)},
|
|
||||||
) from err
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"entity": {
|
|
||||||
"binary_sensor": {
|
|
||||||
"paused": {
|
|
||||||
"default": "mdi:timer-pause"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"button": {
|
|
||||||
"pause": {
|
|
||||||
"default": "mdi:pause"
|
|
||||||
},
|
|
||||||
"resume": {
|
|
||||||
"default": "mdi:play"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"select": {
|
|
||||||
"dimming_time": {
|
|
||||||
"default": "mdi:timer-outline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
"""Casper Glow integration light platform."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from pycasperglow import GlowState
|
|
||||||
|
|
||||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
from homeassistant.util.percentage import (
|
|
||||||
ordered_list_item_to_percentage,
|
|
||||||
percentage_to_ordered_list_item,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import DEFAULT_DIMMING_TIME_MINUTES, SORTED_BRIGHTNESS_LEVELS
|
|
||||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
|
||||||
from .entity import CasperGlowEntity
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
|
||||||
|
|
||||||
|
|
||||||
def _ha_brightness_to_device_pct(brightness: int) -> int:
|
|
||||||
"""Convert HA brightness (1-255) to device percentage by snapping to nearest."""
|
|
||||||
return percentage_to_ordered_list_item(
|
|
||||||
SORTED_BRIGHTNESS_LEVELS, round(brightness * 100 / 255)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _device_pct_to_ha_brightness(pct: int) -> int:
|
|
||||||
"""Convert device brightness percentage (60-100) to HA brightness (1-255)."""
|
|
||||||
percent = ordered_list_item_to_percentage(SORTED_BRIGHTNESS_LEVELS, pct)
|
|
||||||
return round(percent * 255 / 100)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: CasperGlowConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the light platform for Casper Glow."""
|
|
||||||
async_add_entities([CasperGlowLight(entry.runtime_data)])
|
|
||||||
|
|
||||||
|
|
||||||
class CasperGlowLight(CasperGlowEntity, LightEntity):
|
|
||||||
"""Representation of a Casper Glow light."""
|
|
||||||
|
|
||||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
|
||||||
_attr_name = None
|
|
||||||
|
|
||||||
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
|
|
||||||
"""Initialize a Casper Glow light."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._attr_unique_id = format_mac(coordinator.device.address)
|
|
||||||
self._update_from_state(coordinator.device.state)
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Register state update callback when entity is added."""
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
self.async_on_remove(
|
|
||||||
self._device.register_callback(self._async_handle_state_update)
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _update_from_state(self, state: GlowState) -> None:
|
|
||||||
"""Update entity attributes from device state."""
|
|
||||||
if state.is_on is not None:
|
|
||||||
self._attr_is_on = state.is_on
|
|
||||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
|
||||||
if state.brightness_level is not None:
|
|
||||||
self._attr_brightness = _device_pct_to_ha_brightness(state.brightness_level)
|
|
||||||
self.coordinator.last_brightness_pct = state.brightness_level
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_handle_state_update(self, state: GlowState) -> None:
|
|
||||||
"""Handle a state update from the device."""
|
|
||||||
self._update_from_state(state)
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
||||||
"""Turn the light on."""
|
|
||||||
brightness_pct: int | None = None
|
|
||||||
if ATTR_BRIGHTNESS in kwargs:
|
|
||||||
brightness_pct = _ha_brightness_to_device_pct(kwargs[ATTR_BRIGHTNESS])
|
|
||||||
|
|
||||||
await self._async_command(self._device.turn_on())
|
|
||||||
self._attr_is_on = True
|
|
||||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
|
||||||
if brightness_pct is not None:
|
|
||||||
await self._async_command(
|
|
||||||
self._device.set_brightness_and_dimming_time(
|
|
||||||
brightness_pct,
|
|
||||||
self.coordinator.last_dimming_time_minutes
|
|
||||||
if self.coordinator.last_dimming_time_minutes is not None
|
|
||||||
else DEFAULT_DIMMING_TIME_MINUTES,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self._attr_brightness = _device_pct_to_ha_brightness(brightness_pct)
|
|
||||||
self.coordinator.last_brightness_pct = brightness_pct
|
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
||||||
"""Turn the light off."""
|
|
||||||
await self._async_command(self._device.turn_off())
|
|
||||||
self._attr_is_on = False
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "casper_glow",
|
|
||||||
"name": "Casper Glow",
|
|
||||||
"bluetooth": [
|
|
||||||
{
|
|
||||||
"connectable": true,
|
|
||||||
"local_name": "Jar*"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"codeowners": ["@mikeodr"],
|
|
||||||
"config_flow": true,
|
|
||||||
"dependencies": ["bluetooth_adapters"],
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/casper_glow",
|
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
|
||||||
"loggers": ["pycasperglow"],
|
|
||||||
"quality_scale": "silver",
|
|
||||||
"requirements": ["pycasperglow==1.1.0"]
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
rules:
|
|
||||||
# Bronze
|
|
||||||
action-setup:
|
|
||||||
status: exempt
|
|
||||||
comment: No custom services.
|
|
||||||
appropriate-polling: done
|
|
||||||
brands: done
|
|
||||||
common-modules: done
|
|
||||||
config-flow-test-coverage: done
|
|
||||||
config-flow: done
|
|
||||||
dependency-transparency: done
|
|
||||||
docs-actions:
|
|
||||||
status: exempt
|
|
||||||
comment: No custom actions/services.
|
|
||||||
docs-high-level-description: done
|
|
||||||
docs-installation-instructions: done
|
|
||||||
docs-removal-instructions: done
|
|
||||||
entity-event-setup: done
|
|
||||||
entity-unique-id: done
|
|
||||||
has-entity-name: done
|
|
||||||
runtime-data: done
|
|
||||||
test-before-configure: done
|
|
||||||
test-before-setup: done
|
|
||||||
unique-config-entry: done
|
|
||||||
|
|
||||||
# Silver
|
|
||||||
action-exceptions: done
|
|
||||||
config-entry-unloading: done
|
|
||||||
docs-configuration-parameters: done
|
|
||||||
docs-installation-parameters: done
|
|
||||||
entity-unavailable: done
|
|
||||||
integration-owner: done
|
|
||||||
log-when-unavailable: done
|
|
||||||
parallel-updates: done
|
|
||||||
reauthentication-flow:
|
|
||||||
status: exempt
|
|
||||||
comment: Bluetooth device with no authentication credentials.
|
|
||||||
test-coverage: done
|
|
||||||
|
|
||||||
# Gold
|
|
||||||
devices: done
|
|
||||||
diagnostics: todo
|
|
||||||
discovery-update-info:
|
|
||||||
status: exempt
|
|
||||||
comment: No network discovery.
|
|
||||||
discovery: done
|
|
||||||
docs-data-update: done
|
|
||||||
docs-examples: todo
|
|
||||||
docs-known-limitations: done
|
|
||||||
docs-supported-devices: done
|
|
||||||
docs-supported-functions: done
|
|
||||||
docs-troubleshooting: done
|
|
||||||
docs-use-cases: todo
|
|
||||||
dynamic-devices: todo
|
|
||||||
entity-category: done
|
|
||||||
entity-device-class:
|
|
||||||
status: exempt
|
|
||||||
comment: No applicable device classes for binary_sensor, button, light, or select entities.
|
|
||||||
entity-disabled-by-default: todo
|
|
||||||
entity-translations: done
|
|
||||||
exception-translations: done
|
|
||||||
icon-translations: done
|
|
||||||
reconfiguration-flow: todo
|
|
||||||
repair-issues: todo
|
|
||||||
stale-devices: todo
|
|
||||||
|
|
||||||
# Platinum
|
|
||||||
async-dependency: done
|
|
||||||
inject-websession:
|
|
||||||
status: exempt
|
|
||||||
comment: No web session is used by this integration.
|
|
||||||
strict-typing: done
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
"""Casper Glow integration select platform for dimming time."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pycasperglow import GlowState
|
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity
|
|
||||||
from homeassistant.const import EntityCategory, UnitOfTime
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
|
||||||
|
|
||||||
from .const import DIMMING_TIME_OPTIONS
|
|
||||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
|
||||||
from .entity import CasperGlowEntity
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: CasperGlowConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the select platform for Casper Glow."""
|
|
||||||
async_add_entities([CasperGlowDimmingTimeSelect(entry.runtime_data)])
|
|
||||||
|
|
||||||
|
|
||||||
class CasperGlowDimmingTimeSelect(CasperGlowEntity, SelectEntity, RestoreEntity):
|
|
||||||
"""Select entity for Casper Glow dimming time."""
|
|
||||||
|
|
||||||
_attr_translation_key = "dimming_time"
|
|
||||||
_attr_entity_category = EntityCategory.CONFIG
|
|
||||||
_attr_options = list(DIMMING_TIME_OPTIONS)
|
|
||||||
_attr_unit_of_measurement = UnitOfTime.MINUTES
|
|
||||||
|
|
||||||
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
|
|
||||||
"""Initialize the dimming time select entity."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_dimming_time"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_option(self) -> str | None:
|
|
||||||
"""Return the currently selected dimming time from the coordinator."""
|
|
||||||
if self.coordinator.last_dimming_time_minutes is None:
|
|
||||||
return None
|
|
||||||
return str(self.coordinator.last_dimming_time_minutes)
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Restore last known dimming time and register state update callback."""
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
if self.coordinator.last_dimming_time_minutes is None and (
|
|
||||||
last_state := await self.async_get_last_state()
|
|
||||||
):
|
|
||||||
if last_state.state in DIMMING_TIME_OPTIONS:
|
|
||||||
self.coordinator.last_dimming_time_minutes = int(last_state.state)
|
|
||||||
self.async_on_remove(
|
|
||||||
self._device.register_callback(self._async_handle_state_update)
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_handle_state_update(self, state: GlowState) -> None:
|
|
||||||
"""Handle a state update from the device."""
|
|
||||||
if state.brightness_level is not None:
|
|
||||||
self.coordinator.last_brightness_pct = state.brightness_level
|
|
||||||
if (
|
|
||||||
state.configured_dimming_time_minutes is not None
|
|
||||||
and self.coordinator.last_dimming_time_minutes is None
|
|
||||||
):
|
|
||||||
self.coordinator.last_dimming_time_minutes = (
|
|
||||||
state.configured_dimming_time_minutes
|
|
||||||
)
|
|
||||||
# Dimming time is not part of the device state
|
|
||||||
# that is provided via BLE update. Therefore
|
|
||||||
# we need to trigger a state update for the select entity
|
|
||||||
# to update the current state.
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
async def async_select_option(self, option: str) -> None:
|
|
||||||
"""Set the dimming time."""
|
|
||||||
await self._async_command(
|
|
||||||
self._device.set_brightness_and_dimming_time(
|
|
||||||
self.coordinator.last_brightness_pct, int(option)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.coordinator.last_dimming_time_minutes = int(option)
|
|
||||||
# Dimming time is not part of the device state
|
|
||||||
# that is provided via BLE update. Therefore
|
|
||||||
# we need to trigger a state update for the select entity
|
|
||||||
# to update the current state.
|
|
||||||
self.async_write_ha_state()
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
|
||||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
|
||||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
|
||||||
},
|
|
||||||
"flow_title": "{name}",
|
|
||||||
"step": {
|
|
||||||
"bluetooth_confirm": {
|
|
||||||
"description": "Do you want to set up {name}?"
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"data": {
|
|
||||||
"address": "Bluetooth address"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"address": "The Bluetooth address of the Casper Glow light"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity": {
|
|
||||||
"binary_sensor": {
|
|
||||||
"paused": {
|
|
||||||
"name": "Dimming paused"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"button": {
|
|
||||||
"pause": {
|
|
||||||
"name": "Pause dimming"
|
|
||||||
},
|
|
||||||
"resume": {
|
|
||||||
"name": "Resume dimming"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"select": {
|
|
||||||
"dimming_time": {
|
|
||||||
"name": "Dimming time"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exceptions": {
|
|
||||||
"communication_error": {
|
|
||||||
"message": "An error occurred while communicating with the Casper Glow: {error}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +1,20 @@
|
|||||||
{
|
{
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"chess960_daily_draw": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess960_daily_lost": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess960_daily_rating": {
|
|
||||||
"default": "mdi:chart-line"
|
|
||||||
},
|
|
||||||
"chess960_daily_won": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess_blitz_draw": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess_blitz_lost": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess_blitz_rating": {
|
|
||||||
"default": "mdi:chart-line"
|
|
||||||
},
|
|
||||||
"chess_blitz_won": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess_bullet_draw": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess_bullet_lost": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess_bullet_rating": {
|
|
||||||
"default": "mdi:chart-line"
|
|
||||||
},
|
|
||||||
"chess_bullet_won": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess_daily_draw": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess_daily_lost": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess_daily_rating": {
|
"chess_daily_rating": {
|
||||||
"default": "mdi:chart-line"
|
"default": "mdi:chart-line"
|
||||||
},
|
},
|
||||||
"chess_daily_won": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess_rapid_draw": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess_rapid_lost": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"chess_rapid_rating": {
|
|
||||||
"default": "mdi:chart-line"
|
|
||||||
},
|
|
||||||
"chess_rapid_won": {
|
|
||||||
"default": "mdi:chess-pawn"
|
|
||||||
},
|
|
||||||
"followers": {
|
"followers": {
|
||||||
"default": "mdi:account-multiple"
|
"default": "mdi:account-multiple"
|
||||||
|
},
|
||||||
|
"total_daily_draw": {
|
||||||
|
"default": "mdi:chess-pawn"
|
||||||
|
},
|
||||||
|
"total_daily_lost": {
|
||||||
|
"default": "mdi:chess-pawn"
|
||||||
|
},
|
||||||
|
"total_daily_won": {
|
||||||
|
"default": "mdi:chess-pawn"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Any
|
|
||||||
|
|
||||||
from chess_com_api import PlayerStats
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
@@ -27,14 +24,7 @@ class ChessEntityDescription(SensorEntityDescription):
|
|||||||
value_fn: Callable[[ChessData], float]
|
value_fn: Callable[[ChessData], float]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True, frozen=True)
|
SENSORS: tuple[ChessEntityDescription, ...] = (
|
||||||
class ChessModeEntityDescription(SensorEntityDescription):
|
|
||||||
"""Sensor description for a Chess.com game mode."""
|
|
||||||
|
|
||||||
value_fn: Callable[[dict[str, Any]], float]
|
|
||||||
|
|
||||||
|
|
||||||
PLAYER_SENSORS: tuple[ChessEntityDescription, ...] = (
|
|
||||||
ChessEntityDescription(
|
ChessEntityDescription(
|
||||||
key="followers",
|
key="followers",
|
||||||
translation_key="followers",
|
translation_key="followers",
|
||||||
@@ -43,46 +33,35 @@ PLAYER_SENSORS: tuple[ChessEntityDescription, ...] = (
|
|||||||
value_fn=lambda state: state.player.followers,
|
value_fn=lambda state: state.player.followers,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
)
|
ChessEntityDescription(
|
||||||
|
key="chess_daily_rating",
|
||||||
GAME_MODE_SENSORS: tuple[ChessModeEntityDescription, ...] = (
|
translation_key="chess_daily_rating",
|
||||||
ChessModeEntityDescription(
|
|
||||||
key="rating",
|
|
||||||
translation_key="rating",
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda mode: mode["last"]["rating"],
|
value_fn=lambda state: state.stats.chess_daily["last"]["rating"],
|
||||||
),
|
),
|
||||||
ChessModeEntityDescription(
|
ChessEntityDescription(
|
||||||
key="won",
|
key="total_daily_won",
|
||||||
translation_key="won",
|
translation_key="total_daily_won",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
value_fn=lambda mode: mode["record"]["win"],
|
value_fn=lambda state: state.stats.chess_daily["record"]["win"],
|
||||||
),
|
),
|
||||||
ChessModeEntityDescription(
|
ChessEntityDescription(
|
||||||
key="lost",
|
key="total_daily_lost",
|
||||||
translation_key="lost",
|
translation_key="total_daily_lost",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
value_fn=lambda mode: mode["record"]["loss"],
|
value_fn=lambda state: state.stats.chess_daily["record"]["loss"],
|
||||||
),
|
),
|
||||||
ChessModeEntityDescription(
|
ChessEntityDescription(
|
||||||
key="draw",
|
key="total_daily_draw",
|
||||||
translation_key="draw",
|
translation_key="total_daily_draw",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
value_fn=lambda mode: mode["record"]["draw"],
|
value_fn=lambda state: state.stats.chess_daily["record"]["draw"],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
GAME_MODES: dict[str, Callable[[PlayerStats], dict[str, Any] | None]] = {
|
|
||||||
"chess_daily": lambda stats: stats.chess_daily,
|
|
||||||
"chess_rapid": lambda stats: stats.chess_rapid,
|
|
||||||
"chess_bullet": lambda stats: stats.chess_bullet,
|
|
||||||
"chess_blitz": lambda stats: stats.chess_blitz,
|
|
||||||
"chess960_daily": lambda stats: stats.chess960_daily,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -92,22 +71,13 @@ async def async_setup_entry(
|
|||||||
"""Initialize the entries."""
|
"""Initialize the entries."""
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
entities: list[SensorEntity] = [
|
async_add_entities(
|
||||||
ChessPlayerSensor(coordinator, description) for description in PLAYER_SENSORS
|
ChessPlayerSensor(coordinator, description) for description in SENSORS
|
||||||
]
|
)
|
||||||
|
|
||||||
for game_mode, stats_fn in GAME_MODES.items():
|
|
||||||
if stats_fn(coordinator.data.stats) is not None:
|
|
||||||
entities.extend(
|
|
||||||
ChessGameModeSensor(coordinator, description, game_mode, stats_fn)
|
|
||||||
for description in GAME_MODE_SENSORS
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
|
||||||
|
|
||||||
|
|
||||||
class ChessPlayerSensor(ChessEntity, SensorEntity):
|
class ChessPlayerSensor(ChessEntity, SensorEntity):
|
||||||
"""Chess.com player sensor."""
|
"""Chess.com sensor."""
|
||||||
|
|
||||||
entity_description: ChessEntityDescription
|
entity_description: ChessEntityDescription
|
||||||
|
|
||||||
@@ -125,33 +95,3 @@ class ChessPlayerSensor(ChessEntity, SensorEntity):
|
|||||||
def native_value(self) -> float:
|
def native_value(self) -> float:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self.entity_description.value_fn(self.coordinator.data)
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
|
||||||
class ChessGameModeSensor(ChessEntity, SensorEntity):
|
|
||||||
"""Chess.com game mode sensor."""
|
|
||||||
|
|
||||||
entity_description: ChessModeEntityDescription
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: ChessCoordinator,
|
|
||||||
description: ChessModeEntityDescription,
|
|
||||||
game_mode: str,
|
|
||||||
stats_fn: Callable[[PlayerStats], dict[str, Any] | None],
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the sensor."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self.entity_description = description
|
|
||||||
self._stats_fn = stats_fn
|
|
||||||
self._attr_unique_id = (
|
|
||||||
f"{coordinator.config_entry.unique_id}.{game_mode}.{description.key}"
|
|
||||||
)
|
|
||||||
self._attr_translation_key = f"{game_mode}_{description.translation_key}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> float:
|
|
||||||
"""Return the state of the sensor."""
|
|
||||||
mode_data = self._stats_fn(self.coordinator.data.stats)
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
assert mode_data is not None
|
|
||||||
return self.entity_description.value_fn(mode_data)
|
|
||||||
|
|||||||
@@ -23,84 +23,24 @@
|
|||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"chess960_daily_draw": {
|
|
||||||
"name": "Total daily Chess960 games drawn",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess960_daily_lost": {
|
|
||||||
"name": "Total daily Chess960 games lost",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess960_daily_rating": {
|
|
||||||
"name": "Daily Chess960 rating"
|
|
||||||
},
|
|
||||||
"chess960_daily_won": {
|
|
||||||
"name": "Total daily Chess960 games won",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess_blitz_draw": {
|
|
||||||
"name": "Total blitz chess games drawn",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess_blitz_lost": {
|
|
||||||
"name": "Total blitz chess games lost",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess_blitz_rating": {
|
|
||||||
"name": "Blitz chess rating"
|
|
||||||
},
|
|
||||||
"chess_blitz_won": {
|
|
||||||
"name": "Total blitz chess games won",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess_bullet_draw": {
|
|
||||||
"name": "Total bullet chess games drawn",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess_bullet_lost": {
|
|
||||||
"name": "Total bullet chess games lost",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess_bullet_rating": {
|
|
||||||
"name": "Bullet chess rating"
|
|
||||||
},
|
|
||||||
"chess_bullet_won": {
|
|
||||||
"name": "Total bullet chess games won",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess_daily_draw": {
|
|
||||||
"name": "Total daily chess games drawn",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess_daily_lost": {
|
|
||||||
"name": "Total daily chess games lost",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess_daily_rating": {
|
"chess_daily_rating": {
|
||||||
"name": "Daily chess rating"
|
"name": "Daily chess rating"
|
||||||
},
|
},
|
||||||
"chess_daily_won": {
|
|
||||||
"name": "Total daily chess games won",
|
|
||||||
"unit_of_measurement": "games"
|
|
||||||
},
|
|
||||||
"chess_rapid_draw": {
|
|
||||||
"name": "Total rapid chess games drawn",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess_rapid_lost": {
|
|
||||||
"name": "Total rapid chess games lost",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"chess_rapid_rating": {
|
|
||||||
"name": "Rapid chess rating"
|
|
||||||
},
|
|
||||||
"chess_rapid_won": {
|
|
||||||
"name": "Total rapid chess games won",
|
|
||||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
|
||||||
},
|
|
||||||
"followers": {
|
"followers": {
|
||||||
"name": "Followers",
|
"name": "Followers",
|
||||||
"unit_of_measurement": "followers"
|
"unit_of_measurement": "followers"
|
||||||
|
},
|
||||||
|
"total_daily_draw": {
|
||||||
|
"name": "Total chess games drawn",
|
||||||
|
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::total_daily_won::unit_of_measurement%]"
|
||||||
|
},
|
||||||
|
"total_daily_lost": {
|
||||||
|
"name": "Total chess games lost",
|
||||||
|
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::total_daily_won::unit_of_measurement%]"
|
||||||
|
},
|
||||||
|
"total_daily_won": {
|
||||||
|
"name": "Total chess games won",
|
||||||
|
"unit_of_measurement": "games"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,72 +1,12 @@
|
|||||||
"""Provides conditions for climates."""
|
"""Provides conditions for climates."""
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
|
|
||||||
from homeassistant.core import HomeAssistant, State
|
|
||||||
from homeassistant.helpers import config_validation as cv
|
|
||||||
from homeassistant.helpers.automation import DomainSpec
|
from homeassistant.helpers.automation import DomainSpec
|
||||||
from homeassistant.helpers.condition import (
|
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||||
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
|
|
||||||
Condition,
|
|
||||||
ConditionConfig,
|
|
||||||
EntityConditionBase,
|
|
||||||
EntityNumericalConditionWithUnitBase,
|
|
||||||
make_entity_numerical_condition,
|
|
||||||
make_entity_state_condition,
|
|
||||||
)
|
|
||||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
|
||||||
|
|
||||||
from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
|
||||||
|
|
||||||
CONF_HVAC_MODE = "hvac_mode"
|
|
||||||
|
|
||||||
_HVAC_MODE_CONDITION_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_OPTIONS): {
|
|
||||||
vol.Required(CONF_HVAC_MODE): vol.All(
|
|
||||||
cv.ensure_list, vol.Length(min=1), [vol.Coerce(HVACMode)]
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ClimateHVACModeCondition(EntityConditionBase):
|
|
||||||
"""Condition for climate HVAC mode."""
|
|
||||||
|
|
||||||
_domain_specs = {DOMAIN: DomainSpec()}
|
|
||||||
_schema = _HVAC_MODE_CONDITION_SCHEMA
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
|
||||||
"""Initialize the HVAC mode condition."""
|
|
||||||
super().__init__(hass, config)
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
assert config.options is not None
|
|
||||||
self._hvac_modes: set[str] = set(config.options[CONF_HVAC_MODE])
|
|
||||||
|
|
||||||
def is_valid_state(self, entity_state: State) -> bool:
|
|
||||||
"""Check if the state matches any of the expected HVAC modes."""
|
|
||||||
return entity_state.state in self._hvac_modes
|
|
||||||
|
|
||||||
|
|
||||||
class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
|
|
||||||
"""Mixin for climate target temperature conditions with unit conversion."""
|
|
||||||
|
|
||||||
_base_unit = UnitOfTemperature.CELSIUS
|
|
||||||
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
|
|
||||||
_unit_converter = TemperatureConverter
|
|
||||||
|
|
||||||
def _get_entity_unit(self, entity_state: State) -> str | None:
|
|
||||||
"""Get the temperature unit of a climate entity from its state."""
|
|
||||||
# Climate entities convert temperatures to the system unit via show_temp
|
|
||||||
return self._hass.config.units.temperature_unit
|
|
||||||
|
|
||||||
|
from .const import ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
||||||
|
|
||||||
CONDITIONS: dict[str, type[Condition]] = {
|
CONDITIONS: dict[str, type[Condition]] = {
|
||||||
"is_hvac_mode": ClimateHVACModeCondition,
|
|
||||||
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
|
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
|
||||||
"is_on": make_entity_state_condition(
|
"is_on": make_entity_state_condition(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@@ -88,11 +28,6 @@ CONDITIONS: dict[str, type[Condition]] = {
|
|||||||
"is_heating": make_entity_state_condition(
|
"is_heating": make_entity_state_condition(
|
||||||
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
|
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
|
||||||
),
|
),
|
||||||
"target_humidity": make_entity_numerical_condition(
|
|
||||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
|
||||||
valid_unit="%",
|
|
||||||
),
|
|
||||||
"target_temperature": ClimateTargetTemperatureCondition,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
.condition_common: &condition_common
|
.condition_common: &condition_common
|
||||||
target: &condition_climate_target
|
target:
|
||||||
entity:
|
entity:
|
||||||
domain: climate
|
domain: climate
|
||||||
fields:
|
fields:
|
||||||
behavior: &condition_behavior
|
behavior:
|
||||||
required: true
|
required: true
|
||||||
default: any
|
default: any
|
||||||
selector:
|
selector:
|
||||||
@@ -13,75 +13,8 @@
|
|||||||
- all
|
- all
|
||||||
- any
|
- any
|
||||||
|
|
||||||
.humidity_threshold_entity: &humidity_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: "%"
|
|
||||||
- domain: sensor
|
|
||||||
device_class: humidity
|
|
||||||
- domain: number
|
|
||||||
device_class: humidity
|
|
||||||
|
|
||||||
.humidity_threshold_number: &humidity_threshold_number
|
|
||||||
min: 0
|
|
||||||
max: 100
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: "%"
|
|
||||||
|
|
||||||
.temperature_units: &temperature_units
|
|
||||||
- "°C"
|
|
||||||
- "°F"
|
|
||||||
|
|
||||||
.temperature_threshold_entity: &temperature_threshold_entity
|
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *temperature_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: temperature
|
|
||||||
- domain: number
|
|
||||||
device_class: temperature
|
|
||||||
|
|
||||||
is_off: *condition_common
|
is_off: *condition_common
|
||||||
is_on: *condition_common
|
is_on: *condition_common
|
||||||
is_cooling: *condition_common
|
is_cooling: *condition_common
|
||||||
is_drying: *condition_common
|
is_drying: *condition_common
|
||||||
is_heating: *condition_common
|
is_heating: *condition_common
|
||||||
|
|
||||||
is_hvac_mode:
|
|
||||||
target: *condition_climate_target
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
hvac_mode:
|
|
||||||
context:
|
|
||||||
filter_target: target
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
state:
|
|
||||||
hide_states:
|
|
||||||
- unavailable
|
|
||||||
- unknown
|
|
||||||
multiple: true
|
|
||||||
|
|
||||||
target_humidity:
|
|
||||||
target: *condition_climate_target
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *humidity_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number: *humidity_threshold_number
|
|
||||||
|
|
||||||
target_temperature:
|
|
||||||
target: *condition_climate_target
|
|
||||||
fields:
|
|
||||||
behavior: *condition_behavior
|
|
||||||
threshold:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *temperature_threshold_entity
|
|
||||||
mode: is
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *temperature_units
|
|
||||||
|
|||||||
@@ -9,20 +9,11 @@
|
|||||||
"is_heating": {
|
"is_heating": {
|
||||||
"condition": "mdi:fire"
|
"condition": "mdi:fire"
|
||||||
},
|
},
|
||||||
"is_hvac_mode": {
|
|
||||||
"condition": "mdi:thermostat"
|
|
||||||
},
|
|
||||||
"is_off": {
|
"is_off": {
|
||||||
"condition": "mdi:power-off"
|
"condition": "mdi:power-off"
|
||||||
},
|
},
|
||||||
"is_on": {
|
"is_on": {
|
||||||
"condition": "mdi:power-on"
|
"condition": "mdi:power-on"
|
||||||
},
|
|
||||||
"target_humidity": {
|
|
||||||
"condition": "mdi:water-percent"
|
|
||||||
},
|
|
||||||
"target_temperature": {
|
|
||||||
"condition": "mdi:thermometer"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity_component": {
|
"entity_component": {
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ set_preset_mode:
|
|||||||
required: true
|
required: true
|
||||||
example: "away"
|
example: "away"
|
||||||
selector:
|
selector:
|
||||||
state:
|
text:
|
||||||
attribute: preset_mode
|
|
||||||
|
|
||||||
set_temperature:
|
set_temperature:
|
||||||
target:
|
target:
|
||||||
@@ -56,10 +55,16 @@ set_temperature:
|
|||||||
mode: box
|
mode: box
|
||||||
hvac_mode:
|
hvac_mode:
|
||||||
selector:
|
selector:
|
||||||
state:
|
select:
|
||||||
hide_states:
|
options:
|
||||||
- unavailable
|
- "off"
|
||||||
- unknown
|
- "auto"
|
||||||
|
- "cool"
|
||||||
|
- "dry"
|
||||||
|
- "fan_only"
|
||||||
|
- "heat_cool"
|
||||||
|
- "heat"
|
||||||
|
translation_key: hvac_mode
|
||||||
set_humidity:
|
set_humidity:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
@@ -86,8 +91,7 @@ set_fan_mode:
|
|||||||
required: true
|
required: true
|
||||||
example: "low"
|
example: "low"
|
||||||
selector:
|
selector:
|
||||||
state:
|
text:
|
||||||
attribute: fan_mode
|
|
||||||
|
|
||||||
set_hvac_mode:
|
set_hvac_mode:
|
||||||
target:
|
target:
|
||||||
@@ -111,8 +115,7 @@ set_swing_mode:
|
|||||||
required: true
|
required: true
|
||||||
example: "on"
|
example: "on"
|
||||||
selector:
|
selector:
|
||||||
state:
|
text:
|
||||||
attribute: swing_mode
|
|
||||||
|
|
||||||
set_swing_horizontal_mode:
|
set_swing_horizontal_mode:
|
||||||
target:
|
target:
|
||||||
@@ -125,8 +128,7 @@ set_swing_horizontal_mode:
|
|||||||
required: true
|
required: true
|
||||||
example: "on"
|
example: "on"
|
||||||
selector:
|
selector:
|
||||||
state:
|
text:
|
||||||
attribute: swing_horizontal_mode
|
|
||||||
|
|
||||||
turn_on:
|
turn_on:
|
||||||
target:
|
target:
|
||||||
|
|||||||
@@ -2,13 +2,8 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"condition_behavior_description": "How the state should match on the targeted climate-control devices.",
|
"condition_behavior_description": "How the state should match on the targeted climate-control devices.",
|
||||||
"condition_behavior_name": "Behavior",
|
"condition_behavior_name": "Behavior",
|
||||||
"condition_threshold_description": "What to test for and threshold values.",
|
|
||||||
"condition_threshold_name": "Threshold configuration",
|
|
||||||
"trigger_behavior_description": "The behavior of the targeted climates to trigger on.",
|
"trigger_behavior_description": "The behavior of the targeted climates to trigger on.",
|
||||||
"trigger_behavior_name": "Behavior",
|
"trigger_behavior_name": "Behavior"
|
||||||
"trigger_threshold_changed_description": "Which changes to trigger on and threshold values.",
|
|
||||||
"trigger_threshold_crossed_description": "Which threshold crossing to trigger on and threshold values.",
|
|
||||||
"trigger_threshold_name": "Threshold configuration"
|
|
||||||
},
|
},
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"is_cooling": {
|
"is_cooling": {
|
||||||
@@ -41,20 +36,6 @@
|
|||||||
},
|
},
|
||||||
"name": "Climate-control device is heating"
|
"name": "Climate-control device is heating"
|
||||||
},
|
},
|
||||||
"is_hvac_mode": {
|
|
||||||
"description": "Tests if one or more climate-control devices are set to a specific HVAC mode.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"hvac_mode": {
|
|
||||||
"description": "The HVAC modes to test for.",
|
|
||||||
"name": "Modes"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Climate-control device HVAC mode"
|
|
||||||
},
|
|
||||||
"is_off": {
|
"is_off": {
|
||||||
"description": "Tests if one or more climate-control devices are off.",
|
"description": "Tests if one or more climate-control devices are off.",
|
||||||
"fields": {
|
"fields": {
|
||||||
@@ -74,34 +55,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Climate-control device is on"
|
"name": "Climate-control device is on"
|
||||||
},
|
|
||||||
"target_humidity": {
|
|
||||||
"description": "Tests the humidity setpoint of one or more climate-control devices.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::climate::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::climate::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Climate-control device target humidity"
|
|
||||||
},
|
|
||||||
"target_temperature": {
|
|
||||||
"description": "Tests the temperature setpoint of one or more climate-control devices.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"description": "[%key:component::climate::common::condition_threshold_description%]",
|
|
||||||
"name": "[%key:component::climate::common::condition_threshold_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Climate-control device target temperature"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"device_automation": {
|
"device_automation": {
|
||||||
@@ -288,77 +241,102 @@
|
|||||||
"any": "Any"
|
"any": "Any"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"hvac_mode": {
|
||||||
|
"options": {
|
||||||
|
"auto": "[%key:common::state::auto%]",
|
||||||
|
"cool": "Cool",
|
||||||
|
"dry": "Dry",
|
||||||
|
"fan_only": "Fan only",
|
||||||
|
"heat": "Heat",
|
||||||
|
"heat_cool": "Heat/cool",
|
||||||
|
"off": "[%key:common::state::off%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number_or_entity": {
|
||||||
|
"choices": {
|
||||||
|
"entity": "Entity",
|
||||||
|
"number": "Number"
|
||||||
|
}
|
||||||
|
},
|
||||||
"trigger_behavior": {
|
"trigger_behavior": {
|
||||||
"options": {
|
"options": {
|
||||||
"any": "Any",
|
"any": "Any",
|
||||||
"first": "First",
|
"first": "First",
|
||||||
"last": "Last"
|
"last": "Last"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"trigger_threshold_type": {
|
||||||
|
"options": {
|
||||||
|
"above": "Above a value",
|
||||||
|
"below": "Below a value",
|
||||||
|
"between": "In a range",
|
||||||
|
"outside": "Outside a range"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"set_fan_mode": {
|
"set_fan_mode": {
|
||||||
"description": "Sets the fan mode of a climate-control device.",
|
"description": "Sets fan operation mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"fan_mode": {
|
"fan_mode": {
|
||||||
"description": "Fan operation mode.",
|
"description": "Fan operation mode.",
|
||||||
"name": "Fan mode"
|
"name": "Fan mode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Set climate-control device fan mode"
|
"name": "Set fan mode"
|
||||||
},
|
},
|
||||||
"set_humidity": {
|
"set_humidity": {
|
||||||
"description": "Sets the target humidity of a climate-control device.",
|
"description": "Sets target humidity.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"humidity": {
|
"humidity": {
|
||||||
"description": "Target humidity.",
|
"description": "Target humidity.",
|
||||||
"name": "Humidity"
|
"name": "Humidity"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Set climate-control device target humidity"
|
"name": "Set target humidity"
|
||||||
},
|
},
|
||||||
"set_hvac_mode": {
|
"set_hvac_mode": {
|
||||||
"description": "Sets the HVAC mode of a climate-control device.",
|
"description": "Sets HVAC operation mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"hvac_mode": {
|
"hvac_mode": {
|
||||||
"description": "HVAC operation mode.",
|
"description": "HVAC operation mode.",
|
||||||
"name": "HVAC mode"
|
"name": "HVAC mode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Set climate-control device HVAC mode"
|
"name": "Set HVAC mode"
|
||||||
},
|
},
|
||||||
"set_preset_mode": {
|
"set_preset_mode": {
|
||||||
"description": "Sets the preset mode of a climate-control device.",
|
"description": "Sets preset mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"preset_mode": {
|
"preset_mode": {
|
||||||
"description": "Preset mode.",
|
"description": "Preset mode.",
|
||||||
"name": "Preset mode"
|
"name": "Preset mode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Set climate-control device preset mode"
|
"name": "Set preset mode"
|
||||||
},
|
},
|
||||||
"set_swing_horizontal_mode": {
|
"set_swing_horizontal_mode": {
|
||||||
"description": "Sets the horizontal swing mode of a climate-control device.",
|
"description": "Sets horizontal swing operation mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"swing_horizontal_mode": {
|
"swing_horizontal_mode": {
|
||||||
"description": "Horizontal swing operation mode.",
|
"description": "Horizontal swing operation mode.",
|
||||||
"name": "Horizontal swing mode"
|
"name": "Horizontal swing mode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Set climate-control device horizontal swing mode"
|
"name": "Set horizontal swing mode"
|
||||||
},
|
},
|
||||||
"set_swing_mode": {
|
"set_swing_mode": {
|
||||||
"description": "Sets the swing mode of a climate-control device.",
|
"description": "Sets swing operation mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"swing_mode": {
|
"swing_mode": {
|
||||||
"description": "Swing operation mode.",
|
"description": "Swing operation mode.",
|
||||||
"name": "Swing mode"
|
"name": "Swing mode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Set climate-control device swing mode"
|
"name": "Set swing mode"
|
||||||
},
|
},
|
||||||
"set_temperature": {
|
"set_temperature": {
|
||||||
"description": "Sets the target temperature of a climate-control device.",
|
"description": "Sets the temperature setpoint.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"hvac_mode": {
|
"hvac_mode": {
|
||||||
"description": "HVAC operation mode.",
|
"description": "HVAC operation mode.",
|
||||||
@@ -377,19 +355,19 @@
|
|||||||
"name": "Target temperature"
|
"name": "Target temperature"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Set climate-control device target temperature"
|
"name": "Set target temperature"
|
||||||
},
|
},
|
||||||
"toggle": {
|
"toggle": {
|
||||||
"description": "Toggles a climate-control device on/off.",
|
"description": "Toggles climate device, from on to off, or off to on.",
|
||||||
"name": "Toggle climate-control device"
|
"name": "[%key:common::action::toggle%]"
|
||||||
},
|
},
|
||||||
"turn_off": {
|
"turn_off": {
|
||||||
"description": "Turns off a climate-control device.",
|
"description": "Turns climate device off.",
|
||||||
"name": "Turn off climate-control device"
|
"name": "[%key:common::action::turn_off%]"
|
||||||
},
|
},
|
||||||
"turn_on": {
|
"turn_on": {
|
||||||
"description": "Turns on a climate-control device.",
|
"description": "Turns climate device on.",
|
||||||
"name": "Turn on climate-control device"
|
"name": "[%key:common::action::turn_on%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Climate",
|
"title": "Climate",
|
||||||
@@ -441,9 +419,13 @@
|
|||||||
"target_humidity_changed": {
|
"target_humidity_changed": {
|
||||||
"description": "Triggers after the humidity setpoint of one or more climate-control devices changes.",
|
"description": "Triggers after the humidity setpoint of one or more climate-control devices changes.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"threshold": {
|
"above": {
|
||||||
"description": "[%key:component::climate::common::trigger_threshold_changed_description%]",
|
"description": "Trigger when the target humidity is above this value.",
|
||||||
"name": "[%key:component::climate::common::trigger_threshold_name%]"
|
"name": "Above"
|
||||||
|
},
|
||||||
|
"below": {
|
||||||
|
"description": "Trigger when the target humidity is below this value.",
|
||||||
|
"name": "Below"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Climate-control device target humidity changed"
|
"name": "Climate-control device target humidity changed"
|
||||||
@@ -455,9 +437,17 @@
|
|||||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||||
},
|
},
|
||||||
"threshold": {
|
"lower_limit": {
|
||||||
"description": "[%key:component::climate::common::trigger_threshold_crossed_description%]",
|
"description": "Lower threshold limit.",
|
||||||
"name": "[%key:component::climate::common::trigger_threshold_name%]"
|
"name": "Lower threshold"
|
||||||
|
},
|
||||||
|
"threshold_type": {
|
||||||
|
"description": "Type of threshold crossing to trigger on.",
|
||||||
|
"name": "Threshold type"
|
||||||
|
},
|
||||||
|
"upper_limit": {
|
||||||
|
"description": "Upper threshold limit.",
|
||||||
|
"name": "Upper threshold"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Climate-control device target humidity crossed threshold"
|
"name": "Climate-control device target humidity crossed threshold"
|
||||||
@@ -465,9 +455,13 @@
|
|||||||
"target_temperature_changed": {
|
"target_temperature_changed": {
|
||||||
"description": "Triggers after the temperature setpoint of one or more climate-control devices changes.",
|
"description": "Triggers after the temperature setpoint of one or more climate-control devices changes.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"threshold": {
|
"above": {
|
||||||
"description": "[%key:component::climate::common::trigger_threshold_changed_description%]",
|
"description": "Trigger when the target temperature is above this value.",
|
||||||
"name": "[%key:component::climate::common::trigger_threshold_name%]"
|
"name": "Above"
|
||||||
|
},
|
||||||
|
"below": {
|
||||||
|
"description": "Trigger when the target temperature is below this value.",
|
||||||
|
"name": "Below"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Climate-control device target temperature changed"
|
"name": "Climate-control device target temperature changed"
|
||||||
@@ -479,9 +473,17 @@
|
|||||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||||
},
|
},
|
||||||
"threshold": {
|
"lower_limit": {
|
||||||
"description": "[%key:component::climate::common::trigger_threshold_crossed_description%]",
|
"description": "Lower threshold limit.",
|
||||||
"name": "[%key:component::climate::common::trigger_threshold_name%]"
|
"name": "Lower threshold"
|
||||||
|
},
|
||||||
|
"threshold_type": {
|
||||||
|
"description": "Type of threshold crossing to trigger on.",
|
||||||
|
"name": "Threshold type"
|
||||||
|
},
|
||||||
|
"upper_limit": {
|
||||||
|
"description": "Upper threshold limit.",
|
||||||
|
"name": "Upper threshold"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Climate-control device target temperature crossed threshold"
|
"name": "Climate-control device target temperature crossed threshold"
|
||||||
|
|||||||
@@ -2,15 +2,12 @@
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
|
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.automation import DomainSpec
|
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||||
from homeassistant.helpers.trigger import (
|
from homeassistant.helpers.trigger import (
|
||||||
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
|
||||||
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
|
|
||||||
EntityNumericalStateTriggerWithUnitBase,
|
|
||||||
EntityTargetStateTriggerBase,
|
EntityTargetStateTriggerBase,
|
||||||
Trigger,
|
Trigger,
|
||||||
TriggerConfig,
|
TriggerConfig,
|
||||||
@@ -19,7 +16,6 @@ from homeassistant.helpers.trigger import (
|
|||||||
make_entity_target_state_trigger,
|
make_entity_target_state_trigger,
|
||||||
make_entity_transition_trigger,
|
make_entity_transition_trigger,
|
||||||
)
|
)
|
||||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
|
||||||
|
|
||||||
from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
||||||
|
|
||||||
@@ -48,33 +44,6 @@ class HVACModeChangedTrigger(EntityTargetStateTriggerBase):
|
|||||||
self._to_states = set(self._options[CONF_HVAC_MODE])
|
self._to_states = set(self._options[CONF_HVAC_MODE])
|
||||||
|
|
||||||
|
|
||||||
class _ClimateTargetTemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitBase):
|
|
||||||
"""Mixin for climate target temperature triggers with unit conversion."""
|
|
||||||
|
|
||||||
_base_unit = UnitOfTemperature.CELSIUS
|
|
||||||
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
|
|
||||||
_unit_converter = TemperatureConverter
|
|
||||||
|
|
||||||
def _get_entity_unit(self, state: State) -> str | None:
|
|
||||||
"""Get the temperature unit of a climate entity from its state."""
|
|
||||||
# Climate entities convert temperatures to the system unit via show_temp
|
|
||||||
return self._hass.config.units.temperature_unit
|
|
||||||
|
|
||||||
|
|
||||||
class ClimateTargetTemperatureChangedTrigger(
|
|
||||||
_ClimateTargetTemperatureTriggerMixin,
|
|
||||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
|
||||||
):
|
|
||||||
"""Trigger for climate target temperature value changes."""
|
|
||||||
|
|
||||||
|
|
||||||
class ClimateTargetTemperatureCrossedThresholdTrigger(
|
|
||||||
_ClimateTargetTemperatureTriggerMixin,
|
|
||||||
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
|
|
||||||
):
|
|
||||||
"""Trigger for climate target temperature value crossing a threshold."""
|
|
||||||
|
|
||||||
|
|
||||||
TRIGGERS: dict[str, type[Trigger]] = {
|
TRIGGERS: dict[str, type[Trigger]] = {
|
||||||
"hvac_mode_changed": HVACModeChangedTrigger,
|
"hvac_mode_changed": HVACModeChangedTrigger,
|
||||||
"started_cooling": make_entity_target_state_trigger(
|
"started_cooling": make_entity_target_state_trigger(
|
||||||
@@ -84,15 +53,17 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
|||||||
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.DRYING
|
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.DRYING
|
||||||
),
|
),
|
||||||
"target_humidity_changed": make_entity_numerical_state_changed_trigger(
|
"target_humidity_changed": make_entity_numerical_state_changed_trigger(
|
||||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)}
|
||||||
valid_unit="%",
|
|
||||||
),
|
),
|
||||||
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)}
|
||||||
valid_unit="%",
|
),
|
||||||
|
"target_temperature_changed": make_entity_numerical_state_changed_trigger(
|
||||||
|
{DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||||
|
),
|
||||||
|
"target_temperature_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||||
|
{DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||||
),
|
),
|
||||||
"target_temperature_changed": ClimateTargetTemperatureChangedTrigger,
|
|
||||||
"target_temperature_crossed_threshold": ClimateTargetTemperatureCrossedThresholdTrigger,
|
|
||||||
"turned_off": make_entity_target_state_trigger(DOMAIN, HVACMode.OFF),
|
"turned_off": make_entity_target_state_trigger(DOMAIN, HVACMode.OFF),
|
||||||
"turned_on": make_entity_transition_trigger(
|
"turned_on": make_entity_transition_trigger(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
|||||||
@@ -14,31 +14,36 @@
|
|||||||
- last
|
- last
|
||||||
- any
|
- any
|
||||||
|
|
||||||
.humidity_threshold_entity: &humidity_threshold_entity
|
.number_or_entity: &number_or_entity
|
||||||
- domain: input_number
|
required: false
|
||||||
unit_of_measurement: "%"
|
selector:
|
||||||
- domain: sensor
|
choose:
|
||||||
device_class: humidity
|
choices:
|
||||||
- domain: number
|
number:
|
||||||
device_class: humidity
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
entity:
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
domain:
|
||||||
|
- input_number
|
||||||
|
- number
|
||||||
|
- sensor
|
||||||
|
translation_key: number_or_entity
|
||||||
|
|
||||||
.humidity_threshold_number: &humidity_threshold_number
|
.trigger_threshold_type: &trigger_threshold_type
|
||||||
min: 0
|
required: true
|
||||||
max: 100
|
default: above
|
||||||
mode: box
|
selector:
|
||||||
unit_of_measurement: "%"
|
select:
|
||||||
|
options:
|
||||||
.temperature_units: &temperature_units
|
- above
|
||||||
- "°C"
|
- below
|
||||||
- "°F"
|
- between
|
||||||
|
- outside
|
||||||
.temperature_threshold_entity: &temperature_threshold_entity
|
translation_key: trigger_threshold_type
|
||||||
- domain: input_number
|
|
||||||
unit_of_measurement: *temperature_units
|
|
||||||
- domain: sensor
|
|
||||||
device_class: temperature
|
|
||||||
- domain: number
|
|
||||||
device_class: temperature
|
|
||||||
|
|
||||||
started_cooling: *trigger_common
|
started_cooling: *trigger_common
|
||||||
started_drying: *trigger_common
|
started_drying: *trigger_common
|
||||||
@@ -64,49 +69,27 @@ hvac_mode_changed:
|
|||||||
target_humidity_changed:
|
target_humidity_changed:
|
||||||
target: *trigger_climate_target
|
target: *trigger_climate_target
|
||||||
fields:
|
fields:
|
||||||
threshold:
|
above: *number_or_entity
|
||||||
required: true
|
below: *number_or_entity
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *humidity_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number: *humidity_threshold_number
|
|
||||||
|
|
||||||
target_humidity_crossed_threshold:
|
target_humidity_crossed_threshold:
|
||||||
target: *trigger_climate_target
|
target: *trigger_climate_target
|
||||||
fields:
|
fields:
|
||||||
behavior: *trigger_behavior
|
behavior: *trigger_behavior
|
||||||
threshold:
|
threshold_type: *trigger_threshold_type
|
||||||
required: true
|
lower_limit: *number_or_entity
|
||||||
selector:
|
upper_limit: *number_or_entity
|
||||||
numeric_threshold:
|
|
||||||
entity: *humidity_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number: *humidity_threshold_number
|
|
||||||
|
|
||||||
target_temperature_changed:
|
target_temperature_changed:
|
||||||
target: *trigger_climate_target
|
target: *trigger_climate_target
|
||||||
fields:
|
fields:
|
||||||
threshold:
|
above: *number_or_entity
|
||||||
required: true
|
below: *number_or_entity
|
||||||
selector:
|
|
||||||
numeric_threshold:
|
|
||||||
entity: *temperature_threshold_entity
|
|
||||||
mode: changed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *temperature_units
|
|
||||||
|
|
||||||
target_temperature_crossed_threshold:
|
target_temperature_crossed_threshold:
|
||||||
target: *trigger_climate_target
|
target: *trigger_climate_target
|
||||||
fields:
|
fields:
|
||||||
behavior: *trigger_behavior
|
behavior: *trigger_behavior
|
||||||
threshold:
|
threshold_type: *trigger_threshold_type
|
||||||
required: true
|
lower_limit: *number_or_entity
|
||||||
selector:
|
upper_limit: *number_or_entity
|
||||||
numeric_threshold:
|
|
||||||
entity: *temperature_threshold_entity
|
|
||||||
mode: crossed
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
unit_of_measurement: *temperature_units
|
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ class CloudBackupAgent(BackupAgent):
|
|||||||
base64md5hash=base64md5hash,
|
base64md5hash=base64md5hash,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
size=size,
|
size=size,
|
||||||
on_progress=on_progress,
|
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
except CloudApiNonRetryableError as err:
|
except CloudApiNonRetryableError as err:
|
||||||
|
|||||||
@@ -13,6 +13,6 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||||
"requirements": ["hass-nabucasa==2.2.0", "openai==2.21.0"],
|
"requirements": ["hass-nabucasa==2.0.0", "openai==2.21.0"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,11 +75,11 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"remote_connect": {
|
"remote_connect": {
|
||||||
"description": "Makes the instance UI accessible from outside of the local network by enabling your Home Assistant Cloud connection.",
|
"description": "Makes the instance UI accessible from outside of the local network by enabling your Home Assistant Cloud connection.",
|
||||||
"name": "Enable Home Assistant Cloud remote access"
|
"name": "Enable remote access"
|
||||||
},
|
},
|
||||||
"remote_disconnect": {
|
"remote_disconnect": {
|
||||||
"description": "Disconnects the instance UI from Home Assistant Cloud. This disables access to it from outside your local network.",
|
"description": "Disconnects the instance UI from Home Assistant Cloud. This disables access to it from outside your local network.",
|
||||||
"name": "Disable Home Assistant Cloud remote access"
|
"name": "Disable remote access"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"system_health": {
|
"system_health": {
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class R2BackupAgent(BackupAgent):
|
|||||||
if backup.size < MULTIPART_MIN_PART_SIZE_BYTES:
|
if backup.size < MULTIPART_MIN_PART_SIZE_BYTES:
|
||||||
await self._upload_simple(tar_filename, open_stream)
|
await self._upload_simple(tar_filename, open_stream)
|
||||||
else:
|
else:
|
||||||
await self._upload_multipart(tar_filename, open_stream, on_progress)
|
await self._upload_multipart(tar_filename, open_stream)
|
||||||
|
|
||||||
# Upload the metadata file
|
# Upload the metadata file
|
||||||
metadata_content = json.dumps(backup.as_dict())
|
metadata_content = json.dumps(backup.as_dict())
|
||||||
@@ -185,13 +185,11 @@ class R2BackupAgent(BackupAgent):
|
|||||||
self,
|
self,
|
||||||
tar_filename: str,
|
tar_filename: str,
|
||||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||||
on_progress: OnProgressCallback,
|
):
|
||||||
) -> None:
|
|
||||||
"""Upload a large file using multipart upload.
|
"""Upload a large file using multipart upload.
|
||||||
|
|
||||||
:param tar_filename: The target filename for the backup.
|
:param tar_filename: The target filename for the backup.
|
||||||
:param open_stream: A function returning an async iterator that yields bytes.
|
:param open_stream: A function returning an async iterator that yields bytes.
|
||||||
:param on_progress: A callback to report the number of uploaded bytes.
|
|
||||||
"""
|
"""
|
||||||
_LOGGER.debug("Starting multipart upload for %s", tar_filename)
|
_LOGGER.debug("Starting multipart upload for %s", tar_filename)
|
||||||
key = self._with_prefix(tar_filename)
|
key = self._with_prefix(tar_filename)
|
||||||
@@ -205,7 +203,6 @@ class R2BackupAgent(BackupAgent):
|
|||||||
part_number = 1
|
part_number = 1
|
||||||
buffer = bytearray() # bytes buffer to store the data
|
buffer = bytearray() # bytes buffer to store the data
|
||||||
offset = 0 # start index of unread data inside buffer
|
offset = 0 # start index of unread data inside buffer
|
||||||
bytes_uploaded = 0
|
|
||||||
|
|
||||||
stream = await open_stream()
|
stream = await open_stream()
|
||||||
async for chunk in stream:
|
async for chunk in stream:
|
||||||
@@ -234,8 +231,6 @@ class R2BackupAgent(BackupAgent):
|
|||||||
Body=part_data.tobytes(),
|
Body=part_data.tobytes(),
|
||||||
)
|
)
|
||||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||||
bytes_uploaded += len(part_data)
|
|
||||||
on_progress(bytes_uploaded=bytes_uploaded)
|
|
||||||
part_number += 1
|
part_number += 1
|
||||||
finally:
|
finally:
|
||||||
view.release()
|
view.release()
|
||||||
@@ -264,8 +259,6 @@ class R2BackupAgent(BackupAgent):
|
|||||||
Body=remaining_data.tobytes(),
|
Body=remaining_data.tobytes(),
|
||||||
)
|
)
|
||||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||||
bytes_uploaded += len(remaining_data)
|
|
||||||
on_progress(bytes_uploaded=bytes_uploaded)
|
|
||||||
|
|
||||||
await cast(Any, self._client).complete_multipart_upload(
|
await cast(Any, self._client).complete_multipart_upload(
|
||||||
Bucket=self._bucket,
|
Bucket=self._bucket,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from aiocomelit import ComelitSerialBridgeObject
|
|||||||
from aiocomelit.const import CLIMATE
|
from aiocomelit.const import CLIMATE
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
|
DOMAIN as CLIMATE_DOMAIN,
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
HVACAction,
|
HVACAction,
|
||||||
@@ -91,7 +92,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
entities: list[ClimateEntity] = []
|
entities: list[ClimateEntity] = []
|
||||||
for device in coordinator.data[CLIMATE].values():
|
for device in coordinator.data[CLIMATE].values():
|
||||||
values = load_api_data(device, "climate")
|
values = load_api_data(device, CLIMATE_DOMAIN)
|
||||||
if values[0] == 0 and values[4] == 0:
|
if values[0] == 0 and values[4] == 0:
|
||||||
# No climate data, device is only a humidifier/dehumidifier
|
# No climate data, device is only a humidifier/dehumidifier
|
||||||
|
|
||||||
@@ -139,7 +140,7 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
|
|||||||
def _update_attributes(self) -> None:
|
def _update_attributes(self) -> None:
|
||||||
"""Update class attributes."""
|
"""Update class attributes."""
|
||||||
device = self.coordinator.data[CLIMATE][self._device.index]
|
device = self.coordinator.data[CLIMATE][self._device.index]
|
||||||
values = load_api_data(device, "climate")
|
values = load_api_data(device, CLIMATE_DOMAIN)
|
||||||
|
|
||||||
_active = values[1]
|
_active = values[1]
|
||||||
_mode = values[2] # Values from API: "O", "L", "U"
|
_mode = values[2] # Values from API: "O", "L", "U"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from aiocomelit import ComelitSerialBridgeObject
|
|||||||
from aiocomelit.const import CLIMATE
|
from aiocomelit.const import CLIMATE
|
||||||
|
|
||||||
from homeassistant.components.humidifier import (
|
from homeassistant.components.humidifier import (
|
||||||
|
DOMAIN as HUMIDIFIER_DOMAIN,
|
||||||
MODE_AUTO,
|
MODE_AUTO,
|
||||||
MODE_NORMAL,
|
MODE_NORMAL,
|
||||||
HumidifierAction,
|
HumidifierAction,
|
||||||
@@ -67,7 +68,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
entities: list[ComelitHumidifierEntity] = []
|
entities: list[ComelitHumidifierEntity] = []
|
||||||
for device in coordinator.data[CLIMATE].values():
|
for device in coordinator.data[CLIMATE].values():
|
||||||
values = load_api_data(device, "humidifier")
|
values = load_api_data(device, HUMIDIFIER_DOMAIN)
|
||||||
if values[0] == 0 and values[4] == 0:
|
if values[0] == 0 and values[4] == 0:
|
||||||
# No humidity data, device is only a climate
|
# No humidity data, device is only a climate
|
||||||
|
|
||||||
@@ -141,7 +142,7 @@ class ComelitHumidifierEntity(ComelitBridgeBaseEntity, HumidifierEntity):
|
|||||||
def _update_attributes(self) -> None:
|
def _update_attributes(self) -> None:
|
||||||
"""Update class attributes."""
|
"""Update class attributes."""
|
||||||
device = self.coordinator.data[CLIMATE][self._device.index]
|
device = self.coordinator.data[CLIMATE][self._device.index]
|
||||||
values = load_api_data(device, "humidifier")
|
values = load_api_data(device, HUMIDIFIER_DOMAIN)
|
||||||
|
|
||||||
_active = values[1]
|
_active = values[1]
|
||||||
_mode = values[2] # Values from API: "O", "L", "U"
|
_mode = values[2] # Values from API: "O", "L", "U"
|
||||||
|
|||||||
@@ -113,6 +113,9 @@
|
|||||||
"humidity_while_off": {
|
"humidity_while_off": {
|
||||||
"message": "Cannot change humidity while off"
|
"message": "Cannot change humidity while off"
|
||||||
},
|
},
|
||||||
|
"invalid_clima_data": {
|
||||||
|
"message": "Invalid 'clima' data"
|
||||||
|
},
|
||||||
"update_failed": {
|
"update_failed": {
|
||||||
"message": "Failed to update data: {error}"
|
"message": "Failed to update data: {error}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
from collections.abc import Awaitable, Callable, Coroutine
|
from collections.abc import Awaitable, Callable, Coroutine
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import TYPE_CHECKING, Any, Concatenate, Literal
|
from typing import TYPE_CHECKING, Any, Concatenate
|
||||||
|
|
||||||
from aiocomelit.api import ComelitSerialBridgeObject
|
from aiocomelit.api import ComelitSerialBridgeObject
|
||||||
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
|
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
|
||||||
from aiohttp import ClientSession, CookieJar
|
from aiohttp import ClientSession, CookieJar
|
||||||
|
|
||||||
|
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
@@ -29,19 +30,17 @@ async def async_client_session(hass: HomeAssistant) -> ClientSession:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_api_data(
|
def load_api_data(device: ComelitSerialBridgeObject, domain: str) -> list[Any]:
|
||||||
device: ComelitSerialBridgeObject,
|
|
||||||
domain: Literal["climate", "humidifier"],
|
|
||||||
) -> list[Any]:
|
|
||||||
"""Load data from the API."""
|
"""Load data from the API."""
|
||||||
# This function is called when the data is loaded from the API.
|
# This function is called when the data is loaded from the API
|
||||||
# For climate and humidifier device.val is always a list.
|
if not isinstance(device.val, list):
|
||||||
if TYPE_CHECKING:
|
raise HomeAssistantError(
|
||||||
assert isinstance(device.val, list)
|
translation_domain=domain, translation_key="invalid_clima_data"
|
||||||
|
)
|
||||||
# CLIMATE has a 2 item tuple:
|
# CLIMATE has a 2 item tuple:
|
||||||
# - first for Clima
|
# - first for Clima
|
||||||
# - second for Humidifier
|
# - second for Humidifier
|
||||||
return device.val[0] if domain == "climate" else device.val[1]
|
return device.val[0] if domain == CLIMATE_DOMAIN else device.val[1]
|
||||||
|
|
||||||
|
|
||||||
async def cleanup_stale_entity(
|
async def cleanup_stale_entity(
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||||
"integration_type": "entity",
|
"integration_type": "entity",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.3.24"]
|
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.3.3"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"process": {
|
"process": {
|
||||||
"description": "Sends text to a conversation agent for processing.",
|
"description": "Launches a conversation from a transcribed text.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"agent_id": {
|
"agent_id": {
|
||||||
"description": "Conversation agent to process your request. The conversation agent is the brains of your assistant. It processes the incoming text commands.",
|
"description": "Conversation agent to process your request. The conversation agent is the brains of your assistant. It processes the incoming text commands.",
|
||||||
@@ -25,10 +25,10 @@
|
|||||||
"name": "Text"
|
"name": "Text"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Process conversation"
|
"name": "Process"
|
||||||
},
|
},
|
||||||
"reload": {
|
"reload": {
|
||||||
"description": "Reloads the intent configuration of conversation agents.",
|
"description": "Reloads the intent configuration.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"agent_id": {
|
"agent_id": {
|
||||||
"description": "Conversation agent to reload.",
|
"description": "Conversation agent to reload.",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"name": "[%key:common::config_flow::data::language%]"
|
"name": "[%key:common::config_flow::data::language%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Reload conversation agents"
|
"name": "[%key:common::action::reload%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Conversation"
|
"title": "Conversation"
|
||||||
|
|||||||
@@ -12,22 +12,5 @@
|
|||||||
"set_value": {
|
"set_value": {
|
||||||
"service": "mdi:counter"
|
"service": "mdi:counter"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"triggers": {
|
|
||||||
"decremented": {
|
|
||||||
"trigger": "mdi:numeric-negative-1"
|
|
||||||
},
|
|
||||||
"incremented": {
|
|
||||||
"trigger": "mdi:numeric-positive-1"
|
|
||||||
},
|
|
||||||
"maximum_reached": {
|
|
||||||
"trigger": "mdi:sort-numeric-ascending-variant"
|
|
||||||
},
|
|
||||||
"minimum_reached": {
|
|
||||||
"trigger": "mdi:sort-numeric-descending-variant"
|
|
||||||
},
|
|
||||||
"reset": {
|
|
||||||
"trigger": "mdi:refresh"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
|
||||||
"trigger_behavior_description": "The behavior of the targeted counters to trigger on.",
|
|
||||||
"trigger_behavior_name": "Behavior"
|
|
||||||
},
|
|
||||||
"entity_component": {
|
"entity_component": {
|
||||||
"_": {
|
"_": {
|
||||||
"name": "[%key:component::counter::title%]",
|
"name": "[%key:component::counter::title%]",
|
||||||
@@ -29,78 +25,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"trigger_behavior": {
|
|
||||||
"options": {
|
|
||||||
"any": "Any",
|
|
||||||
"first": "First",
|
|
||||||
"last": "Last"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"services": {
|
"services": {
|
||||||
"decrement": {
|
"decrement": {
|
||||||
"description": "Decrements a counter by its step size.",
|
"description": "Decrements a counter by its step size.",
|
||||||
"name": "Decrement counter"
|
"name": "Decrement"
|
||||||
},
|
},
|
||||||
"increment": {
|
"increment": {
|
||||||
"description": "Increments a counter by its step size.",
|
"description": "Increments a counter by its step size.",
|
||||||
"name": "Increment counter"
|
"name": "Increment"
|
||||||
},
|
},
|
||||||
"reset": {
|
"reset": {
|
||||||
"description": "Resets a counter to its initial value.",
|
"description": "Resets a counter to its initial value.",
|
||||||
"name": "Reset counter"
|
"name": "Reset"
|
||||||
},
|
},
|
||||||
"set_value": {
|
"set_value": {
|
||||||
"description": "Sets a counter to a specific value.",
|
"description": "Sets the counter to a specific value.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"value": {
|
"value": {
|
||||||
"description": "The new counter value the entity should be set to.",
|
"description": "The new counter value the entity should be set to.",
|
||||||
"name": "Value"
|
"name": "Value"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Set counter value"
|
"name": "Set"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Counter",
|
"title": "Counter"
|
||||||
"triggers": {
|
|
||||||
"decremented": {
|
|
||||||
"description": "Triggers after one or more counters decrement.",
|
|
||||||
"name": "Counter decremented"
|
|
||||||
},
|
|
||||||
"incremented": {
|
|
||||||
"description": "Triggers after one or more counters increment.",
|
|
||||||
"name": "Counter incremented"
|
|
||||||
},
|
|
||||||
"maximum_reached": {
|
|
||||||
"description": "Triggers after one or more counters reach their maximum value.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::counter::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::counter::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Counter reached maximum"
|
|
||||||
},
|
|
||||||
"minimum_reached": {
|
|
||||||
"description": "Triggers after one or more counters reach their minimum value.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::counter::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::counter::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Counter reached minimum"
|
|
||||||
},
|
|
||||||
"reset": {
|
|
||||||
"description": "Triggers after one or more counters are reset.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::counter::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::counter::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Counter reset"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
"""Provides triggers for counters."""
|
|
||||||
|
|
||||||
from homeassistant.const import (
|
|
||||||
CONF_MAXIMUM,
|
|
||||||
CONF_MINIMUM,
|
|
||||||
STATE_UNAVAILABLE,
|
|
||||||
STATE_UNKNOWN,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant, State
|
|
||||||
from homeassistant.helpers.automation import DomainSpec
|
|
||||||
from homeassistant.helpers.trigger import (
|
|
||||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
|
||||||
EntityTriggerBase,
|
|
||||||
Trigger,
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import CONF_INITIAL, DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
def _is_integer_state(state: State) -> bool:
|
|
||||||
"""Return True if the state's value can be interpreted as an integer."""
|
|
||||||
try:
|
|
||||||
int(state.state)
|
|
||||||
except TypeError, ValueError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class CounterBaseIntegerTrigger(EntityTriggerBase):
|
|
||||||
"""Base trigger for valid counter integer states."""
|
|
||||||
|
|
||||||
_domain_specs = {DOMAIN: DomainSpec()}
|
|
||||||
_schema = ENTITY_STATE_TRIGGER_SCHEMA
|
|
||||||
|
|
||||||
def is_valid_state(self, state: State) -> bool:
|
|
||||||
"""Check if the new state is valid."""
|
|
||||||
return _is_integer_state(state)
|
|
||||||
|
|
||||||
|
|
||||||
class CounterDecrementedTrigger(CounterBaseIntegerTrigger):
|
|
||||||
"""Trigger for when a counter is decremented."""
|
|
||||||
|
|
||||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
|
||||||
"""Check if the origin state is valid and the state has changed."""
|
|
||||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
|
||||||
return False
|
|
||||||
return int(from_state.state) > int(to_state.state)
|
|
||||||
|
|
||||||
|
|
||||||
class CounterIncrementedTrigger(CounterBaseIntegerTrigger):
|
|
||||||
"""Trigger for when a counter is incremented."""
|
|
||||||
|
|
||||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
|
||||||
"""Check if the origin state is valid and the state has changed."""
|
|
||||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
|
||||||
return False
|
|
||||||
return int(from_state.state) < int(to_state.state)
|
|
||||||
|
|
||||||
|
|
||||||
class CounterValueBaseTrigger(EntityTriggerBase):
|
|
||||||
"""Base trigger for counter value changes."""
|
|
||||||
|
|
||||||
_domain_specs = {DOMAIN: DomainSpec()}
|
|
||||||
|
|
||||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
|
||||||
"""Check if the origin state is valid and the state has changed."""
|
|
||||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
|
||||||
return False
|
|
||||||
return from_state.state != to_state.state
|
|
||||||
|
|
||||||
|
|
||||||
class CounterMaxReachedTrigger(CounterValueBaseTrigger):
|
|
||||||
"""Trigger for when a counter reaches its maximum value."""
|
|
||||||
|
|
||||||
def is_valid_state(self, state: State) -> bool:
|
|
||||||
"""Check if the new state matches the expected state(s)."""
|
|
||||||
if (max_value := state.attributes.get(CONF_MAXIMUM)) is None:
|
|
||||||
return False
|
|
||||||
return state.state == str(max_value)
|
|
||||||
|
|
||||||
|
|
||||||
class CounterMinReachedTrigger(CounterValueBaseTrigger):
|
|
||||||
"""Trigger for when a counter reaches its minimum value."""
|
|
||||||
|
|
||||||
def is_valid_state(self, state: State) -> bool:
|
|
||||||
"""Check if the new state matches the expected state(s)."""
|
|
||||||
if (min_value := state.attributes.get(CONF_MINIMUM)) is None:
|
|
||||||
return False
|
|
||||||
return state.state == str(min_value)
|
|
||||||
|
|
||||||
|
|
||||||
class CounterResetTrigger(CounterValueBaseTrigger):
|
|
||||||
"""Trigger for reset of counter entities."""
|
|
||||||
|
|
||||||
def is_valid_state(self, state: State) -> bool:
|
|
||||||
"""Check if the new state matches the expected state(s)."""
|
|
||||||
if (init_state := state.attributes.get(CONF_INITIAL)) is None:
|
|
||||||
return False
|
|
||||||
return state.state == str(init_state)
|
|
||||||
|
|
||||||
|
|
||||||
TRIGGERS: dict[str, type[Trigger]] = {
|
|
||||||
"decremented": CounterDecrementedTrigger,
|
|
||||||
"incremented": CounterIncrementedTrigger,
|
|
||||||
"maximum_reached": CounterMaxReachedTrigger,
|
|
||||||
"minimum_reached": CounterMinReachedTrigger,
|
|
||||||
"reset": CounterResetTrigger,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
|
||||||
"""Return the triggers for counters."""
|
|
||||||
return TRIGGERS
|
|
||||||
@@ -1,23 +1,19 @@
|
|||||||
"""Provides conditions for covers."""
|
"""Provides conditions for covers."""
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
|
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State, split_entity_id
|
||||||
from homeassistant.helpers.condition import Condition, EntityConditionBase
|
from homeassistant.helpers.condition import Condition, EntityConditionBase
|
||||||
|
|
||||||
from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
|
from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
|
||||||
from .models import CoverDomainSpec
|
from .models import CoverDomainSpec
|
||||||
|
|
||||||
|
|
||||||
class CoverConditionBase(EntityConditionBase):
|
class CoverConditionBase(EntityConditionBase[CoverDomainSpec]):
|
||||||
"""Base condition for cover state checks."""
|
"""Base condition for cover state checks."""
|
||||||
|
|
||||||
_domain_specs: Mapping[str, CoverDomainSpec]
|
|
||||||
|
|
||||||
def is_valid_state(self, entity_state: State) -> bool:
|
def is_valid_state(self, entity_state: State) -> bool:
|
||||||
"""Check if the state matches the expected cover state."""
|
"""Check if the state matches the expected cover state."""
|
||||||
domain_spec = self._domain_specs[entity_state.domain]
|
domain_spec = self._domain_specs[split_entity_id(entity_state.entity_id)[0]]
|
||||||
if domain_spec.value_source is not None:
|
if domain_spec.value_source is not None:
|
||||||
return (
|
return (
|
||||||
entity_state.attributes.get(domain_spec.value_source)
|
entity_state.attributes.get(domain_spec.value_source)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user