Compare commits

...

53 Commits

Author SHA1 Message Date
Erik
ce1e6e02f6 Format 2026-03-30 15:19:22 +02:00
Erik
69dcac1a19 Simplify 2026-03-30 15:15:18 +02:00
Erik
4bc29274c7 Simplify tests 2026-03-30 15:11:00 +02:00
Erik
cfc353be69 Set up calendar frontend resources when first platform is set up 2026-03-30 14:53:13 +02:00
Simone Chemelli
51a5f5793f Improve Nuki tests and avoid dns lookups (#166506) 2026-03-26 15:12:17 +01:00
Erik Montnemery
33f11f2263 Remove number entity support from battery triggers and conditions (#166593) 2026-03-26 14:46:39 +01:00
Erik Montnemery
45069b623c Remove number entity support from moisture triggers and conditions (#166596) 2026-03-26 14:40:56 +01:00
Abílio Costa
5defb4dbff Add todo to experimental triggers (#166591) 2026-03-26 14:36:16 +01:00
Ronald van der Meer
bc7c3f0617 Bump pooldose 0.9.0 (#166589) 2026-03-26 14:32:52 +01:00
Devin Slick
704c0d1eb0 Bump lojack-api to 0.7.2 (#166560)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:15:04 +01:00
John Meyers
6c864a1725 Update rainmachine solar radiation to reflect it is per day, not per … (#166040) 2026-03-26 14:11:12 +01:00
reneboer
299c6556bb Bump renault-api to 0.5.7 (#166586) 2026-03-26 13:16:50 +01:00
Erik Montnemery
f0fc98cb66 Remove class NumericalDomainSpec (#166588) 2026-03-26 13:13:07 +01:00
Ariel Ebersberger
cd63d14e6f Add battery triggers (#166258) 2026-03-26 11:51:49 +01:00
Simone Chemelli
30dfd23da8 Improve MySensors tests and avoid dns lookups (#166509) 2026-03-26 11:51:45 +01:00
AlCalzone
d39ef523b8 Revert: Create repair issue for legacy Z-Wave Door state sensors that are still in use (#166583) 2026-03-26 11:45:34 +01:00
Erik Montnemery
b6c2fbb8c0 Adjust some trigger and condition schemas (#166568) 2026-03-26 11:32:39 +01:00
tronikos
758d5469aa Add Google Drive backup upload progress (#166549) 2026-03-26 10:31:07 +01:00
Keilin Bickar
ea99f88d10 Bump sense-energy to 0.14.0 (#166550) 2026-03-26 10:27:02 +01:00
Keilin Bickar
0a8f76864c Bump asyncsleepiq to 1.7.1 (#166552) 2026-03-26 10:25:29 +01:00
Erik Montnemery
ad522d723c Add trigger humidifier.mode_changed (#166241)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-03-26 10:03:49 +01:00
dependabot[bot]
0f41a311c8 Bump dawidd6/action-download-artifact from 16 to 19 (#166564) 2026-03-26 07:45:40 +01:00
Fabian Munkes
412a9a050e Bump music-assistant-client to 1.3.4 (#166567) 2026-03-26 07:45:05 +01:00
dependabot[bot]
d5efc3abd5 Bump actions/cache from 5.0.3 to 5.0.4 (#166563) 2026-03-26 07:41:07 +01:00
dependabot[bot]
a205623d52 Bump codecov/codecov-action from 5.5.2 to 5.5.3 (#166562) 2026-03-26 07:38:31 +01:00
dependabot[bot]
8208eecf8c Bump j178/prek-action from 1.1.1 to 2.0.0 (#166561) 2026-03-26 07:37:25 +01:00
Erik Montnemery
f84398eb9c Speed up trigger tests (#166522) 2026-03-26 00:51:14 +01:00
Franck Nijhof
aca5adb673 Improve conversation action naming consistency (#166542) 2026-03-26 00:34:22 +01:00
Franck Nijhof
f361d01b8b Improve dashboard action naming consistency (#166539) 2026-03-26 00:34:08 +01:00
Franck Nijhof
d2cef2d26e Improve cloud action naming consistency (#166516) 2026-03-26 00:33:48 +01:00
Abílio Costa
90524e53ec Revert "Instruct copilot to place main comment in collapsible section" (#166543) 2026-03-25 22:15:21 +00:00
Franck Nijhof
668d220400 Improve script action naming consistency (#166517) 2026-03-25 22:14:19 +00:00
Franck Nijhof
9e28db0535 Improve valve action naming consistency (#166521) 2026-03-25 22:13:56 +00:00
Franck Nijhof
c5807463fd Improve humidifier action naming consistency (#166524) 2026-03-25 22:13:12 +00:00
Franck Nijhof
f72a9e52f5 Improve counter action naming consistency (#166526) 2026-03-25 22:11:16 +00:00
Franck Nijhof
619582bd03 Improve image action naming consistency (#166527) 2026-03-25 22:10:50 +00:00
Franck Nijhof
bcc02d7adc Improve automation action naming consistency (#166525) 2026-03-25 22:08:49 +00:00
Franck Nijhof
a9083d5362 Improve weather action naming consistency (#166540) 2026-03-25 22:08:29 +00:00
Franck Nijhof
dd89fa0f5b Improve device tracker action naming consistency (#166534) 2026-03-25 22:04:37 +00:00
Franck Nijhof
88d0bd5a1d Improve group action naming consistency (#166537) 2026-03-25 22:03:15 +00:00
Franck Nijhof
a045c2907f Improve logger action naming consistency (#166538) 2026-03-25 22:02:16 +00:00
Franck Nijhof
bcca7655f8 Improve water heater action naming consistency (#166535)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-25 22:01:53 +00:00
Jordan Harvey
269ef5f824 Bump pyanglianwater to 3.1.2 (#166531) 2026-03-25 22:33:24 +01:00
Erik Montnemery
c80a9aab71 Add trigger water_heater.operation_mode_changed (#166450) 2026-03-25 21:54:34 +01:00
balloob-travel
33180a658a Validate port ranges in URL validator (#166059)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-25 21:44:30 +01:00
Erik Montnemery
c5955ada1a Use NumericThresholdSelector in numeric conditions (#166507) 2026-03-25 20:57:12 +01:00
Abílio Costa
fd7d936a0d Instruct copilot to place main comment in collapsible section (#166503) 2026-03-25 20:45:39 +01:00
Franck Nijhof
84cd137bae Bump version to 2026.5.0dev0 (#166512) 2026-03-25 20:24:07 +01:00
johanzander
3a77a638d5 growatt_server: use human-readable labels in exception messages (#166024)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-03-25 20:00:47 +01:00
Christian Lackas
599f4f01d0 Add HmIP-FLC support to HomematicIP Cloud (#165827) 2026-03-25 19:58:18 +01:00
Joakim Plate
bd298e92d0 Rework patching and handling of client runner in arcam (#165747) 2026-03-25 19:55:59 +01:00
Leon Grave
fabbfd93df Add dynamic devices to freshr (#165942) 2026-03-25 19:49:08 +01:00
Simone Chemelli
1ecbc44368 Improve KNX tests and avoid dns lookups (#166508) 2026-03-25 19:47:57 +01:00
173 changed files with 3619 additions and 2941 deletions

View File

@@ -112,7 +112,7 @@ jobs:
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend
@@ -123,7 +123,7 @@ jobs:
- name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: OHF-Voice/intents-package

View File

@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 3
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.4"
HA_SHORT_VERSION: "2026.5"
ADDITIONAL_PYTHON_VERSIONS: "[]"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
@@ -280,7 +280,7 @@ jobs:
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run prek
uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1.1.1
uses: j178/prek-action@79f765515bd648eb4d6bb1b17277b7cb22cb6468 # v2.0.0
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
RUFF_OUTPUT_FORMAT: github
@@ -301,7 +301,7 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1.1.1
uses: j178/prek-action@79f765515bd648eb4d6bb1b17277b7cb22cb6468 # v2.0.0
with:
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
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
key: >-
@@ -372,7 +372,7 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
@@ -384,7 +384,7 @@ jobs:
env.HA_SHORT_VERSION }}-
- name: Check if apt cache exists
id: cache-apt-check
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
path: |
@@ -430,7 +430,7 @@ jobs:
fi
- name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -484,7 +484,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -515,7 +515,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -552,7 +552,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -643,7 +643,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -694,7 +694,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -747,7 +747,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
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
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -812,7 +812,7 @@ jobs:
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .mypy_cache
key: >-
@@ -854,7 +854,7 @@ jobs:
- base
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -887,7 +887,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -930,7 +930,7 @@ jobs:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -964,7 +964,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -1080,7 +1080,7 @@ jobs:
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -1115,7 +1115,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -1238,7 +1238,7 @@ jobs:
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -1275,7 +1275,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -1392,7 +1392,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
with:
fail_ci_if_error: true
flags: full-suite
@@ -1421,7 +1421,7 @@ jobs:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -1455,7 +1455,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
fail-on-cache-miss: true
@@ -1563,7 +1563,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
@@ -1591,7 +1591,7 @@ jobs:
with:
pattern: test-results-*
- name: Upload test results to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
with:
report_type: test_results
fail_ci_if_error: true

View File

@@ -13,7 +13,7 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
Condition,
make_entity_numerical_condition,
@@ -59,18 +59,18 @@ CONDITIONS: dict[str, type[Condition]] = {
"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: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CarbonMonoxideConcentrationConverter,
),
"is_ozone_value": make_entity_numerical_condition_with_unit(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
OzoneConcentrationConverter,
),
"is_voc_value": make_entity_numerical_condition_with_unit(
{
SENSOR_DOMAIN: NumericalDomainSpec(
SENSOR_DOMAIN: DomainSpec(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
)
},
@@ -79,7 +79,7 @@ CONDITIONS: dict[str, type[Condition]] = {
),
"is_voc_ratio_value": make_entity_numerical_condition_with_unit(
{
SENSOR_DOMAIN: NumericalDomainSpec(
SENSOR_DOMAIN: DomainSpec(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
)
},
@@ -87,59 +87,43 @@ CONDITIONS: dict[str, type[Condition]] = {
UnitlessRatioConverter,
),
"is_no_value": make_entity_numerical_condition_with_unit(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
)
},
{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: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
)
},
{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: NumericalDomainSpec(
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
)
},
{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: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
),
"is_pm1_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"is_pm25_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"is_pm4_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"is_pm10_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"is_n2o_value": make_entity_numerical_condition(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROUS_OXIDE
)
},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
}

View File

@@ -10,366 +10,155 @@
- all
- any
# --- Number or entity selectors ---
# --- Unit lists for multi-unit pollutants ---
.number_or_entity_co: &number_or_entity_co
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "ppm"
- "mg/m³"
- "μg/m³"
- domain: sensor
device_class: carbon_monoxide
- domain: number
device_class: carbon_monoxide
translation_key: number_or_entity
.co_units: &co_units
- "ppb"
- "ppm"
- "mg/m³"
- "μg/m³"
.number_or_entity_co2: &number_or_entity_co2
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "ppm"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "ppm"
- domain: sensor
device_class: carbon_dioxide
- domain: number
device_class: carbon_dioxide
translation_key: number_or_entity
.ozone_units: &ozone_units
- "ppb"
- "ppm"
- "μg/m³"
.number_or_entity_pm1: &number_or_entity_pm1
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm1
- domain: number
device_class: pm1
translation_key: number_or_entity
.voc_units: &voc_units
- "μg/m³"
- "mg/m³"
.number_or_entity_pm25: &number_or_entity_pm25
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm25
- domain: number
device_class: pm25
translation_key: number_or_entity
.voc_ratio_units: &voc_ratio_units
- "ppb"
- "ppm"
.number_or_entity_pm4: &number_or_entity_pm4
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm4
- domain: number
device_class: pm4
translation_key: number_or_entity
.no_units: &no_units
- "ppb"
- "μg/m³"
.number_or_entity_pm10: &number_or_entity_pm10
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm10
- domain: number
device_class: pm10
translation_key: number_or_entity
.no2_units: &no2_units
- "ppb"
- "ppm"
- "μg/m³"
.number_or_entity_ozone: &number_or_entity_ozone
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "ppm"
- "μg/m³"
- domain: sensor
device_class: ozone
- domain: number
device_class: ozone
translation_key: number_or_entity
.so2_units: &so2_units
- "ppb"
- "μg/m³"
.number_or_entity_voc: &number_or_entity_voc
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "μg/m³"
- "mg/m³"
- domain: sensor
device_class: volatile_organic_compounds
- domain: number
device_class: volatile_organic_compounds
translation_key: number_or_entity
# --- Entity filter anchors ---
.number_or_entity_voc_ratio: &number_or_entity_voc_ratio
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "ppm"
- domain: sensor
device_class: volatile_organic_compounds_parts
- domain: number
device_class: volatile_organic_compounds_parts
translation_key: number_or_entity
.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
.number_or_entity_no: &number_or_entity_no
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "μg/m³"
- domain: sensor
device_class: nitrogen_monoxide
- domain: number
device_class: nitrogen_monoxide
translation_key: number_or_entity
.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
.number_or_entity_no2: &number_or_entity_no2
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "ppm"
- "μg/m³"
- domain: sensor
device_class: nitrogen_dioxide
- domain: number
device_class: nitrogen_dioxide
translation_key: number_or_entity
.pm1_threshold_entity: &pm1_threshold_entity
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm1
- domain: number
device_class: pm1
.number_or_entity_n2o: &number_or_entity_n2o
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: nitrous_oxide
- domain: number
device_class: nitrous_oxide
translation_key: number_or_entity
.pm25_threshold_entity: &pm25_threshold_entity
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm25
- domain: number
device_class: pm25
.number_or_entity_so2: &number_or_entity_so2
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "μg/m³"
- domain: sensor
device_class: sulphur_dioxide
- domain: number
device_class: sulphur_dioxide
translation_key: number_or_entity
.pm4_threshold_entity: &pm4_threshold_entity
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm4
- domain: number
device_class: pm4
# --- Unit selectors ---
.pm10_threshold_entity: &pm10_threshold_entity
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm10
- domain: number
device_class: pm10
.unit_co: &unit_co
required: false
selector:
select:
options:
- "ppb"
- "ppm"
- "mg/m³"
- "μg/m³"
.ozone_threshold_entity: &ozone_threshold_entity
- domain: input_number
unit_of_measurement: *ozone_units
- domain: sensor
device_class: ozone
- domain: number
device_class: ozone
.unit_ozone: &unit_ozone
required: false
selector:
select:
options:
- "ppb"
- "ppm"
- "μg/m³"
.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
.unit_no2: &unit_no2
required: false
selector:
select:
options:
- "ppb"
- "ppm"
- "μg/m³"
.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
.unit_no: &unit_no
required: false
selector:
select:
options:
- "ppb"
- "μg/m³"
.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
.unit_so2: &unit_so2
required: false
selector:
select:
options:
- "ppb"
- "μg/m³"
.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
.unit_voc: &unit_voc
required: false
selector:
select:
options:
- "μg/m³"
- "mg/m³"
.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
.unit_voc_ratio: &unit_voc_ratio
required: false
selector:
select:
options:
- "ppb"
- "ppm"
.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 ---
@@ -491,57 +280,99 @@ is_co_value:
target: *target_co_sensor
fields:
behavior: *condition_behavior
above: *number_or_entity_co
below: *number_or_entity_co
unit: *unit_co
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
above: *number_or_entity_ozone
below: *number_or_entity_ozone
unit: *unit_ozone
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
above: *number_or_entity_voc
below: *number_or_entity_voc
unit: *unit_voc
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
above: *number_or_entity_voc_ratio
below: *number_or_entity_voc_ratio
unit: *unit_voc_ratio
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
above: *number_or_entity_no
below: *number_or_entity_no
unit: *unit_no
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
above: *number_or_entity_no2
below: *number_or_entity_no2
unit: *unit_no2
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
above: *number_or_entity_so2
below: *number_or_entity_so2
unit: *unit_so2
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 ---
@@ -549,40 +380,70 @@ is_co2_value:
target: *target_co2
fields:
behavior: *condition_behavior
above: *number_or_entity_co2
below: *number_or_entity_co2
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
above: *number_or_entity_pm1
below: *number_or_entity_pm1
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
above: *number_or_entity_pm25
below: *number_or_entity_pm25
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
above: *number_or_entity_pm4
below: *number_or_entity_pm4
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
above: *number_or_entity_pm10
below: *number_or_entity_pm10
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
above: *number_or_entity_n2o
below: *number_or_entity_n2o
threshold:
required: true
selector:
numeric_threshold:
entity: *n2o_threshold_entity
mode: is
number: *ugm3_threshold_number

View File

@@ -1,13 +1,9 @@
{
"common": {
"condition_above_description": "Require the value to be above this value.",
"condition_above_name": "Above",
"condition_behavior_description": "How the value should match on the targeted entities.",
"condition_behavior_name": "Behavior",
"condition_below_description": "Require the value to be below this value.",
"condition_below_name": "Below",
"condition_unit_description": "All values will be converted to this unit when evaluating the condition.",
"condition_unit_name": "Unit of measurement",
"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.",
@@ -18,17 +14,13 @@
"is_co2_value": {
"description": "Tests the carbon dioxide level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
"threshold": {
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
},
"name": "Carbon dioxide value"
@@ -56,21 +48,13 @@
"is_co_value": {
"description": "Tests the carbon monoxide level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_name%]"
"threshold": {
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
},
"name": "Carbon monoxide value"
@@ -98,17 +82,13 @@
"is_n2o_value": {
"description": "Tests the nitrous oxide level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
"threshold": {
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
},
"name": "Nitrous oxide value"
@@ -116,21 +96,13 @@
"is_no2_value": {
"description": "Tests the nitrogen dioxide level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_name%]"
"threshold": {
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
},
"name": "Nitrogen dioxide value"
@@ -138,21 +110,13 @@
"is_no_value": {
"description": "Tests the nitrogen monoxide level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_name%]"
"threshold": {
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
},
"name": "Nitrogen monoxide value"
@@ -160,21 +124,13 @@
"is_ozone_value": {
"description": "Tests the ozone level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_name%]"
"threshold": {
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
},
"name": "Ozone value"
@@ -182,17 +138,13 @@
"is_pm10_value": {
"description": "Tests the PM10 level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
"threshold": {
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
},
"name": "PM10 value"
@@ -200,17 +152,13 @@
"is_pm1_value": {
"description": "Tests the PM1 level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
"threshold": {
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
},
"name": "PM1 value"
@@ -218,17 +166,13 @@
"is_pm25_value": {
"description": "Tests the PM2.5 level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
"threshold": {
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
},
"name": "PM2.5 value"
@@ -236,17 +180,13 @@
"is_pm4_value": {
"description": "Tests the PM4 level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
"threshold": {
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
},
"name": "PM4 value"
@@ -274,21 +214,13 @@
"is_so2_value": {
"description": "Tests the sulphur dioxide level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_name%]"
"threshold": {
"description": "[%key:component::air_quality::common::condition_threshold_description%]",
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
},
"name": "Sulphur dioxide value"
@@ -296,21 +228,13 @@
"is_voc_ratio_value": {
"description": "Tests the volatile organic compounds ratio of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_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"
@@ -318,21 +242,13 @@
"is_voc_value": {
"description": "Tests the volatile organic compounds level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_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"
@@ -345,12 +261,6 @@
"any": "Any"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -13,7 +13,7 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
EntityTargetStateTriggerBase,
Trigger,
@@ -64,28 +64,28 @@ TRIGGERS: dict[str, type[Trigger]] = {
"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: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
{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: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
{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: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
{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: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
{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: NumericalDomainSpec(
SENSOR_DOMAIN: DomainSpec(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
)
},
@@ -94,7 +94,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
),
"voc_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
SENSOR_DOMAIN: DomainSpec(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
)
},
@@ -103,7 +103,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
),
"voc_ratio_changed": make_entity_numerical_state_changed_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
SENSOR_DOMAIN: DomainSpec(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
)
},
@@ -112,7 +112,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
),
"voc_ratio_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
SENSOR_DOMAIN: DomainSpec(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
)
},
@@ -120,114 +120,82 @@ TRIGGERS: dict[str, type[Trigger]] = {
UnitlessRatioConverter,
),
"no_changed": make_entity_numerical_state_changed_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
)
},
{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: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
)
},
{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: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
)
},
{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: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
)
},
{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: NumericalDomainSpec(
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
)
},
{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: NumericalDomainSpec(
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
)
},
{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: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
{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: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
),
"pm1_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
{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: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"pm25_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
{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: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"pm4_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
{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: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"pm10_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
{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: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"n2o_changed": make_entity_numerical_state_changed_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROUS_OXIDE
)
},
{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: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROUS_OXIDE
)
},
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
}

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze",
"requirements": ["pyanglianwater==3.1.1"]
"requirements": ["pyanglianwater==3.1.2"]
}

View File

@@ -2,8 +2,8 @@
import asyncio
from asyncio import timeout
from contextlib import AsyncExitStack
import logging
from typing import Any
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
@@ -54,36 +54,31 @@ async def _run_client(
client = runtime_data.client
coordinators = runtime_data.coordinators
def _listen(_: Any) -> None:
for coordinator in coordinators.values():
coordinator.async_notify_data_updated()
while True:
try:
async with timeout(interval):
await client.start()
async with AsyncExitStack() as stack:
async with timeout(interval):
await client.start()
stack.push_async_callback(client.stop)
_LOGGER.debug("Client connected %s", client.host)
_LOGGER.debug("Client connected %s", client.host)
try:
for coordinator in coordinators.values():
await coordinator.state.start()
with client.listen(_listen):
try:
for coordinator in coordinators.values():
coordinator.async_notify_connected()
await client.process()
finally:
await client.stop()
await stack.enter_async_context(
coordinator.async_monitor_client()
)
_LOGGER.debug("Client disconnected %s", client.host)
for coordinator in coordinators.values():
coordinator.async_notify_disconnected()
await client.process()
finally:
_LOGGER.debug("Client disconnected %s", client.host)
except ConnectionFailed:
await asyncio.sleep(interval)
pass
except TimeoutError:
continue
except Exception:
_LOGGER.exception("Unexpected exception, aborting arcam client")
return
await asyncio.sleep(interval)

View File

@@ -2,11 +2,13 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from dataclasses import dataclass
import logging
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
from arcam.fmj.client import AmxDuetResponse, Client, ResponsePacket
from arcam.fmj.state import State
from homeassistant.config_entries import ConfigEntry
@@ -51,7 +53,7 @@ class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
)
self.client = client
self.state = State(client, zone)
self.last_update_success = False
self.update_in_progress = False
name = config_entry.title
unique_id = config_entry.unique_id or config_entry.entry_id
@@ -74,24 +76,34 @@ class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
async def _async_update_data(self) -> None:
"""Fetch data for manual refresh."""
try:
self.update_in_progress = True
await self.state.update()
except ConnectionFailed as err:
raise UpdateFailed(
f"Connection failed during update for zone {self.state.zn}"
) from err
finally:
self.update_in_progress = False
@callback
def async_notify_data_updated(self) -> None:
"""Notify that new data has been received from the device."""
self.async_set_updated_data(None)
def _async_notify_packet(self, packet: ResponsePacket | AmxDuetResponse) -> None:
"""Packet callback to detect changes to state."""
if (
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()
@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())

View File

@@ -26,3 +26,8 @@ class ArcamFmjEntity(CoordinatorEntity[ArcamFmjCoordinator]):
if description is not None:
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
self.entity_description = description
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.coordinator.client.connected

View File

@@ -155,6 +155,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"air_quality",
"alarm_control_panel",
"assist_satellite",
"battery",
"button",
"climate",
"counter",
@@ -185,6 +186,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"switch",
"temperature",
"text",
"todo",
"update",
"vacuum",
"water_heater",

View File

@@ -78,11 +78,11 @@
"services": {
"reload": {
"description": "Reloads the automation configuration.",
"name": "[%key:common::action::reload%]"
"name": "Reload automations"
},
"toggle": {
"description": "Toggles (enable / disable) an automation.",
"name": "[%key:common::action::toggle%]"
"name": "Toggle automation"
},
"trigger": {
"description": "Triggers the actions of an automation.",
@@ -92,7 +92,7 @@
"name": "Skip conditions"
}
},
"name": "Trigger"
"name": "Trigger automation"
},
"turn_off": {
"description": "Disables an automation.",
@@ -102,11 +102,11 @@
"name": "Stop actions"
}
},
"name": "[%key:common::action::turn_off%]"
"name": "Turn off automation"
},
"turn_on": {
"description": "Enables an automation.",
"name": "[%key:common::action::turn_on%]"
"name": "Turn on automation"
}
},
"title": "Automation"

View File

@@ -1,4 +1,4 @@
"""Integration for battery conditions."""
"""Integration for battery triggers and conditions."""
from __future__ import annotations

View File

@@ -6,7 +6,6 @@ from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
@@ -27,7 +26,6 @@ BATTERY_CHARGING_DOMAIN_SPECS = {
}
BATTERY_PERCENTAGE_DOMAIN_SPECS = {
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.BATTERY),
}
CONDITIONS: dict[str, type[Condition]] = {

View File

@@ -14,24 +14,17 @@
- all
- any
.number_or_entity: &number_or_entity
required: false
selector:
choose:
choices:
number:
selector:
number:
unit_of_measurement: "%"
entity:
selector:
entity:
filter:
domain:
- input_number
- number
- sensor
translation_key: number_or_entity
.battery_threshold_entity: &battery_threshold_entity
- domain: input_number
unit_of_measurement: "%"
- domain: sensor
device_class: battery
.battery_threshold_number: &battery_threshold_number
min: 0
max: 100
mode: box
unit_of_measurement: "%"
is_low: *condition_common
@@ -58,9 +51,12 @@ is_level:
entity:
- domain: sensor
device_class: battery
- domain: number
device_class: battery
fields:
behavior: *condition_behavior
above: *number_or_entity
below: *number_or_entity
threshold:
required: true
selector:
numeric_threshold:
entity: *battery_threshold_entity
mode: is
number: *battery_threshold_number

View File

@@ -15,5 +15,25 @@
"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"
}
}
}

View File

@@ -1,7 +1,14 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted batteries.",
"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 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": {
@@ -17,17 +24,13 @@
"is_level": {
"description": "Tests the battery level of one or more batteries.",
"fields": {
"above": {
"description": "Require the battery percentage to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::battery::common::condition_behavior_description%]",
"name": "[%key:component::battery::common::condition_behavior_name%]"
},
"below": {
"description": "Require the battery percentage to be below this value.",
"name": "Below"
"threshold": {
"description": "[%key:component::battery::common::condition_threshold_description%]",
"name": "[%key:component::battery::common::condition_threshold_name%]"
}
},
"name": "Battery level"
@@ -70,12 +73,79 @@
"any": "Any"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Battery"
"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"
}
}
}

View File

@@ -0,0 +1,54 @@
"""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

View File

@@ -0,0 +1,81 @@
.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: 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

View File

@@ -37,7 +37,7 @@ from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_point_in_time
from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from homeassistant.util.json import JsonValueType
@@ -308,19 +308,10 @@ SERVICE_GET_EVENTS_SCHEMA: Final = vol.All(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Track states and offer events for calendars."""
component = hass.data[DATA_COMPONENT] = EntityComponent[CalendarEntity](
component = hass.data[DATA_COMPONENT] = CalendarEntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
hass.http.register_view(CalendarListView(component))
hass.http.register_view(CalendarEventView(component))
frontend.async_register_built_in_panel(hass, "calendar", "calendar", "mdi:calendar")
websocket_api.async_register_command(hass, handle_calendar_event_create)
websocket_api.async_register_command(hass, handle_calendar_event_delete)
websocket_api.async_register_command(hass, handle_calendar_event_update)
component.async_register_entity_service(
CREATE_EVENT_SERVICE,
CREATE_EVENT_SCHEMA,
@@ -667,6 +658,53 @@ class CalendarEntity(Entity):
raise NotImplementedError
class CalendarEntityComponent(EntityComponent[CalendarEntity]):
"""Calendar entity component.
Sets up frontend resources and websocket API when the first platform is added.
"""
_frontend_loaded: bool = False
async def async_setup_entry(self, config_entry: ConfigEntry) -> bool:
"""Set up a config entry."""
result = await super().async_setup_entry(config_entry)
if not self._frontend_loaded:
self._register_frontend_resources()
return result
async def async_setup_platform(
self,
platform_type: str,
platform_config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up a platform for this component."""
await super().async_setup_platform(
platform_type, platform_config, discovery_info
)
if not self._frontend_loaded:
self._register_frontend_resources()
def _register_frontend_resources(self) -> None:
"""Register frontend resources for calendar."""
self._frontend_loaded = True
self.hass.http.register_view(CalendarListView(self))
self.hass.http.register_view(CalendarEventView(self))
frontend.async_register_built_in_panel(
self.hass, "calendar", "calendar", "mdi:calendar"
)
websocket_api.async_register_command(self.hass, handle_calendar_event_create)
websocket_api.async_register_command(self.hass, handle_calendar_event_delete)
websocket_api.async_register_command(self.hass, handle_calendar_event_update)
class CalendarEventView(http.HomeAssistantView):
"""View to retrieve calendar content."""

View File

@@ -2,7 +2,7 @@
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
Condition,
EntityNumericalConditionWithUnitBase,
@@ -18,7 +18,7 @@ class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
"""Mixin for climate target temperature conditions with unit conversion."""
_base_unit = UnitOfTemperature.CELSIUS
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
_unit_converter = TemperatureConverter
def _get_entity_unit(self, entity_state: State) -> str | None:
@@ -50,7 +50,7 @@ CONDITIONS: dict[str, type[Condition]] = {
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
),
"target_humidity": make_entity_numerical_condition(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit="%",
),
"target_temperature": ClimateTargetTemperatureCondition,

View File

@@ -13,58 +13,31 @@
- all
- any
.number_or_entity_humidity: &number_or_entity_humidity
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "%"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "%"
- domain: sensor
device_class: humidity
- domain: number
device_class: humidity
translation_key: number_or_entity
.humidity_threshold_entity: &humidity_threshold_entity
- domain: input_number
unit_of_measurement: "%"
- domain: sensor
device_class: humidity
- domain: number
device_class: humidity
.number_or_entity_temperature: &number_or_entity_temperature
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "°C"
- "°F"
- domain: sensor
device_class: temperature
- domain: number
device_class: temperature
translation_key: number_or_entity
.humidity_threshold_number: &humidity_threshold_number
min: 0
max: 100
mode: box
unit_of_measurement: "%"
.condition_unit_temperature: &condition_unit_temperature
required: false
selector:
select:
options:
- "°C"
- "°F"
.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_on: *condition_common
@@ -76,13 +49,24 @@ target_humidity:
target: *condition_climate_target
fields:
behavior: *condition_behavior
above: *number_or_entity_humidity
below: *number_or_entity_humidity
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
above: *number_or_entity_temperature
below: *number_or_entity_temperature
unit: *condition_unit_temperature
threshold:
required: true
selector:
numeric_threshold:
entity: *temperature_threshold_entity
mode: is
number:
mode: box
unit_of_measurement: *temperature_units

View File

@@ -2,6 +2,8 @@
"common": {
"condition_behavior_description": "How the state should match on the targeted climate-control devices.",
"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_name": "Behavior",
"trigger_threshold_changed_description": "Which changes to trigger on and threshold values.",
@@ -62,17 +64,13 @@
"target_humidity": {
"description": "Tests the humidity setpoint of one or more climate-control devices.",
"fields": {
"above": {
"description": "Require the target humidity to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::climate::common::condition_behavior_description%]",
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"below": {
"description": "Require the target humidity to be below this value.",
"name": "Below"
"threshold": {
"description": "[%key:component::climate::common::condition_threshold_description%]",
"name": "[%key:component::climate::common::condition_threshold_name%]"
}
},
"name": "Climate-control device target humidity"
@@ -80,21 +78,13 @@
"target_temperature": {
"description": "Tests the temperature setpoint of one or more climate-control devices.",
"fields": {
"above": {
"description": "Require the target temperature to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::climate::common::condition_behavior_description%]",
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"below": {
"description": "Require the target temperature to be below this value.",
"name": "Below"
},
"unit": {
"description": "All values will be converted to this unit when evaluating the condition.",
"name": "Unit of measurement"
"threshold": {
"description": "[%key:component::climate::common::condition_threshold_description%]",
"name": "[%key:component::climate::common::condition_threshold_name%]"
}
},
"name": "Climate-control device target temperature"
@@ -284,12 +274,6 @@
"any": "Any"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -5,7 +5,7 @@ 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, NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
EntityNumericalStateChangedTriggerWithUnitBase,
@@ -52,7 +52,7 @@ class _ClimateTargetTemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitB
"""Mixin for climate target temperature triggers with unit conversion."""
_base_unit = UnitOfTemperature.CELSIUS
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
_unit_converter = TemperatureConverter
def _get_entity_unit(self, state: State) -> str | None:
@@ -84,11 +84,11 @@ TRIGGERS: dict[str, type[Trigger]] = {
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.DRYING
),
"target_humidity_changed": make_entity_numerical_state_changed_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit="%",
),
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit="%",
),
"target_temperature_changed": ClimateTargetTemperatureChangedTrigger,

View File

@@ -75,11 +75,11 @@
"services": {
"remote_connect": {
"description": "Makes the instance UI accessible from outside of the local network by enabling your Home Assistant Cloud connection.",
"name": "Enable remote access"
"name": "Enable Home Assistant Cloud remote access"
},
"remote_disconnect": {
"description": "Disconnects the instance UI from Home Assistant Cloud. This disables access to it from outside your local network.",
"name": "Disable remote access"
"name": "Disable Home Assistant Cloud remote access"
}
},
"system_health": {

View File

@@ -6,7 +6,7 @@
},
"services": {
"process": {
"description": "Launches a conversation from a transcribed text.",
"description": "Sends text to a conversation agent for processing.",
"fields": {
"agent_id": {
"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": "Process"
"name": "Process conversation"
},
"reload": {
"description": "Reloads the intent configuration.",
"description": "Reloads the intent configuration of conversation agents.",
"fields": {
"agent_id": {
"description": "Conversation agent to reload.",
@@ -39,7 +39,7 @@
"name": "[%key:common::config_flow::data::language%]"
}
},
"name": "[%key:common::action::reload%]"
"name": "Reload conversation agents"
}
},
"title": "Conversation"

View File

@@ -41,25 +41,25 @@
"services": {
"decrement": {
"description": "Decrements a counter by its step size.",
"name": "Decrement"
"name": "Decrement counter"
},
"increment": {
"description": "Increments a counter by its step size.",
"name": "Increment"
"name": "Increment counter"
},
"reset": {
"description": "Resets a counter to its initial value.",
"name": "Reset"
"name": "Reset counter"
},
"set_value": {
"description": "Sets the counter to a specific value.",
"description": "Sets a counter to a specific value.",
"fields": {
"value": {
"description": "The new counter value the entity should be set to.",
"name": "Value"
}
},
"name": "Set"
"name": "Set counter value"
}
},
"title": "Counter",

View File

@@ -1,5 +1,7 @@
"""Provides conditions for covers."""
from collections.abc import Mapping
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.condition import Condition, EntityConditionBase
@@ -8,9 +10,11 @@ from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
from .models import CoverDomainSpec
class CoverConditionBase(EntityConditionBase[CoverDomainSpec]):
class CoverConditionBase(EntityConditionBase):
"""Base condition for cover state checks."""
_domain_specs: Mapping[str, CoverDomainSpec]
def is_valid_state(self, entity_state: State) -> bool:
"""Check if the state matches the expected cover state."""
domain_spec = self._domain_specs[entity_state.domain]

View File

@@ -1,5 +1,7 @@
"""Provides triggers for covers."""
from collections.abc import Mapping
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.trigger import EntityTriggerBase, Trigger
@@ -8,9 +10,11 @@ from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
from .models import CoverDomainSpec
class CoverTriggerBase(EntityTriggerBase[CoverDomainSpec]):
class CoverTriggerBase(EntityTriggerBase):
"""Base trigger for cover state changes."""
_domain_specs: Mapping[str, CoverDomainSpec]
def _get_value(self, state: State) -> str | bool | None:
"""Extract the relevant value from state based on domain spec."""
domain_spec = self._domain_specs[state.domain]

View File

@@ -120,7 +120,7 @@
"name": "MAC address"
}
},
"name": "See"
"name": "See device tracker"
}
},
"title": "Device tracker",

View File

@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.13.8"]
"requirements": ["sense-energy==0.14.0"]
}

View File

@@ -3,7 +3,7 @@
import asyncio
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from .coordinator import (
FreshrConfigEntry,
@@ -21,10 +21,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: FreshrConfigEntry) -> bo
await devices_coordinator.async_config_entry_first_refresh()
readings: dict[str, FreshrReadingsCoordinator] = {
device.id: FreshrReadingsCoordinator(
device_id: FreshrReadingsCoordinator(
hass, entry, device, devices_coordinator.client
)
for device in devices_coordinator.data
for device_id, device in devices_coordinator.data.items()
}
await asyncio.gather(
*(
@@ -38,6 +38,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: FreshrConfigEntry) -> bo
readings=readings,
)
known_devices: set[str] = set(readings)
@callback
def _handle_coordinator_update() -> None:
current = set(devices_coordinator.data)
removed_ids = known_devices - current
if removed_ids:
known_devices.difference_update(removed_ids)
for device_id in removed_ids:
entry.runtime_data.readings.pop(device_id, None)
new_ids = current - known_devices
if not new_ids:
return
known_devices.update(new_ids)
for device_id in new_ids:
device = devices_coordinator.data[device_id]
readings_coordinator = FreshrReadingsCoordinator(
hass, entry, device, devices_coordinator.client
)
entry.runtime_data.readings[device_id] = readings_coordinator
hass.async_create_task(
readings_coordinator.async_refresh(),
name=f"freshr_readings_refresh_{device_id}",
)
entry.async_on_unload(
devices_coordinator.async_add_listener(_handle_coordinator_update)
)
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True

View File

@@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -32,7 +33,7 @@ class FreshrData:
type FreshrConfigEntry = ConfigEntry[FreshrData]
class FreshrDevicesCoordinator(DataUpdateCoordinator[list[DeviceSummary]]):
class FreshrDevicesCoordinator(DataUpdateCoordinator[dict[str, DeviceSummary]]):
"""Coordinator that refreshes the device list once an hour."""
config_entry: FreshrConfigEntry
@@ -48,7 +49,7 @@ class FreshrDevicesCoordinator(DataUpdateCoordinator[list[DeviceSummary]]):
)
self.client = FreshrClient(session=async_create_clientsession(hass))
async def _async_update_data(self) -> list[DeviceSummary]:
async def _async_update_data(self) -> dict[str, DeviceSummary]:
"""Fetch the list of devices from the Fresh-r API."""
username = self.config_entry.data[CONF_USERNAME]
password = self.config_entry.data[CONF_PASSWORD]
@@ -68,8 +69,23 @@ class FreshrDevicesCoordinator(DataUpdateCoordinator[list[DeviceSummary]]):
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
else:
return devices
current = {device.id: device for device in devices}
if self.data is not None:
stale_ids = set(self.data) - set(current)
if stale_ids:
device_registry = dr.async_get(self.hass)
for device_id in stale_ids:
if device := device_registry.async_get_device(
identifiers={(DOMAIN, device_id)}
):
device_registry.async_update_device(
device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
return current
class FreshrReadingsCoordinator(DataUpdateCoordinator[DeviceReadings]):

View File

@@ -45,7 +45,9 @@ rules:
discovery-update-info:
status: exempt
comment: Integration connects to a cloud service; no local network discovery is possible.
discovery: todo
discovery:
status: exempt
comment: No local network discovery of devices is possible (no zeroconf, mdns or other discovery mechanisms).
docs-data-update: done
docs-examples: done
docs-known-limitations: done
@@ -53,7 +55,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: todo
dynamic-devices: done
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
@@ -64,7 +66,7 @@ rules:
repair-issues:
status: exempt
comment: No actionable repair scenarios exist; authentication failures are handled via the reauthentication flow.
stale-devices: todo
stale-devices: done
# Platinum
async-dependency: done

View File

@@ -20,7 +20,7 @@ from homeassistant.const import (
UnitOfTemperature,
UnitOfVolumeFlowRate,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -112,26 +112,43 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Fresh-r sensors from a config entry."""
entities: list[FreshrSensor] = []
for device in config_entry.runtime_data.devices.data:
descriptions = SENSOR_TYPES.get(
device.device_type, SENSOR_TYPES[DeviceType.FRESH_R]
)
device_info = DeviceInfo(
identifiers={(DOMAIN, device.id)},
name=_DEVICE_TYPE_NAMES.get(device.device_type, "Fresh-r"),
serial_number=device.id,
manufacturer="Fresh-r",
)
entities.extend(
FreshrSensor(
config_entry.runtime_data.readings[device.id],
description,
device_info,
coordinator = config_entry.runtime_data.devices
known_devices: set[str] = set()
@callback
def _check_devices() -> None:
current = set(coordinator.data)
removed_ids = known_devices - current
if removed_ids:
known_devices.difference_update(removed_ids)
new_ids = current - known_devices
if not new_ids:
return
known_devices.update(new_ids)
entities: list[FreshrSensor] = []
for device_id in new_ids:
device = coordinator.data[device_id]
descriptions = SENSOR_TYPES.get(
device.device_type, SENSOR_TYPES[DeviceType.FRESH_R]
)
for description in descriptions
)
async_add_entities(entities)
device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
name=_DEVICE_TYPE_NAMES.get(device.device_type, "Fresh-r"),
serial_number=device_id,
manufacturer="Fresh-r",
)
entities.extend(
FreshrSensor(
config_entry.runtime_data.readings[device_id],
description,
device_info,
)
for description in descriptions
)
async_add_entities(entities)
_check_devices()
config_entry.async_on_unload(coordinator.async_add_listener(_check_devices))
class FreshrSensor(CoordinatorEntity[FreshrReadingsCoordinator], SensorEntity):

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from collections.abc import AsyncIterator, Callable, Coroutine
from functools import wraps
import logging
from typing import Any
@@ -84,8 +85,22 @@ class GoogleDriveBackupAgent(BackupAgent):
:param open_stream: A function returning an async iterator that yields bytes.
:param backup: Metadata about the backup that should be uploaded.
"""
@wraps(open_stream)
async def wrapped_open_stream() -> AsyncIterator[bytes]:
stream = await open_stream()
async def _progress_stream() -> AsyncIterator[bytes]:
bytes_uploaded = 0
async for chunk in stream:
yield chunk
bytes_uploaded += len(chunk)
on_progress(bytes_uploaded=bytes_uploaded)
return _progress_stream()
try:
await self._client.async_upload_backup(open_stream, backup)
await self._client.async_upload_backup(wrapped_open_stream, backup)
except (GoogleDriveApiError, HomeAssistantError, TimeoutError) as err:
raise BackupAgentError(f"Failed to upload backup: {err}") from err

View File

@@ -296,7 +296,7 @@
"services": {
"reload": {
"description": "Reloads group configuration, entities, and notify services from YAML-configuration.",
"name": "[%key:common::action::reload%]"
"name": "Reload groups"
},
"remove": {
"description": "Removes a group.",
@@ -306,10 +306,10 @@
"name": "[%key:component::group::services::set::fields::object_id::name%]"
}
},
"name": "Remove"
"name": "Remove group"
},
"set": {
"description": "Creates/Updates a group.",
"description": "Creates or updates a group.",
"fields": {
"add_entities": {
"description": "List of members to be added to the group. Cannot be used in combination with `Entities` or `Remove entities`.",
@@ -340,7 +340,7 @@
"name": "Remove entities"
}
},
"name": "Set"
"name": "Set group"
}
},
"title": "Group"

View File

@@ -87,22 +87,26 @@ def _get_coordinator(
return coordinators[serial_number]
def _parse_time_str(time_str: str, field_name: str) -> time:
def _parse_time_str(
time_str: str,
translation_key: str,
translation_placeholders: dict[str, str] | None = None,
) -> time:
"""Parse a time string (HH:MM or HH:MM:SS) to a datetime.time object."""
parts = time_str.split(":")
if len(parts) not in (2, 3):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_time_format",
translation_placeholders={"field_name": field_name},
translation_key=translation_key,
translation_placeholders=translation_placeholders or {},
)
try:
return datetime.strptime(f"{parts[0]}:{parts[1]}", "%H:%M").time()
except (ValueError, IndexError) as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_time_format",
translation_placeholders={"field_name": field_name},
translation_key=translation_key,
translation_placeholders=translation_placeholders or {},
) from err
@@ -142,8 +146,8 @@ def async_setup_services(hass: HomeAssistant) -> None:
)
batt_mode: int = valid_modes[batt_mode_str]
start_time = _parse_time_str(start_time_str, "start_time")
end_time = _parse_time_str(end_time_str, "end_time")
start_time = _parse_time_str(start_time_str, "invalid_time_format_start_time")
end_time = _parse_time_str(end_time_str, "invalid_time_format_end_time")
coordinator: GrowattCoordinator = _get_coordinator(hass, device_id, "min")
await coordinator.update_time_segment(
@@ -192,11 +196,13 @@ def async_setup_services(hass: HomeAssistant) -> None:
cached = current["periods"][i - 1]
start = _parse_time_str(
call.data.get(f"period_{i}_start", cached["start_time"]),
f"period_{i}_start",
"invalid_time_format_period_start",
{"period": str(i)},
)
end = _parse_time_str(
call.data.get(f"period_{i}_end", cached["end_time"]),
f"period_{i}_end",
"invalid_time_format_period_end",
{"period": str(i)},
)
enabled: bool = call.data.get(f"period_{i}_enabled", cached["enabled"])
periods.append({"start_time": start, "end_time": end, "enabled": enabled})
@@ -238,11 +244,13 @@ def async_setup_services(hass: HomeAssistant) -> None:
cached = current["periods"][i - 1]
start = _parse_time_str(
call.data.get(f"period_{i}_start", cached["start_time"]),
f"period_{i}_start",
"invalid_time_format_period_start",
{"period": str(i)},
)
end = _parse_time_str(
call.data.get(f"period_{i}_end", cached["end_time"]),
f"period_{i}_end",
"invalid_time_format_period_end",
{"period": str(i)},
)
enabled: bool = call.data.get(f"period_{i}_enabled", cached["enabled"])
periods.append({"start_time": start, "end_time": end, "enabled": enabled})

View File

@@ -579,7 +579,7 @@
"message": "Growatt API error: {error}"
},
"device_not_configured": {
"message": "{device_type} device {serial_number} is not configured for services."
"message": "{device_type} device {serial_number} is not configured for actions."
},
"device_not_found": {
"message": "Device {device_id} not found in the device registry."
@@ -591,22 +591,31 @@
"message": "{batt_mode} is not a valid battery mode. Allowed values: {allowed_modes}."
},
"invalid_charge_power": {
"message": "charge_power must be between 0 and 100, got {value}."
"message": "'Charge power' must be between 0 and 100, got {value}."
},
"invalid_charge_stop_soc": {
"message": "charge_stop_soc must be between 0 and 100, got {value}."
"message": "'Charge stop SOC' must be between 0 and 100, got {value}."
},
"invalid_discharge_power": {
"message": "discharge_power must be between 0 and 100, got {value}."
"message": "'Discharge power' must be between 0 and 100, got {value}."
},
"invalid_discharge_stop_soc": {
"message": "discharge_stop_soc must be between 0 and 100, got {value}."
"message": "'Discharge stop SOC' must be between 0 and 100, got {value}."
},
"invalid_segment_id": {
"message": "segment_id must be between 1 and 9, got {segment_id}."
"message": "'Segment ID' must be between 1 and 9, got {segment_id}."
},
"invalid_time_format": {
"message": "{field_name} must be in HH:MM or HH:MM:SS format."
"invalid_time_format_end_time": {
"message": "'End time' must be in HH:MM or HH:MM:SS format."
},
"invalid_time_format_period_end": {
"message": "'Period {period} end' must be in HH:MM or HH:MM:SS format."
},
"invalid_time_format_period_start": {
"message": "'Period {period} start' must be in HH:MM or HH:MM:SS format."
},
"invalid_time_format_start_time": {
"message": "'Start time' must be in HH:MM or HH:MM:SS format."
},
"no_devices_configured": {
"message": "No {device_type} devices with token authentication are configured. Actions require {device_type} devices with V1 API access."
@@ -636,27 +645,27 @@
},
"services": {
"read_ac_charge_times": {
"description": "Read AC charge time periods from an SPH device.",
"description": "Reads AC charge time periods from an SPH device.",
"fields": {
"device_id": {
"description": "The Growatt SPH device to read from.",
"name": "Device"
"description": "[%key:component::growatt_server::services::read_time_segments::fields::device_id::description%]",
"name": "[%key:component::growatt_server::services::read_time_segments::fields::device_id::name%]"
}
},
"name": "Read AC charge times"
},
"read_ac_discharge_times": {
"description": "Read AC discharge time periods from an SPH device.",
"description": "Reads AC discharge time periods from an SPH device.",
"fields": {
"device_id": {
"description": "[%key:component::growatt_server::services::read_ac_charge_times::fields::device_id::description%]",
"name": "[%key:component::growatt_server::services::read_ac_charge_times::fields::device_id::name%]"
"description": "[%key:component::growatt_server::services::read_time_segments::fields::device_id::description%]",
"name": "[%key:component::growatt_server::services::read_time_segments::fields::device_id::name%]"
}
},
"name": "Read AC discharge times"
},
"read_time_segments": {
"description": "Read all time segments from a supported inverter.",
"description": "Reads all time segments from a supported inverter.",
"fields": {
"device_id": {
"description": "The Growatt device to perform the action on.",
@@ -666,7 +675,7 @@
"name": "Read time segments"
},
"update_time_segment": {
"description": "Update a time segment for supported inverters.",
"description": "Updates a time segment for supported inverters.",
"fields": {
"batt_mode": {
"description": "Battery operation mode for this time segment.",
@@ -696,7 +705,7 @@
"name": "Update time segment"
},
"write_ac_charge_times": {
"description": "Write AC charge time periods to an SPH device.",
"description": "Writes AC charge time periods to an SPH device.",
"fields": {
"charge_power": {
"description": "Charge power limit (%).",
@@ -707,8 +716,8 @@
"name": "Charge stop SOC"
},
"device_id": {
"description": "[%key:component::growatt_server::services::read_ac_charge_times::fields::device_id::description%]",
"name": "[%key:component::growatt_server::services::read_ac_charge_times::fields::device_id::name%]"
"description": "[%key:component::growatt_server::services::read_time_segments::fields::device_id::description%]",
"name": "[%key:component::growatt_server::services::read_time_segments::fields::device_id::name%]"
},
"mains_enabled": {
"description": "Enable AC (mains) charging.",
@@ -754,11 +763,11 @@
"name": "Write AC charge times"
},
"write_ac_discharge_times": {
"description": "Write AC discharge time periods to an SPH device.",
"description": "Writes AC discharge time periods to an SPH device.",
"fields": {
"device_id": {
"description": "[%key:component::growatt_server::services::read_ac_charge_times::fields::device_id::description%]",
"name": "[%key:component::growatt_server::services::read_ac_charge_times::fields::device_id::name%]"
"description": "[%key:component::growatt_server::services::read_time_segments::fields::device_id::description%]",
"name": "[%key:component::growatt_server::services::read_time_segments::fields::device_id::name%]"
},
"discharge_power": {
"description": "Discharge power limit (%).",

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any
from homematicip.base.enums import SmokeDetectorAlarmType, WindowState
from homematicip.base.enums import LockState, SmokeDetectorAlarmType, WindowState
from homematicip.base.functionalChannels import MultiModeInputChannel
from homematicip.device import (
AccelerationSensor,
@@ -74,6 +74,30 @@ SAM_DEVICE_ATTRIBUTES = {
}
def _is_full_flush_lock_controller(device: object) -> bool:
"""Return whether the device is an HmIP-FLC."""
return getattr(device, "modelType", None) == "HmIP-FLC" and hasattr(
device, "functionalChannels"
)
def _get_channel_by_role(
device: object,
functional_channel_type: str,
channel_role: str,
) -> object | None:
"""Return the matching functional channel for the device."""
for channel in getattr(device, "functionalChannels", []):
channel_type = getattr(channel, "functionalChannelType", None)
channel_type_name = getattr(channel_type, "name", channel_type)
if channel_type_name != functional_channel_type:
continue
if getattr(channel, "channelRole", None) != channel_role:
continue
return channel
return None
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
@@ -122,6 +146,9 @@ async def async_setup_entry(
entities.append(
HomematicipPluggableMainsFailureSurveillanceSensor(hap, device)
)
if _is_full_flush_lock_controller(device):
entities.append(HomematicipFullFlushLockControllerLocked(hap, device))
entities.append(HomematicipFullFlushLockControllerGlassBreak(hap, device))
if isinstance(device, PresenceDetectorIndoor):
entities.append(HomematicipPresenceDetector(hap, device))
if isinstance(device, SmokeDetector):
@@ -298,6 +325,55 @@ class HomematicipMotionDetector(HomematicipGenericEntity, BinarySensorEntity):
return self._device.motionDetected
class HomematicipFullFlushLockControllerLocked(
HomematicipGenericEntity, BinarySensorEntity
):
"""Representation of the HomematicIP full flush lock controller lock state."""
_attr_device_class = BinarySensorDeviceClass.LOCK
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the full flush lock controller lock sensor."""
super().__init__(hap, device, post="Locked")
@property
def is_on(self) -> bool:
"""Return true if the controlled lock is locked."""
channel = _get_channel_by_role(
self._device,
"MULTI_MODE_LOCK_INPUT_CHANNEL",
"DOOR_LOCK_SENSOR",
)
if channel is None:
return False
lock_state = getattr(channel, "lockState", None)
return getattr(lock_state, "name", lock_state) == LockState.LOCKED.name
class HomematicipFullFlushLockControllerGlassBreak(
HomematicipGenericEntity, BinarySensorEntity
):
"""Representation of the HomematicIP full flush lock controller glass state."""
_attr_device_class = BinarySensorDeviceClass.PROBLEM
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the full flush lock controller glass break sensor."""
super().__init__(hap, device, post="Glass break")
@property
def is_on(self) -> bool:
"""Return true if glass break has been detected."""
channel = _get_channel_by_role(
self._device,
"MULTI_MODE_LOCK_INPUT_CHANNEL",
"DOOR_LOCK_SENSOR",
)
if channel is None:
return False
return bool(getattr(channel, "glassBroken", False))
class HomematicipPresenceDetector(HomematicipGenericEntity, BinarySensorEntity):
"""Representation of the HomematicIP presence detector."""

View File

@@ -12,6 +12,13 @@ from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
def _is_full_flush_lock_controller(device: object) -> bool:
"""Return whether the device is an HmIP-FLC."""
return getattr(device, "modelType", None) == "HmIP-FLC" and hasattr(
device, "send_start_impulse_async"
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
@@ -20,11 +27,17 @@ async def async_setup_entry(
"""Set up the HomematicIP button from a config entry."""
hap = config_entry.runtime_data
async_add_entities(
entities: list[ButtonEntity] = [
HomematicipGarageDoorControllerButton(hap, device)
for device in hap.home.devices
if isinstance(device, WallMountedGarageDoorController)
]
entities.extend(
HomematicipFullFlushLockControllerButton(hap, device)
for device in hap.home.devices
if _is_full_flush_lock_controller(device)
)
async_add_entities(entities)
class HomematicipGarageDoorControllerButton(HomematicipGenericEntity, ButtonEntity):
@@ -38,3 +51,16 @@ class HomematicipGarageDoorControllerButton(HomematicipGenericEntity, ButtonEnti
async def async_press(self) -> None:
"""Handle the button press."""
await self._device.send_start_impulse_async()
class HomematicipFullFlushLockControllerButton(HomematicipGenericEntity, ButtonEntity):
"""Representation of the HomematicIP full flush lock controller opener."""
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the full flush lock controller opener button."""
super().__init__(hap, device, post="Door opener")
self._attr_icon = "mdi:door-open"
async def async_press(self) -> None:
"""Handle the button press."""
await self._device.send_start_impulse_async()

View File

@@ -2,7 +2,7 @@
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
Condition,
make_entity_numerical_condition,
@@ -21,7 +21,7 @@ CONDITIONS: dict[str, type[Condition]] = {
{DOMAIN: DomainSpec(value_source=ATTR_ACTION)}, HumidifierAction.HUMIDIFYING
),
"is_target_humidity": make_entity_numerical_condition(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit=PERCENTAGE,
),
}

View File

@@ -13,27 +13,19 @@
- all
- any
.number_or_entity: &number_or_entity
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "%"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "%"
- domain: sensor
device_class: humidity
- domain: number
device_class: humidity
translation_key: number_or_entity
.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: "%"
is_off: *condition_common
is_on: *condition_common
@@ -44,5 +36,10 @@ is_target_humidity:
target: *condition_humidifier_target
fields:
behavior: *condition_behavior
above: *number_or_entity
below: *number_or_entity
threshold:
required: true
selector:
numeric_threshold:
entity: *humidity_threshold_entity
mode: is
number: *humidity_threshold_number

View File

@@ -67,6 +67,9 @@
}
},
"triggers": {
"mode_changed": {
"trigger": "mdi:air-humidifier"
},
"started_drying": {
"trigger": "mdi:arrow-down-bold"
},

View File

@@ -2,6 +2,8 @@
"common": {
"condition_behavior_description": "How the state should match on the targeted humidifiers.",
"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 humidifiers to trigger on.",
"trigger_behavior_name": "Behavior"
},
@@ -49,17 +51,13 @@
"is_target_humidity": {
"description": "Tests the target humidity of one or more humidifiers.",
"fields": {
"above": {
"description": "Require the target humidity to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::humidifier::common::condition_behavior_description%]",
"name": "[%key:component::humidifier::common::condition_behavior_name%]"
},
"below": {
"description": "Require the target humidity to be below this value.",
"name": "Below"
"threshold": {
"description": "[%key:component::humidifier::common::condition_threshold_description%]",
"name": "[%key:component::humidifier::common::condition_threshold_name%]"
}
},
"name": "Humidifier target humidity"
@@ -159,12 +157,6 @@
"any": "Any"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
@@ -175,40 +167,54 @@
},
"services": {
"set_humidity": {
"description": "Sets the target humidity.",
"description": "Sets the target humidity of a humidifier.",
"fields": {
"humidity": {
"description": "Target humidity.",
"name": "Humidity"
}
},
"name": "Set humidity"
"name": "Set humidifier target humidity"
},
"set_mode": {
"description": "Sets the humidifier operation mode.",
"description": "Sets the mode of a humidifier.",
"fields": {
"mode": {
"description": "Operation mode. For example, \"normal\", \"eco\", or \"away\". For a list of possible values, refer to the integration documentation.",
"name": "Mode"
}
},
"name": "Set mode"
"name": "Set humidifier mode"
},
"toggle": {
"description": "Toggles the humidifier on/off.",
"name": "[%key:common::action::toggle%]"
"description": "Toggles a humidifier on/off.",
"name": "Toggle humidifier"
},
"turn_off": {
"description": "Turns the humidifier off.",
"name": "[%key:common::action::turn_off%]"
"description": "Turns off a humidifier.",
"name": "Turn off humidifier"
},
"turn_on": {
"description": "Turns the humidifier on.",
"name": "[%key:common::action::turn_on%]"
"description": "Turns on a humidifier.",
"name": "Turn on humidifier"
}
},
"title": "Humidifier",
"triggers": {
"mode_changed": {
"description": "Triggers after the operation mode of one or more humidifiers changes.",
"fields": {
"behavior": {
"description": "[%key:component::humidifier::common::trigger_behavior_description%]",
"name": "[%key:component::humidifier::common::trigger_behavior_name%]"
},
"mode": {
"description": "The operation modes to trigger on.",
"name": "Mode"
}
},
"name": "Humidifier mode changed"
},
"started_drying": {
"description": "Triggers after one or more humidifiers start drying.",
"fields": {

View File

@@ -1,13 +1,65 @@
"""Provides triggers for humidifiers."""
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_target_state_trigger
import voluptuous as vol
from homeassistant.const import ATTR_MODE, CONF_OPTIONS, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
EntityTargetStateTriggerBase,
Trigger,
TriggerConfig,
make_entity_target_state_trigger,
)
from .const import ATTR_ACTION, DOMAIN, HumidifierAction, HumidifierEntityFeature
CONF_MODE = "mode"
MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
{
vol.Required(CONF_OPTIONS): {
vol.Required(CONF_MODE): vol.All(cv.ensure_list, vol.Length(min=1), [str]),
},
}
)
def _supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
"""Test if an entity supports the specified features."""
try:
return bool(get_supported_features(hass, entity_id) & features)
except HomeAssistantError:
return False
class ModeChangedTrigger(EntityTargetStateTriggerBase):
"""Trigger for humidifier mode changes."""
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_MODE)}
_schema = MODE_CHANGED_TRIGGER_SCHEMA
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize the mode trigger."""
super().__init__(hass, config)
self._to_states = set(self._options[CONF_MODE])
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if _supports_feature(self._hass, entity_id, HumidifierEntityFeature.MODES)
}
from .const import ATTR_ACTION, DOMAIN, HumidifierAction
TRIGGERS: dict[str, type[Trigger]] = {
"mode_changed": ModeChangedTrigger,
"started_drying": make_entity_target_state_trigger(
{DOMAIN: DomainSpec(value_source=ATTR_ACTION)}, HumidifierAction.DRYING
),

View File

@@ -1,9 +1,9 @@
.trigger_common: &trigger_common
target:
target: &trigger_humidifier_target
entity:
domain: humidifier
fields:
behavior:
behavior: &trigger_behavior
required: true
default: any
selector:
@@ -18,3 +18,16 @@ started_drying: *trigger_common
started_humidifying: *trigger_common
turned_on: *trigger_common
turned_off: *trigger_common
mode_changed:
target: *trigger_humidifier_target
fields:
behavior: *trigger_behavior
mode:
context:
filter_target: target
required: true
selector:
state:
attribute: available_modes
multiple: true

View File

@@ -14,14 +14,14 @@ from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDevic
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import Condition, make_entity_numerical_condition
HUMIDITY_DOMAIN_SPECS = {
CLIMATE_DOMAIN: NumericalDomainSpec(
CLIMATE_DOMAIN: DomainSpec(
value_source=CLIMATE_ATTR_CURRENT_HUMIDITY,
),
HUMIDIFIER_DOMAIN: NumericalDomainSpec(
HUMIDIFIER_DOMAIN: DomainSpec(
value_source=HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
),
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.HUMIDITY),

View File

@@ -1,24 +1,16 @@
.number_or_entity: &number_or_entity
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "%"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "%"
- domain: number
device_class: humidity
- domain: sensor
device_class: humidity
translation_key: number_or_entity
.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: "%"
is_value:
target:
@@ -39,5 +31,10 @@ is_value:
options:
- all
- any
above: *number_or_entity
below: *number_or_entity
threshold:
required: true
selector:
numeric_threshold:
entity: *humidity_threshold_entity
mode: is
number: *humidity_threshold_number

View File

@@ -2,6 +2,8 @@
"common": {
"condition_behavior_description": "How the state 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.",
@@ -12,17 +14,13 @@
"is_value": {
"description": "Tests if a relative humidity value is above a threshold, below a threshold, or in a range of values.",
"fields": {
"above": {
"description": "Require the relative humidity to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::humidity::common::condition_behavior_description%]",
"name": "[%key:component::humidity::common::condition_behavior_name%]"
},
"below": {
"description": "Require the relative humidity to be below this value.",
"name": "Below"
"threshold": {
"description": "[%key:component::humidity::common::condition_threshold_description%]",
"name": "[%key:component::humidity::common::condition_threshold_name%]"
}
},
"name": "Relative humidity"
@@ -35,12 +33,6 @@
"any": "Any"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -16,24 +16,24 @@ from homeassistant.components.weather import (
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import NumericalDomainSpec
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,
)
HUMIDITY_DOMAIN_SPECS: dict[str, NumericalDomainSpec] = {
CLIMATE_DOMAIN: NumericalDomainSpec(
HUMIDITY_DOMAIN_SPECS: dict[str, DomainSpec] = {
CLIMATE_DOMAIN: DomainSpec(
value_source=CLIMATE_ATTR_CURRENT_HUMIDITY,
),
HUMIDIFIER_DOMAIN: NumericalDomainSpec(
HUMIDIFIER_DOMAIN: DomainSpec(
value_source=HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
),
SENSOR_DOMAIN: NumericalDomainSpec(
SENSOR_DOMAIN: DomainSpec(
device_class=SensorDeviceClass.HUMIDITY,
),
WEATHER_DOMAIN: NumericalDomainSpec(
WEATHER_DOMAIN: DomainSpec(
value_source=ATTR_WEATHER_HUMIDITY,
),
}

View File

@@ -14,27 +14,6 @@
- all
- any
.number_or_entity: &number_or_entity
required: false
selector:
choose:
choices:
number:
selector:
number:
unit_of_measurement: "lx"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "lx"
- domain: number
device_class: illuminance
- domain: sensor
device_class: illuminance
translation_key: number_or_entity
is_detected: *detected_condition_common
is_not_detected: *detected_condition_common
@@ -48,5 +27,19 @@ is_value:
device_class: illuminance
fields:
behavior: *condition_behavior
above: *number_or_entity
below: *number_or_entity
threshold:
required: true
selector:
numeric_threshold:
entity:
- domain: input_number
unit_of_measurement: "lx"
- domain: sensor
device_class: illuminance
- domain: number
device_class: illuminance
mode: is
number:
min: 0
mode: box
unit_of_measurement: "lx"

View File

@@ -2,6 +2,8 @@
"common": {
"condition_behavior_description": "How the state 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.",
@@ -32,17 +34,13 @@
"is_value": {
"description": "Tests the illuminance value.",
"fields": {
"above": {
"description": "Require the illuminance to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::illuminance::common::condition_behavior_description%]",
"name": "[%key:component::illuminance::common::condition_behavior_name%]"
},
"below": {
"description": "Require the illuminance to be below this value.",
"name": "Below"
"threshold": {
"description": "[%key:component::illuminance::common::condition_threshold_description%]",
"name": "[%key:component::illuminance::common::condition_threshold_name%]"
}
},
"name": "Illuminance"
@@ -55,12 +53,6 @@
"any": "Any"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -10,7 +10,7 @@ from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDevic
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import LIGHT_LUX, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
Trigger,
make_entity_numerical_state_changed_trigger,
@@ -18,9 +18,9 @@ from homeassistant.helpers.trigger import (
make_entity_target_state_trigger,
)
ILLUMINANCE_DOMAIN_SPECS: dict[str, NumericalDomainSpec] = {
NUMBER_DOMAIN: NumericalDomainSpec(device_class=NumberDeviceClass.ILLUMINANCE),
SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.ILLUMINANCE),
ILLUMINANCE_DOMAIN_SPECS: dict[str, DomainSpec] = {
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.ILLUMINANCE),
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.ILLUMINANCE),
}
TRIGGERS: dict[str, type[Trigger]] = {

View File

@@ -13,7 +13,7 @@
"name": "Filename"
}
},
"name": "Take snapshot"
"name": "Take image snapshot"
}
},
"title": "Image"

View File

@@ -1,40 +1,54 @@
"""Provides triggers for lights."""
from typing import Any
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import NumericalDomainSpec
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
EntityNumericalStateChangedTriggerBase,
EntityNumericalStateCrossedThresholdTriggerBase,
EntityNumericalStateTriggerBase,
Trigger,
make_entity_numerical_state_changed_trigger,
make_entity_numerical_state_crossed_threshold_trigger,
make_entity_target_state_trigger,
)
from . import ATTR_BRIGHTNESS
from .const import DOMAIN
def _convert_uint8_to_percentage(value: Any) -> float:
"""Convert a uint8 value (0-255) to a percentage (0-100)."""
return (float(value) / 255.0) * 100.0
BRIGHTNESS_DOMAIN_SPECS = {
DOMAIN: NumericalDomainSpec(
value_source=ATTR_BRIGHTNESS,
value_converter=_convert_uint8_to_percentage,
),
DOMAIN: DomainSpec(value_source=ATTR_BRIGHTNESS),
}
class BrightnessTriggerMixin(EntityNumericalStateTriggerBase):
"""Mixin for brightness triggers."""
_domain_specs = BRIGHTNESS_DOMAIN_SPECS
_valid_unit = "%"
def _get_tracked_value(self, state: State) -> float | None:
"""Get tracked brightness as a percentage."""
value = super()._get_tracked_value(state)
if value is None:
return None
# Convert uint8 value (0-255) to a percentage (0-100)
return (value / 255.0) * 100.0
class BrightnessChangedTrigger(
EntityNumericalStateChangedTriggerBase, BrightnessTriggerMixin
):
"""Trigger for light brightness changes."""
class BrightnessCrossedThresholdTrigger(
EntityNumericalStateCrossedThresholdTriggerBase, BrightnessTriggerMixin
):
"""Trigger for light brightness crossing a threshold."""
TRIGGERS: dict[str, type[Trigger]] = {
"brightness_changed": make_entity_numerical_state_changed_trigger(
BRIGHTNESS_DOMAIN_SPECS, valid_unit="%"
),
"brightness_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
BRIGHTNESS_DOMAIN_SPECS, valid_unit="%"
),
"brightness_changed": BrightnessChangedTrigger,
"brightness_crossed_threshold": BrightnessCrossedThresholdTrigger,
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
}

View File

@@ -20,11 +20,11 @@
"name": "Level"
}
},
"name": "Set default level"
"name": "Set logger default level"
},
"set_level": {
"description": "Sets the log level for one or more integrations.",
"name": "Set level"
"name": "Set logger level"
}
}
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["lojack_api"],
"quality_scale": "silver",
"requirements": ["lojack-api==0.7.1"]
"requirements": ["lojack-api==0.7.2"]
}

View File

@@ -13,7 +13,7 @@
"services": {
"reload_resources": {
"description": "Reloads dashboard resources from the YAML-configuration.",
"name": "Reload resources"
"name": "Reload dashboard resources"
}
},
"system_health": {

View File

@@ -6,7 +6,6 @@ from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
@@ -25,7 +24,6 @@ _MOISTURE_BINARY_DOMAIN_SPECS = {
_MOISTURE_NUMERICAL_DOMAIN_SPECS = {
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.MOISTURE),
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.MOISTURE),
}
CONDITIONS: dict[str, type[Condition]] = {

View File

@@ -14,26 +14,17 @@
- all
- any
.number_or_entity: &number_or_entity
required: false
selector:
choose:
choices:
number:
selector:
number:
unit_of_measurement: "%"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "%"
- domain: number
device_class: moisture
- domain: sensor
device_class: moisture
translation_key: number_or_entity
.moisture_threshold_entity: &moisture_threshold_entity
- domain: input_number
unit_of_measurement: "%"
- domain: sensor
device_class: moisture
.moisture_threshold_number: &moisture_threshold_number
min: 0
max: 100
mode: box
unit_of_measurement: "%"
is_detected: *detected_condition_common
@@ -44,9 +35,12 @@ is_value:
entity:
- domain: sensor
device_class: moisture
- domain: number
device_class: moisture
fields:
behavior: *condition_behavior
above: *number_or_entity
below: *number_or_entity
threshold:
required: true
selector:
numeric_threshold:
entity: *moisture_threshold_entity
mode: is
number: *moisture_threshold_number

View File

@@ -2,6 +2,8 @@
"common": {
"condition_behavior_description": "How the state 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.",
@@ -32,17 +34,13 @@
"is_value": {
"description": "Tests the moisture level of one or more entities.",
"fields": {
"above": {
"description": "Require the moisture level to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::moisture::common::condition_behavior_description%]",
"name": "[%key:component::moisture::common::condition_behavior_name%]"
},
"below": {
"description": "Require the moisture level to be below this value.",
"name": "Below"
"threshold": {
"description": "[%key:component::moisture::common::condition_threshold_description%]",
"name": "[%key:component::moisture::common::condition_threshold_name%]"
}
},
"name": "Moisture level"
@@ -55,12 +53,6 @@
"any": "Any"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -6,11 +6,10 @@ from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
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, NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
Trigger,
make_entity_numerical_state_changed_trigger,
@@ -22,9 +21,8 @@ MOISTURE_BINARY_DOMAIN_SPECS: dict[str, DomainSpec] = {
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.MOISTURE),
}
MOISTURE_NUMERICAL_DOMAIN_SPECS: dict[str, NumericalDomainSpec] = {
NUMBER_DOMAIN: NumericalDomainSpec(device_class=NumberDeviceClass.MOISTURE),
SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.MOISTURE),
MOISTURE_NUMERICAL_DOMAIN_SPECS: dict[str, DomainSpec] = {
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.MOISTURE),
}

View File

@@ -13,8 +13,6 @@
.moisture_threshold_entity: &moisture_threshold_entity
- domain: input_number
unit_of_measurement: "%"
- domain: number
device_class: moisture
- domain: sensor
device_class: moisture
@@ -31,8 +29,6 @@
.trigger_numerical_target: &trigger_numerical_target
entity:
- domain: number
device_class: moisture
- domain: sensor
device_class: moisture

View File

@@ -10,6 +10,6 @@
"iot_class": "local_push",
"loggers": ["music_assistant"],
"quality_scale": "bronze",
"requirements": ["music-assistant-client==1.3.3"],
"requirements": ["music-assistant-client==1.3.4"],
"zeroconf": ["_mass._tcp.local."]
}

View File

@@ -12,5 +12,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["python-pooldose==0.8.6"]
"requirements": ["python-pooldose==0.9.0"]
}

View File

@@ -6,7 +6,7 @@ from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDevic
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
Condition,
make_entity_numerical_condition_with_unit,
@@ -14,8 +14,8 @@ from homeassistant.helpers.condition import (
from homeassistant.util.unit_conversion import PowerConverter
POWER_DOMAIN_SPECS = {
NUMBER_DOMAIN: NumericalDomainSpec(device_class=NumberDeviceClass.POWER),
SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.POWER),
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.POWER),
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.POWER),
}

View File

@@ -1,43 +1,29 @@
.number_or_entity_power: &number_or_entity_power
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "mW"
- "W"
- "kW"
- "MW"
- "GW"
- "TW"
- "BTU/h"
- domain: sensor
device_class: power
- domain: number
device_class: power
translation_key: number_or_entity
.condition_unit_power: &condition_unit_power
required: false
.condition_behavior: &condition_behavior
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- "mW"
- "W"
- "kW"
- "MW"
- "GW"
- "TW"
- "BTU/h"
- all
- any
.power_units: &power_units
- "mW"
- "W"
- "kW"
- "MW"
- "GW"
- "TW"
- "BTU/h"
.power_threshold_entity: &power_threshold_entity
- domain: input_number
unit_of_measurement: *power_units
- domain: sensor
device_class: power
- domain: number
device_class: power
is_value:
target:
@@ -47,15 +33,13 @@ is_value:
- domain: sensor
device_class: power
fields:
behavior:
behavior: *condition_behavior
threshold:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
above: *number_or_entity_power
below: *number_or_entity_power
unit: *condition_unit_power
numeric_threshold:
entity: *power_threshold_entity
mode: is
number:
mode: box
unit_of_measurement: *power_units

View File

@@ -2,6 +2,8 @@
"common": {
"condition_behavior_description": "How the power 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.",
@@ -12,21 +14,13 @@
"is_value": {
"description": "Tests the power value of one or more entities.",
"fields": {
"above": {
"description": "Require the power to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::power::common::condition_behavior_description%]",
"name": "[%key:component::power::common::condition_behavior_name%]"
},
"below": {
"description": "Require the power to be below this value.",
"name": "Below"
},
"unit": {
"description": "All values will be converted to this unit when evaluating the condition.",
"name": "Unit of measurement"
"threshold": {
"description": "[%key:component::power::common::condition_threshold_description%]",
"name": "[%key:component::power::common::condition_threshold_name%]"
}
},
"name": "Power value"
@@ -39,12 +33,6 @@
"any": "Any"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -6,7 +6,7 @@ from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDevic
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
Trigger,
make_entity_numerical_state_changed_with_unit_trigger,
@@ -14,9 +14,9 @@ from homeassistant.helpers.trigger import (
)
from homeassistant.util.unit_conversion import PowerConverter
POWER_DOMAIN_SPECS: dict[str, NumericalDomainSpec] = {
NUMBER_DOMAIN: NumericalDomainSpec(device_class=NumberDeviceClass.POWER),
SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.POWER),
POWER_DOMAIN_SPECS: dict[str, DomainSpec] = {
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.POWER),
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.POWER),
}

View File

@@ -102,7 +102,7 @@ CV_WX_DATA_VALID_TEMP_RANGE = vol.All(vol.Coerce(float), vol.Range(min=-40.0, ma
CV_WX_DATA_VALID_RAIN_RANGE = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1000.0))
CV_WX_DATA_VALID_WIND_SPEED = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=65.0))
CV_WX_DATA_VALID_PRESSURE = vol.All(vol.Coerce(float), vol.Range(min=60.0, max=110.0))
CV_WX_DATA_VALID_SOLARRAD = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=5.0))
CV_WX_DATA_VALID_SOLARRAD = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=100.0))
SERVICE_NAME_PAUSE_WATERING = "pause_watering"
SERVICE_NAME_PUSH_FLOW_METER_DATA = "push_flow_meter_data"

View File

@@ -131,9 +131,9 @@ push_weather_data:
selector:
number:
min: 0
max: 5
max: 100
step: 0.1
unit_of_measurement: "MJ/m²/h"
unit_of_measurement: "MJ/m²/d"
et:
selector:
number:

View File

@@ -175,7 +175,7 @@
"name": "Measured rainfall"
},
"solarrad": {
"description": "Current solar radiation (MJ/m²/h).",
"description": "Daily solar radiation (MJ/m²/d).",
"name": "Solar radiation"
},
"temperature": {

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "silver",
"requirements": ["renault-api==0.5.6"]
"requirements": ["renault-api==0.5.7"]
}

View File

@@ -51,19 +51,19 @@
"services": {
"reload": {
"description": "Reloads all the available scripts.",
"name": "[%key:common::action::reload%]"
"name": "Reload scripts"
},
"toggle": {
"description": "Starts a script if it isn't running, stops it otherwise.",
"name": "[%key:common::action::toggle%]"
"name": "Toggle script"
},
"turn_off": {
"description": "Stops a running script.",
"name": "[%key:common::action::turn_off%]"
"name": "Turn off script"
},
"turn_on": {
"description": "Runs the sequence of actions defined in a script.",
"name": "[%key:common::action::turn_on%]"
"name": "Turn on script"
}
},
"title": "Script"

View File

@@ -21,5 +21,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["sense_energy"],
"requirements": ["sense-energy==0.13.8"]
"requirements": ["sense-energy==0.14.0"]
}

View File

@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["asyncsleepiq"],
"requirements": ["asyncsleepiq==1.7.0"]
"requirements": ["asyncsleepiq==1.7.1"]
}

View File

@@ -18,7 +18,7 @@ from homeassistant.components.weather import (
)
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
Condition,
EntityNumericalConditionWithUnitBase,
@@ -26,16 +26,16 @@ from homeassistant.helpers.condition import (
from homeassistant.util.unit_conversion import TemperatureConverter
TEMPERATURE_DOMAIN_SPECS = {
CLIMATE_DOMAIN: NumericalDomainSpec(
CLIMATE_DOMAIN: DomainSpec(
value_source=CLIMATE_ATTR_CURRENT_TEMPERATURE,
),
SENSOR_DOMAIN: NumericalDomainSpec(
SENSOR_DOMAIN: DomainSpec(
device_class=SensorDeviceClass.TEMPERATURE,
),
WATER_HEATER_DOMAIN: NumericalDomainSpec(
WATER_HEATER_DOMAIN: DomainSpec(
value_source=WATER_HEATER_ATTR_CURRENT_TEMPERATURE,
),
WEATHER_DOMAIN: NumericalDomainSpec(
WEATHER_DOMAIN: DomainSpec(
value_source=ATTR_WEATHER_TEMPERATURE,
),
}

View File

@@ -1,33 +1,14 @@
.number_or_entity_temperature: &number_or_entity_temperature
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "°C"
- "°F"
- domain: sensor
device_class: temperature
- domain: number
device_class: temperature
translation_key: number_or_entity
.temperature_units: &temperature_units
- "°C"
- "°F"
.condition_unit_temperature: &condition_unit_temperature
required: false
selector:
select:
options:
- "°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_value:
target:
@@ -47,6 +28,12 @@ is_value:
options:
- all
- any
above: *number_or_entity_temperature
below: *number_or_entity_temperature
unit: *condition_unit_temperature
threshold:
required: true
selector:
numeric_threshold:
entity: *temperature_threshold_entity
mode: is
number:
mode: box
unit_of_measurement: *temperature_units

View File

@@ -2,6 +2,8 @@
"common": {
"condition_behavior_description": "How the temperature 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.",
@@ -12,21 +14,13 @@
"is_value": {
"description": "Tests the temperature of one or more entities.",
"fields": {
"above": {
"description": "Require the temperature to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::temperature::common::condition_behavior_description%]",
"name": "[%key:component::temperature::common::condition_behavior_name%]"
},
"below": {
"description": "Require the temperature to be below this value.",
"name": "Below"
},
"unit": {
"description": "All values will be converted to this unit when evaluating the condition.",
"name": "Unit of measurement"
"threshold": {
"description": "[%key:component::temperature::common::condition_threshold_description%]",
"name": "[%key:component::temperature::common::condition_threshold_name%]"
}
},
"name": "Temperature value"
@@ -39,12 +33,6 @@
"any": "Any"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -18,7 +18,7 @@ from homeassistant.components.weather import (
)
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
EntityNumericalStateChangedTriggerWithUnitBase,
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
@@ -28,16 +28,14 @@ from homeassistant.helpers.trigger import (
from homeassistant.util.unit_conversion import TemperatureConverter
TEMPERATURE_DOMAIN_SPECS = {
CLIMATE_DOMAIN: NumericalDomainSpec(
CLIMATE_DOMAIN: DomainSpec(
value_source=CLIMATE_ATTR_CURRENT_TEMPERATURE,
),
SENSOR_DOMAIN: NumericalDomainSpec(
SENSOR_DOMAIN: DomainSpec(
device_class=SensorDeviceClass.TEMPERATURE,
),
WATER_HEATER_DOMAIN: NumericalDomainSpec(
value_source=WATER_HEATER_ATTR_CURRENT_TEMPERATURE
),
WEATHER_DOMAIN: NumericalDomainSpec(
WATER_HEATER_DOMAIN: DomainSpec(value_source=WATER_HEATER_ATTR_CURRENT_TEMPERATURE),
WEATHER_DOMAIN: DomainSpec(
value_source=ATTR_WEATHER_TEMPERATURE,
),
}

View File

@@ -5,14 +5,12 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.input_text import DOMAIN as INPUT_TEXT_DOMAIN
from homeassistant.const import CONF_OPTIONS, CONF_TARGET
from homeassistant.const import CONF_OPTIONS
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
ATTR_BEHAVIOR,
BEHAVIOR_ALL,
BEHAVIOR_ANY,
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
Condition,
ConditionConfig,
EntityConditionBase,
@@ -22,13 +20,9 @@ from .const import DOMAIN
CONF_VALUE = "value"
_TEXT_CONDITION_SCHEMA = vol.Schema(
_TEXT_CONDITION_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
{
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
vol.Required(CONF_OPTIONS): {
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
[BEHAVIOR_ANY, BEHAVIOR_ALL]
),
vol.Required(CONF_VALUE): cv.string,
},
}

View File

@@ -25,11 +25,11 @@
"services": {
"close_valve": {
"description": "Closes a valve.",
"name": "[%key:common::action::close%]"
"name": "Close valve"
},
"open_valve": {
"description": "Opens a valve.",
"name": "[%key:common::action::open%]"
"name": "Open valve"
},
"set_valve_position": {
"description": "Moves a valve to a specific position.",
@@ -39,15 +39,15 @@
"name": "Position"
}
},
"name": "Set position"
"name": "Set valve position"
},
"stop_valve": {
"description": "Stops the valve movement.",
"name": "[%key:common::action::stop%]"
"description": "Stops a valve.",
"name": "Stop valve"
},
"toggle": {
"description": "Toggles a valve open/closed.",
"name": "[%key:common::action::toggle%]"
"name": "Toggle valve"
}
},
"title": "Valve"

View File

@@ -9,17 +9,14 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_OPTIONS,
CONF_TARGET,
STATE_OFF,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
ATTR_BEHAVIOR,
BEHAVIOR_ALL,
BEHAVIOR_ANY,
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
Condition,
ConditionConfig,
EntityConditionBase,
@@ -33,13 +30,9 @@ from .const import DOMAIN
ATTR_OPERATION_MODE = "operation_mode"
_OPERATION_MODE_CONDITION_SCHEMA = vol.Schema(
_OPERATION_MODE_CONDITION_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
{
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
vol.Required(CONF_OPTIONS): {
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
[BEHAVIOR_ANY, BEHAVIOR_ALL]
),
vol.Required(ATTR_OPERATION_MODE): vol.All(
cv.ensure_list, vol.Length(min=1), [str]
),
@@ -80,7 +73,7 @@ class WaterHeaterTargetTemperatureCondition(EntityNumericalConditionWithUnitBase
"""Condition for water heater target temperature."""
_base_unit = UnitOfTemperature.CELSIUS
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
_unit_converter = TemperatureConverter
def _get_entity_unit(self, entity_state: State) -> str | None:

View File

@@ -13,36 +13,17 @@
- all
- any
.number_or_entity_temperature: &number_or_entity_temperature
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "°C"
- "°F"
- domain: sensor
device_class: temperature
- domain: number
device_class: temperature
translation_key: number_or_entity
.temperature_units: &temperature_units
- "°C"
- "°F"
.condition_unit_temperature: &condition_unit_temperature
required: false
selector:
select:
options:
- "°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_on: *condition_common
@@ -67,6 +48,12 @@ is_target_temperature:
target: *condition_water_heater_target
fields:
behavior: *condition_behavior
above: *number_or_entity_temperature
below: *number_or_entity_temperature
unit: *condition_unit_temperature
threshold:
required: true
selector:
numeric_threshold:
entity: *temperature_threshold_entity
mode: is
number:
mode: box
unit_of_measurement: *temperature_units

View File

@@ -53,6 +53,9 @@
}
},
"triggers": {
"operation_mode_changed": {
"trigger": "mdi:water-boiler"
},
"target_temperature_changed": {
"trigger": "mdi:thermometer"
},

View File

@@ -2,6 +2,8 @@
"common": {
"condition_behavior_description": "How the state should match on the targeted water heaters.",
"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 water heaters to trigger on.",
"trigger_behavior_name": "Behavior",
"trigger_threshold_changed_description": "Which changes to trigger on and threshold values.",
@@ -46,21 +48,13 @@
"is_target_temperature": {
"description": "Tests the temperature setpoint of one or more water heaters.",
"fields": {
"above": {
"description": "Require the target temperature to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::water_heater::common::condition_behavior_description%]",
"name": "[%key:component::water_heater::common::condition_behavior_name%]"
},
"below": {
"description": "Require the target temperature to be below this value.",
"name": "Below"
},
"unit": {
"description": "All values will be converted to this unit when evaluating the condition.",
"name": "Unit of measurement"
"threshold": {
"description": "[%key:component::water_heater::common::condition_threshold_description%]",
"name": "[%key:component::water_heater::common::condition_threshold_name%]"
}
},
"name": "Water heater target temperature"
@@ -140,12 +134,6 @@
"any": "Any"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
@@ -156,27 +144,27 @@
},
"services": {
"set_away_mode": {
"description": "Turns away mode on/off.",
"description": "Sets the away mode of a water heater.",
"fields": {
"away_mode": {
"description": "New value of away mode.",
"name": "Away mode"
}
},
"name": "Set away mode"
"name": "Set water heater away mode"
},
"set_operation_mode": {
"description": "Sets the operation mode.",
"description": "Sets the operation mode of a water heater.",
"fields": {
"operation_mode": {
"description": "[%key:component::water_heater::services::set_temperature::fields::operation_mode::description%]",
"name": "[%key:component::water_heater::services::set_temperature::fields::operation_mode::name%]"
}
},
"name": "Set operation mode"
"name": "Set water heater operation mode"
},
"set_temperature": {
"description": "Sets the target temperature.",
"description": "Sets the target temperature of a water heater.",
"fields": {
"operation_mode": {
"description": "New value of the operation mode. For a list of possible modes, refer to the integration documentation.",
@@ -187,19 +175,33 @@
"name": "Temperature"
}
},
"name": "Set temperature"
"name": "Set water heater target temperature"
},
"turn_off": {
"description": "Turns water heater off.",
"name": "[%key:common::action::turn_off%]"
"description": "Turns off a water heater.",
"name": "Turn off water heater"
},
"turn_on": {
"description": "Turns water heater on.",
"name": "[%key:common::action::turn_on%]"
"description": "Turns on a water heater.",
"name": "Turn on water heater"
}
},
"title": "Water heater",
"triggers": {
"operation_mode_changed": {
"description": "Triggers after the operation mode of one or more water heaters changes to a specific mode.",
"fields": {
"behavior": {
"description": "[%key:component::water_heater::common::trigger_behavior_description%]",
"name": "[%key:component::water_heater::common::trigger_behavior_name%]"
},
"operation_mode": {
"description": "The operation modes to trigger on.",
"name": "Operation mode"
}
},
"name": "Water heater operation mode changed"
},
"target_temperature_changed": {
"description": "Triggers after the temperature setpoint of one or more water heaters changes.",
"fields": {

View File

@@ -1,13 +1,24 @@
"""Provides triggers for water heaters."""
from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, UnitOfTemperature
import voluptuous as vol
from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_OPTIONS,
STATE_OFF,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import NumericalDomainSpec
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
EntityNumericalStateChangedTriggerWithUnitBase,
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
EntityNumericalStateTriggerWithUnitBase,
EntityTargetStateTriggerBase,
Trigger,
TriggerConfig,
make_entity_origin_state_trigger,
make_entity_target_state_trigger,
)
@@ -15,6 +26,30 @@ from homeassistant.util.unit_conversion import TemperatureConverter
from .const import DOMAIN
CONF_OPERATION_MODE = "operation_mode"
_OPERATION_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
{
vol.Required(CONF_OPTIONS): {
vol.Required(CONF_OPERATION_MODE): vol.All(
cv.ensure_list, vol.Length(min=1), [str]
),
},
}
)
class WaterHeaterOperationModeChangedTrigger(EntityTargetStateTriggerBase):
"""Trigger for water heater operation mode changes."""
_domain_specs = {DOMAIN: DomainSpec()}
_schema = _OPERATION_MODE_CHANGED_TRIGGER_SCHEMA
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize the operation mode changed trigger."""
super().__init__(hass, config)
self._to_states = set(self._options[CONF_OPERATION_MODE])
class _WaterHeaterTargetTemperatureTriggerMixin(
EntityNumericalStateTriggerWithUnitBase
@@ -22,7 +57,7 @@ class _WaterHeaterTargetTemperatureTriggerMixin(
"""Mixin for water heater target temperature triggers with unit conversion."""
_base_unit = UnitOfTemperature.CELSIUS
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
_unit_converter = TemperatureConverter
def _get_entity_unit(self, state: State) -> str | None:
@@ -46,6 +81,7 @@ class WaterHeaterTargetTemperatureCrossedThresholdTrigger(
TRIGGERS: dict[str, type[Trigger]] = {
"operation_mode_changed": WaterHeaterOperationModeChangedTrigger,
"target_temperature_changed": WaterHeaterTargetTemperatureChangedTrigger,
"target_temperature_crossed_threshold": WaterHeaterTargetTemperatureCrossedThresholdTrigger,
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),

View File

@@ -26,6 +26,22 @@
- domain: number
device_class: temperature
operation_mode_changed:
target: *trigger_water_heater_target
fields:
behavior: *trigger_behavior
operation_mode:
context:
filter_target: target
required: true
selector:
state:
attribute: operation_mode
hide_states:
- unavailable
- unknown
multiple: true
turned_off: *trigger_common
turned_on: *trigger_common

View File

@@ -101,24 +101,24 @@
},
"services": {
"get_forecast": {
"description": "Retrieves the forecast from a selected weather service.",
"description": "Retrieves the forecast from a weather service.",
"fields": {
"type": {
"description": "[%key:component::weather::services::get_forecasts::fields::type::description%]",
"name": "[%key:component::weather::services::get_forecasts::fields::type::name%]"
}
},
"name": "Get forecast"
"name": "Get weather forecast"
},
"get_forecasts": {
"description": "Retrieves the forecast from selected weather services.",
"description": "Retrieves the forecasts from one or more weather services.",
"fields": {
"type": {
"description": "The scope of the weather forecast.",
"name": "Forecast type"
}
},
"name": "Get forecasts"
"name": "Get weather forecasts"
}
},
"title": "Weather"

View File

@@ -18,27 +18,17 @@ from zwave_js_server.const.command_class.notification import (
)
from zwave_js_server.model.driver import Driver
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.components.script import scripts_with_entity
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.start import async_at_started
from .const import DOMAIN
from .entity import NewZwaveDiscoveryInfo, ZWaveBaseEntity
@@ -413,93 +403,6 @@ def is_valid_notification_binary_sensor(
return len(info.primary_value.metadata.states) > 1
@callback
def _async_check_legacy_entity_repair(
hass: HomeAssistant,
driver: Driver,
entity: ZWaveLegacyDoorStateBinarySensor,
) -> None:
"""Schedule a repair issue check once HA has fully started."""
@callback
def _async_do_check(hass: HomeAssistant) -> None:
"""Create or delete a repair issue for a deprecated legacy door state entity."""
ent_reg = er.async_get(hass)
if entity.unique_id is None:
return
entity_id = ent_reg.async_get_entity_id(
BINARY_SENSOR_DOMAIN, DOMAIN, entity.unique_id
)
if entity_id is None:
return
issue_id = f"deprecated_legacy_door_state.{entity_id}"
# Delete any stale repair issue if the entity is disabled or missing —
# the user has already dealt with it.
entity_entry = ent_reg.async_get(entity_id)
if entity_entry is None or entity_entry.disabled:
async_delete_issue(hass, DOMAIN, issue_id)
return
entity_automations = automations_with_entity(hass, entity_id)
entity_scripts = scripts_with_entity(hass, entity_id)
# Delete any stale repair issue if the entity is no longer referenced
# in any automation or script.
if not entity_automations and not entity_scripts:
async_delete_issue(hass, DOMAIN, issue_id)
return
opening_state_value = get_opening_state_notification_value(
entity.info.node, entity.info.primary_value.endpoint
)
if opening_state_value is None:
async_delete_issue(hass, DOMAIN, issue_id)
return
opening_state_unique_id = (
f"{driver.controller.home_id}.{opening_state_value.value_id}"
)
opening_state_entity_id = ent_reg.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, opening_state_unique_id
)
# Delete any stale repair issue if the replacement opening state sensor
# no longer exists for some reason
if opening_state_entity_id is None:
async_delete_issue(hass, DOMAIN, issue_id)
return
items = [
f"- [{item.name or item.original_name or eid}](/config/{domain}/edit/{item.unique_id})"
for domain, entity_ids in (
("automation", entity_automations),
("script", entity_scripts),
)
for eid in entity_ids
if (item := ent_reg.async_get(eid))
]
async_create_issue(
hass,
DOMAIN,
issue_id,
is_fixable=False,
is_persistent=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_legacy_door_state",
translation_placeholders={
"entity_id": entity_id,
"entity_name": entity_entry.name
or entity_entry.original_name
or entity_id,
"opening_state_entity_id": opening_state_entity_id,
"items": "\n".join(items),
},
)
async_at_started(hass, _async_do_check)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ZwaveJSConfigEntry,
@@ -543,9 +446,9 @@ async def async_setup_entry(
isinstance(info, NewZwaveDiscoveryInfo)
and info.entity_class is ZWaveLegacyDoorStateBinarySensor
):
entity = ZWaveLegacyDoorStateBinarySensor(config_entry, driver, info)
entities.append(entity)
_async_check_legacy_entity_repair(hass, driver, entity)
entities.append(
ZWaveLegacyDoorStateBinarySensor(config_entry, driver, info)
)
elif isinstance(info, NewZwaveDiscoveryInfo):
pass # other entity classes are not migrated yet
elif info.platform_hint == "notification":

View File

@@ -303,10 +303,6 @@
}
},
"issues": {
"deprecated_legacy_door_state": {
"description": "The binary sensor `{entity_id}` is deprecated because it has been replaced with the opening state sensor `{opening_state_entity_id}`.\n\nThe entity was found in the following automations or scripts:\n{items}\n\nPlease update the above automations or scripts to use the opening state sensor `{opening_state_entity_id}` and disable the binary sensor `{entity_id}` to fix this issue.\n\nNote that `{opening_state_entity_id}` reports three states:\n- Closed\n- Open\n- Tilted (if supported by the device).",
"title": "Deprecation: {entity_name}"
},
"device_config_file_changed": {
"fix_flow": {
"abort": {

View File

@@ -16,7 +16,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 4
MINOR_VERSION: Final = 5
PATCH_VERSION: Final = "0.dev0"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"

View File

@@ -1,18 +1,17 @@
"""Helpers for automation."""
from collections.abc import Callable, Mapping
from collections.abc import Mapping
from dataclasses import dataclass
from enum import Enum
from typing import Any, Final
from typing import Any, Final, Self
import voluptuous as vol
from homeassistant.const import CONF_OPTIONS
from homeassistant.core import HomeAssistant, split_entity_id
from . import config_validation as cv
from .entity import get_device_class_or_undefined
from .typing import ConfigType
from .typing import UNDEFINED, ConfigType, UndefinedType
CONF_UNIT: Final = "unit"
@@ -38,14 +37,6 @@ class DomainSpec:
"""Attribute name to extract the value from, or None for state.state."""
@dataclass(frozen=True, slots=True)
class NumericalDomainSpec(DomainSpec):
"""DomainSpec with an optional value converter for numerical triggers."""
value_converter: Callable[[float], float] | None = None
"""Optional converter for numerical values (e.g. uint8 → percentage)."""
def filter_by_domain_specs(
hass: HomeAssistant,
domain_specs: Mapping[str, DomainSpec],
@@ -145,41 +136,29 @@ def move_options_fields_to_top_level(
return new_config
_NUMBER_OR_ENTITY_CHOOSE_SCHEMA = vol.Schema(
{
vol.Required("active_choice"): vol.In(["number", "entity"]),
vol.Optional("entity"): cv.entity_id,
vol.Optional("number"): vol.Coerce(float),
}
)
@dataclass(frozen=True, kw_only=True)
class ThresholdConfig:
"""Configuration for threshold conditions and triggers."""
numerical: bool
entity: str | None
number: float | None
unit: str | None | UndefinedType
def _validate_number_or_entity(value: dict | float | str) -> float | str:
"""Validate number or entity selector result."""
if isinstance(value, dict):
_NUMBER_OR_ENTITY_CHOOSE_SCHEMA(value)
return value[value["active_choice"]] # type: ignore[no-any-return]
return value
@classmethod
def from_config(cls, config: dict[str, Any] | None) -> Self | None:
"""Create ThresholdConfig from config dict."""
if config is None:
return None
entity: str | None = None
number: float | None = None
unit: str | None | UndefinedType = UNDEFINED
numerical = "number" in config
if numerical:
number = config["number"]
unit = config.get("unit_of_measurement", UNDEFINED)
else:
entity = config["entity"]
number_or_entity = vol.All(
_validate_number_or_entity, vol.Any(vol.Coerce(float), cv.entity_id)
)
def validate_unit_set_if_range_numerical[_T: dict[str, Any]](
lower_limit: str, upper_limit: str
) -> Callable[[_T], _T]:
"""Validate that unit is set if upper or lower limit is numerical."""
def _validate_unit_set_if_range_numerical_impl(options: _T) -> _T:
if (
any(
opt in options and not isinstance(options[opt], str)
for opt in (lower_limit, upper_limit)
)
) and CONF_UNIT not in options:
raise vol.Invalid("Unit must be specified when using numerical thresholds.")
return options
return _validate_unit_set_if_range_numerical_impl
return cls(numerical=numerical, number=number, entity=entity, unit=unit)

View File

@@ -78,17 +78,21 @@ from homeassistant.util.yaml import load_yaml_dict
from . import config_validation as cv, entity_registry as er, selector
from .automation import (
CONF_UNIT,
DomainSpec,
ThresholdConfig,
filter_by_domain_specs,
get_absolute_description_key,
get_relative_description_key,
move_options_fields_to_top_level,
number_or_entity,
validate_unit_set_if_range_numerical,
)
from .integration_platform import async_process_integration_platforms
from .selector import TargetSelector
from .selector import (
NumericThresholdMode,
NumericThresholdSelector,
NumericThresholdSelectorConfig,
NumericThresholdType,
TargetSelector,
)
from .target import TargetSelection, async_extract_referenced_entity_ids
from .template import Template, render_complex
from .trace import (
@@ -338,10 +342,10 @@ ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL = vol.Schema(
)
class EntityConditionBase[DomainSpecT: DomainSpec = DomainSpec](Condition):
class EntityConditionBase(Condition):
"""Base class for entity conditions."""
_domain_specs: Mapping[str, DomainSpecT]
_domain_specs: Mapping[str, DomainSpec]
_schema: vol.Schema = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL
@override
@@ -458,36 +462,13 @@ def make_entity_state_condition(
return CustomCondition
def _validate_above_below(config: dict[str, Any]) -> dict[str, Any]:
"""Validate that above < below when both are set."""
above = config.get(CONF_ABOVE)
below = config.get(CONF_BELOW)
if above is None or below is None:
return config
if isinstance(above, str) or isinstance(below, str):
return config
if above >= below:
raise vol.Invalid(
f"A value can never be above {above} and below {below} at the same"
" time. You probably want two different conditions."
)
return config
NUMERICAL_CONDITION_SCHEMA = vol.Schema(
NUMERICAL_CONDITION_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
{
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
vol.Required(CONF_OPTIONS): vol.All(
{
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
[BEHAVIOR_ANY, BEHAVIOR_ALL]
),
vol.Optional(CONF_ABOVE): number_or_entity,
vol.Optional(CONF_BELOW): number_or_entity,
},
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
_validate_above_below,
),
vol.Required(CONF_OPTIONS): {
vol.Required("threshold"): NumericThresholdSelector(
NumericThresholdSelectorConfig(mode=NumericThresholdMode.IS)
),
},
}
)
@@ -503,8 +484,15 @@ class EntityNumericalConditionBase(EntityConditionBase):
super().__init__(hass, config)
if TYPE_CHECKING:
assert config.options is not None
self._above: float | str | None = config.options.get(CONF_ABOVE)
self._below: float | str | None = config.options.get(CONF_BELOW)
threshold_options: dict[str, Any] = config.options["threshold"]
self.threshold = ThresholdConfig.from_config(threshold_options.get("value"))
self.lower_threshold = ThresholdConfig.from_config(
threshold_options.get("value_min")
)
self.upper_threshold = ThresholdConfig.from_config(
threshold_options.get("value_max")
)
self._threshold_type = threshold_options["type"]
def _is_valid_unit(self, unit: str | None) -> bool:
"""Check if the given unit is valid for this condition."""
@@ -512,20 +500,26 @@ class EntityNumericalConditionBase(EntityConditionBase):
return True
return unit == self._valid_unit
def _get_numerical_value(self, entity_or_float: float | str) -> float | None:
"""Get numerical value from float or entity state."""
if isinstance(entity_or_float, str):
if not (ref_state := self._hass.states.get(entity_or_float)):
return None
if not self._is_valid_unit(
ref_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
):
return None
try:
return float(ref_state.state)
except TypeError, ValueError:
return None
return entity_or_float
def _get_threshold_value(self, threshold: ThresholdConfig | None) -> float | None:
"""Get threshold value from float or entity state."""
if threshold is None:
return None
if threshold.numerical:
return threshold.number
if not (entity_state := self._hass.states.get(threshold.entity)): # type: ignore[arg-type]
# Entity not found
return None
if not self._is_valid_unit(
entity_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
):
# Entity unit does not match the expected unit
return None
try:
return float(entity_state.state)
except TypeError, ValueError:
# Entity state is not a valid number
return None
def _get_tracked_value(self, entity_state: State) -> Any:
"""Get the tracked value from a state, with unit validation for state-based values."""
@@ -545,17 +539,27 @@ class EntityNumericalConditionBase(EntityConditionBase):
except TypeError, ValueError:
return False
if self._above is not None:
if (above := self._get_numerical_value(self._above)) is None:
if self._threshold_type == NumericThresholdType.ABOVE:
if (limit := self._get_threshold_value(self.threshold)) is None:
# Entity not found or invalid number, don't trigger
return False
if value <= above:
return value > limit
if self._threshold_type == NumericThresholdType.BELOW:
if (limit := self._get_threshold_value(self.threshold)) is None:
# Entity not found or invalid number, don't trigger
return False
if self._below is not None:
if (below := self._get_numerical_value(self._below)) is None:
return False
if value >= below:
return False
return True
return value < limit
# Mode is BETWEEN or OUTSIDE
lower_limit = self._get_threshold_value(self.lower_threshold)
upper_limit = self._get_threshold_value(self.upper_threshold)
if lower_limit is None or upper_limit is None:
# Entity not found or invalid number, don't trigger
return False
between = lower_limit < value < upper_limit
if self._threshold_type == NumericThresholdType.BETWEEN:
return between
return not between
def make_entity_numerical_condition(
@@ -578,22 +582,16 @@ def _make_numerical_condition_with_unit_schema(
unit_converter: type[BaseUnitConverter],
) -> vol.Schema:
"""Factory for numerical condition schema with unit option."""
return vol.Schema(
return ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
{
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
vol.Required(CONF_OPTIONS): vol.All(
{
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
[BEHAVIOR_ANY, BEHAVIOR_ALL]
),
vol.Optional(CONF_ABOVE): number_or_entity,
vol.Optional(CONF_BELOW): number_or_entity,
vol.Optional(CONF_UNIT): vol.In(unit_converter.VALID_UNITS),
},
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
_validate_above_below,
validate_unit_set_if_range_numerical(CONF_ABOVE, CONF_BELOW),
),
vol.Required(CONF_OPTIONS): {
vol.Required("threshold"): NumericThresholdSelector(
NumericThresholdSelectorConfig(
mode=NumericThresholdMode.IS,
unit_of_measurement=list(unit_converter.VALID_UNITS),
)
),
},
}
)
@@ -602,16 +600,8 @@ class EntityNumericalConditionWithUnitBase(EntityNumericalConditionBase):
"""Condition for numerical state comparisons with unit conversion."""
_base_unit: str | None # Base unit for the tracked value
_manual_limit_unit: str | None # Unit of above/below limits when numbers
_unit_converter: type[BaseUnitConverter]
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize the numerical condition with unit conversion."""
super().__init__(hass, config)
if TYPE_CHECKING:
assert config.options is not None
self._manual_limit_unit = config.options.get(CONF_UNIT)
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Create a schema."""
super().__init_subclass__(**kwargs)
@@ -621,25 +611,34 @@ class EntityNumericalConditionWithUnitBase(EntityNumericalConditionBase):
"""Get the unit of an entity from its state."""
return entity_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
def _get_numerical_value(self, entity_or_float: float | str) -> float | None:
"""Get numerical value from float or entity state."""
if isinstance(entity_or_float, (int, float)):
def _get_threshold_value(self, threshold: ThresholdConfig | None) -> float | None:
"""Get threshold value from float or entity state."""
if threshold is None:
return None
if threshold.numerical:
return self._unit_converter.convert(
entity_or_float, self._manual_limit_unit, self._base_unit
threshold.number, # type: ignore[arg-type]
threshold.unit, # type: ignore[arg-type]
self._base_unit,
)
if not (_state := self._hass.states.get(entity_or_float)):
if not (entity_state := self._hass.states.get(threshold.entity)): # type: ignore[arg-type]
# Entity not found
return None
try:
value = float(_state.state)
value = float(entity_state.state)
except TypeError, ValueError:
# Entity state is not a valid number
return None
try:
return self._unit_converter.convert(
value, _state.attributes.get(ATTR_UNIT_OF_MEASUREMENT), self._base_unit
value,
entity_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT),
self._base_unit,
)
except HomeAssistantError:
# Unit conversion failed (i.e. incompatible units), treat as invalid number
return None
def _get_tracked_value(self, entity_state: State) -> Any:
@@ -663,23 +662,6 @@ class EntityNumericalConditionWithUnitBase(EntityNumericalConditionBase):
except HomeAssistantError:
return None
def is_valid_state(self, entity_state: State) -> bool:
"""Check if the state is within the specified range."""
if (value := self._get_tracked_value(entity_state)) is None:
return False
if self._above is not None:
if (above := self._get_numerical_value(self._above)) is None:
return False
if value <= above:
return False
if self._below is not None:
if (below := self._get_numerical_value(self._below)) is None:
return False
if value >= below:
return False
return True
def make_entity_numerical_condition_with_unit(
domain_specs: Mapping[str, DomainSpec],

View File

@@ -868,11 +868,16 @@ def url(
) -> str:
"""Validate an URL."""
url_in = str(value)
parsed = urlparse(url_in)
if urlparse(url_in).scheme in _schema_list:
return cast(str, vol.Schema(vol.Url())(url_in))
if parsed.scheme not in _schema_list:
raise vol.Invalid("invalid url")
raise vol.Invalid("invalid url")
try:
_port = parsed.port
except ValueError as err:
raise vol.Invalid("invalid url") from err
return cast(str, vol.Schema(vol.Url())(url_in))
def configuration_url(value: Any) -> str:

View File

@@ -16,7 +16,6 @@ from typing import (
Final,
Literal,
Protocol,
Self,
TypedDict,
cast,
override,
@@ -69,7 +68,7 @@ from homeassistant.util.yaml import load_yaml_dict
from . import config_validation as cv, selector
from .automation import (
DomainSpec,
NumericalDomainSpec,
ThresholdConfig,
filter_by_domain_specs,
get_absolute_description_key,
get_relative_description_key,
@@ -336,15 +335,14 @@ ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST = ENTITY_STATE_TRIGGER_SCHEMA.extend(
[BEHAVIOR_FIRST, BEHAVIOR_LAST, BEHAVIOR_ANY]
),
},
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
}
)
class EntityTriggerBase[DomainSpecT: DomainSpec = DomainSpec](Trigger):
class EntityTriggerBase(Trigger):
"""Trigger for entity state changes."""
_domain_specs: Mapping[str, DomainSpecT]
_domain_specs: Mapping[str, DomainSpec]
_schema: vol.Schema = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST
@override
@@ -535,35 +533,7 @@ NUMERICAL_ATTRIBUTE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
)
@dataclass(frozen=True, kw_only=True)
class ThresholdConfig:
"""Configuration for threshold triggers."""
numerical: bool
entity: str | None
number: float | None
unit: str | None | UndefinedType
@classmethod
def from_config(cls, config: dict[str, Any] | None) -> Self | None:
"""Create ThresholdConfig from config dict."""
if config is None:
return None
entity: str | None = None
number: float | None = None
unit: str | None | UndefinedType = UNDEFINED
numerical = "number" in config
if numerical:
number = config["number"]
unit = config.get("unit_of_measurement", UNDEFINED)
else:
entity = config["entity"]
return cls(numerical=numerical, number=number, entity=entity, unit=unit)
class EntityNumericalStateTriggerBase(EntityTriggerBase[NumericalDomainSpec]):
class EntityNumericalStateTriggerBase(EntityTriggerBase):
"""Base class for numerical state and state attribute triggers."""
_valid_unit: str | None | UndefinedType = UNDEFINED
@@ -624,21 +594,12 @@ class EntityNumericalStateTriggerBase(EntityTriggerBase[NumericalDomainSpec]):
# Entity state is not a valid number
return None
def _get_converter(self, state: State) -> Callable[[float], float]:
"""Get the value converter for an entity."""
domain_spec = self._domain_specs[state.domain]
if domain_spec.value_converter is not None:
return domain_spec.value_converter
return lambda x: x
def is_valid_state(self, state: State) -> bool:
"""Check if the new state or state attribute matches the expected one."""
# Handle missing or None value case first to avoid expensive exceptions
if (_attribute_value := self._get_tracked_value(state)) is None:
if (current_value := self._get_tracked_value(state)) is None:
return False
current_value = self._get_converter(state)(_attribute_value)
if self._threshold_type == NumericThresholdType.ANY:
# If the threshold type is "any" we always trigger on valid state
# changes
@@ -774,19 +735,16 @@ class EntityNumericalStateChangedTriggerWithUnitBase(
cls._schema = make_numerical_state_changed_with_unit_schema(cls._unit_converter)
NUMERICAL_ATTRIBUTE_CROSSED_THRESHOLD_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
{
vol.Required(CONF_OPTIONS): vol.All(
{
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
[BEHAVIOR_FIRST, BEHAVIOR_LAST, BEHAVIOR_ANY]
),
NUMERICAL_ATTRIBUTE_CROSSED_THRESHOLD_SCHEMA = (
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
{
vol.Required(CONF_OPTIONS): {
vol.Required("threshold"): NumericThresholdSelector(
NumericThresholdSelectorConfig(mode=NumericThresholdMode.CROSSED)
),
},
)
}
}
)
)
@@ -815,21 +773,16 @@ def _make_numerical_state_crossed_threshold_with_unit_schema(
This trigger only fires when the observed attribute changes from not within to within
the defined threshold.
"""
return ENTITY_STATE_TRIGGER_SCHEMA.extend(
return ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
{
vol.Required(CONF_OPTIONS, default={}): vol.All(
{
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
[BEHAVIOR_FIRST, BEHAVIOR_LAST, BEHAVIOR_ANY]
),
vol.Required("threshold"): NumericThresholdSelector(
NumericThresholdSelectorConfig(
mode=NumericThresholdMode.CROSSED,
unit_of_measurement=list(unit_converter.VALID_UNITS),
)
),
},
)
vol.Required(CONF_OPTIONS, default={}): {
vol.Required("threshold"): NumericThresholdSelector(
NumericThresholdSelectorConfig(
mode=NumericThresholdMode.CROSSED,
unit_of_measurement=list(unit_converter.VALID_UNITS),
)
),
},
}
)
@@ -927,7 +880,7 @@ def make_entity_origin_state_trigger(
def make_entity_numerical_state_changed_trigger(
domain_specs: Mapping[str, NumericalDomainSpec],
domain_specs: Mapping[str, DomainSpec],
valid_unit: str | None | UndefinedType = UNDEFINED,
) -> type[EntityNumericalStateChangedTriggerBase]:
"""Create a trigger for numerical state value change."""
@@ -942,7 +895,7 @@ def make_entity_numerical_state_changed_trigger(
def make_entity_numerical_state_crossed_threshold_trigger(
domain_specs: Mapping[str, NumericalDomainSpec],
domain_specs: Mapping[str, DomainSpec],
valid_unit: str | None | UndefinedType = UNDEFINED,
) -> type[EntityNumericalStateCrossedThresholdTriggerBase]:
"""Create a trigger for numerical state value crossing a threshold."""
@@ -957,7 +910,7 @@ def make_entity_numerical_state_crossed_threshold_trigger(
def make_entity_numerical_state_changed_with_unit_trigger(
domain_specs: Mapping[str, NumericalDomainSpec],
domain_specs: Mapping[str, DomainSpec],
base_unit: str,
unit_converter: type[BaseUnitConverter],
) -> type[EntityNumericalStateChangedTriggerWithUnitBase]:
@@ -974,7 +927,7 @@ def make_entity_numerical_state_changed_with_unit_trigger(
def make_entity_numerical_state_crossed_threshold_with_unit_trigger(
domain_specs: Mapping[str, NumericalDomainSpec],
domain_specs: Mapping[str, DomainSpec],
base_unit: str,
unit_converter: type[BaseUnitConverter],
) -> type[EntityNumericalStateCrossedThresholdTriggerWithUnitBase]:

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2026.4.0.dev0"
version = "2026.5.0.dev0"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."

14
requirements_all.txt generated
View File

@@ -562,7 +562,7 @@ asyncinotify==4.4.0
asyncpysupla==0.0.5
# homeassistant.components.sleepiq
asyncsleepiq==1.7.0
asyncsleepiq==1.7.1
# homeassistant.components.sftp_storage
asyncssh==2.21.0
@@ -1452,7 +1452,7 @@ livisi==0.0.25
locationsharinglib==5.0.1
# homeassistant.components.lojack
lojack-api==0.7.1
lojack-api==0.7.2
# homeassistant.components.london_underground
london-tube-status==0.5
@@ -1564,7 +1564,7 @@ mozart-api==5.3.1.108.2
mullvad-api==1.0.0
# homeassistant.components.music_assistant
music-assistant-client==1.3.3
music-assistant-client==1.3.4
# homeassistant.components.tts
mutagen==1.47.0
@@ -1959,7 +1959,7 @@ pyairobotrest==0.3.0
pyairvisual==2023.08.1
# homeassistant.components.anglian_water
pyanglianwater==3.1.1
pyanglianwater==3.1.2
# homeassistant.components.aprilaire
pyaprilaire==0.9.1
@@ -2651,7 +2651,7 @@ python-overseerr==0.9.0
python-picnic-api2==1.3.1
# homeassistant.components.pooldose
python-pooldose==0.8.6
python-pooldose==0.9.0
# homeassistant.components.hr_energy_qube
python-qube-heatpump==1.7.0
@@ -2823,7 +2823,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.5.6
renault-api==0.5.7
# homeassistant.components.renson
renson-endura-delta==1.7.2
@@ -2902,7 +2902,7 @@ sendgrid==6.8.2
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense-energy==0.13.8
sense-energy==0.14.0
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.1

View File

@@ -523,7 +523,7 @@ async-upnp-client==0.46.2
asyncarve==0.1.1
# homeassistant.components.sleepiq
asyncsleepiq==1.7.0
asyncsleepiq==1.7.1
# homeassistant.components.sftp_storage
asyncssh==2.21.0
@@ -1274,7 +1274,7 @@ libsoundtouch==0.8
livisi==0.0.25
# homeassistant.components.lojack
lojack-api==0.7.1
lojack-api==0.7.2
# homeassistant.components.london_underground
london-tube-status==0.5
@@ -1377,7 +1377,7 @@ mozart-api==5.3.1.108.2
mullvad-api==1.0.0
# homeassistant.components.music_assistant
music-assistant-client==1.3.3
music-assistant-client==1.3.4
# homeassistant.components.tts
mutagen==1.47.0
@@ -1696,7 +1696,7 @@ pyairobotrest==0.3.0
pyairvisual==2023.08.1
# homeassistant.components.anglian_water
pyanglianwater==3.1.1
pyanglianwater==3.1.2
# homeassistant.components.aprilaire
pyaprilaire==0.9.1
@@ -2253,7 +2253,7 @@ python-overseerr==0.9.0
python-picnic-api2==1.3.1
# homeassistant.components.pooldose
python-pooldose==0.8.6
python-pooldose==0.9.0
# homeassistant.components.hr_energy_qube
python-qube-heatpump==1.7.0
@@ -2401,7 +2401,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.5.6
renault-api==0.5.7
# homeassistant.components.renson
renson-endura-delta==1.7.2
@@ -2459,7 +2459,7 @@ securetar==2026.2.0
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense-energy==0.13.8
sense-energy==0.14.0
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.1

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