Compare commits

...

56 Commits

Author SHA1 Message Date
Franck Nijhof
16b8130676 Fix mypy errors for wled 0.22.0 Segment type changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 20:23:52 +00:00
Franck Nijhof
f6cfd69f6b Update tests/components/wled/test_update.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-16 22:10:48 +02:00
Franck Nijhof
87bcb34035 Update wled to v0.22.0 2026-04-16 20:02:48 +00:00
Franck Nijhof
7c0ba4d250 Migrate Twente Milieu sensor unique IDs to snake_case and domainless (#168384) 2026-04-16 21:12:44 +02:00
Retha Runolfsson
6277ef5c21 Create a battery range sensor for switchbot presence sensor (#159096)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-16 18:52:10 +02:00
snek
b75263e486 Add heat/cool dmsr device support (#168279) 2026-04-16 18:41:22 +02:00
Paulus Schoutsen
2087906758 Add Denon rs232 integration (#166923)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-16 12:23:56 -04:00
Artur Pragacz
395d741324 Implement batched service call (#168175) 2026-04-16 18:10:43 +02:00
Richard Kroegel
2bcde89f5a Add sensor platform to eurotronic_cometblue (#168118) 2026-04-16 18:03:03 +02:00
Marc Mueller
74c62c34da Fix shelly test RuntimeWarnings (#168380) 2026-04-16 18:33:30 +03:00
Jan Bouwhuis
810672ea78 Improve scope discovery abbreviation checking for MQTT config options (#168302)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-16 17:15:18 +02:00
sanpo
afe3280aee Fix DLNA local file playback for Sony TA-AN1000 by returning content type for HEAD requests (#165807) 2026-04-16 17:14:24 +02:00
AlCalzone
fc573a0cf6 Fix Z-Wave connection string for encrypted ESPHome proxies (#168370) 2026-04-16 16:52:09 +02:00
Erik Montnemery
7b8978c7e5 Add duration to state based entity conditions (#168348) 2026-04-16 16:12:59 +02:00
renovate[bot]
d99d041e49 Update uv to 0.11.6 (#168237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 16:10:29 +02:00
Ariel Ebersberger
cd15261d1c Fix helper tests for Python 3.14.3 (#168355) 2026-04-16 15:56:50 +02:00
Raphael Hehl
5def2456f0 Unifi access doorbell event type (#168316)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-16 15:54:14 +02:00
David Bonnes
87742dbf4e Deprecate Evohome reset services and corresponding climate preset (#167975) 2026-04-16 15:51:48 +02:00
Artur Pragacz
f5fef37210 Remove bind_hass usage (#168369) 2026-04-16 15:38:59 +02:00
Colin
fa85d0d6c2 Fix openevse charging_current and charging_power units (#167863) 2026-04-16 15:29:32 +02:00
Raphael Hehl
0fa5927fc8 Add quality scale tracking for UniFi Network integration (#168125)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-16 15:00:03 +02:00
Joost Lekkerkerker
5335367493 Check if serialx is pinned (#168358) 2026-04-16 14:50:00 +02:00
Robert Resch
1f6e078d1d Extract dynamically package version at build time in hassfest image (#168347) 2026-04-16 14:40:13 +02:00
Marc Mueller
71d857b5e1 Update pydantic to 2.13.1 (#168311) 2026-04-16 14:34:30 +02:00
Barry vd. Heuvel
0de75a013b Add weheat standby electricity usage (#168363) 2026-04-16 14:33:36 +02:00
Robert Resch
f87ec0a7b8 Just copy explicit files in the Dockerfile (#168197) 2026-04-16 14:30:54 +02:00
Ariel Ebersberger
6d1bd15256 Fix synology_dsm test for Python 3.14.3 (#168359) 2026-04-16 13:23:09 +02:00
Jürgen
9fe9064884 Fix sonos availability (#161024)
Co-authored-by: Pete Sage <76050312+PeteRager@users.noreply.github.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-04-16 12:14:19 +01:00
Jamin
f9f57b00bb Fix VOIP blocking call in event loop (#168331) 2026-04-16 12:14:58 +02:00
johanzander
2b65b06003 Fix unit of measurement for SPH power sensors in growatt_server (#168251)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 12:14:13 +02:00
Leo Periou
206c498027 Bump pyaxencoapi to 1.0.7 (#168286) 2026-04-16 12:10:24 +02:00
renovate[bot]
0ac62b241e Update home-assistant-bluetooth to 2.0.0 (#168353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 12:06:34 +02:00
renovate[bot]
4ba123a1a8 Update PyTurboJPEG to 2.2.0 (#168354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 12:02:56 +02:00
Maciej Bieniek
8b8b39c1b7 Bump imgw-pib to 2.1.0 (#168319) 2026-04-16 11:27:44 +02:00
renovate[bot]
5b70e5f829 Update lru-dict to 1.4.1 (#168336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 11:25:00 +02:00
Erik Montnemery
4f8e7125d4 Add state based condition tests (#168349) 2026-04-16 11:22:14 +02:00
renovate[bot]
baf5e32c59 Update xmltodict to 1.0.4 (#168330)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 10:49:35 +02:00
renovate[bot]
0f0ceaace2 Update PyJWT to 2.12.1 (#168239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Robert Resch <robert@resch.dev>
2026-04-16 10:44:41 +02:00
Andres Ruiz
5ecae7066b Bump waterfurance to 1.6.5 (#168328) 2026-04-16 10:09:25 +02:00
Ronald van der Meer
ac9bf9b7cb Bump python-duco-client to 0.3.1 (#168341) 2026-04-16 10:08:41 +02:00
renovate[bot]
d4a98c3336 Update audioop-lts to 0.2.2 (#168326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 10:07:45 +02:00
dependabot[bot]
f0aae350b5 Bump docker/build-push-action from 7.0.0 to 7.1.0 (#168338)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 10:06:09 +02:00
Paulus Schoutsen
69332ed822 Add SerialSelector (#168263) 2026-04-16 10:45:37 +03:00
Erik Montnemery
32db17fab9 Add duration to more triggers (#168337) 2026-04-16 08:46:58 +02:00
renovate[bot]
84e8cff2ea Update infrared-protocols to 1.2.0 (#168335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 08:31:56 +02:00
Ariel Ebersberger
cfe390e4f6 Migrate demo image_processing to async (#168315) 2026-04-16 08:17:00 +02:00
Erik Montnemery
a9becca321 Add duration to state based entity triggers (#167740) 2026-04-16 07:38:50 +02:00
renovate[bot]
0043a307f0 Update PyTurboJPEG to 1.8.3 (#168329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 05:49:04 +02:00
renovate[bot]
dfb1819800 Update fnv-hash-fast to 2.0.2 (#168327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 17:04:50 -10:00
puddly
12018cf9f4 Migrate remaining Core integrations from pyserial to serialx (#168325) 2026-04-15 22:39:32 -04:00
Franck Nijhof
70368c622e Extend Renovate allowlist with common packages (#168295) 2026-04-15 23:42:32 +02:00
Franck Nijhof
743aef05be Update twentemilieu to 3.0.0 (#168313) 2026-04-15 22:39:42 +02:00
Ariel Ebersberger
49e5b03c08 Migrate hdmi_cec to async (#168306) 2026-04-15 21:51:07 +02:00
Jan Bouwhuis
6bc3fcef36 Fix minor issues in MQTT tests (#168303) 2026-04-15 21:34:44 +02:00
puddly
e3e87185c5 Switch USB integration to list serial ports with serialx (#167615) 2026-04-15 19:22:45 +02:00
epenet
6d83b73cbb Simplify raise-pull-request agent push step (#167739)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 18:10:31 +01:00
277 changed files with 5582 additions and 1232 deletions

View File

@@ -186,15 +186,11 @@ If `CHANGE_TYPE` IS "Breaking change" or "Deprecation", keep the `## Breaking ch
## Step 10: Push Branch and Create PR
```bash
# Get branch name and GitHub username
BRANCH=$(git branch --show-current)
PUSH_REMOTE=$(git config "branch.$BRANCH.remote" 2>/dev/null || git remote | head -1)
GITHUB_USER=$(gh api user --jq .login 2>/dev/null || git remote get-url "$PUSH_REMOTE" | sed -E 's#.*[:/]([^/]+)/([^/]+)(\.git)?$#\1#')
Push the branch with upstream tracking, and create a PR against `home-assistant/core` with the generated title and body:
```bash
# Create PR (gh pr create pushes the branch automatically)
gh pr create --repo home-assistant/core --base dev \
--head "$GITHUB_USER:$BRANCH" \
--draft \
--title "TITLE_HERE" \
--body "$(cat <<'EOF'

44
.github/renovate.json vendored
View File

@@ -78,6 +78,50 @@
"enabled": true,
"labels": ["dependency", "core"]
},
{
"description": "Common Python utilities (allowlisted)",
"matchPackageNames": [
"astral",
"atomicwrites-homeassistant",
"audioop-lts",
"awesomeversion",
"bcrypt",
"ciso8601",
"cronsim",
"defusedxml",
"fnv-hash-fast",
"getmac",
"ical",
"ifaddr",
"lru-dict",
"mutagen",
"propcache",
"pyserial",
"python-slugify",
"PyTurboJPEG",
"securetar",
"standard-aifc",
"standard-telnetlib",
"ulid-transform",
"url-normalize",
"xmltodict"
],
"enabled": true,
"labels": ["dependency"]
},
{
"description": "Home Assistant ecosystem packages (core-maintained, no cooldown)",
"matchPackageNames": [
"hassil",
"home-assistant-bluetooth",
"home-assistant-frontend",
"home-assistant-intents",
"infrared-protocols"
],
"enabled": true,
"minimumReleaseAge": null,
"labels": ["dependency", "core"]
},
{
"description": "Test dependencies (allowlisted)",
"matchPackageNames": [

View File

@@ -530,7 +530,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -543,7 +543,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile

2
CODEOWNERS generated
View File

@@ -362,6 +362,8 @@ CLAUDE.md @home-assistant/core
/tests/components/deluge/ @tkdrob
/homeassistant/components/demo/ @home-assistant/core
/tests/components/demo/ @home-assistant/core
/homeassistant/components/denon_rs232/ @balloob
/tests/components/denon_rs232/ @balloob
/homeassistant/components/denonavr/ @ol-iver @starkillerOG
/tests/components/denonavr/ @ol-iver @starkillerOG
/homeassistant/components/derivative/ @afaucogney @karwosts

5
Dockerfile generated
View File

@@ -28,8 +28,7 @@ COPY rootfs /
COPY --from=ghcr.io/alexxit/go2rtc@sha256:675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae /usr/local/bin/go2rtc /bin/go2rtc
## Setup Home Assistant Core dependencies
COPY requirements.txt homeassistant/
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
COPY --parents requirements.txt homeassistant/package_constraints.txt homeassistant/
RUN \
# Verify go2rtc can be executed
go2rtc --version \
@@ -49,7 +48,7 @@ RUN \
-r homeassistant/requirements_all.txt
## Setup Home Assistant Core
COPY . homeassistant/
COPY --parents LICENSE* README* homeassistant/ pyproject.toml homeassistant/
RUN \
uv pip install \
-e ./homeassistant \

View File

@@ -7,23 +7,31 @@ to speed up the process.
from __future__ import annotations
from collections.abc import Container, Iterable, Sequence
from datetime import timedelta
from functools import lru_cache, partial
from typing import Any
from functools import lru_cache
from typing import Any, override
from jwt import DecodeError, PyJWS, PyJWT
from jwt import DecodeError, PyJWK, PyJWS, PyJWT
from jwt.algorithms import AllowedPublicKeys
from jwt.types import Options
from homeassistant.util.json import json_loads
JWT_TOKEN_CACHE_SIZE = 16
MAX_TOKEN_SIZE = 8192
_VERIFY_KEYS = ("signature", "exp", "nbf", "iat", "aud", "iss", "sub", "jti")
_VERIFY_OPTIONS: dict[str, Any] = {f"verify_{key}": True for key in _VERIFY_KEYS} | {
"require": []
}
_NO_VERIFY_OPTIONS = {f"verify_{key}": False for key in _VERIFY_KEYS}
_NO_VERIFY_OPTIONS = Options(
verify_signature=False,
verify_exp=False,
verify_nbf=False,
verify_iat=False,
verify_aud=False,
verify_iss=False,
verify_sub=False,
verify_jti=False,
require=[],
)
class _PyJWSWithLoadCache(PyJWS):
@@ -38,9 +46,6 @@ class _PyJWSWithLoadCache(PyJWS):
return super()._load(jwt)
_jws = _PyJWSWithLoadCache()
@lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)
def _decode_payload(json_payload: str) -> dict[str, Any]:
"""Decode the payload from a JWS dictionary."""
@@ -56,21 +61,12 @@ def _decode_payload(json_payload: str) -> dict[str, Any]:
class _PyJWTWithVerify(PyJWT):
"""PyJWT with a fast decode implementation."""
def decode_payload(
self, jwt: str, key: str, options: dict[str, Any], algorithms: list[str]
) -> dict[str, Any]:
"""Decode a JWT's payload."""
if len(jwt) > MAX_TOKEN_SIZE:
# Avoid caching impossible tokens
raise DecodeError("Token too large")
return _decode_payload(
_jws.decode_complete(
jwt=jwt,
key=key,
algorithms=algorithms,
options=options,
)["payload"]
)
def __init__(self) -> None:
"""Initialize the PyJWT instance."""
# We require exp and iat claims to be present
super().__init__(Options(require=["exp", "iat"]))
# Override the _jws instance with our cached version
self._jws = _PyJWSWithLoadCache()
def verify_and_decode(
self,
@@ -79,37 +75,70 @@ class _PyJWTWithVerify(PyJWT):
algorithms: list[str],
issuer: str | None = None,
leeway: float | timedelta = 0,
options: dict[str, Any] | None = None,
options: Options | None = None,
) -> dict[str, Any]:
"""Verify a JWT's signature and claims."""
merged_options = {**_VERIFY_OPTIONS, **(options or {})}
payload = self.decode_payload(
return self.decode(
jwt=jwt,
key=key,
options=merged_options,
algorithms=algorithms,
)
# These should never be missing since we verify them
# but this is an additional safeguard to make sure
# nothing slips through.
assert "exp" in payload, "exp claim is required"
assert "iat" in payload, "iat claim is required"
self._validate_claims(
payload=payload,
options=merged_options,
issuer=issuer,
leeway=leeway,
options=options,
)
return payload
@override
def decode(
self,
jwt: str | bytes,
key: AllowedPublicKeys | PyJWK | str | bytes = "",
algorithms: Sequence[str] | None = None,
options: Options | None = None,
verify: bool | None = None,
detached_payload: bytes | None = None,
audience: str | Iterable[str] | None = None,
subject: str | None = None,
issuer: str | Container[str] | None = None,
leeway: float | timedelta = 0,
**kwargs: Any,
) -> dict[str, Any]:
"""Decode a JWT, verifying the signature and claims."""
if len(jwt) > MAX_TOKEN_SIZE:
# Avoid caching impossible tokens
raise DecodeError("Token too large")
return super().decode(
jwt=jwt,
key=key,
algorithms=algorithms,
options=options,
verify=verify,
detached_payload=detached_payload,
audience=audience,
subject=subject,
issuer=issuer,
leeway=leeway,
**kwargs,
)
@override
def _decode_payload(self, decoded: dict[str, Any]) -> dict[str, Any]:
return _decode_payload(decoded["payload"])
_jwt = _PyJWTWithVerify()
verify_and_decode = _jwt.verify_and_decode
unverified_hs256_token_decode = lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)(
partial(
_jwt.decode_payload, key="", algorithms=["HS256"], options=_NO_VERIFY_OPTIONS
@lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)
def unverified_hs256_token_decode(jwt: str) -> dict[str, Any]:
"""Decode a JWT without verifying the signature."""
return _jwt.decode(
jwt=jwt,
key="",
algorithms=["HS256"],
options=_NO_VERIFY_OPTIONS,
)
)
__all__ = [
"unverified_hs256_token_decode",

View File

@@ -1,5 +1,5 @@
{
"domain": "denon",
"name": "Denon",
"integrations": ["denon", "denonavr", "heos"]
"integrations": ["denon", "denonavr", "denon_rs232", "heos"]
}

View File

@@ -6,10 +6,11 @@ from typing import Final
from homeassistant.const import STATE_OFF, STATE_ON
CONF_READ_TIMEOUT: Final = "timeout"
CONF_WRITE_TIMEOUT: Final = "write_timeout"
DEFAULT_NAME: Final = "Acer Projector"
DEFAULT_TIMEOUT: Final = 1
DEFAULT_READ_TIMEOUT: Final = 1
DEFAULT_WRITE_TIMEOUT: Final = 1
ECO_MODE: Final = "ECO Mode"

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/acer_projector",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["pyserial==3.5"]
"requirements": ["serialx==1.2.2"]
}

View File

@@ -6,7 +6,7 @@ import logging
import re
from typing import Any
import serial
from serialx import Serial, SerialException
import voluptuous as vol
from homeassistant.components.switch import (
@@ -16,21 +16,22 @@ from homeassistant.components.switch import (
from homeassistant.const import (
CONF_FILENAME,
CONF_NAME,
CONF_TIMEOUT,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
CMD_DICT,
CONF_READ_TIMEOUT,
CONF_WRITE_TIMEOUT,
DEFAULT_NAME,
DEFAULT_TIMEOUT,
DEFAULT_READ_TIMEOUT,
DEFAULT_WRITE_TIMEOUT,
ECO_MODE,
ICON,
@@ -45,7 +46,7 @@ PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_FILENAME): cv.isdevice,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_READ_TIMEOUT, default=DEFAULT_READ_TIMEOUT): cv.positive_int,
vol.Optional(
CONF_WRITE_TIMEOUT, default=DEFAULT_WRITE_TIMEOUT
): cv.positive_int,
@@ -62,10 +63,10 @@ def setup_platform(
"""Connect with serial port and return Acer Projector."""
serial_port = config[CONF_FILENAME]
name = config[CONF_NAME]
timeout = config[CONF_TIMEOUT]
read_timeout = config[CONF_READ_TIMEOUT]
write_timeout = config[CONF_WRITE_TIMEOUT]
add_entities([AcerSwitch(serial_port, name, timeout, write_timeout)], True)
add_entities([AcerSwitch(serial_port, name, read_timeout, write_timeout)], True)
class AcerSwitch(SwitchEntity):
@@ -77,14 +78,14 @@ class AcerSwitch(SwitchEntity):
self,
serial_port: str,
name: str,
timeout: int,
read_timeout: int,
write_timeout: int,
) -> None:
"""Init of the Acer projector."""
self.serial = serial.Serial(
port=serial_port, timeout=timeout, write_timeout=write_timeout
)
self._serial_port = serial_port
self._read_timeout = read_timeout
self._write_timeout = write_timeout
self._attr_name = name
self._attributes = {
LAMP_HOURS: STATE_UNKNOWN,
@@ -94,22 +95,26 @@ class AcerSwitch(SwitchEntity):
def _write_read(self, msg: str) -> str:
"""Write to the projector and read the return."""
ret = ""
# Sometimes the projector won't answer for no reason or the projector
# was disconnected during runtime.
# This way the projector can be reconnected and will still work
try:
if not self.serial.is_open:
self.serial.open()
self.serial.write(msg.encode("utf-8"))
# Size is an experience value there is no real limit.
# AFAIK there is no limit and no end character so we will usually
# need to wait for timeout
ret = self.serial.read_until(size=20).decode("utf-8")
except serial.SerialException:
_LOGGER.error("Problem communicating with %s", self._serial_port)
self.serial.close()
return ret
with Serial.from_url(
self._serial_port,
read_timeout=self._read_timeout,
write_timeout=self._write_timeout,
) as serial:
serial.write(msg.encode("utf-8"))
# Size is an experience value there is no real limit.
# AFAIK there is no limit and no end character so we will usually
# need to wait for timeout
return serial.read_until(size=20).decode("utf-8")
except (OSError, SerialException, TimeoutError) as exc:
raise HomeAssistantError(
f"Problem communicating with {self._serial_port}"
) from exc
def _write_read_format(self, msg: str) -> str:
"""Write msg, obtain answer and format output."""

View File

@@ -3,6 +3,7 @@
"condition_behavior_name": "Condition passes if",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
"trigger_threshold_name": "Threshold type"
},
"conditions": {
@@ -249,6 +250,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}
@@ -269,6 +273,9 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
}
},
"name": "Carbon monoxide cleared"
@@ -279,6 +286,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}
@@ -290,6 +300,9 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
}
},
"name": "Carbon monoxide detected"
@@ -299,6 +312,9 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
}
},
"name": "Gas cleared"
@@ -308,6 +324,9 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
}
},
"name": "Gas detected"
@@ -327,6 +346,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}
@@ -348,6 +370,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}
@@ -369,6 +394,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}
@@ -390,6 +418,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}
@@ -411,6 +442,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}
@@ -432,6 +466,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}
@@ -453,6 +490,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}
@@ -474,6 +514,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}
@@ -485,6 +528,9 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
}
},
"name": "Smoke cleared"
@@ -494,6 +540,9 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
}
},
"name": "Smoke detected"
@@ -513,6 +562,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}
@@ -534,6 +586,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}
@@ -555,6 +610,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
}

View File

@@ -9,6 +9,11 @@
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
selector:
duration:
# --- Unit lists for multi-unit pollutants ---
@@ -163,6 +168,7 @@
# Binary sensor detected/cleared trigger fields
.trigger_binary_fields: &trigger_binary_fields
behavior: *trigger_behavior
for: *trigger_for
# --- Binary sensor targets ---
@@ -294,6 +300,7 @@ co_crossed_threshold:
target: *target_co_sensor
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -320,6 +327,7 @@ co2_crossed_threshold:
target: *target_co2
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -344,6 +352,7 @@ pm1_crossed_threshold:
target: *target_pm1
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -368,6 +377,7 @@ pm25_crossed_threshold:
target: *target_pm25
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -392,6 +402,7 @@ pm4_crossed_threshold:
target: *target_pm4
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -416,6 +427,7 @@ pm10_crossed_threshold:
target: *target_pm10
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -442,6 +454,7 @@ ozone_crossed_threshold:
target: *target_ozone
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -470,6 +483,7 @@ voc_crossed_threshold:
target: *target_voc
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -498,6 +512,7 @@ voc_ratio_crossed_threshold:
target: *target_voc_ratio
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -526,6 +541,7 @@ no_crossed_threshold:
target: *target_no
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -554,6 +570,7 @@ no2_crossed_threshold:
target: *target_no2
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -580,6 +597,7 @@ n2o_crossed_threshold:
target: *target_n2o
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -606,6 +624,7 @@ so2_crossed_threshold:
target: *target_so2
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:

View File

@@ -1,7 +1,8 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"is_armed": {
@@ -234,6 +235,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
}
},
"name": "Alarm armed"
@@ -243,6 +247,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
}
},
"name": "Alarm armed away"
@@ -252,6 +259,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
}
},
"name": "Alarm armed home"
@@ -261,6 +271,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
}
},
"name": "Alarm armed night"
@@ -270,6 +283,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
}
},
"name": "Alarm armed vacation"
@@ -279,6 +295,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
}
},
"name": "Alarm disarmed"
@@ -288,6 +307,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::trigger_for_name%]"
}
},
"name": "Alarm triggered"

View File

@@ -13,6 +13,11 @@
- last
- any
translation_key: trigger_behavior
for:
required: true
default: 00:00:00
selector:
duration:
armed: *trigger_common

View File

@@ -1,7 +1,8 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"is_idle": {
@@ -160,6 +161,9 @@
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::assist_satellite::common::trigger_for_name%]"
}
},
"name": "Satellite became idle"
@@ -169,6 +173,9 @@
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::assist_satellite::common::trigger_for_name%]"
}
},
"name": "Satellite started listening"
@@ -178,6 +185,9 @@
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::assist_satellite::common::trigger_for_name%]"
}
},
"name": "Satellite started processing"
@@ -187,6 +197,9 @@
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::assist_satellite::common::trigger_for_name%]"
}
},
"name": "Satellite started responding"

View File

@@ -13,6 +13,11 @@
- last
- any
translation_key: trigger_behavior
for:
required: true
default: 00:00:00
selector:
duration:
idle: *trigger_common
listening: *trigger_common

View File

@@ -157,7 +157,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2AuthorizeCallbackView
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util import dt as dt_util
from homeassistant.util.hass_dict import HassKey
@@ -173,7 +172,6 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
DELETE_CURRENT_TOKEN_DELAY = 2
@bind_hass
def create_auth_code(
hass: HomeAssistant, client_id: str, credential: Credentials
) -> str:

View File

@@ -83,7 +83,6 @@ from homeassistant.helpers.trace import (
trace_path,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.dt import parse_datetime
from homeassistant.util.hass_dict import HassKey
@@ -238,7 +237,6 @@ class IfAction(Protocol):
"""AND all conditions."""
@bind_hass
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
"""Return true if specified automation entity_id is on.

View File

@@ -3,6 +3,7 @@
"condition_behavior_name": "Condition passes if",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
"trigger_threshold_name": "Threshold type"
},
"conditions": {
@@ -87,6 +88,9 @@
"behavior": {
"name": "[%key:component::battery::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::battery::common::trigger_threshold_name%]"
}
@@ -98,6 +102,9 @@
"fields": {
"behavior": {
"name": "[%key:component::battery::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::trigger_for_name%]"
}
},
"name": "Battery low"
@@ -107,6 +114,9 @@
"fields": {
"behavior": {
"name": "[%key:component::battery::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::trigger_for_name%]"
}
},
"name": "Battery not low"
@@ -116,6 +126,9 @@
"fields": {
"behavior": {
"name": "[%key:component::battery::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::trigger_for_name%]"
}
},
"name": "Battery started charging"
@@ -125,6 +138,9 @@
"fields": {
"behavior": {
"name": "[%key:component::battery::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::trigger_for_name%]"
}
},
"name": "Battery stopped charging"

View File

@@ -9,6 +9,11 @@
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
selector:
duration:
.battery_threshold_entity: &battery_threshold_entity
- domain: input_number
@@ -42,21 +47,25 @@
low:
fields:
behavior: *trigger_behavior
for: *trigger_for
target: *trigger_target_battery
not_low:
fields:
behavior: *trigger_behavior
for: *trigger_for
target: *trigger_target_battery
started_charging:
fields:
behavior: *trigger_behavior
for: *trigger_for
target: *trigger_target_charging
stopped_charging:
fields:
behavior: *trigger_behavior
for: *trigger_for
target: *trigger_target_charging
level_changed:
@@ -74,6 +83,7 @@ level_crossed_threshold:
target: *trigger_target_percentage
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:

View File

@@ -58,7 +58,6 @@ from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.network import get_url
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, VolDictType
from homeassistant.loader import bind_hass
from .const import (
CAMERA_IMAGE_TIMEOUT,
@@ -163,7 +162,6 @@ class CameraCapabilities:
frontend_stream_types: set[StreamType]
@bind_hass
async def async_request_stream(hass: HomeAssistant, entity_id: str, fmt: str) -> str:
"""Request a stream for a camera entity."""
camera = get_camera_from_entity_id(hass, entity_id)
@@ -212,7 +210,6 @@ async def _async_get_image(
raise HomeAssistantError("Unable to get image")
@bind_hass
async def async_get_image(
hass: HomeAssistant,
entity_id: str,
@@ -247,14 +244,12 @@ async def _async_get_stream_image(
return None
@bind_hass
async def async_get_stream_source(hass: HomeAssistant, entity_id: str) -> str | None:
"""Fetch the stream source for a camera entity."""
camera = get_camera_from_entity_id(hass, entity_id)
return await camera.stream_source()
@bind_hass
async def async_get_mjpeg_stream(
hass: HomeAssistant, request: web.Request, entity_id: str
) -> web.StreamResponse | None:

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/camera",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["PyTurboJPEG==1.8.0"]
"requirements": ["PyTurboJPEG==2.2.0"]
}

View File

@@ -3,6 +3,7 @@
"condition_behavior_name": "Condition passes if",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
"trigger_threshold_name": "Threshold type"
},
"conditions": {
@@ -385,6 +386,9 @@
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::trigger_for_name%]"
},
"hvac_mode": {
"description": "The HVAC modes to trigger on.",
"name": "Modes"
@@ -397,6 +401,9 @@
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Climate-control device started cooling"
@@ -406,6 +413,9 @@
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Climate-control device started drying"
@@ -415,6 +425,9 @@
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Climate-control device started heating"
@@ -434,6 +447,9 @@
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::climate::common::trigger_threshold_name%]"
}
@@ -455,6 +471,9 @@
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::climate::common::trigger_threshold_name%]"
}
@@ -466,6 +485,9 @@
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Climate-control device turned off"
@@ -475,6 +497,9 @@
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Climate-control device turned on"

View File

@@ -13,6 +13,11 @@
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
selector:
duration:
.humidity_threshold_entity: &humidity_threshold_entity
- domain: input_number
@@ -50,6 +55,7 @@ hvac_mode_changed:
target: *trigger_climate_target
fields:
behavior: *trigger_behavior
for: *trigger_for
hvac_mode:
context:
filter_target: target
@@ -76,6 +82,7 @@ target_humidity_crossed_threshold:
target: *trigger_climate_target
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
@@ -101,6 +108,7 @@ target_temperature_crossed_threshold:
target: *trigger_climate_target
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:

View File

@@ -36,7 +36,7 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration, bind_hass
from homeassistant.loader import async_get_integration
from homeassistant.util.signal_type import SignalType
# Pre-import backup to avoid it being imported
@@ -181,7 +181,6 @@ class CloudConnectionState(Enum):
CLOUD_DISCONNECTED = "cloud_disconnected"
@bind_hass
@callback
def async_is_logged_in(hass: HomeAssistant) -> bool:
"""Test if user is logged in.
@@ -191,7 +190,6 @@ def async_is_logged_in(hass: HomeAssistant) -> bool:
return DATA_CLOUD in hass.data and hass.data[DATA_CLOUD].is_logged_in
@bind_hass
@callback
def async_is_connected(hass: HomeAssistant) -> bool:
"""Test if connected to the cloud."""
@@ -207,7 +205,6 @@ def async_listen_connection_change(
return async_dispatcher_connect(hass, SIGNAL_CLOUD_CONNECTION_STATE, target)
@bind_hass
@callback
def async_active_subscription(hass: HomeAssistant) -> bool:
"""Test if user has an active subscription."""
@@ -230,7 +227,6 @@ async def async_get_or_create_cloudhook(hass: HomeAssistant, webhook_id: str) ->
return await async_create_cloudhook(hass, webhook_id)
@bind_hass
async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str:
"""Create a cloudhook."""
if not async_is_connected(hass):
@@ -245,7 +241,6 @@ async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str:
return cloudhook_url
@bind_hass
async def async_delete_cloudhook(hass: HomeAssistant, webhook_id: str) -> None:
"""Delete a cloudhook."""
if DATA_CLOUD not in hass.data:
@@ -272,7 +267,6 @@ def async_listen_cloudhook_change(
)
@bind_hass
@callback
def async_remote_ui_url(hass: HomeAssistant) -> str:
"""Get the remote UI URL."""

View File

@@ -25,7 +25,6 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.async_ import run_callback_threadsafe
_KEY_INSTANCE = "configurator"
@@ -54,7 +53,6 @@ type ConfiguratorCallback = Callable[[list[dict[str, str]]], None]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
@bind_hass
@async_callback
def async_request_config(
hass: HomeAssistant,
@@ -93,7 +91,6 @@ def async_request_config(
return request_id
@bind_hass
def request_config(hass: HomeAssistant, *args: Any, **kwargs: Any) -> str:
"""Create a new request for configuration.
@@ -104,7 +101,6 @@ def request_config(hass: HomeAssistant, *args: Any, **kwargs: Any) -> str:
).result()
@bind_hass
@async_callback
def async_notify_errors(hass: HomeAssistant, request_id: str, error: str) -> None:
"""Add errors to a config request."""
@@ -112,7 +108,6 @@ def async_notify_errors(hass: HomeAssistant, request_id: str, error: str) -> Non
_get_requests(hass)[request_id].async_notify_errors(request_id, error)
@bind_hass
def notify_errors(hass: HomeAssistant, request_id: str, error: str) -> None:
"""Add errors to a config request."""
return run_callback_threadsafe(
@@ -120,7 +115,6 @@ def notify_errors(hass: HomeAssistant, request_id: str, error: str) -> None:
).result()
@bind_hass
@async_callback
def async_request_done(hass: HomeAssistant, request_id: str) -> None:
"""Mark a configuration request as done."""
@@ -128,7 +122,6 @@ def async_request_done(hass: HomeAssistant, request_id: str) -> None:
_get_requests(hass).pop(request_id).async_request_done(request_id)
@bind_hass
def request_done(hass: HomeAssistant, request_id: str) -> None:
"""Mark a configuration request as done."""
return run_callback_threadsafe(

View File

@@ -23,7 +23,6 @@ from homeassistant.helpers import config_validation as cv, intent
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from .agent_manager import (
AgentInfo,
@@ -127,7 +126,6 @@ CONFIG_SCHEMA = vol.Schema(
@callback
@bind_hass
def async_set_agent(
hass: HomeAssistant,
config_entry: ConfigEntry,
@@ -138,7 +136,6 @@ def async_set_agent(
@callback
@bind_hass
def async_unset_agent(
hass: HomeAssistant,
config_entry: ConfigEntry,

View File

@@ -1,6 +1,7 @@
{
"common": {
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"is_value": {
@@ -96,6 +97,9 @@
"fields": {
"behavior": {
"name": "[%key:component::counter::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::counter::common::trigger_for_name%]"
}
},
"name": "Counter reached maximum"
@@ -105,6 +109,9 @@
"fields": {
"behavior": {
"name": "[%key:component::counter::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::counter::common::trigger_for_name%]"
}
},
"name": "Counter reached minimum"
@@ -114,6 +121,9 @@
"fields": {
"behavior": {
"name": "[%key:component::counter::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::counter::common::trigger_for_name%]"
}
},
"name": "Counter reset"

View File

@@ -13,6 +13,11 @@
- first
- last
- any
for:
required: true
default: 00:00:00
selector:
duration:
incremented:
target:

View File

@@ -29,7 +29,6 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.hass_dict import HassKey
from .condition import make_cover_is_closed_condition, make_cover_is_open_condition
@@ -87,7 +86,6 @@ __all__ = [
]
@bind_hass
def is_closed(hass: HomeAssistant, entity_id: str) -> bool:
"""Return if the cover is closed based on the statemachine."""
return hass.states.is_state(entity_id, CoverState.CLOSED)

View File

@@ -1,7 +1,8 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"awning_is_closed": {
@@ -254,6 +255,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::trigger_for_name%]"
}
},
"name": "Awning closed"
@@ -263,6 +267,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::trigger_for_name%]"
}
},
"name": "Awning opened"
@@ -272,6 +279,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::trigger_for_name%]"
}
},
"name": "Blind closed"
@@ -281,6 +291,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::trigger_for_name%]"
}
},
"name": "Blind opened"
@@ -290,6 +303,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::trigger_for_name%]"
}
},
"name": "Curtain closed"
@@ -299,6 +315,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::trigger_for_name%]"
}
},
"name": "Curtain opened"
@@ -308,6 +327,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::trigger_for_name%]"
}
},
"name": "Shade closed"
@@ -317,6 +339,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::trigger_for_name%]"
}
},
"name": "Shade opened"
@@ -326,6 +351,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::trigger_for_name%]"
}
},
"name": "Shutter closed"
@@ -335,6 +363,9 @@
"fields": {
"behavior": {
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::cover::common::trigger_for_name%]"
}
},
"name": "Shutter opened"

View File

@@ -9,6 +9,11 @@
- first
- last
- any
for:
required: true
default: 00:00:00
selector:
duration:
awning_closed:
fields: *trigger_common_fields

View File

@@ -45,7 +45,7 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity):
"""Return minimum confidence for send events."""
return 80
def process_image(self, image: bytes) -> None:
async def async_process_image(self, image: bytes) -> None:
"""Process image."""
demo_data = [
FaceInformation(
@@ -58,4 +58,4 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity):
FaceInformation(confidence=62.53, name="Luna"),
]
self.process_faces(demo_data, 4)
self.async_process_faces(demo_data, 4)

View File

@@ -0,0 +1,57 @@
"""The Denon RS232 integration."""
from __future__ import annotations
from denon_rs232 import DenonReceiver, ReceiverState
from denon_rs232.models import MODELS
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_DEVICE, CONF_MODEL, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from .const import LOGGER, DenonRS232ConfigEntry
PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: DenonRS232ConfigEntry) -> bool:
"""Set up Denon RS232 from a config entry."""
port = entry.data[CONF_DEVICE]
model = MODELS[entry.data[CONF_MODEL]]
receiver = DenonReceiver(port, model=model)
try:
await receiver.connect()
await receiver.query_state()
except (ConnectionError, OSError, TimeoutError) as err:
LOGGER.error("Error connecting to Denon receiver at %s: %s", port, err)
if receiver.connected:
await receiver.disconnect()
raise ConfigEntryNotReady from err
entry.runtime_data = receiver
@callback
def _on_disconnect(state: ReceiverState | None) -> None:
# Only reload if the entry is still loaded. During entry removal,
# disconnect() fires this callback but the entry is already gone.
if state is None and entry.state is ConfigEntryState.LOADED:
LOGGER.warning("Denon receiver disconnected, reloading config entry")
hass.config_entries.async_schedule_reload(entry.entry_id)
entry.async_on_unload(receiver.subscribe(_on_disconnect))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: DenonRS232ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
await entry.runtime_data.disconnect()
return unload_ok

View File

@@ -0,0 +1,119 @@
"""Config flow for the Denon RS232 integration."""
from __future__ import annotations
from typing import Any
from denon_rs232 import DenonReceiver
from denon_rs232.models import MODELS
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_DEVICE, CONF_MODEL
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
SerialSelector,
)
from .const import DOMAIN, LOGGER
CONF_MODEL_NAME = "model_name"
# Build a flat list of (model_key, individual_name) pairs by splitting
# grouped names like "AVR-3803 / AVC-3570 / AVR-2803" into separate entries.
# Sorted alphabetically with "Other" at the bottom.
MODEL_OPTIONS: list[tuple[str, str]] = sorted(
(
(_key, _name)
for _key, _model in MODELS.items()
if _key != "other"
for _name in _model.name.split(" / ")
),
key=lambda x: x[1],
)
MODEL_OPTIONS.append(("other", "Other"))
async def _async_attempt_connect(port: str, model_key: str) -> str | None:
"""Attempt to connect to the receiver at the given port.
Returns None on success, error on failure.
"""
model = MODELS[model_key]
receiver = DenonReceiver(port, model=model)
try:
await receiver.connect()
except (
# When the port contains invalid connection data
ValueError,
# If it is a remote port, and we cannot connect
ConnectionError,
OSError,
TimeoutError,
):
return "cannot_connect"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception")
return "unknown"
else:
await receiver.disconnect()
return None
class DenonRS232ConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Denon RS232."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
model_key, _, model_name = user_input[CONF_MODEL].partition(":")
resolved_name = model_name if model_key != "other" else None
self._async_abort_entries_match({CONF_DEVICE: user_input[CONF_DEVICE]})
error = await _async_attempt_connect(user_input[CONF_DEVICE], model_key)
if not error:
return self.async_create_entry(
title=resolved_name or "Denon Receiver",
data={
CONF_DEVICE: user_input[CONF_DEVICE],
CONF_MODEL: model_key,
CONF_MODEL_NAME: resolved_name,
},
)
errors["base"] = error
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_MODEL): SelectSelector(
SelectSelectorConfig(
options=[
SelectOptionDict(
value=f"{key}:{name}",
label=name,
)
for key, name in MODEL_OPTIONS
],
mode=SelectSelectorMode.DROPDOWN,
translation_key="model",
)
),
vol.Required(CONF_DEVICE): SerialSelector(),
}
),
user_input or {},
),
errors=errors,
)

View File

@@ -0,0 +1,12 @@
"""Constants for the Denon RS232 integration."""
import logging
from denon_rs232 import DenonReceiver
from homeassistant.config_entries import ConfigEntry
LOGGER = logging.getLogger(__package__)
DOMAIN = "denon_rs232"
type DenonRS232ConfigEntry = ConfigEntry[DenonReceiver]

View File

@@ -0,0 +1,13 @@
{
"domain": "denon_rs232",
"name": "Denon RS232",
"codeowners": ["@balloob"],
"config_flow": true,
"dependencies": ["usb"],
"documentation": "https://www.home-assistant.io/integrations/denon_rs232",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["denon_rs232"],
"quality_scale": "bronze",
"requirements": ["denon-rs232==4.1.0"]
}

View File

@@ -0,0 +1,235 @@
"""Media player platform for the Denon RS232 integration."""
from __future__ import annotations
from typing import Literal, cast
from denon_rs232 import (
MIN_VOLUME_DB,
VOLUME_DB_RANGE,
DenonReceiver,
InputSource,
MainPlayer,
ReceiverState,
ZonePlayer,
)
from homeassistant.components.media_player import (
MediaPlayerDeviceClass,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .config_flow import CONF_MODEL_NAME
from .const import DOMAIN, DenonRS232ConfigEntry
INPUT_SOURCE_DENON_TO_HA: dict[InputSource, str] = {
InputSource.PHONO: "phono",
InputSource.CD: "cd",
InputSource.TUNER: "tuner",
InputSource.DVD: "dvd",
InputSource.VDP: "vdp",
InputSource.TV: "tv",
InputSource.DBS_SAT: "dbs_sat",
InputSource.VCR_1: "vcr_1",
InputSource.VCR_2: "vcr_2",
InputSource.VCR_3: "vcr_3",
InputSource.V_AUX: "v_aux",
InputSource.CDR_TAPE1: "cdr_tape1",
InputSource.MD_TAPE2: "md_tape2",
InputSource.HDP: "hdp",
InputSource.DVR: "dvr",
InputSource.TV_CBL: "tv_cbl",
InputSource.SAT: "sat",
InputSource.NET_USB: "net_usb",
InputSource.DOCK: "dock",
InputSource.IPOD: "ipod",
InputSource.BD: "bd",
InputSource.SAT_CBL: "sat_cbl",
InputSource.MPLAY: "mplay",
InputSource.GAME: "game",
InputSource.AUX1: "aux1",
InputSource.AUX2: "aux2",
InputSource.NET: "net",
InputSource.BT: "bt",
InputSource.USB_IPOD: "usb_ipod",
InputSource.EIGHT_K: "eight_k",
InputSource.PANDORA: "pandora",
InputSource.SIRIUSXM: "siriusxm",
InputSource.SPOTIFY: "spotify",
InputSource.FLICKR: "flickr",
InputSource.IRADIO: "iradio",
InputSource.SERVER: "server",
InputSource.FAVORITES: "favorites",
InputSource.LASTFM: "lastfm",
InputSource.XM: "xm",
InputSource.SIRIUS: "sirius",
InputSource.HDRADIO: "hdradio",
InputSource.DAB: "dab",
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: DenonRS232ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Denon RS232 media player."""
receiver = config_entry.runtime_data
entities = [DenonRS232MediaPlayer(receiver, receiver.main, config_entry, "main")]
if receiver.zone_2.power is not None:
entities.append(
DenonRS232MediaPlayer(receiver, receiver.zone_2, config_entry, "zone_2")
)
if receiver.zone_3.power is not None:
entities.append(
DenonRS232MediaPlayer(receiver, receiver.zone_3, config_entry, "zone_3")
)
async_add_entities(entities)
class DenonRS232MediaPlayer(MediaPlayerEntity):
"""Representation of a Denon receiver controlled over RS232."""
_attr_device_class = MediaPlayerDeviceClass.RECEIVER
_attr_has_entity_name = True
_attr_translation_key = "receiver"
_attr_should_poll = False
_volume_min = MIN_VOLUME_DB
_volume_range = VOLUME_DB_RANGE
def __init__(
self,
receiver: DenonReceiver,
player: MainPlayer | ZonePlayer,
config_entry: DenonRS232ConfigEntry,
zone: Literal["main", "zone_2", "zone_3"],
) -> None:
"""Initialize the media player."""
self._receiver = receiver
self._player = player
self._is_main = zone == "main"
model = receiver.model
assert model is not None # We always set this
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, config_entry.entry_id)},
manufacturer="Denon",
model_id=config_entry.data.get(CONF_MODEL_NAME),
)
self._attr_unique_id = f"{config_entry.entry_id}_{zone}"
self._attr_source_list = sorted(
INPUT_SOURCE_DENON_TO_HA[source] for source in model.input_sources
)
self._attr_supported_features = (
MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.SELECT_SOURCE
)
if zone == "main":
self._attr_name = None
self._attr_supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
else:
self._attr_name = "Zone 2" if zone == "zone_2" else "Zone 3"
self._async_update_from_player()
async def async_added_to_hass(self) -> None:
"""Subscribe to receiver state updates."""
self.async_on_remove(self._receiver.subscribe(self._async_on_state_update))
@callback
def _async_on_state_update(self, state: ReceiverState | None) -> None:
"""Handle a state update from the receiver."""
if state is None:
self._attr_available = False
else:
self._attr_available = True
self._async_update_from_player()
self.async_write_ha_state()
@callback
def _async_update_from_player(self) -> None:
"""Update entity attributes from the shared player object."""
if self._player.power is None:
self._attr_state = None
else:
self._attr_state = (
MediaPlayerState.ON if self._player.power else MediaPlayerState.OFF
)
source = self._player.input_source
self._attr_source = INPUT_SOURCE_DENON_TO_HA.get(source) if source else None
volume_min = self._player.volume_min
volume_max = self._player.volume_max
if volume_min is not None:
self._volume_min = volume_min
if volume_max is not None and volume_max > volume_min:
self._volume_range = volume_max - volume_min
volume = self._player.volume
if volume is not None:
self._attr_volume_level = (volume - self._volume_min) / self._volume_range
else:
self._attr_volume_level = None
if self._is_main:
self._attr_is_volume_muted = cast(MainPlayer, self._player).mute
async def async_turn_on(self) -> None:
"""Turn the receiver on."""
await self._player.power_on()
async def async_turn_off(self) -> None:
"""Turn the receiver off."""
await self._player.power_standby()
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
db = volume * self._volume_range + self._volume_min
await self._player.set_volume(db)
async def async_volume_up(self) -> None:
"""Volume up."""
await self._player.volume_up()
async def async_volume_down(self) -> None:
"""Volume down."""
await self._player.volume_down()
async def async_mute_volume(self, mute: bool) -> None:
"""Mute or unmute."""
player = cast(MainPlayer, self._player)
if mute:
await player.mute_on()
else:
await player.mute_off()
async def async_select_source(self, source: str) -> None:
"""Select input source."""
input_source = next(
(
input_source
for input_source, ha_source in INPUT_SOURCE_DENON_TO_HA.items()
if ha_source == source
),
None,
)
if input_source is None:
raise HomeAssistantError("Invalid source")
await self._player.select_input_source(input_source)

View File

@@ -0,0 +1,64 @@
rules:
# Bronze
action-setup: done
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: "The integration does not create dynamic devices."
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices:
status: exempt
comment: "The integration does not create devices that can become stale."
# Platinum
async-dependency: done
inject-websession: todo
strict-typing: todo

View File

@@ -0,0 +1,84 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"device": "[%key:common::config_flow::data::port%]",
"model": "Receiver model"
},
"data_description": {
"device": "Serial port path to connect to",
"model": "Determines available features"
}
}
}
},
"entity": {
"media_player": {
"receiver": {
"state_attributes": {
"source": {
"state": {
"aux1": "Aux 1",
"aux2": "Aux 2",
"bd": "BD Player",
"bt": "Bluetooth",
"cd": "CD",
"cdr_tape1": "CDR/Tape 1",
"dab": "DAB",
"dbs_sat": "DBS/Sat",
"dock": "Dock",
"dvd": "DVD",
"dvr": "DVR",
"eight_k": "8K",
"favorites": "Favorites",
"flickr": "Flickr",
"game": "Game",
"hdp": "HDP",
"hdradio": "HD Radio",
"ipod": "iPod",
"iradio": "Internet Radio",
"lastfm": "Last.fm",
"md_tape2": "MD/Tape 2",
"mplay": "Media Player",
"net": "HEOS Music",
"net_usb": "Network/USB",
"pandora": "Pandora",
"phono": "Phono",
"sat": "Sat",
"sat_cbl": "Satellite/Cable",
"server": "Server",
"sirius": "Sirius",
"siriusxm": "SiriusXM",
"spotify": "Spotify",
"tuner": "Tuner",
"tv": "TV Audio",
"tv_cbl": "TV/Cable",
"usb_ipod": "USB/iPod",
"v_aux": "V. Aux",
"vcr_1": "VCR 1",
"vcr_2": "VCR 2",
"vcr_3": "VCR 3",
"vdp": "VDP",
"xm": "XM"
}
}
}
}
}
},
"selector": {
"model": {
"options": {
"other": "Other"
}
}
}
}

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from .config_entry import ( # noqa: F401
ScannerEntity,
@@ -52,7 +51,6 @@ from .legacy import ( # noqa: F401
)
@bind_hass
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
"""Return the state if any or a specified device is home."""
return hass.states.is_state(entity_id, STATE_HOME)

View File

@@ -1,7 +1,8 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"is_home": {
@@ -126,6 +127,9 @@
"fields": {
"behavior": {
"name": "[%key:component::device_tracker::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::device_tracker::common::trigger_for_name%]"
}
},
"name": "Entered home"
@@ -135,6 +139,9 @@
"fields": {
"behavior": {
"name": "[%key:component::device_tracker::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::device_tracker::common::trigger_for_name%]"
}
},
"name": "Left home"

View File

@@ -13,6 +13,11 @@
- last
- any
translation_key: trigger_behavior
for:
required: true
default: 00:00:00
selector:
duration:
entered_home: *trigger_common
left_home: *trigger_common

View File

@@ -1,7 +1,8 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"is_closed": {
@@ -45,6 +46,9 @@
"fields": {
"behavior": {
"name": "[%key:component::door::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::door::common::trigger_for_name%]"
}
},
"name": "Door closed"
@@ -54,6 +58,9 @@
"fields": {
"behavior": {
"name": "[%key:component::door::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::door::common::trigger_for_name%]"
}
},
"name": "Door opened"

View File

@@ -9,6 +9,11 @@
- first
- last
- any
for:
required: true
default: 00:00:00
selector:
duration:
closed:
fields: *trigger_common_fields

View File

@@ -87,6 +87,7 @@ class MbusDeviceType(IntEnum):
GAS = 3
HEAT = 4
WATER = 7
HEAT_COOL = 12
SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
@@ -571,6 +572,16 @@ SENSORS_MBUS_DEVICE_TYPE: dict[int, tuple[DSMRSensorEntityDescription, ...]] = {
state_class=SensorStateClass.TOTAL_INCREASING,
),
),
MbusDeviceType.HEAT_COOL: (
DSMRSensorEntityDescription(
key="heat_reading",
translation_key="heat_meter_reading",
obis_reference="MBUS_METER_READING",
is_heat=True,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
),
}

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["duco"],
"quality_scale": "bronze",
"requirements": ["python-duco-client==0.3.0"]
"requirements": ["python-duco-client==0.3.1"]
}

View File

@@ -18,6 +18,7 @@ from .coordinator import CometBlueConfigEntry, CometBlueDataUpdateCoordinator
PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.CLIMATE,
Platform.SENSOR,
]

View File

@@ -32,6 +32,7 @@ class CometBlueCoordinatorData:
temperatures: dict[str, float | int] = field(default_factory=dict)
holiday: dict = field(default_factory=dict)
battery: int | None = None
class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorData]):
@@ -53,6 +54,7 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
)
self.device = cometblue
self.address = cometblue.client.address
self.data = CometBlueCoordinatorData()
async def send_command(
self,
@@ -64,11 +66,11 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
LOGGER.debug("Updating device %s with '%s'", self.name, payload)
retry_count = 0
while retry_count < MAX_RETRIES:
retry_count += 1
try:
async with self.device:
return await function(**payload)
except (InvalidByteValueError, TimeoutError, BleakError) as ex:
retry_count += 1
if retry_count >= MAX_RETRIES:
raise HomeAssistantError(
f"Error sending command to '{self.name}': {ex}"
@@ -88,20 +90,23 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
async def _async_update_data(self) -> CometBlueCoordinatorData:
"""Poll the device."""
data: CometBlueCoordinatorData = CometBlueCoordinatorData()
data = CometBlueCoordinatorData()
retry_count = 0
while retry_count < MAX_RETRIES and not data.temperatures:
try:
retry_count += 1
async with self.device:
# temperatures are required and must trigger a retry if not available
if not data.temperatures:
data.temperatures = await self.device.get_temperature_async()
# holiday is optional and should not trigger a retry
# holiday and battery are optional and should not trigger a retry
try:
if not data.holiday:
data.holiday = await self.device.get_holiday_async(1) or {}
if not data.battery:
data.battery = await self.device.get_battery_async()
except InvalidByteValueError as ex:
LOGGER.warning(
"Failed to retrieve optional data for %s: %s (%s)",
@@ -110,7 +115,6 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
ex,
)
except (InvalidByteValueError, TimeoutError, BleakError) as ex:
retry_count += 1
if retry_count >= MAX_RETRIES:
raise UpdateFailed(
f"Error retrieving data: {ex}", retry_after=30
@@ -128,5 +132,9 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
) from ex
# If one value was not retrieved correctly, keep the old value
if not data.holiday:
data.holiday = self.data.holiday
if not data.battery:
data.battery = self.data.battery
LOGGER.debug("Received data for %s: %s", self.name, data)
return data

View File

@@ -0,0 +1,53 @@
"""Comet Blue sensor integration."""
from __future__ import annotations
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import CometBlueConfigEntry, CometBlueDataUpdateCoordinator
from .entity import CometBlueBluetoothEntity
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: CometBlueConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the client entities."""
coordinator = entry.runtime_data
entities = [CometBlueBatterySensorEntity(coordinator)]
async_add_entities(entities)
class CometBlueBatterySensorEntity(CometBlueBluetoothEntity, SensorEntity):
"""Representation of a sensor."""
def __init__(
self,
coordinator: CometBlueDataUpdateCoordinator,
) -> None:
"""Initialize CometBlueSensorEntity."""
super().__init__(coordinator)
self.entity_description = SensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
)
self._attr_unique_id = f"{coordinator.address}-{self.entity_description.key}"
@property
def native_value(self) -> float | None:
"""Return the entity value to represent the entity state."""
return self.coordinator.data.battery

View File

@@ -39,9 +39,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from .const import ATTR_DURATION, ATTR_PERIOD, DOMAIN, EVOHOME_DATA, EvoService
from .const import (
ATTR_DURATION,
ATTR_PERIOD,
DOMAIN,
EVOHOME_DATA,
RESET_BREAKS_IN_HA_VERSION,
EvoService,
)
from .coordinator import EvoDataUpdateCoordinator
from .entity import EvoChild, EvoEntity, is_valid_zone
from .helpers import async_create_deprecation_issue_once
_LOGGER = logging.getLogger(__name__)
@@ -185,6 +193,11 @@ class EvoZone(EvoChild, EvoClimateEntity):
async def async_clear_zone_override(self) -> None:
"""Clear the zone override (if any) and return to following its schedule."""
async_create_deprecation_issue_once(
self.hass,
"deprecated_clear_zone_override_service",
RESET_BREAKS_IN_HA_VERSION,
)
await self.coordinator.call_client_api(self._evo_device.reset())
async def async_set_zone_override(
@@ -447,6 +460,13 @@ class EvoController(EvoClimateEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode; if None, then revert to 'Auto' mode."""
if preset_mode == PRESET_RESET:
async_create_deprecation_issue_once(
self.hass,
"deprecated_preset_reset",
RESET_BREAKS_IN_HA_VERSION,
)
await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EvoSystemMode.AUTO))
@callback

View File

@@ -26,6 +26,9 @@ ATTR_DURATION: Final = "duration" # number of minutes, <24h
ATTR_PERIOD: Final = "period" # number of days
ATTR_SETPOINT: Final = "setpoint"
# Support for the reset service calls/presets is being deprecated
RESET_BREAKS_IN_HA_VERSION: Final = "2026.11.0"
@unique
class EvoService(StrEnum):

View File

@@ -0,0 +1,36 @@
"""Helpers for the Evohome integration."""
from __future__ import annotations
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import issue_registry as ir
from .const import DOMAIN
@callback
def async_create_deprecation_issue_once(
hass: HomeAssistant,
issue_id: str,
breaks_in_ha_version: str,
translation_key: str | None = None,
translation_placeholders: dict[str, str] | None = None,
) -> None:
"""Create or update a deprecation issue entry."""
placeholders = {
**(translation_placeholders or {}),
"breaks_in_ha_version": breaks_in_ha_version,
}
ir.async_get(hass).async_get_or_create(
DOMAIN,
issue_id,
breaks_in_ha_version=breaks_in_ha_version,
is_fixable=False,
is_persistent=True,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key=translation_key or issue_id,
translation_placeholders=placeholders,
)

View File

@@ -22,8 +22,16 @@ from homeassistant.helpers import config_validation as cv, service
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.service import verify_domain_control
from .const import ATTR_DURATION, ATTR_PERIOD, ATTR_SETPOINT, DOMAIN, EvoService
from .const import (
ATTR_DURATION,
ATTR_PERIOD,
ATTR_SETPOINT,
DOMAIN,
RESET_BREAKS_IN_HA_VERSION,
EvoService,
)
from .coordinator import EvoDataUpdateCoordinator
from .helpers import async_create_deprecation_issue_once
# System service schemas (registered as domain services)
SET_SYSTEM_MODE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {
@@ -158,6 +166,13 @@ def setup_service_functions(
# via that service call may be able to emulate the reset even if the system
# doesn't support AutoWithReset natively
if call.service == EvoService.RESET_SYSTEM:
async_create_deprecation_issue_once(
hass,
"deprecated_reset_system_service",
RESET_BREAKS_IN_HA_VERSION,
)
if call.service == EvoService.SET_SYSTEM_MODE:
_validate_set_system_mode_params(coordinator.tcs, call.data)

View File

@@ -19,9 +19,23 @@
"message": "Only zones support the `{service}` action"
}
},
"issues": {
"deprecated_clear_zone_override_service": {
"description": "Using the `clear_zone_override` action is deprecated and will stop working in Home Assistant {breaks_in_ha_version}. Use the zone's Reset button instead.",
"title": "Evohome clear zone override action is deprecated"
},
"deprecated_preset_reset": {
"description": "Using the `Reset` preset on an Evohome controller is deprecated and will stop working in Home Assistant {breaks_in_ha_version}. Use the system's Reset button instead.",
"title": "Evohome Reset preset is deprecated"
},
"deprecated_reset_system_service": {
"description": "Using the `reset_system` action is deprecated and will stop working in Home Assistant {breaks_in_ha_version}. Use the system's Reset button instead.",
"title": "Evohome reset system action is deprecated"
}
},
"services": {
"clear_zone_override": {
"description": "Sets the zone to follow its schedule.",
"description": "Sets a zone to follow its schedule (deprecated).",
"name": "Clear zone override"
},
"refresh_system": {
@@ -29,11 +43,11 @@
"name": "Refresh system"
},
"reset_system": {
"description": "Sets the system mode to `Auto` mode and resets all the zones to follow their schedules. Not all Evohome systems support this feature (i.e. `AutoWithReset` mode).",
"description": "Sets a system's mode to `Auto` mode and resets all its zones to follow their schedules (deprecated). Some older systems may not support this feature.",
"name": "Reset system"
},
"set_dhw_override": {
"description": "Overrides the DHW state, either indefinitely or for a specified duration, after which it will revert to following its schedule.",
"description": "Overrides a DHW's state, either indefinitely or for a specified duration, after which it will revert to following its schedule.",
"fields": {
"duration": {
"description": "The DHW will revert to its schedule after this time. If 0 the change is until the next scheduled setpoint.",
@@ -47,7 +61,7 @@
"name": "Set DHW override"
},
"set_system_mode": {
"description": "Sets the system mode, either indefinitely or until a specified end time, after which it will revert to `Auto`. Not all systems support all modes.",
"description": "Sets a system's mode, either indefinitely or until a specified end time, after which it will revert to `Auto`. Not all systems support all modes.",
"fields": {
"duration": {
"description": "The duration in hours; used only with `AutoWithEco` mode (up to 24 hours).",
@@ -65,7 +79,7 @@
"name": "Set system mode"
},
"set_zone_override": {
"description": "Overrides the zone setpoint, either indefinitely or for a specified duration, after which it will revert to following its schedule.",
"description": "Overrides a zone's setpoint, either indefinitely or for a specified duration, after which it will revert to following its schedule.",
"fields": {
"duration": {
"description": "The zone will revert to its schedule after this time. If 0 the change is until the next scheduled setpoint.",

View File

@@ -25,7 +25,6 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.percentage import (
percentage_to_ranged_value,
@@ -88,7 +87,6 @@ class NotValidPresetModeError(ServiceValidationError):
)
@bind_hass
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
"""Return if the fans are on based on the statemachine."""
entity = hass.states.get(entity_id)

View File

@@ -1,7 +1,8 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"is_off": {
@@ -196,6 +197,9 @@
"fields": {
"behavior": {
"name": "[%key:component::fan::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::fan::common::trigger_for_name%]"
}
},
"name": "Fan turned off"
@@ -205,6 +209,9 @@
"fields": {
"behavior": {
"name": "[%key:component::fan::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::fan::common::trigger_for_name%]"
}
},
"name": "Fan turned on"

View File

@@ -13,6 +13,11 @@
- last
- any
translation_key: trigger_behavior
for:
required: true
default: 00:00:00
selector:
duration:
turned_on: *trigger_common
turned_off: *trigger_common

View File

@@ -20,7 +20,6 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.system_info import is_official_image
from .const import (
@@ -71,7 +70,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
@bind_hass
def get_ffmpeg_manager(hass: HomeAssistant) -> FFmpegManager:
"""Return the FFmpegManager."""
if DATA_FFMPEG not in hass.data:
@@ -79,7 +77,6 @@ def get_ffmpeg_manager(hass: HomeAssistant) -> FFmpegManager:
return hass.data[DATA_FFMPEG]
@bind_hass
async def async_get_image(
hass: HomeAssistant,
input_source: str,

View File

@@ -9,7 +9,7 @@
"iot_class": "local_polling",
"loggers": ["fritzconnection"],
"quality_scale": "gold",
"requirements": ["fritzconnection[qr]==1.15.1", "xmltodict==1.0.2"],
"requirements": ["fritzconnection[qr]==1.15.1", "xmltodict==1.0.4"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:fritzbox:1"

View File

@@ -34,7 +34,7 @@ from homeassistant.helpers.json import json_dumps_sorted
from homeassistant.helpers.storage import Store
from homeassistant.helpers.translation import async_get_translations
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration, bind_hass
from homeassistant.loader import async_get_integration
from homeassistant.util.hass_dict import HassKey
from .pr_download import download_pr_artifact
@@ -354,7 +354,6 @@ class Panel:
return response
@bind_hass
@callback
def async_register_built_in_panel(
hass: HomeAssistant,
@@ -393,7 +392,6 @@ def async_register_built_in_panel(
hass.bus.async_fire(EVENT_PANELS_UPDATED)
@bind_hass
@callback
def async_remove_panel(
hass: HomeAssistant, frontend_url_path: str, *, warn_if_unknown: bool = True

View File

@@ -1,7 +1,8 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"is_closed": {
@@ -45,6 +46,9 @@
"fields": {
"behavior": {
"name": "[%key:component::garage_door::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::garage_door::common::trigger_for_name%]"
}
},
"name": "Garage door closed"
@@ -54,6 +58,9 @@
"fields": {
"behavior": {
"name": "[%key:component::garage_door::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::garage_door::common::trigger_for_name%]"
}
},
"name": "Garage door opened"

View File

@@ -9,6 +9,11 @@
- first
- last
- any
for:
required: true
default: 00:00:00
selector:
duration:
closed:
fields: *trigger_common_fields

View File

@@ -1,7 +1,8 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"is_closed": {
@@ -45,6 +46,9 @@
"fields": {
"behavior": {
"name": "[%key:component::gate::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::gate::common::trigger_for_name%]"
}
},
"name": "Gate closed"
@@ -54,6 +58,9 @@
"fields": {
"behavior": {
"name": "[%key:component::gate::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::gate::common::trigger_for_name%]"
}
},
"name": "Gate opened"

View File

@@ -9,6 +9,11 @@
- first
- last
- any
for:
required: true
default: 00:00:00
selector:
duration:
closed:
fields: *trigger_common_fields

View File

@@ -28,7 +28,6 @@ from homeassistant.helpers.group import (
)
from homeassistant.helpers.reload import async_reload_integration_platforms
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
#
# Below we ensure the config_flow is imported so it does not need the import
@@ -103,7 +102,6 @@ CONFIG_SCHEMA = vol.Schema(
)
@bind_hass
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
"""Test if the group state is in its ON-state."""
if REG_KEY not in hass.data:
@@ -117,11 +115,10 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool:
# expand_entity_ids and get_entity_ids are for backwards compatibility only
expand_entity_ids = bind_hass(_expand_entity_ids)
get_entity_ids = bind_hass(_get_entity_ids)
expand_entity_ids = _expand_entity_ids
get_entity_ids = _get_entity_ids
@bind_hass
def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Get all groups that contain this entity.

View File

@@ -72,7 +72,7 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
key="mix_export_to_grid",
translation_key="mix_export_to_grid",
api_key="pacToGridTotal",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -80,7 +80,7 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
key="mix_import_from_grid",
translation_key="mix_import_from_grid",
api_key="pacToUserR",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),

View File

@@ -26,7 +26,6 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.loader import bind_hass
from .const import (
ATTR_AUTO_UPDATE,
@@ -74,7 +73,6 @@ _LOGGER = logging.getLogger(__name__)
@callback
@bind_hass
def get_info(hass: HomeAssistant) -> dict[str, Any] | None:
"""Return generic information from Supervisor.
@@ -84,7 +82,6 @@ def get_info(hass: HomeAssistant) -> dict[str, Any] | None:
@callback
@bind_hass
def get_host_info(hass: HomeAssistant) -> dict[str, Any] | None:
"""Return generic host information.
@@ -94,7 +91,6 @@ def get_host_info(hass: HomeAssistant) -> dict[str, Any] | None:
@callback
@bind_hass
def get_store(hass: HomeAssistant) -> dict[str, Any] | None:
"""Return store information.
@@ -104,7 +100,6 @@ def get_store(hass: HomeAssistant) -> dict[str, Any] | None:
@callback
@bind_hass
def get_supervisor_info(hass: HomeAssistant) -> dict[str, Any] | None:
"""Return Supervisor information.
@@ -114,7 +109,6 @@ def get_supervisor_info(hass: HomeAssistant) -> dict[str, Any] | None:
@callback
@bind_hass
def get_network_info(hass: HomeAssistant) -> dict[str, Any] | None:
"""Return Host Network information.
@@ -124,7 +118,6 @@ def get_network_info(hass: HomeAssistant) -> dict[str, Any] | None:
@callback
@bind_hass
def get_addons_info(hass: HomeAssistant) -> dict[str, dict[str, Any] | None] | None:
"""Return Addons info.
@@ -143,7 +136,6 @@ def get_addons_list(hass: HomeAssistant) -> list[dict[str, Any]] | None:
@callback
@bind_hass
def get_addons_stats(hass: HomeAssistant) -> dict[str, dict[str, Any] | None]:
"""Return Addons stats.
@@ -153,7 +145,6 @@ def get_addons_stats(hass: HomeAssistant) -> dict[str, dict[str, Any] | None]:
@callback
@bind_hass
def get_core_stats(hass: HomeAssistant) -> dict[str, Any]:
"""Return core stats.
@@ -163,7 +154,6 @@ def get_core_stats(hass: HomeAssistant) -> dict[str, Any]:
@callback
@bind_hass
def get_supervisor_stats(hass: HomeAssistant) -> dict[str, Any]:
"""Return supervisor stats.
@@ -173,7 +163,6 @@ def get_supervisor_stats(hass: HomeAssistant) -> dict[str, Any]:
@callback
@bind_hass
def get_os_info(hass: HomeAssistant) -> dict[str, Any] | None:
"""Return OS information.
@@ -183,7 +172,6 @@ def get_os_info(hass: HomeAssistant) -> dict[str, Any] | None:
@callback
@bind_hass
def get_core_info(hass: HomeAssistant) -> dict[str, Any] | None:
"""Return Home Assistant Core information from Supervisor.
@@ -193,7 +181,6 @@ def get_core_info(hass: HomeAssistant) -> dict[str, Any] | None:
@callback
@bind_hass
def get_issues_info(hass: HomeAssistant) -> SupervisorIssues | None:
"""Return Supervisor issues info.

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Any
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from .const import DOMAIN, EVENT_HDMI_CEC_UNAVAILABLE
@@ -55,9 +56,10 @@ class CecEntity(Entity):
else:
self._attr_name = f"{self._device.type_name} {self._logical_address} ({self._device.osd_name})"
@callback
def _hdmi_cec_unavailable(self, callback_event):
self._attr_available = False
self.schedule_update_ha_state(False)
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Register HDMI callbacks after initialization."""

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
from typing import Any
from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand
from pycec.const import (
@@ -31,7 +30,6 @@ from homeassistant.components.media_player import (
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -45,20 +43,20 @@ _LOGGER = logging.getLogger(__name__)
ENTITY_ID_FORMAT = MP_DOMAIN + ".{}"
def setup_platform(
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Find and return HDMI devices as +switches."""
"""Find and return HDMI devices as media players."""
if discovery_info and ATTR_NEW in discovery_info:
_LOGGER.debug("Setting up HDMI devices %s", discovery_info[ATTR_NEW])
entities = []
for device in discovery_info[ATTR_NEW]:
hdmi_device = hass.data[DOMAIN][device]
entities.append(CecPlayerEntity(hdmi_device, hdmi_device.logical_address))
add_entities(entities, True)
async_add_entities(entities, True)
class CecPlayerEntity(CecEntity, MediaPlayerEntity):
@@ -79,78 +77,61 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity):
def send_playback(self, key):
"""Send playback status to CEC adapter."""
self._device.async_send_command(CecCommand(key, dst=self._logical_address))
self._device.send_command(CecCommand(key, dst=self._logical_address))
def mute_volume(self, mute: bool) -> None:
async def async_mute_volume(self, mute: bool) -> None:
"""Mute volume."""
self.send_keypress(KEY_MUTE_TOGGLE)
def media_previous_track(self) -> None:
async def async_media_previous_track(self) -> None:
"""Go to previous track."""
self.send_keypress(KEY_BACKWARD)
def turn_on(self) -> None:
async def async_turn_on(self) -> None:
"""Turn device on."""
self._device.turn_on()
self._attr_state = MediaPlayerState.ON
self.async_write_ha_state()
def clear_playlist(self) -> None:
"""Clear players playlist."""
raise NotImplementedError
def turn_off(self) -> None:
async def async_turn_off(self) -> None:
"""Turn device off."""
self._device.turn_off()
self._attr_state = MediaPlayerState.OFF
self.async_write_ha_state()
def media_stop(self) -> None:
async def async_media_stop(self) -> None:
"""Stop playback."""
self.send_keypress(KEY_STOP)
self._attr_state = MediaPlayerState.IDLE
self.async_write_ha_state()
def play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
) -> None:
"""Not supported."""
raise NotImplementedError
def media_next_track(self) -> None:
async def async_media_next_track(self) -> None:
"""Skip to next track."""
self.send_keypress(KEY_FORWARD)
def media_seek(self, position: float) -> None:
"""Not supported."""
raise NotImplementedError
def set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
raise NotImplementedError
def media_pause(self) -> None:
async def async_media_pause(self) -> None:
"""Pause playback."""
self.send_keypress(KEY_PAUSE)
self._attr_state = MediaPlayerState.PAUSED
self.async_write_ha_state()
def select_source(self, source: str) -> None:
"""Not supported."""
raise NotImplementedError
def media_play(self) -> None:
async def async_media_play(self) -> None:
"""Start playback."""
self.send_keypress(KEY_PLAY)
self._attr_state = MediaPlayerState.PLAYING
self.async_write_ha_state()
def volume_up(self) -> None:
async def async_volume_up(self) -> None:
"""Increase volume."""
_LOGGER.debug("%s: volume up", self._logical_address)
self.send_keypress(KEY_VOLUME_UP)
def volume_down(self) -> None:
async def async_volume_down(self) -> None:
"""Decrease volume."""
_LOGGER.debug("%s: volume down", self._logical_address)
self.send_keypress(KEY_VOLUME_DOWN)
def update(self) -> None:
async def async_update(self) -> None:
"""Update device status."""
device = self._device
if device.power_status in [POWER_OFF, 3]:

View File

@@ -20,10 +20,10 @@ _LOGGER = logging.getLogger(__name__)
ENTITY_ID_FORMAT = SWITCH_DOMAIN + ".{}"
def setup_platform(
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Find and return HDMI devices as switches."""
@@ -33,7 +33,7 @@ def setup_platform(
for device in discovery_info[ATTR_NEW]:
hdmi_device = hass.data[DOMAIN][device]
entities.append(CecSwitchEntity(hdmi_device, hdmi_device.logical_address))
add_entities(entities, True)
async_add_entities(entities, True)
class CecSwitchEntity(CecEntity, SwitchEntity):
@@ -44,19 +44,19 @@ class CecSwitchEntity(CecEntity, SwitchEntity):
CecEntity.__init__(self, device, logical)
self.entity_id = f"{SWITCH_DOMAIN}.hdmi_{hex(self._logical_address)[2:]}"
def turn_on(self, **kwargs: Any) -> None:
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn device on."""
self._device.turn_on()
self._attr_is_on = True
self.schedule_update_ha_state(force_refresh=False)
self.async_write_ha_state()
def turn_off(self, **kwargs: Any) -> None:
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn device off."""
self._device.turn_off()
self._attr_is_on = False
self.schedule_update_ha_state(force_refresh=False)
self.async_write_ha_state()
def update(self) -> None:
async def async_update(self) -> None:
"""Update device status."""
device = self._device
if device.power_status in {POWER_OFF, 3}:

View File

@@ -10,7 +10,7 @@
"loggers": ["pyhap"],
"requirements": [
"HAP-python==5.0.0",
"fnv-hash-fast==2.0.0",
"fnv-hash-fast==2.0.2",
"homekit-audio-proxy==1.2.1",
"PyQRCode==1.2.1",
"base36==0.1.1"

View File

@@ -51,7 +51,6 @@ from homeassistant.helpers.http import (
from homeassistant.helpers.importlib import async_import_module
from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.setup import (
SetupPhases,
async_start_setup,
@@ -175,7 +174,6 @@ class ConfData(TypedDict, total=False):
ssl_profile: str
@bind_hass
async def async_get_last_config(hass: HomeAssistant) -> dict[str, Any] | None:
"""Return the last known working config."""
store = storage.Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY)

View File

@@ -24,7 +24,6 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.hass_dict import HassKey
from .const import ( # noqa: F401
@@ -78,7 +77,6 @@ DEVICE_CLASSES = [cls.value for cls in HumidifierDeviceClass]
# mypy: disallow-any-generics
@bind_hass
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
"""Return if the humidifier is on based on the statemachine.

View File

@@ -2,7 +2,8 @@
"common": {
"condition_behavior_name": "Condition passes if",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"is_drying": {
@@ -211,6 +212,9 @@
"behavior": {
"name": "[%key:component::humidifier::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::humidifier::common::trigger_for_name%]"
},
"mode": {
"description": "The operation modes to trigger on.",
"name": "Mode"
@@ -223,6 +227,9 @@
"fields": {
"behavior": {
"name": "[%key:component::humidifier::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::humidifier::common::trigger_for_name%]"
}
},
"name": "Humidifier started drying"
@@ -232,6 +239,9 @@
"fields": {
"behavior": {
"name": "[%key:component::humidifier::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::humidifier::common::trigger_for_name%]"
}
},
"name": "Humidifier started humidifying"
@@ -241,6 +251,9 @@
"fields": {
"behavior": {
"name": "[%key:component::humidifier::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::humidifier::common::trigger_for_name%]"
}
},
"name": "Humidifier turned off"
@@ -250,6 +263,9 @@
"fields": {
"behavior": {
"name": "[%key:component::humidifier::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::humidifier::common::trigger_for_name%]"
}
},
"name": "Humidifier turned on"

View File

@@ -13,6 +13,11 @@
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
selector:
duration:
started_drying: *trigger_common
started_humidifying: *trigger_common
@@ -23,6 +28,7 @@ mode_changed:
target: *trigger_humidifier_target
fields:
behavior: *trigger_behavior
for: *trigger_for
mode:
context:
filter_target: target

View File

@@ -3,6 +3,7 @@
"condition_behavior_name": "Condition passes if",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
"trigger_threshold_name": "Threshold type"
},
"conditions": {
@@ -51,6 +52,9 @@
"behavior": {
"name": "[%key:component::humidity::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::humidity::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::humidity::common::trigger_threshold_name%]"
}

View File

@@ -9,6 +9,11 @@
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
selector:
duration:
.humidity_threshold_entity: &humidity_threshold_entity
- domain: input_number
@@ -47,6 +52,7 @@ crossed_threshold:
target: *trigger_target
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:

View File

@@ -3,6 +3,7 @@
"condition_behavior_name": "Condition passes if",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
"trigger_threshold_name": "Threshold type"
},
"conditions": {
@@ -68,6 +69,9 @@
"fields": {
"behavior": {
"name": "[%key:component::illuminance::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::illuminance::common::trigger_for_name%]"
}
},
"name": "Light cleared"
@@ -78,6 +82,9 @@
"behavior": {
"name": "[%key:component::illuminance::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::illuminance::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::illuminance::common::trigger_threshold_name%]"
}
@@ -89,6 +96,9 @@
"fields": {
"behavior": {
"name": "[%key:component::illuminance::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::illuminance::common::trigger_for_name%]"
}
},
"name": "Light detected"

View File

@@ -9,6 +9,11 @@
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
selector:
duration:
.illuminance_threshold_entity: &illuminance_threshold_entity
- domain: input_number
@@ -55,6 +60,7 @@ crossed_threshold:
target: *trigger_numerical_target
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["imgw_pib==2.0.2"]
"requirements": ["imgw_pib==2.1.0"]
}

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/infrared",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["infrared-protocols==1.1.0"]
"requirements": ["infrared-protocols==1.2.0"]
}

View File

@@ -26,7 +26,6 @@ from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.helpers.service
from homeassistant.helpers.storage import Store
from homeassistant.helpers.typing import ConfigType, VolDictType
from homeassistant.loader import bind_hass
DOMAIN = "input_boolean"
@@ -81,7 +80,6 @@ class InputBooleanStorageCollection(collection.DictStorageCollection):
return {CONF_ID: item[CONF_ID]} | update_data
@bind_hass
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
"""Test if input_boolean is True."""
return hass.states.is_state(entity_id, STATE_ON)

View File

@@ -1,7 +1,8 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"is_docked": {
@@ -98,6 +99,9 @@
"fields": {
"behavior": {
"name": "[%key:component::lawn_mower::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::lawn_mower::common::trigger_for_name%]"
}
},
"name": "Lawn mower returned to dock"
@@ -107,6 +111,9 @@
"fields": {
"behavior": {
"name": "[%key:component::lawn_mower::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::lawn_mower::common::trigger_for_name%]"
}
},
"name": "Lawn mower encountered an error"
@@ -116,6 +123,9 @@
"fields": {
"behavior": {
"name": "[%key:component::lawn_mower::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::lawn_mower::common::trigger_for_name%]"
}
},
"name": "Lawn mower paused mowing"
@@ -125,6 +135,9 @@
"fields": {
"behavior": {
"name": "[%key:component::lawn_mower::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::lawn_mower::common::trigger_for_name%]"
}
},
"name": "Lawn mower started mowing"
@@ -134,6 +147,9 @@
"fields": {
"behavior": {
"name": "[%key:component::lawn_mower::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::lawn_mower::common::trigger_for_name%]"
}
},
"name": "Lawn mower started returning to dock"

View File

@@ -13,6 +13,11 @@
- last
- any
translation_key: trigger_behavior
for:
required: true
default: 00:00:00
selector:
duration:
docked: *trigger_common
errored: *trigger_common

View File

@@ -26,7 +26,6 @@ from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.frame import ReportBehavior, report_usage
from homeassistant.helpers.typing import ConfigType, VolDictType
from homeassistant.loader import bind_hass
from homeassistant.util import color as color_util
from .const import ( # noqa: F401
@@ -223,7 +222,6 @@ LIGHT_TURN_OFF_SCHEMA: VolDictType = {
_LOGGER = logging.getLogger(__name__)
@bind_hass
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
"""Return if the lights are on based on the statemachine."""
return hass.states.is_state(entity_id, STATE_ON)

View File

@@ -36,6 +36,7 @@
"field_xy_color_name": "XY-color",
"section_advanced_fields_name": "Advanced options",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
"trigger_threshold_name": "Threshold type"
},
"conditions": {
@@ -515,6 +516,9 @@
"behavior": {
"name": "[%key:component::light::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::light::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::light::common::trigger_threshold_name%]"
}
@@ -526,6 +530,9 @@
"fields": {
"behavior": {
"name": "[%key:component::light::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::light::common::trigger_for_name%]"
}
},
"name": "Light turned off"
@@ -535,6 +542,9 @@
"fields": {
"behavior": {
"name": "[%key:component::light::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::light::common::trigger_for_name%]"
}
},
"name": "Light turned on"

View File

@@ -13,6 +13,11 @@
- last
- any
translation_key: trigger_behavior
for: &trigger_for
required: true
default: 00:00:00
selector:
duration:
.brightness_threshold_entity: &brightness_threshold_entity
- domain: input_number
@@ -46,6 +51,7 @@ brightness_crossed_threshold:
target: *trigger_light_target
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:

View File

@@ -1,7 +1,8 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"is_jammed": {
@@ -146,6 +147,9 @@
"fields": {
"behavior": {
"name": "[%key:component::lock::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::lock::common::trigger_for_name%]"
}
},
"name": "Lock jammed"
@@ -155,6 +159,9 @@
"fields": {
"behavior": {
"name": "[%key:component::lock::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::lock::common::trigger_for_name%]"
}
},
"name": "Lock locked"
@@ -164,6 +171,9 @@
"fields": {
"behavior": {
"name": "[%key:component::lock::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::lock::common::trigger_for_name%]"
}
},
"name": "Lock opened"
@@ -173,6 +183,9 @@
"fields": {
"behavior": {
"name": "[%key:component::lock::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::lock::common::trigger_for_name%]"
}
},
"name": "Lock unlocked"

View File

@@ -13,6 +13,11 @@
- last
- any
translation_key: trigger_behavior
for:
required: true
default: 00:00:00
selector:
duration:
jammed: *trigger_common
locked: *trigger_common

View File

@@ -30,7 +30,6 @@ from homeassistant.helpers.integration_platform import (
async_process_integration_platforms,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.event_type import EventType
from . import rest_api, websocket_api
@@ -62,7 +61,6 @@ LOG_MESSAGE_SCHEMA = vol.Schema(
)
@bind_hass
def log_entry(
hass: HomeAssistant,
name: str,
@@ -76,7 +74,6 @@ def log_entry(
@callback
@bind_hass
def async_log_entry(
hass: HomeAssistant,
name: str,

View File

@@ -59,7 +59,6 @@ from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.network import get_url
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.hass_dict import HassKey
from .browse_media import ( # noqa: F401
@@ -246,7 +245,6 @@ class _ImageCache(TypedDict):
_ENTITY_IMAGE_CACHE = _ImageCache(images=collections.OrderedDict(), maxsize=16)
@bind_hass
def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool:
"""Return true if specified media player entity_id is on.

View File

@@ -1,7 +1,8 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"trigger_behavior_name": "Trigger when"
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
"conditions": {
"is_not_playing": {
@@ -438,6 +439,9 @@
"fields": {
"behavior": {
"name": "[%key:component::media_player::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::media_player::common::trigger_for_name%]"
}
},
"name": "Media player paused playing"
@@ -447,6 +451,9 @@
"fields": {
"behavior": {
"name": "[%key:component::media_player::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::media_player::common::trigger_for_name%]"
}
},
"name": "Media player started playing"
@@ -456,6 +463,9 @@
"fields": {
"behavior": {
"name": "[%key:component::media_player::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::media_player::common::trigger_for_name%]"
}
},
"name": "Media player stopped playing"
@@ -465,6 +475,9 @@
"fields": {
"behavior": {
"name": "[%key:component::media_player::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::media_player::common::trigger_for_name%]"
}
},
"name": "Media player turned off"
@@ -474,6 +487,9 @@
"fields": {
"behavior": {
"name": "[%key:component::media_player::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::media_player::common::trigger_for_name%]"
}
},
"name": "Media player turned on"

View File

@@ -13,6 +13,11 @@
- first
- last
- any
for:
required: true
default: 00:00:00
selector:
duration:
paused_playing: *trigger_common
started_playing: *trigger_common

View File

@@ -8,7 +8,6 @@ from homeassistant.components.media_player import BrowseError, BrowseMedia
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.frame import report_usage
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.loader import bind_hass
from .const import DOMAIN, MEDIA_SOURCE_DATA
from .error import UnknownMediaSource, Unresolvable
@@ -37,7 +36,6 @@ def _get_media_item(
return item
@bind_hass
async def async_browse_media(
hass: HomeAssistant,
media_content_id: str | None,
@@ -71,7 +69,6 @@ async def async_browse_media(
return item
@bind_hass
async def async_resolve_media(
hass: HomeAssistant,
media_content_id: str,

View File

@@ -314,7 +314,7 @@ class LocalMediaView(http.HomeAssistantView):
async def head(
self, request: web.Request, source_dir_id: str, location: str
) -> None:
) -> web.Response:
"""Handle a HEAD request.
This is sent by some DLNA renderers, like Samsung ones, prior to sending
@@ -322,7 +322,9 @@ class LocalMediaView(http.HomeAssistantView):
Check whether the location exists or not.
"""
await self._validate_media_path(source_dir_id, location)
media_path = await self._validate_media_path(source_dir_id, location)
mime_type, _ = mimetypes.guess_type(str(media_path))
return web.Response(content_type=mime_type)
async def get(
self, request: web.Request, source_dir_id: str, location: str

View File

@@ -3,6 +3,7 @@
"condition_behavior_name": "Condition passes if",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
"trigger_threshold_name": "Threshold type"
},
"conditions": {
@@ -68,6 +69,9 @@
"fields": {
"behavior": {
"name": "[%key:component::moisture::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::moisture::common::trigger_for_name%]"
}
},
"name": "Moisture cleared"
@@ -78,6 +82,9 @@
"behavior": {
"name": "[%key:component::moisture::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::moisture::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::moisture::common::trigger_threshold_name%]"
}
@@ -89,6 +96,9 @@
"fields": {
"behavior": {
"name": "[%key:component::moisture::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::moisture::common::trigger_for_name%]"
}
},
"name": "Moisture detected"

View File

@@ -9,6 +9,11 @@
- first
- last
- any
for: &trigger_for
required: true
default: 00:00:00
selector:
duration:
.moisture_threshold_entity: &moisture_threshold_entity
- domain: input_number
@@ -57,6 +62,7 @@ crossed_threshold:
target: *trigger_numerical_target
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:

View File

@@ -15,8 +15,12 @@ _MOTION_DOMAIN_SPECS = {
CONDITIONS: dict[str, type[Condition]] = {
"is_detected": make_entity_state_condition(_MOTION_DOMAIN_SPECS, STATE_ON),
"is_not_detected": make_entity_state_condition(_MOTION_DOMAIN_SPECS, STATE_OFF),
"is_detected": make_entity_state_condition(
_MOTION_DOMAIN_SPECS, STATE_ON, support_duration=True
),
"is_not_detected": make_entity_state_condition(
_MOTION_DOMAIN_SPECS, STATE_OFF, support_duration=True
),
}

View File

@@ -8,6 +8,11 @@
options:
- all
- any
for:
required: true
default: 00:00:00
selector:
duration:
is_detected:
fields: *condition_common_fields

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