mirror of
https://github.com/home-assistant/core.git
synced 2026-04-16 22:56:08 +02:00
Compare commits
56 Commits
python-3.1
...
frenck-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16b8130676 | ||
|
|
f6cfd69f6b | ||
|
|
87bcb34035 | ||
|
|
7c0ba4d250 | ||
|
|
6277ef5c21 | ||
|
|
b75263e486 | ||
|
|
2087906758 | ||
|
|
395d741324 | ||
|
|
2bcde89f5a | ||
|
|
74c62c34da | ||
|
|
810672ea78 | ||
|
|
afe3280aee | ||
|
|
fc573a0cf6 | ||
|
|
7b8978c7e5 | ||
|
|
d99d041e49 | ||
|
|
cd15261d1c | ||
|
|
5def2456f0 | ||
|
|
87742dbf4e | ||
|
|
f5fef37210 | ||
|
|
fa85d0d6c2 | ||
|
|
0fa5927fc8 | ||
|
|
5335367493 | ||
|
|
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 | ||
|
|
6d83b73cbb |
@@ -186,15 +186,11 @@ If `CHANGE_TYPE` IS "Breaking change" or "Deprecation", keep the `## Breaking ch
|
||||
|
||||
## Step 10: Push Branch and Create PR
|
||||
|
||||
```bash
|
||||
# Get branch name and GitHub username
|
||||
BRANCH=$(git branch --show-current)
|
||||
PUSH_REMOTE=$(git config "branch.$BRANCH.remote" 2>/dev/null || git remote | head -1)
|
||||
GITHUB_USER=$(gh api user --jq .login 2>/dev/null || git remote get-url "$PUSH_REMOTE" | sed -E 's#.*[:/]([^/]+)/([^/]+)(\.git)?$#\1#')
|
||||
Push the branch with upstream tracking, and create a PR against `home-assistant/core` with the generated title and body:
|
||||
|
||||
```bash
|
||||
# Create PR (gh pr create pushes the branch automatically)
|
||||
gh pr create --repo home-assistant/core --base dev \
|
||||
--head "$GITHUB_USER:$BRANCH" \
|
||||
--draft \
|
||||
--title "TITLE_HERE" \
|
||||
--body "$(cat <<'EOF'
|
||||
|
||||
44
.github/renovate.json
vendored
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
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -362,6 +362,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/deluge/ @tkdrob
|
||||
/homeassistant/components/demo/ @home-assistant/core
|
||||
/tests/components/demo/ @home-assistant/core
|
||||
/homeassistant/components/denon_rs232/ @balloob
|
||||
/tests/components/denon_rs232/ @balloob
|
||||
/homeassistant/components/denonavr/ @ol-iver @starkillerOG
|
||||
/tests/components/denonavr/ @ol-iver @starkillerOG
|
||||
/homeassistant/components/derivative/ @afaucogney @karwosts
|
||||
|
||||
5
Dockerfile
generated
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",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "denon",
|
||||
"name": "Denon",
|
||||
"integrations": ["denon", "denonavr", "heos"]
|
||||
"integrations": ["denon", "denonavr", "denon_rs232", "heos"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -157,7 +157,6 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2AuthorizeCallbackView
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -173,7 +172,6 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
DELETE_CURRENT_TOKEN_DELAY = 2
|
||||
|
||||
|
||||
@bind_hass
|
||||
def create_auth_code(
|
||||
hass: HomeAssistant, client_id: str, credential: Credentials
|
||||
) -> str:
|
||||
|
||||
@@ -83,7 +83,6 @@ from homeassistant.helpers.trace import (
|
||||
trace_path,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.dt import parse_datetime
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -238,7 +237,6 @@ class IfAction(Protocol):
|
||||
"""AND all conditions."""
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Return true if specified automation entity_id is on.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -58,7 +58,6 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.network import get_url
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType, VolDictType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import (
|
||||
CAMERA_IMAGE_TIMEOUT,
|
||||
@@ -163,7 +162,6 @@ class CameraCapabilities:
|
||||
frontend_stream_types: set[StreamType]
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_request_stream(hass: HomeAssistant, entity_id: str, fmt: str) -> str:
|
||||
"""Request a stream for a camera entity."""
|
||||
camera = get_camera_from_entity_id(hass, entity_id)
|
||||
@@ -212,7 +210,6 @@ async def _async_get_image(
|
||||
raise HomeAssistantError("Unable to get image")
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_image(
|
||||
hass: HomeAssistant,
|
||||
entity_id: str,
|
||||
@@ -247,14 +244,12 @@ async def _async_get_stream_image(
|
||||
return None
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_stream_source(hass: HomeAssistant, entity_id: str) -> str | None:
|
||||
"""Fetch the stream source for a camera entity."""
|
||||
camera = get_camera_from_entity_id(hass, entity_id)
|
||||
return await camera.stream_source()
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_mjpeg_stream(
|
||||
hass: HomeAssistant, request: web.Request, entity_id: str
|
||||
) -> web.StreamResponse | None:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -36,7 +36,7 @@ from homeassistant.helpers.dispatcher import (
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import async_get_integration, bind_hass
|
||||
from homeassistant.loader import async_get_integration
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
|
||||
# Pre-import backup to avoid it being imported
|
||||
@@ -181,7 +181,6 @@ class CloudConnectionState(Enum):
|
||||
CLOUD_DISCONNECTED = "cloud_disconnected"
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_is_logged_in(hass: HomeAssistant) -> bool:
|
||||
"""Test if user is logged in.
|
||||
@@ -191,7 +190,6 @@ def async_is_logged_in(hass: HomeAssistant) -> bool:
|
||||
return DATA_CLOUD in hass.data and hass.data[DATA_CLOUD].is_logged_in
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_is_connected(hass: HomeAssistant) -> bool:
|
||||
"""Test if connected to the cloud."""
|
||||
@@ -207,7 +205,6 @@ def async_listen_connection_change(
|
||||
return async_dispatcher_connect(hass, SIGNAL_CLOUD_CONNECTION_STATE, target)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_active_subscription(hass: HomeAssistant) -> bool:
|
||||
"""Test if user has an active subscription."""
|
||||
@@ -230,7 +227,6 @@ async def async_get_or_create_cloudhook(hass: HomeAssistant, webhook_id: str) ->
|
||||
return await async_create_cloudhook(hass, webhook_id)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str:
|
||||
"""Create a cloudhook."""
|
||||
if not async_is_connected(hass):
|
||||
@@ -245,7 +241,6 @@ async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str:
|
||||
return cloudhook_url
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_delete_cloudhook(hass: HomeAssistant, webhook_id: str) -> None:
|
||||
"""Delete a cloudhook."""
|
||||
if DATA_CLOUD not in hass.data:
|
||||
@@ -272,7 +267,6 @@ def async_listen_cloudhook_change(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_remote_ui_url(hass: HomeAssistant) -> str:
|
||||
"""Get the remote UI URL."""
|
||||
|
||||
@@ -25,7 +25,6 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
||||
_KEY_INSTANCE = "configurator"
|
||||
@@ -54,7 +53,6 @@ type ConfiguratorCallback = Callable[[list[dict[str, str]]], None]
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@async_callback
|
||||
def async_request_config(
|
||||
hass: HomeAssistant,
|
||||
@@ -93,7 +91,6 @@ def async_request_config(
|
||||
return request_id
|
||||
|
||||
|
||||
@bind_hass
|
||||
def request_config(hass: HomeAssistant, *args: Any, **kwargs: Any) -> str:
|
||||
"""Create a new request for configuration.
|
||||
|
||||
@@ -104,7 +101,6 @@ def request_config(hass: HomeAssistant, *args: Any, **kwargs: Any) -> str:
|
||||
).result()
|
||||
|
||||
|
||||
@bind_hass
|
||||
@async_callback
|
||||
def async_notify_errors(hass: HomeAssistant, request_id: str, error: str) -> None:
|
||||
"""Add errors to a config request."""
|
||||
@@ -112,7 +108,6 @@ def async_notify_errors(hass: HomeAssistant, request_id: str, error: str) -> Non
|
||||
_get_requests(hass)[request_id].async_notify_errors(request_id, error)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def notify_errors(hass: HomeAssistant, request_id: str, error: str) -> None:
|
||||
"""Add errors to a config request."""
|
||||
return run_callback_threadsafe(
|
||||
@@ -120,7 +115,6 @@ def notify_errors(hass: HomeAssistant, request_id: str, error: str) -> None:
|
||||
).result()
|
||||
|
||||
|
||||
@bind_hass
|
||||
@async_callback
|
||||
def async_request_done(hass: HomeAssistant, request_id: str) -> None:
|
||||
"""Mark a configuration request as done."""
|
||||
@@ -128,7 +122,6 @@ def async_request_done(hass: HomeAssistant, request_id: str) -> None:
|
||||
_get_requests(hass).pop(request_id).async_request_done(request_id)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def request_done(hass: HomeAssistant, request_id: str) -> None:
|
||||
"""Mark a configuration request as done."""
|
||||
return run_callback_threadsafe(
|
||||
|
||||
@@ -23,7 +23,6 @@ from homeassistant.helpers import config_validation as cv, intent
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.reload import async_integration_yaml_config
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .agent_manager import (
|
||||
AgentInfo,
|
||||
@@ -127,7 +126,6 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_set_agent(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -138,7 +136,6 @@ def async_set_agent(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_unset_agent(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -29,7 +29,6 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .condition import make_cover_is_closed_condition, make_cover_is_open_condition
|
||||
@@ -87,7 +86,6 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_closed(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Return if the cover is closed based on the statemachine."""
|
||||
return hass.states.is_state(entity_id, CoverState.CLOSED)
|
||||
|
||||
@@ -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)
|
||||
|
||||
57
homeassistant/components/denon_rs232/__init__.py
Normal file
57
homeassistant/components/denon_rs232/__init__.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""The Denon RS232 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from denon_rs232 import DenonReceiver, ReceiverState
|
||||
from denon_rs232.models import MODELS
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MODEL, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import LOGGER, DenonRS232ConfigEntry
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: DenonRS232ConfigEntry) -> bool:
|
||||
"""Set up Denon RS232 from a config entry."""
|
||||
port = entry.data[CONF_DEVICE]
|
||||
model = MODELS[entry.data[CONF_MODEL]]
|
||||
receiver = DenonReceiver(port, model=model)
|
||||
|
||||
try:
|
||||
await receiver.connect()
|
||||
await receiver.query_state()
|
||||
except (ConnectionError, OSError, TimeoutError) as err:
|
||||
LOGGER.error("Error connecting to Denon receiver at %s: %s", port, err)
|
||||
if receiver.connected:
|
||||
await receiver.disconnect()
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
entry.runtime_data = receiver
|
||||
|
||||
@callback
|
||||
def _on_disconnect(state: ReceiverState | None) -> None:
|
||||
# Only reload if the entry is still loaded. During entry removal,
|
||||
# disconnect() fires this callback but the entry is already gone.
|
||||
if state is None and entry.state is ConfigEntryState.LOADED:
|
||||
LOGGER.warning("Denon receiver disconnected, reloading config entry")
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(receiver.subscribe(_on_disconnect))
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DenonRS232ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
await entry.runtime_data.disconnect()
|
||||
|
||||
return unload_ok
|
||||
119
homeassistant/components/denon_rs232/config_flow.py
Normal file
119
homeassistant/components/denon_rs232/config_flow.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Config flow for the Denon RS232 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from denon_rs232 import DenonReceiver
|
||||
from denon_rs232.models import MODELS
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MODEL
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
SerialSelector,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
CONF_MODEL_NAME = "model_name"
|
||||
|
||||
# Build a flat list of (model_key, individual_name) pairs by splitting
|
||||
# grouped names like "AVR-3803 / AVC-3570 / AVR-2803" into separate entries.
|
||||
# Sorted alphabetically with "Other" at the bottom.
|
||||
MODEL_OPTIONS: list[tuple[str, str]] = sorted(
|
||||
(
|
||||
(_key, _name)
|
||||
for _key, _model in MODELS.items()
|
||||
if _key != "other"
|
||||
for _name in _model.name.split(" / ")
|
||||
),
|
||||
key=lambda x: x[1],
|
||||
)
|
||||
MODEL_OPTIONS.append(("other", "Other"))
|
||||
|
||||
|
||||
async def _async_attempt_connect(port: str, model_key: str) -> str | None:
|
||||
"""Attempt to connect to the receiver at the given port.
|
||||
|
||||
Returns None on success, error on failure.
|
||||
"""
|
||||
model = MODELS[model_key]
|
||||
receiver = DenonReceiver(port, model=model)
|
||||
|
||||
try:
|
||||
await receiver.connect()
|
||||
except (
|
||||
# When the port contains invalid connection data
|
||||
ValueError,
|
||||
# If it is a remote port, and we cannot connect
|
||||
ConnectionError,
|
||||
OSError,
|
||||
TimeoutError,
|
||||
):
|
||||
return "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
return "unknown"
|
||||
else:
|
||||
await receiver.disconnect()
|
||||
return None
|
||||
|
||||
|
||||
class DenonRS232ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Denon RS232."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
model_key, _, model_name = user_input[CONF_MODEL].partition(":")
|
||||
resolved_name = model_name if model_key != "other" else None
|
||||
|
||||
self._async_abort_entries_match({CONF_DEVICE: user_input[CONF_DEVICE]})
|
||||
error = await _async_attempt_connect(user_input[CONF_DEVICE], model_key)
|
||||
if not error:
|
||||
return self.async_create_entry(
|
||||
title=resolved_name or "Denon Receiver",
|
||||
data={
|
||||
CONF_DEVICE: user_input[CONF_DEVICE],
|
||||
CONF_MODEL: model_key,
|
||||
CONF_MODEL_NAME: resolved_name,
|
||||
},
|
||||
)
|
||||
errors["base"] = error
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_MODEL): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
SelectOptionDict(
|
||||
value=f"{key}:{name}",
|
||||
label=name,
|
||||
)
|
||||
for key, name in MODEL_OPTIONS
|
||||
],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="model",
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_DEVICE): SerialSelector(),
|
||||
}
|
||||
),
|
||||
user_input or {},
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
12
homeassistant/components/denon_rs232/const.py
Normal file
12
homeassistant/components/denon_rs232/const.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Constants for the Denon RS232 integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from denon_rs232 import DenonReceiver
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
DOMAIN = "denon_rs232"
|
||||
|
||||
type DenonRS232ConfigEntry = ConfigEntry[DenonReceiver]
|
||||
13
homeassistant/components/denon_rs232/manifest.json
Normal file
13
homeassistant/components/denon_rs232/manifest.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "denon_rs232",
|
||||
"name": "Denon RS232",
|
||||
"codeowners": ["@balloob"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["usb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/denon_rs232",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["denon_rs232"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["denon-rs232==4.1.0"]
|
||||
}
|
||||
235
homeassistant/components/denon_rs232/media_player.py
Normal file
235
homeassistant/components/denon_rs232/media_player.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""Media player platform for the Denon RS232 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, cast
|
||||
|
||||
from denon_rs232 import (
|
||||
MIN_VOLUME_DB,
|
||||
VOLUME_DB_RANGE,
|
||||
DenonReceiver,
|
||||
InputSource,
|
||||
MainPlayer,
|
||||
ReceiverState,
|
||||
ZonePlayer,
|
||||
)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDeviceClass,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .config_flow import CONF_MODEL_NAME
|
||||
from .const import DOMAIN, DenonRS232ConfigEntry
|
||||
|
||||
INPUT_SOURCE_DENON_TO_HA: dict[InputSource, str] = {
|
||||
InputSource.PHONO: "phono",
|
||||
InputSource.CD: "cd",
|
||||
InputSource.TUNER: "tuner",
|
||||
InputSource.DVD: "dvd",
|
||||
InputSource.VDP: "vdp",
|
||||
InputSource.TV: "tv",
|
||||
InputSource.DBS_SAT: "dbs_sat",
|
||||
InputSource.VCR_1: "vcr_1",
|
||||
InputSource.VCR_2: "vcr_2",
|
||||
InputSource.VCR_3: "vcr_3",
|
||||
InputSource.V_AUX: "v_aux",
|
||||
InputSource.CDR_TAPE1: "cdr_tape1",
|
||||
InputSource.MD_TAPE2: "md_tape2",
|
||||
InputSource.HDP: "hdp",
|
||||
InputSource.DVR: "dvr",
|
||||
InputSource.TV_CBL: "tv_cbl",
|
||||
InputSource.SAT: "sat",
|
||||
InputSource.NET_USB: "net_usb",
|
||||
InputSource.DOCK: "dock",
|
||||
InputSource.IPOD: "ipod",
|
||||
InputSource.BD: "bd",
|
||||
InputSource.SAT_CBL: "sat_cbl",
|
||||
InputSource.MPLAY: "mplay",
|
||||
InputSource.GAME: "game",
|
||||
InputSource.AUX1: "aux1",
|
||||
InputSource.AUX2: "aux2",
|
||||
InputSource.NET: "net",
|
||||
InputSource.BT: "bt",
|
||||
InputSource.USB_IPOD: "usb_ipod",
|
||||
InputSource.EIGHT_K: "eight_k",
|
||||
InputSource.PANDORA: "pandora",
|
||||
InputSource.SIRIUSXM: "siriusxm",
|
||||
InputSource.SPOTIFY: "spotify",
|
||||
InputSource.FLICKR: "flickr",
|
||||
InputSource.IRADIO: "iradio",
|
||||
InputSource.SERVER: "server",
|
||||
InputSource.FAVORITES: "favorites",
|
||||
InputSource.LASTFM: "lastfm",
|
||||
InputSource.XM: "xm",
|
||||
InputSource.SIRIUS: "sirius",
|
||||
InputSource.HDRADIO: "hdradio",
|
||||
InputSource.DAB: "dab",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: DenonRS232ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Denon RS232 media player."""
|
||||
receiver = config_entry.runtime_data
|
||||
entities = [DenonRS232MediaPlayer(receiver, receiver.main, config_entry, "main")]
|
||||
|
||||
if receiver.zone_2.power is not None:
|
||||
entities.append(
|
||||
DenonRS232MediaPlayer(receiver, receiver.zone_2, config_entry, "zone_2")
|
||||
)
|
||||
if receiver.zone_3.power is not None:
|
||||
entities.append(
|
||||
DenonRS232MediaPlayer(receiver, receiver.zone_3, config_entry, "zone_3")
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class DenonRS232MediaPlayer(MediaPlayerEntity):
|
||||
"""Representation of a Denon receiver controlled over RS232."""
|
||||
|
||||
_attr_device_class = MediaPlayerDeviceClass.RECEIVER
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "receiver"
|
||||
_attr_should_poll = False
|
||||
|
||||
_volume_min = MIN_VOLUME_DB
|
||||
_volume_range = VOLUME_DB_RANGE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
receiver: DenonReceiver,
|
||||
player: MainPlayer | ZonePlayer,
|
||||
config_entry: DenonRS232ConfigEntry,
|
||||
zone: Literal["main", "zone_2", "zone_3"],
|
||||
) -> None:
|
||||
"""Initialize the media player."""
|
||||
self._receiver = receiver
|
||||
self._player = player
|
||||
self._is_main = zone == "main"
|
||||
|
||||
model = receiver.model
|
||||
assert model is not None # We always set this
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer="Denon",
|
||||
model_id=config_entry.data.get(CONF_MODEL_NAME),
|
||||
)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{zone}"
|
||||
|
||||
self._attr_source_list = sorted(
|
||||
INPUT_SOURCE_DENON_TO_HA[source] for source in model.input_sources
|
||||
)
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
)
|
||||
|
||||
if zone == "main":
|
||||
self._attr_name = None
|
||||
self._attr_supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
else:
|
||||
self._attr_name = "Zone 2" if zone == "zone_2" else "Zone 3"
|
||||
|
||||
self._async_update_from_player()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to receiver state updates."""
|
||||
self.async_on_remove(self._receiver.subscribe(self._async_on_state_update))
|
||||
|
||||
@callback
|
||||
def _async_on_state_update(self, state: ReceiverState | None) -> None:
|
||||
"""Handle a state update from the receiver."""
|
||||
if state is None:
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
self._async_update_from_player()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_from_player(self) -> None:
|
||||
"""Update entity attributes from the shared player object."""
|
||||
if self._player.power is None:
|
||||
self._attr_state = None
|
||||
else:
|
||||
self._attr_state = (
|
||||
MediaPlayerState.ON if self._player.power else MediaPlayerState.OFF
|
||||
)
|
||||
|
||||
source = self._player.input_source
|
||||
self._attr_source = INPUT_SOURCE_DENON_TO_HA.get(source) if source else None
|
||||
|
||||
volume_min = self._player.volume_min
|
||||
volume_max = self._player.volume_max
|
||||
if volume_min is not None:
|
||||
self._volume_min = volume_min
|
||||
|
||||
if volume_max is not None and volume_max > volume_min:
|
||||
self._volume_range = volume_max - volume_min
|
||||
|
||||
volume = self._player.volume
|
||||
if volume is not None:
|
||||
self._attr_volume_level = (volume - self._volume_min) / self._volume_range
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
|
||||
if self._is_main:
|
||||
self._attr_is_volume_muted = cast(MainPlayer, self._player).mute
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the receiver on."""
|
||||
await self._player.power_on()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the receiver off."""
|
||||
await self._player.power_standby()
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
db = volume * self._volume_range + self._volume_min
|
||||
await self._player.set_volume(db)
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up."""
|
||||
await self._player.volume_up()
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down."""
|
||||
await self._player.volume_down()
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute or unmute."""
|
||||
player = cast(MainPlayer, self._player)
|
||||
if mute:
|
||||
await player.mute_on()
|
||||
else:
|
||||
await player.mute_off()
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
input_source = next(
|
||||
(
|
||||
input_source
|
||||
for input_source, ha_source in INPUT_SOURCE_DENON_TO_HA.items()
|
||||
if ha_source == source
|
||||
),
|
||||
None,
|
||||
)
|
||||
if input_source is None:
|
||||
raise HomeAssistantError("Invalid source")
|
||||
|
||||
await self._player.select_input_source(input_source)
|
||||
64
homeassistant/components/denon_rs232/quality_scale.yaml
Normal file
64
homeassistant/components/denon_rs232/quality_scale.yaml
Normal file
@@ -0,0 +1,64 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: "The integration does not create dynamic devices."
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: "The integration does not create devices that can become stale."
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
84
homeassistant/components/denon_rs232/strings.json
Normal file
84
homeassistant/components/denon_rs232/strings.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"device": "[%key:common::config_flow::data::port%]",
|
||||
"model": "Receiver model"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Serial port path to connect to",
|
||||
"model": "Determines available features"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"media_player": {
|
||||
"receiver": {
|
||||
"state_attributes": {
|
||||
"source": {
|
||||
"state": {
|
||||
"aux1": "Aux 1",
|
||||
"aux2": "Aux 2",
|
||||
"bd": "BD Player",
|
||||
"bt": "Bluetooth",
|
||||
"cd": "CD",
|
||||
"cdr_tape1": "CDR/Tape 1",
|
||||
"dab": "DAB",
|
||||
"dbs_sat": "DBS/Sat",
|
||||
"dock": "Dock",
|
||||
"dvd": "DVD",
|
||||
"dvr": "DVR",
|
||||
"eight_k": "8K",
|
||||
"favorites": "Favorites",
|
||||
"flickr": "Flickr",
|
||||
"game": "Game",
|
||||
"hdp": "HDP",
|
||||
"hdradio": "HD Radio",
|
||||
"ipod": "iPod",
|
||||
"iradio": "Internet Radio",
|
||||
"lastfm": "Last.fm",
|
||||
"md_tape2": "MD/Tape 2",
|
||||
"mplay": "Media Player",
|
||||
"net": "HEOS Music",
|
||||
"net_usb": "Network/USB",
|
||||
"pandora": "Pandora",
|
||||
"phono": "Phono",
|
||||
"sat": "Sat",
|
||||
"sat_cbl": "Satellite/Cable",
|
||||
"server": "Server",
|
||||
"sirius": "Sirius",
|
||||
"siriusxm": "SiriusXM",
|
||||
"spotify": "Spotify",
|
||||
"tuner": "Tuner",
|
||||
"tv": "TV Audio",
|
||||
"tv_cbl": "TV/Cable",
|
||||
"usb_ipod": "USB/iPod",
|
||||
"v_aux": "V. Aux",
|
||||
"vcr_1": "VCR 1",
|
||||
"vcr_2": "VCR 2",
|
||||
"vcr_3": "VCR 3",
|
||||
"vdp": "VDP",
|
||||
"xm": "XM"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"model": {
|
||||
"options": {
|
||||
"other": "Other"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .config_entry import ( # noqa: F401
|
||||
ScannerEntity,
|
||||
@@ -52,7 +51,6 @@ from .legacy import ( # noqa: F401
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Return the state if any or a specified device is home."""
|
||||
return hass.states.is_state(entity_id, STATE_HOME)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -87,6 +87,7 @@ class MbusDeviceType(IntEnum):
|
||||
GAS = 3
|
||||
HEAT = 4
|
||||
WATER = 7
|
||||
HEAT_COOL = 12
|
||||
|
||||
|
||||
SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
@@ -571,6 +572,16 @@ SENSORS_MBUS_DEVICE_TYPE: dict[int, tuple[DSMRSensorEntityDescription, ...]] = {
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
),
|
||||
MbusDeviceType.HEAT_COOL: (
|
||||
DSMRSensorEntityDescription(
|
||||
key="heat_reading",
|
||||
translation_key="heat_meter_reading",
|
||||
obis_reference="MBUS_METER_READING",
|
||||
is_heat=True,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ from .coordinator import CometBlueConfigEntry, CometBlueDataUpdateCoordinator
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BUTTON,
|
||||
Platform.CLIMATE,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ class CometBlueCoordinatorData:
|
||||
|
||||
temperatures: dict[str, float | int] = field(default_factory=dict)
|
||||
holiday: dict = field(default_factory=dict)
|
||||
battery: int | None = None
|
||||
|
||||
|
||||
class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorData]):
|
||||
@@ -53,6 +54,7 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
|
||||
)
|
||||
self.device = cometblue
|
||||
self.address = cometblue.client.address
|
||||
self.data = CometBlueCoordinatorData()
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
@@ -64,11 +66,11 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
|
||||
LOGGER.debug("Updating device %s with '%s'", self.name, payload)
|
||||
retry_count = 0
|
||||
while retry_count < MAX_RETRIES:
|
||||
retry_count += 1
|
||||
try:
|
||||
async with self.device:
|
||||
return await function(**payload)
|
||||
except (InvalidByteValueError, TimeoutError, BleakError) as ex:
|
||||
retry_count += 1
|
||||
if retry_count >= MAX_RETRIES:
|
||||
raise HomeAssistantError(
|
||||
f"Error sending command to '{self.name}': {ex}"
|
||||
@@ -88,20 +90,23 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
|
||||
|
||||
async def _async_update_data(self) -> CometBlueCoordinatorData:
|
||||
"""Poll the device."""
|
||||
data: CometBlueCoordinatorData = CometBlueCoordinatorData()
|
||||
data = CometBlueCoordinatorData()
|
||||
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < MAX_RETRIES and not data.temperatures:
|
||||
try:
|
||||
retry_count += 1
|
||||
async with self.device:
|
||||
# temperatures are required and must trigger a retry if not available
|
||||
if not data.temperatures:
|
||||
data.temperatures = await self.device.get_temperature_async()
|
||||
# holiday is optional and should not trigger a retry
|
||||
# holiday and battery are optional and should not trigger a retry
|
||||
try:
|
||||
if not data.holiday:
|
||||
data.holiday = await self.device.get_holiday_async(1) or {}
|
||||
if not data.battery:
|
||||
data.battery = await self.device.get_battery_async()
|
||||
except InvalidByteValueError as ex:
|
||||
LOGGER.warning(
|
||||
"Failed to retrieve optional data for %s: %s (%s)",
|
||||
@@ -110,7 +115,6 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
|
||||
ex,
|
||||
)
|
||||
except (InvalidByteValueError, TimeoutError, BleakError) as ex:
|
||||
retry_count += 1
|
||||
if retry_count >= MAX_RETRIES:
|
||||
raise UpdateFailed(
|
||||
f"Error retrieving data: {ex}", retry_after=30
|
||||
@@ -128,5 +132,9 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
|
||||
) from ex
|
||||
|
||||
# If one value was not retrieved correctly, keep the old value
|
||||
if not data.holiday:
|
||||
data.holiday = self.data.holiday
|
||||
if not data.battery:
|
||||
data.battery = self.data.battery
|
||||
LOGGER.debug("Received data for %s: %s", self.name, data)
|
||||
return data
|
||||
|
||||
53
homeassistant/components/eurotronic_cometblue/sensor.py
Normal file
53
homeassistant/components/eurotronic_cometblue/sensor.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Comet Blue sensor integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import CometBlueConfigEntry, CometBlueDataUpdateCoordinator
|
||||
from .entity import CometBlueBluetoothEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: CometBlueConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the client entities."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
entities = [CometBlueBatterySensorEntity(coordinator)]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class CometBlueBatterySensorEntity(CometBlueBluetoothEntity, SensorEntity):
|
||||
"""Representation of a sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: CometBlueDataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize CometBlueSensorEntity."""
|
||||
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = SensorEntityDescription(
|
||||
key="battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.address}-{self.entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
return self.coordinator.data.battery
|
||||
@@ -39,9 +39,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ATTR_DURATION, ATTR_PERIOD, DOMAIN, EVOHOME_DATA, EvoService
|
||||
from .const import (
|
||||
ATTR_DURATION,
|
||||
ATTR_PERIOD,
|
||||
DOMAIN,
|
||||
EVOHOME_DATA,
|
||||
RESET_BREAKS_IN_HA_VERSION,
|
||||
EvoService,
|
||||
)
|
||||
from .coordinator import EvoDataUpdateCoordinator
|
||||
from .entity import EvoChild, EvoEntity, is_valid_zone
|
||||
from .helpers import async_create_deprecation_issue_once
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -185,6 +193,11 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
|
||||
async def async_clear_zone_override(self) -> None:
|
||||
"""Clear the zone override (if any) and return to following its schedule."""
|
||||
async_create_deprecation_issue_once(
|
||||
self.hass,
|
||||
"deprecated_clear_zone_override_service",
|
||||
RESET_BREAKS_IN_HA_VERSION,
|
||||
)
|
||||
await self.coordinator.call_client_api(self._evo_device.reset())
|
||||
|
||||
async def async_set_zone_override(
|
||||
@@ -447,6 +460,13 @@ class EvoController(EvoClimateEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode; if None, then revert to 'Auto' mode."""
|
||||
if preset_mode == PRESET_RESET:
|
||||
async_create_deprecation_issue_once(
|
||||
self.hass,
|
||||
"deprecated_preset_reset",
|
||||
RESET_BREAKS_IN_HA_VERSION,
|
||||
)
|
||||
|
||||
await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EvoSystemMode.AUTO))
|
||||
|
||||
@callback
|
||||
|
||||
@@ -26,6 +26,9 @@ ATTR_DURATION: Final = "duration" # number of minutes, <24h
|
||||
ATTR_PERIOD: Final = "period" # number of days
|
||||
ATTR_SETPOINT: Final = "setpoint"
|
||||
|
||||
# Support for the reset service calls/presets is being deprecated
|
||||
RESET_BREAKS_IN_HA_VERSION: Final = "2026.11.0"
|
||||
|
||||
|
||||
@unique
|
||||
class EvoService(StrEnum):
|
||||
|
||||
36
homeassistant/components/evohome/helpers.py
Normal file
36
homeassistant/components/evohome/helpers.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Helpers for the Evohome integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_deprecation_issue_once(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
breaks_in_ha_version: str,
|
||||
translation_key: str | None = None,
|
||||
translation_placeholders: dict[str, str] | None = None,
|
||||
) -> None:
|
||||
"""Create or update a deprecation issue entry."""
|
||||
|
||||
placeholders = {
|
||||
**(translation_placeholders or {}),
|
||||
"breaks_in_ha_version": breaks_in_ha_version,
|
||||
}
|
||||
|
||||
ir.async_get(hass).async_get_or_create(
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
breaks_in_ha_version=breaks_in_ha_version,
|
||||
is_fixable=False,
|
||||
is_persistent=True,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=translation_key or issue_id,
|
||||
translation_placeholders=placeholders,
|
||||
)
|
||||
@@ -22,8 +22,16 @@ from homeassistant.helpers import config_validation as cv, service
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.service import verify_domain_control
|
||||
|
||||
from .const import ATTR_DURATION, ATTR_PERIOD, ATTR_SETPOINT, DOMAIN, EvoService
|
||||
from .const import (
|
||||
ATTR_DURATION,
|
||||
ATTR_PERIOD,
|
||||
ATTR_SETPOINT,
|
||||
DOMAIN,
|
||||
RESET_BREAKS_IN_HA_VERSION,
|
||||
EvoService,
|
||||
)
|
||||
from .coordinator import EvoDataUpdateCoordinator
|
||||
from .helpers import async_create_deprecation_issue_once
|
||||
|
||||
# System service schemas (registered as domain services)
|
||||
SET_SYSTEM_MODE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {
|
||||
@@ -158,6 +166,13 @@ def setup_service_functions(
|
||||
# via that service call may be able to emulate the reset even if the system
|
||||
# doesn't support AutoWithReset natively
|
||||
|
||||
if call.service == EvoService.RESET_SYSTEM:
|
||||
async_create_deprecation_issue_once(
|
||||
hass,
|
||||
"deprecated_reset_system_service",
|
||||
RESET_BREAKS_IN_HA_VERSION,
|
||||
)
|
||||
|
||||
if call.service == EvoService.SET_SYSTEM_MODE:
|
||||
_validate_set_system_mode_params(coordinator.tcs, call.data)
|
||||
|
||||
|
||||
@@ -19,9 +19,23 @@
|
||||
"message": "Only zones support the `{service}` action"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_clear_zone_override_service": {
|
||||
"description": "Using the `clear_zone_override` action is deprecated and will stop working in Home Assistant {breaks_in_ha_version}. Use the zone's Reset button instead.",
|
||||
"title": "Evohome clear zone override action is deprecated"
|
||||
},
|
||||
"deprecated_preset_reset": {
|
||||
"description": "Using the `Reset` preset on an Evohome controller is deprecated and will stop working in Home Assistant {breaks_in_ha_version}. Use the system's Reset button instead.",
|
||||
"title": "Evohome Reset preset is deprecated"
|
||||
},
|
||||
"deprecated_reset_system_service": {
|
||||
"description": "Using the `reset_system` action is deprecated and will stop working in Home Assistant {breaks_in_ha_version}. Use the system's Reset button instead.",
|
||||
"title": "Evohome reset system action is deprecated"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"clear_zone_override": {
|
||||
"description": "Sets the zone to follow its schedule.",
|
||||
"description": "Sets a zone to follow its schedule (deprecated).",
|
||||
"name": "Clear zone override"
|
||||
},
|
||||
"refresh_system": {
|
||||
@@ -29,11 +43,11 @@
|
||||
"name": "Refresh system"
|
||||
},
|
||||
"reset_system": {
|
||||
"description": "Sets the system mode to `Auto` mode and resets all the zones to follow their schedules. Not all Evohome systems support this feature (i.e. `AutoWithReset` mode).",
|
||||
"description": "Sets a system's mode to `Auto` mode and resets all its zones to follow their schedules (deprecated). Some older systems may not support this feature.",
|
||||
"name": "Reset system"
|
||||
},
|
||||
"set_dhw_override": {
|
||||
"description": "Overrides the DHW state, either indefinitely or for a specified duration, after which it will revert to following its schedule.",
|
||||
"description": "Overrides a DHW's state, either indefinitely or for a specified duration, after which it will revert to following its schedule.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"description": "The DHW will revert to its schedule after this time. If 0 the change is until the next scheduled setpoint.",
|
||||
@@ -47,7 +61,7 @@
|
||||
"name": "Set DHW override"
|
||||
},
|
||||
"set_system_mode": {
|
||||
"description": "Sets the system mode, either indefinitely or until a specified end time, after which it will revert to `Auto`. Not all systems support all modes.",
|
||||
"description": "Sets a system's mode, either indefinitely or until a specified end time, after which it will revert to `Auto`. Not all systems support all modes.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"description": "The duration in hours; used only with `AutoWithEco` mode (up to 24 hours).",
|
||||
@@ -65,7 +79,7 @@
|
||||
"name": "Set system mode"
|
||||
},
|
||||
"set_zone_override": {
|
||||
"description": "Overrides the zone setpoint, either indefinitely or for a specified duration, after which it will revert to following its schedule.",
|
||||
"description": "Overrides a zone's setpoint, either indefinitely or for a specified duration, after which it will revert to following its schedule.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"description": "The zone will revert to its schedule after this time. If 0 the change is until the next scheduled setpoint.",
|
||||
|
||||
@@ -25,7 +25,6 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.percentage import (
|
||||
percentage_to_ranged_value,
|
||||
@@ -88,7 +87,6 @@ class NotValidPresetModeError(ServiceValidationError):
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Return if the fans are on based on the statemachine."""
|
||||
entity = hass.states.get(entity_id)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,7 +20,6 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.system_info import is_official_image
|
||||
|
||||
from .const import (
|
||||
@@ -71,7 +70,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@bind_hass
|
||||
def get_ffmpeg_manager(hass: HomeAssistant) -> FFmpegManager:
|
||||
"""Return the FFmpegManager."""
|
||||
if DATA_FFMPEG not in hass.data:
|
||||
@@ -79,7 +77,6 @@ def get_ffmpeg_manager(hass: HomeAssistant) -> FFmpegManager:
|
||||
return hass.data[DATA_FFMPEG]
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_image(
|
||||
hass: HomeAssistant,
|
||||
input_source: str,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -34,7 +34,7 @@ from homeassistant.helpers.json import json_dumps_sorted
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import async_get_integration, bind_hass
|
||||
from homeassistant.loader import async_get_integration
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .pr_download import download_pr_artifact
|
||||
@@ -354,7 +354,6 @@ class Panel:
|
||||
return response
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_register_built_in_panel(
|
||||
hass: HomeAssistant,
|
||||
@@ -393,7 +392,6 @@ def async_register_built_in_panel(
|
||||
hass.bus.async_fire(EVENT_PANELS_UPDATED)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_remove_panel(
|
||||
hass: HomeAssistant, frontend_url_path: str, *, warn_if_unknown: bool = True
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,7 +28,6 @@ from homeassistant.helpers.group import (
|
||||
)
|
||||
from homeassistant.helpers.reload import async_reload_integration_platforms
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
#
|
||||
# Below we ensure the config_flow is imported so it does not need the import
|
||||
@@ -103,7 +102,6 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Test if the group state is in its ON-state."""
|
||||
if REG_KEY not in hass.data:
|
||||
@@ -117,11 +115,10 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
|
||||
|
||||
# expand_entity_ids and get_entity_ids are for backwards compatibility only
|
||||
expand_entity_ids = bind_hass(_expand_entity_ids)
|
||||
get_entity_ids = bind_hass(_get_entity_ids)
|
||||
expand_entity_ids = _expand_entity_ids
|
||||
get_entity_ids = _get_entity_ids
|
||||
|
||||
|
||||
@bind_hass
|
||||
def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]:
|
||||
"""Get all groups that contain this entity.
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -26,7 +26,6 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import (
|
||||
ATTR_AUTO_UPDATE,
|
||||
@@ -74,7 +73,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
"""Return generic information from Supervisor.
|
||||
|
||||
@@ -84,7 +82,6 @@ def get_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_host_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
"""Return generic host information.
|
||||
|
||||
@@ -94,7 +91,6 @@ def get_host_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_store(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
"""Return store information.
|
||||
|
||||
@@ -104,7 +100,6 @@ def get_store(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_supervisor_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
"""Return Supervisor information.
|
||||
|
||||
@@ -114,7 +109,6 @@ def get_supervisor_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_network_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
"""Return Host Network information.
|
||||
|
||||
@@ -124,7 +118,6 @@ def get_network_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_addons_info(hass: HomeAssistant) -> dict[str, dict[str, Any] | None] | None:
|
||||
"""Return Addons info.
|
||||
|
||||
@@ -143,7 +136,6 @@ def get_addons_list(hass: HomeAssistant) -> list[dict[str, Any]] | None:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_addons_stats(hass: HomeAssistant) -> dict[str, dict[str, Any] | None]:
|
||||
"""Return Addons stats.
|
||||
|
||||
@@ -153,7 +145,6 @@ def get_addons_stats(hass: HomeAssistant) -> dict[str, dict[str, Any] | None]:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_core_stats(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"""Return core stats.
|
||||
|
||||
@@ -163,7 +154,6 @@ def get_core_stats(hass: HomeAssistant) -> dict[str, Any]:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_supervisor_stats(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"""Return supervisor stats.
|
||||
|
||||
@@ -173,7 +163,6 @@ def get_supervisor_stats(hass: HomeAssistant) -> dict[str, Any]:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_os_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
"""Return OS information.
|
||||
|
||||
@@ -183,7 +172,6 @@ def get_os_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_core_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
"""Return Home Assistant Core information from Supervisor.
|
||||
|
||||
@@ -193,7 +181,6 @@ def get_core_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_issues_info(hass: HomeAssistant) -> SupervisorIssues | None:
|
||||
"""Return Supervisor issues info.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -51,7 +51,6 @@ from homeassistant.helpers.http import (
|
||||
from homeassistant.helpers.importlib import async_import_module
|
||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.setup import (
|
||||
SetupPhases,
|
||||
async_start_setup,
|
||||
@@ -175,7 +174,6 @@ class ConfData(TypedDict, total=False):
|
||||
ssl_profile: str
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_last_config(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
"""Return the last known working config."""
|
||||
store = storage.Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
@@ -24,7 +24,6 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import ( # noqa: F401
|
||||
@@ -78,7 +77,6 @@ DEVICE_CLASSES = [cls.value for cls in HumidifierDeviceClass]
|
||||
# mypy: disallow-any-generics
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Return if the humidifier is on based on the statemachine.
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
import homeassistant.helpers.service
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.typing import ConfigType, VolDictType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
DOMAIN = "input_boolean"
|
||||
|
||||
@@ -81,7 +80,6 @@ class InputBooleanStorageCollection(collection.DictStorageCollection):
|
||||
return {CONF_ID: item[CONF_ID]} | update_data
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Test if input_boolean is True."""
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,7 +26,6 @@ from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.frame import ReportBehavior, report_usage
|
||||
from homeassistant.helpers.typing import ConfigType, VolDictType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from .const import ( # noqa: F401
|
||||
@@ -223,7 +222,6 @@ LIGHT_TURN_OFF_SCHEMA: VolDictType = {
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Return if the lights are on based on the statemachine."""
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,7 +30,6 @@ from homeassistant.helpers.integration_platform import (
|
||||
async_process_integration_platforms,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.event_type import EventType
|
||||
|
||||
from . import rest_api, websocket_api
|
||||
@@ -62,7 +61,6 @@ LOG_MESSAGE_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def log_entry(
|
||||
hass: HomeAssistant,
|
||||
name: str,
|
||||
@@ -76,7 +74,6 @@ def log_entry(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_log_entry(
|
||||
hass: HomeAssistant,
|
||||
name: str,
|
||||
|
||||
@@ -59,7 +59,6 @@ from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.network import get_url
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .browse_media import ( # noqa: F401
|
||||
@@ -246,7 +245,6 @@ class _ImageCache(TypedDict):
|
||||
_ENTITY_IMAGE_CACHE = _ImageCache(images=collections.OrderedDict(), maxsize=16)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool:
|
||||
"""Return true if specified media player entity_id is on.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,6 @@ from homeassistant.components.media_player import BrowseError, BrowseMedia
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.frame import report_usage
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import DOMAIN, MEDIA_SOURCE_DATA
|
||||
from .error import UnknownMediaSource, Unresolvable
|
||||
@@ -37,7 +36,6 @@ def _get_media_item(
|
||||
return item
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_browse_media(
|
||||
hass: HomeAssistant,
|
||||
media_content_id: str | None,
|
||||
@@ -71,7 +69,6 @@ async def async_browse_media(
|
||||
return item
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_resolve_media(
|
||||
hass: HomeAssistant,
|
||||
media_content_id: str,
|
||||
|
||||
@@ -314,7 +314,7 @@ class LocalMediaView(http.HomeAssistantView):
|
||||
|
||||
async def head(
|
||||
self, request: web.Request, source_dir_id: str, location: str
|
||||
) -> None:
|
||||
) -> web.Response:
|
||||
"""Handle a HEAD request.
|
||||
|
||||
This is sent by some DLNA renderers, like Samsung ones, prior to sending
|
||||
@@ -322,7 +322,9 @@ class LocalMediaView(http.HomeAssistantView):
|
||||
|
||||
Check whether the location exists or not.
|
||||
"""
|
||||
await self._validate_media_path(source_dir_id, location)
|
||||
media_path = await self._validate_media_path(source_dir_id, location)
|
||||
mime_type, _ = mimetypes.guess_type(str(media_path))
|
||||
return web.Response(content_type=mime_type)
|
||||
|
||||
async def get(
|
||||
self, request: web.Request, source_dir_id: str, location: str
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -15,8 +15,12 @@ _MOTION_DOMAIN_SPECS = {
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_detected": make_entity_state_condition(_MOTION_DOMAIN_SPECS, STATE_ON),
|
||||
"is_not_detected": make_entity_state_condition(_MOTION_DOMAIN_SPECS, STATE_OFF),
|
||||
"is_detected": make_entity_state_condition(
|
||||
_MOTION_DOMAIN_SPECS, STATE_ON, support_duration=True
|
||||
),
|
||||
"is_not_detected": make_entity_state_condition(
|
||||
_MOTION_DOMAIN_SPECS, STATE_OFF, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_detected:
|
||||
fields: *condition_common_fields
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user