mirror of
https://github.com/home-assistant/core.git
synced 2026-04-16 14:46:15 +02:00
Compare commits
33 Commits
mqtt-test-
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f6e078d1d | ||
|
|
71d857b5e1 | ||
|
|
0de75a013b | ||
|
|
f87ec0a7b8 | ||
|
|
6d1bd15256 | ||
|
|
9fe9064884 | ||
|
|
f9f57b00bb | ||
|
|
2b65b06003 | ||
|
|
206c498027 | ||
|
|
0ac62b241e | ||
|
|
4ba123a1a8 | ||
|
|
8b8b39c1b7 | ||
|
|
5b70e5f829 | ||
|
|
4f8e7125d4 | ||
|
|
baf5e32c59 | ||
|
|
0f0ceaace2 | ||
|
|
5ecae7066b | ||
|
|
ac9bf9b7cb | ||
|
|
d4a98c3336 | ||
|
|
f0aae350b5 | ||
|
|
69332ed822 | ||
|
|
32db17fab9 | ||
|
|
84e8cff2ea | ||
|
|
cfe390e4f6 | ||
|
|
a9becca321 | ||
|
|
0043a307f0 | ||
|
|
dfb1819800 | ||
|
|
12018cf9f4 | ||
|
|
70368c622e | ||
|
|
743aef05be | ||
|
|
49e5b03c08 | ||
|
|
6bc3fcef36 | ||
|
|
e3e87185c5 |
44
.github/renovate.json
vendored
44
.github/renovate.json
vendored
@@ -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": [
|
||||
|
||||
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -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
|
||||
|
||||
5
Dockerfile
generated
5
Dockerfile
generated
@@ -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 \
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
armed: *trigger_common
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
incremented:
|
||||
target:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
awning_closed:
|
||||
fields: *trigger_common_fields
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
closed:
|
||||
fields: *trigger_common_fields
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
closed:
|
||||
fields: *trigger_common_fields
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
closed:
|
||||
fields: *trigger_common_fields
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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}:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
paused_playing: *trigger_common
|
||||
started_playing: *trigger_common
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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_detected": {
|
||||
@@ -45,6 +46,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::motion::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::motion::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Motion cleared"
|
||||
@@ -54,6 +58,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::motion::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::motion::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Motion detected"
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
detected:
|
||||
fields: *trigger_common_fields
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyaxencoapi==1.0.6"]
|
||||
"requirements": ["pyaxencoapi==1.0.7"]
|
||||
}
|
||||
|
||||
@@ -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_detected": {
|
||||
@@ -45,6 +46,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::occupancy::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::occupancy::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Occupancy cleared"
|
||||
@@ -54,6 +58,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::occupancy::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::occupancy::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Occupancy detected"
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
detected:
|
||||
fields: *trigger_common_fields
|
||||
|
||||
@@ -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": {
|
||||
@@ -77,6 +78,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::person::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::person::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Entered home"
|
||||
@@ -86,6 +90,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::person::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::person::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Left home"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::power::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::power::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::power::common::trigger_threshold_name%]"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
for: &trigger_for
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
.power_units: &power_units
|
||||
- "mW"
|
||||
@@ -49,6 +54,7 @@ crossed_threshold:
|
||||
target: *trigger_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"SQLAlchemy==2.0.49",
|
||||
"fnv-hash-fast==2.0.0",
|
||||
"fnv-hash-fast==2.0.2",
|
||||
"psutil-home-assistant==0.0.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
@@ -159,6 +160,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::remote::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::remote::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Remote turned off"
|
||||
@@ -168,6 +172,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::remote::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::remote::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Remote turned on"
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/rest",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["jsonpath==0.82.2", "xmltodict==1.0.2"]
|
||||
"requirements": ["jsonpath==0.82.2", "xmltodict==1.0.4"]
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
@@ -76,6 +77,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::schedule::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::schedule::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Schedule block ended"
|
||||
@@ -85,6 +89,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::schedule::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::schedule::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Schedule block started"
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"codeowners": ["@fabaff"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/serial",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyserial-asyncio-fast==0.16"]
|
||||
"requirements": ["serialx==1.2.2"]
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import Task
|
||||
import json
|
||||
import logging
|
||||
|
||||
from serial import SerialException
|
||||
import serial_asyncio_fast as serial_asyncio
|
||||
from serialx import Parity, SerialException, StopBits, open_serial_connection
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -18,6 +18,7 @@ from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE, EVENT_HOMEASSIST
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -33,9 +34,9 @@ CONF_DSRDTR = "dsrdtr"
|
||||
|
||||
DEFAULT_NAME = "Serial Sensor"
|
||||
DEFAULT_BAUDRATE = 9600
|
||||
DEFAULT_BYTESIZE = serial_asyncio.serial.EIGHTBITS
|
||||
DEFAULT_PARITY = serial_asyncio.serial.PARITY_NONE
|
||||
DEFAULT_STOPBITS = serial_asyncio.serial.STOPBITS_ONE
|
||||
DEFAULT_BYTESIZE = 8
|
||||
DEFAULT_PARITY = Parity.NONE
|
||||
DEFAULT_STOPBITS = StopBits.ONE
|
||||
DEFAULT_XONXOFF = False
|
||||
DEFAULT_RTSCTS = False
|
||||
DEFAULT_DSRDTR = False
|
||||
@@ -46,28 +47,21 @@ PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
vol.Optional(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): cv.positive_int,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_BYTESIZE, default=DEFAULT_BYTESIZE): vol.In(
|
||||
[
|
||||
serial_asyncio.serial.FIVEBITS,
|
||||
serial_asyncio.serial.SIXBITS,
|
||||
serial_asyncio.serial.SEVENBITS,
|
||||
serial_asyncio.serial.EIGHTBITS,
|
||||
]
|
||||
),
|
||||
vol.Optional(CONF_BYTESIZE, default=DEFAULT_BYTESIZE): vol.In([5, 6, 7, 8]),
|
||||
vol.Optional(CONF_PARITY, default=DEFAULT_PARITY): vol.In(
|
||||
[
|
||||
serial_asyncio.serial.PARITY_NONE,
|
||||
serial_asyncio.serial.PARITY_EVEN,
|
||||
serial_asyncio.serial.PARITY_ODD,
|
||||
serial_asyncio.serial.PARITY_MARK,
|
||||
serial_asyncio.serial.PARITY_SPACE,
|
||||
Parity.NONE,
|
||||
Parity.EVEN,
|
||||
Parity.ODD,
|
||||
Parity.MARK,
|
||||
Parity.SPACE,
|
||||
]
|
||||
),
|
||||
vol.Optional(CONF_STOPBITS, default=DEFAULT_STOPBITS): vol.In(
|
||||
[
|
||||
serial_asyncio.serial.STOPBITS_ONE,
|
||||
serial_asyncio.serial.STOPBITS_ONE_POINT_FIVE,
|
||||
serial_asyncio.serial.STOPBITS_TWO,
|
||||
StopBits.ONE,
|
||||
StopBits.ONE_POINT_FIVE,
|
||||
StopBits.TWO,
|
||||
]
|
||||
),
|
||||
vol.Optional(CONF_XONXOFF, default=DEFAULT_XONXOFF): cv.boolean,
|
||||
@@ -84,28 +78,17 @@ async def async_setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Serial sensor platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
port = config.get(CONF_SERIAL_PORT)
|
||||
baudrate = config.get(CONF_BAUDRATE)
|
||||
bytesize = config.get(CONF_BYTESIZE)
|
||||
parity = config.get(CONF_PARITY)
|
||||
stopbits = config.get(CONF_STOPBITS)
|
||||
xonxoff = config.get(CONF_XONXOFF)
|
||||
rtscts = config.get(CONF_RTSCTS)
|
||||
dsrdtr = config.get(CONF_DSRDTR)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
sensor = SerialSensor(
|
||||
name,
|
||||
port,
|
||||
baudrate,
|
||||
bytesize,
|
||||
parity,
|
||||
stopbits,
|
||||
xonxoff,
|
||||
rtscts,
|
||||
dsrdtr,
|
||||
value_template,
|
||||
name=config[CONF_NAME],
|
||||
port=config[CONF_SERIAL_PORT],
|
||||
baudrate=config[CONF_BAUDRATE],
|
||||
bytesize=config[CONF_BYTESIZE],
|
||||
parity=config[CONF_PARITY],
|
||||
stopbits=config[CONF_STOPBITS],
|
||||
xonxoff=config[CONF_XONXOFF],
|
||||
rtscts=config[CONF_RTSCTS],
|
||||
dsrdtr=config[CONF_DSRDTR],
|
||||
value_template=config.get(CONF_VALUE_TEMPLATE),
|
||||
)
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, sensor.stop_serial_read)
|
||||
@@ -119,17 +102,17 @@ class SerialSensor(SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
port,
|
||||
baudrate,
|
||||
bytesize,
|
||||
parity,
|
||||
stopbits,
|
||||
xonxoff,
|
||||
rtscts,
|
||||
dsrdtr,
|
||||
value_template,
|
||||
):
|
||||
name: str,
|
||||
port: str,
|
||||
baudrate: int,
|
||||
bytesize: int,
|
||||
parity: Parity,
|
||||
stopbits: StopBits,
|
||||
xonxoff: bool,
|
||||
rtscts: bool,
|
||||
dsrdtr: bool,
|
||||
value_template: Template | None,
|
||||
) -> None:
|
||||
"""Initialize the Serial sensor."""
|
||||
self._attr_name = name
|
||||
self._port = port
|
||||
@@ -140,12 +123,12 @@ class SerialSensor(SensorEntity):
|
||||
self._xonxoff = xonxoff
|
||||
self._rtscts = rtscts
|
||||
self._dsrdtr = dsrdtr
|
||||
self._serial_loop_task = None
|
||||
self._serial_loop_task: Task[None] | None = None
|
||||
self._template = value_template
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle when an entity is about to be added to Home Assistant."""
|
||||
self._serial_loop_task = self.hass.loop.create_task(
|
||||
self._serial_loop_task = self.hass.async_create_background_task(
|
||||
self.serial_read(
|
||||
self._port,
|
||||
self._baudrate,
|
||||
@@ -155,26 +138,31 @@ class SerialSensor(SensorEntity):
|
||||
self._xonxoff,
|
||||
self._rtscts,
|
||||
self._dsrdtr,
|
||||
)
|
||||
),
|
||||
"Serial reader",
|
||||
)
|
||||
|
||||
async def serial_read(
|
||||
self,
|
||||
device,
|
||||
baudrate,
|
||||
bytesize,
|
||||
parity,
|
||||
stopbits,
|
||||
xonxoff,
|
||||
rtscts,
|
||||
dsrdtr,
|
||||
device: str,
|
||||
baudrate: int,
|
||||
bytesize: int,
|
||||
parity: Parity,
|
||||
stopbits: StopBits,
|
||||
xonxoff: bool,
|
||||
rtscts: bool,
|
||||
dsrdtr: bool,
|
||||
**kwargs,
|
||||
):
|
||||
"""Read the data from the port."""
|
||||
logged_error = False
|
||||
|
||||
while True:
|
||||
reader = None
|
||||
writer = None
|
||||
|
||||
try:
|
||||
reader, _ = await serial_asyncio.open_serial_connection(
|
||||
reader, writer = await open_serial_connection(
|
||||
url=device,
|
||||
baudrate=baudrate,
|
||||
bytesize=bytesize,
|
||||
@@ -185,8 +173,7 @@ class SerialSensor(SensorEntity):
|
||||
dsrdtr=dsrdtr,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
except SerialException:
|
||||
except OSError, SerialException, TimeoutError:
|
||||
if not logged_error:
|
||||
_LOGGER.exception(
|
||||
"Unable to connect to the serial device %s. Will retry", device
|
||||
@@ -197,15 +184,15 @@ class SerialSensor(SensorEntity):
|
||||
_LOGGER.debug("Serial device %s connected", device)
|
||||
while True:
|
||||
try:
|
||||
line = await reader.readline()
|
||||
except SerialException:
|
||||
line_bytes = await reader.readline()
|
||||
except OSError, SerialException:
|
||||
_LOGGER.exception(
|
||||
"Error while reading serial device %s", device
|
||||
)
|
||||
await self._handle_error()
|
||||
break
|
||||
else:
|
||||
line = line.decode("utf-8").strip()
|
||||
line = line_bytes.decode("utf-8").strip()
|
||||
|
||||
try:
|
||||
data = json.loads(line)
|
||||
@@ -223,6 +210,10 @@ class SerialSensor(SensorEntity):
|
||||
_LOGGER.debug("Received: %s", line)
|
||||
self._attr_native_value = line
|
||||
self.async_write_ha_state()
|
||||
finally:
|
||||
if writer is not None:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
async def _handle_error(self):
|
||||
"""Handle error for serial connection."""
|
||||
|
||||
@@ -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": {
|
||||
@@ -87,6 +88,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::siren::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::siren::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Siren turned off"
|
||||
@@ -96,6 +100,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::siren::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::siren::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Siren turned on"
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Iterator
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from soco import SoCo
|
||||
from soco import SoCo, SoCoException
|
||||
from soco.alarms import Alarm, Alarms
|
||||
from soco.events_base import Event as SonosEvent
|
||||
|
||||
@@ -30,6 +30,7 @@ class SonosAlarms(SonosHouseholdCoordinator):
|
||||
super().__init__(*args)
|
||||
self.alarms: Alarms = Alarms()
|
||||
self.created_alarm_ids: set[str] = set()
|
||||
self._household_mismatch_logged = False
|
||||
|
||||
def __iter__(self) -> Iterator:
|
||||
"""Return an iterator for the known alarms."""
|
||||
@@ -76,21 +77,40 @@ class SonosAlarms(SonosHouseholdCoordinator):
|
||||
await self.async_update_entities(speaker.soco, event_id)
|
||||
|
||||
@soco_error()
|
||||
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
|
||||
"""Update cache of known alarms and return if cache has changed."""
|
||||
self.alarms.update(soco)
|
||||
def update_cache(
|
||||
self,
|
||||
soco: SoCo,
|
||||
update_id: int | None = None,
|
||||
) -> bool:
|
||||
"""Update cache of known alarms and return whether any were seen."""
|
||||
try:
|
||||
self.alarms.update(soco)
|
||||
except SoCoException as err:
|
||||
err_msg = str(err)
|
||||
# Only catch the specific household mismatch error
|
||||
if "Alarm list UID" in err_msg and "does not match" in err_msg:
|
||||
if not self._household_mismatch_logged:
|
||||
_LOGGER.warning(
|
||||
"Sonos alarms for %s cannot be updated due to a household mismatch. "
|
||||
"This is a known limitation in setups with multiple households. "
|
||||
"You can safely ignore this warning, or to silence it, remove the "
|
||||
"affected household from your Sonos system. Error: %s",
|
||||
soco.player_name,
|
||||
err_msg,
|
||||
)
|
||||
self._household_mismatch_logged = True
|
||||
return False
|
||||
# Let all other exceptions bubble up to be handled by @soco_error()
|
||||
raise
|
||||
|
||||
if update_id and self.alarms.last_id < update_id:
|
||||
# Skip updates if latest query result is outdated or lagging
|
||||
return False
|
||||
|
||||
if (
|
||||
self.last_processed_event_id
|
||||
and self.alarms.last_id <= self.last_processed_event_id
|
||||
):
|
||||
# Skip updates already processed
|
||||
return False
|
||||
|
||||
_LOGGER.debug(
|
||||
"Updating processed event %s from %s (was %s)",
|
||||
self.alarms.last_id,
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/startca",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["xmltodict==1.0.2"]
|
||||
"requirements": ["xmltodict==1.0.4"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["PyTurboJPEG==1.8.0", "av==16.0.1", "numpy==2.3.2"]
|
||||
"requirements": ["PyTurboJPEG==2.2.0", "av==16.0.1", "numpy==2.3.2"]
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
@@ -101,6 +102,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::switch::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::switch::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Switch turned off"
|
||||
@@ -110,6 +114,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::switch::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::switch::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Switch turned on"
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ted5000",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["xmltodict==1.0.2"]
|
||||
"requirements": ["xmltodict==1.0.4"]
|
||||
}
|
||||
|
||||
@@ -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::temperature::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::temperature::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::temperature::common::trigger_threshold_name%]"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
for: &trigger_for
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
.temperature_units: &temperature_units
|
||||
- "°C"
|
||||
@@ -47,6 +52,7 @@ crossed_threshold:
|
||||
target: *trigger_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["twentemilieu"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["twentemilieu==2.2.1"]
|
||||
"requirements": ["twentemilieu==3.0.0"]
|
||||
}
|
||||
|
||||
@@ -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_available": {
|
||||
@@ -125,6 +126,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::update::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::update::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Update became available"
|
||||
|
||||
@@ -13,5 +13,10 @@
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
update_became_available: *trigger_common
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine, Sequence
|
||||
import dataclasses
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import os
|
||||
@@ -26,24 +27,20 @@ from homeassistant.core import (
|
||||
from homeassistant.helpers import config_validation as cv, discovery_flow
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.service_info.usb import UsbServiceInfo as _UsbServiceInfo
|
||||
from homeassistant.helpers.service_info.usb import UsbServiceInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import USBMatcher, async_get_usb
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import DOMAIN
|
||||
from .models import (
|
||||
SerialDevice, # noqa: F401
|
||||
USBDevice,
|
||||
)
|
||||
from .models import SerialDevice, USBDevice
|
||||
from .utils import (
|
||||
async_scan_serial_ports,
|
||||
scan_serial_ports, # noqa: F401
|
||||
usb_device_from_path, # noqa: F401
|
||||
usb_device_from_port, # noqa: F401
|
||||
scan_serial_ports,
|
||||
usb_device_from_path,
|
||||
usb_device_matches_matcher,
|
||||
usb_service_info_from_device,
|
||||
usb_unique_id_from_service_info, # noqa: F401
|
||||
usb_unique_id_from_service_info,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -56,9 +53,17 @@ REQUEST_SCAN_COOLDOWN = 10 # 10 second cooldown
|
||||
ADD_REMOVE_SCAN_COOLDOWN = 5 # 5 second cooldown to give devices a chance to register
|
||||
|
||||
__all__ = [
|
||||
"SerialDevice",
|
||||
"USBCallbackMatcher",
|
||||
"USBDevice",
|
||||
"async_register_port_event_callback",
|
||||
"async_register_scan_request_callback",
|
||||
"async_scan_serial_ports",
|
||||
"scan_serial_ports",
|
||||
"usb_device_from_path",
|
||||
"usb_device_matches_matcher",
|
||||
"usb_service_info_from_device",
|
||||
"usb_unique_id_from_service_info",
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
@@ -163,6 +168,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
await usb_discovery.async_setup()
|
||||
hass.data[_USB_DATA] = usb_discovery
|
||||
websocket_api.async_register_command(hass, websocket_usb_scan)
|
||||
websocket_api.async_register_command(hass, websocket_usb_list_serial_ports)
|
||||
|
||||
return True
|
||||
|
||||
@@ -358,7 +364,7 @@ class USBDiscovery:
|
||||
|
||||
for matcher in matched:
|
||||
for flow in self.hass.config_entries.flow.async_progress_by_init_data_type(
|
||||
_UsbServiceInfo,
|
||||
UsbServiceInfo,
|
||||
lambda flow_service_info: flow_service_info == service_info,
|
||||
):
|
||||
if matcher["domain"] != flow["handler"]:
|
||||
@@ -477,3 +483,23 @@ async def websocket_usb_scan(
|
||||
"""Scan for new usb devices."""
|
||||
await async_request_scan(hass)
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({vol.Required("type"): "usb/list_serial_ports"})
|
||||
@websocket_api.async_response
|
||||
async def websocket_usb_list_serial_ports(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""List available serial ports."""
|
||||
try:
|
||||
ports = await async_scan_serial_ports(hass)
|
||||
except OSError as err:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
|
||||
return
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
[dataclasses.asdict(port) for port in ports],
|
||||
)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["aiousbwatcher==1.1.1", "pyserial==3.5"]
|
||||
"requirements": ["aiousbwatcher==1.1.1", "serialx==1.2.2"]
|
||||
}
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
import dataclasses
|
||||
import fnmatch
|
||||
import os
|
||||
|
||||
from serial.tools.list_ports import comports
|
||||
from serial.tools.list_ports_common import ListPortInfo
|
||||
from serialx import SerialPortInfo, list_serial_ports
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.service_info.usb import UsbServiceInfo
|
||||
@@ -17,8 +15,8 @@ from homeassistant.loader import USBMatcher
|
||||
from .models import SerialDevice, USBDevice
|
||||
|
||||
|
||||
def usb_device_from_port(port: ListPortInfo) -> USBDevice:
|
||||
"""Convert serial ListPortInfo to USBDevice."""
|
||||
def usb_device_from_port(port: SerialPortInfo) -> USBDevice:
|
||||
"""Convert serialx SerialPortInfo to USBDevice."""
|
||||
assert port.vid is not None
|
||||
assert port.pid is not None
|
||||
|
||||
@@ -28,53 +26,30 @@ def usb_device_from_port(port: ListPortInfo) -> USBDevice:
|
||||
pid=f"{hex(port.pid)[2:]:0>4}".upper(),
|
||||
serial_number=port.serial_number,
|
||||
manufacturer=port.manufacturer,
|
||||
description=port.description,
|
||||
description=port.product,
|
||||
)
|
||||
|
||||
|
||||
def serial_device_from_port(port: ListPortInfo) -> SerialDevice:
|
||||
"""Convert serial ListPortInfo to SerialDevice."""
|
||||
def serial_device_from_port(port: SerialPortInfo) -> SerialDevice:
|
||||
"""Convert serialx SerialPortInfo to SerialDevice."""
|
||||
return SerialDevice(
|
||||
device=port.device,
|
||||
serial_number=port.serial_number,
|
||||
manufacturer=port.manufacturer,
|
||||
description=port.description,
|
||||
description=port.product,
|
||||
)
|
||||
|
||||
|
||||
def usb_serial_device_from_port(port: ListPortInfo) -> USBDevice | SerialDevice:
|
||||
"""Convert serial ListPortInfo to USBDevice or SerialDevice."""
|
||||
if port.vid is not None or port.pid is not None:
|
||||
assert port.vid is not None
|
||||
assert port.pid is not None
|
||||
|
||||
def usb_serial_device_from_port(port: SerialPortInfo) -> USBDevice | SerialDevice:
|
||||
"""Convert serialx SerialPortInfo to USBDevice or SerialDevice."""
|
||||
if port.vid is not None and port.pid is not None:
|
||||
return usb_device_from_port(port)
|
||||
return serial_device_from_port(port)
|
||||
|
||||
|
||||
def scan_serial_ports() -> Sequence[USBDevice | SerialDevice]:
|
||||
"""Scan serial ports and return USB and other serial devices."""
|
||||
|
||||
# Scan all symlinks first
|
||||
by_id = "/dev/serial/by-id"
|
||||
realpath_to_by_id: dict[str, str] = {}
|
||||
if os.path.isdir(by_id):
|
||||
for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()):
|
||||
realpath_to_by_id[os.path.realpath(path)] = path
|
||||
|
||||
serial_ports = []
|
||||
|
||||
for port in comports():
|
||||
device = usb_serial_device_from_port(port)
|
||||
device_path = realpath_to_by_id.get(port.device, port.device)
|
||||
|
||||
if device_path != port.device:
|
||||
# Prefer the unique /dev/serial/by-id/ path if it exists
|
||||
device = dataclasses.replace(device, device=device_path)
|
||||
|
||||
serial_ports.append(device)
|
||||
|
||||
return serial_ports
|
||||
return [usb_serial_device_from_port(port) for port in list_serial_ports()]
|
||||
|
||||
|
||||
async def async_scan_serial_ports(
|
||||
|
||||
@@ -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_cleaning": {
|
||||
@@ -190,6 +191,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::vacuum::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::vacuum::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Vacuum returned to dock"
|
||||
@@ -199,6 +203,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::vacuum::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::vacuum::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Vacuum encountered an error"
|
||||
@@ -208,6 +215,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::vacuum::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::vacuum::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Vacuum cleaner paused cleaning"
|
||||
@@ -217,6 +227,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::vacuum::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::vacuum::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Vacuum cleaner started cleaning"
|
||||
@@ -226,6 +239,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::vacuum::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::vacuum::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Vacuum cleaner started returning to dock"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
@@ -96,6 +97,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::valve::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::valve::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Valve closed"
|
||||
@@ -105,6 +109,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::valve::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::valve::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Valve opened"
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
closed: *trigger_common
|
||||
opened: *trigger_common
|
||||
|
||||
@@ -150,11 +150,6 @@ class PreRecordMessageProtocol(RtpDatagramProtocol):
|
||||
if self.transport is None:
|
||||
return
|
||||
|
||||
if self._audio_bytes is None:
|
||||
# 16Khz, 16-bit mono audio message
|
||||
file_path = Path(__file__).parent / self.file_name
|
||||
self._audio_bytes = file_path.read_bytes()
|
||||
|
||||
if self._audio_task is None:
|
||||
self._audio_task = self.hass.async_create_background_task(
|
||||
self._play_message(),
|
||||
@@ -162,6 +157,11 @@ class PreRecordMessageProtocol(RtpDatagramProtocol):
|
||||
)
|
||||
|
||||
async def _play_message(self) -> None:
|
||||
if self._audio_bytes is None:
|
||||
_LOGGER.debug("Loading audio from file %s", self.file_name)
|
||||
self._audio_bytes = await self._load_audio()
|
||||
_LOGGER.debug("Read %s bytes", len(self._audio_bytes))
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
self.send_audio,
|
||||
@@ -175,3 +175,8 @@ class PreRecordMessageProtocol(RtpDatagramProtocol):
|
||||
|
||||
# Allow message to play again
|
||||
self._audio_task = None
|
||||
|
||||
async def _load_audio(self) -> bytes:
|
||||
# 16Khz, 16-bit mono audio message
|
||||
file_path = Path(__file__).parent / self.file_name
|
||||
return await self.hass.async_add_executor_job(file_path.read_bytes)
|
||||
|
||||
@@ -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": {
|
||||
@@ -184,6 +185,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::water_heater::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::water_heater::common::trigger_for_name%]"
|
||||
},
|
||||
"operation_mode": {
|
||||
"description": "The operation modes to trigger on.",
|
||||
"name": "Operation mode"
|
||||
@@ -206,6 +210,9 @@
|
||||
"behavior": {
|
||||
"name": "[%key:component::water_heater::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::water_heater::common::trigger_for_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::water_heater::common::trigger_threshold_name%]"
|
||||
}
|
||||
@@ -217,6 +224,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::water_heater::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::water_heater::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Water heater turned off"
|
||||
@@ -226,6 +236,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::water_heater::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::water_heater::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Water heater turned on"
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
for: &trigger_for
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
.temperature_units: &temperature_units
|
||||
- "°C"
|
||||
@@ -30,6 +35,7 @@ operation_mode_changed:
|
||||
target: *trigger_water_heater_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
operation_mode:
|
||||
context:
|
||||
filter_target: target
|
||||
@@ -62,6 +68,7 @@ target_temperature_crossed_threshold:
|
||||
target: *trigger_water_heater_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
for: *trigger_for
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["waterfurnace"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["waterfurnace==1.6.2"]
|
||||
"requirements": ["waterfurnace==1.6.5"]
|
||||
}
|
||||
|
||||
@@ -245,6 +245,14 @@ ENERGY_SENSORS = [
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda status: status.energy_in_defrost,
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="electricity_used_standby",
|
||||
key="electricity_used_standby",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda status: status.energy_in_standby,
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="energy_output_heating",
|
||||
key="energy_output_heating",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user