mirror of
https://github.com/home-assistant/core.git
synced 2026-04-16 22:56:08 +02:00
Compare commits
2 Commits
dev
...
ariel-pyth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
973ddf3476 | ||
|
|
de6e8bd19d |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -362,8 +362,6 @@ 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,7 +28,8 @@ COPY rootfs /
|
||||
COPY --from=ghcr.io/alexxit/go2rtc@sha256:675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae /usr/local/bin/go2rtc /bin/go2rtc
|
||||
|
||||
## Setup Home Assistant Core dependencies
|
||||
COPY --parents requirements.txt homeassistant/package_constraints.txt homeassistant/
|
||||
COPY requirements.txt homeassistant/
|
||||
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
|
||||
RUN \
|
||||
# Verify go2rtc can be executed
|
||||
go2rtc --version \
|
||||
@@ -48,7 +49,7 @@ RUN \
|
||||
-r homeassistant/requirements_all.txt
|
||||
|
||||
## Setup Home Assistant Core
|
||||
COPY --parents LICENSE* README* homeassistant/ pyproject.toml homeassistant/
|
||||
COPY . homeassistant/
|
||||
RUN \
|
||||
uv pip install \
|
||||
-e ./homeassistant \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "denon",
|
||||
"name": "Denon",
|
||||
"integrations": ["denon", "denonavr", "denon_rs232", "heos"]
|
||||
"integrations": ["denon", "denonavr", "heos"]
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ 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
|
||||
|
||||
@@ -172,6 +173,7 @@ 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,6 +83,7 @@ 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
|
||||
|
||||
@@ -237,6 +238,7 @@ 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.
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ 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,
|
||||
@@ -162,6 +163,7 @@ 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)
|
||||
@@ -210,6 +212,7 @@ async def _async_get_image(
|
||||
raise HomeAssistantError("Unable to get image")
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_image(
|
||||
hass: HomeAssistant,
|
||||
entity_id: str,
|
||||
@@ -244,12 +247,14 @@ 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:
|
||||
|
||||
@@ -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
|
||||
from homeassistant.loader import async_get_integration, bind_hass
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
|
||||
# Pre-import backup to avoid it being imported
|
||||
@@ -181,6 +181,7 @@ class CloudConnectionState(Enum):
|
||||
CLOUD_DISCONNECTED = "cloud_disconnected"
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_is_logged_in(hass: HomeAssistant) -> bool:
|
||||
"""Test if user is logged in.
|
||||
@@ -190,6 +191,7 @@ 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."""
|
||||
@@ -205,6 +207,7 @@ 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."""
|
||||
@@ -227,6 +230,7 @@ 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):
|
||||
@@ -241,6 +245,7 @@ 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:
|
||||
@@ -267,6 +272,7 @@ def async_listen_cloudhook_change(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_remote_ui_url(hass: HomeAssistant) -> str:
|
||||
"""Get the remote UI URL."""
|
||||
|
||||
@@ -25,6 +25,7 @@ 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"
|
||||
@@ -53,6 +54,7 @@ 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,
|
||||
@@ -91,6 +93,7 @@ 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.
|
||||
|
||||
@@ -101,6 +104,7 @@ 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."""
|
||||
@@ -108,6 +112,7 @@ 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(
|
||||
@@ -115,6 +120,7 @@ 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."""
|
||||
@@ -122,6 +128,7 @@ 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,6 +23,7 @@ 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,
|
||||
@@ -126,6 +127,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_set_agent(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -136,6 +138,7 @@ def async_set_agent(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_unset_agent(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
|
||||
@@ -29,6 +29,7 @@ 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
|
||||
@@ -86,6 +87,7 @@ __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,57 +0,0 @@
|
||||
"""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
|
||||
@@ -1,119 +0,0 @@
|
||||
"""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,
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
"""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]
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,64 +0,0 @@
|
||||
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
|
||||
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"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,6 +5,7 @@ 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,
|
||||
@@ -51,6 +52,7 @@ 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)
|
||||
|
||||
@@ -87,7 +87,6 @@ class MbusDeviceType(IntEnum):
|
||||
GAS = 3
|
||||
HEAT = 4
|
||||
WATER = 7
|
||||
HEAT_COOL = 12
|
||||
|
||||
|
||||
SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
@@ -572,16 +571,6 @@ 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,
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ from .coordinator import CometBlueConfigEntry, CometBlueDataUpdateCoordinator
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BUTTON,
|
||||
Platform.CLIMATE,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ 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]):
|
||||
@@ -54,7 +53,6 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
|
||||
)
|
||||
self.device = cometblue
|
||||
self.address = cometblue.client.address
|
||||
self.data = CometBlueCoordinatorData()
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
@@ -66,11 +64,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}"
|
||||
@@ -90,23 +88,20 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
|
||||
|
||||
async def _async_update_data(self) -> CometBlueCoordinatorData:
|
||||
"""Poll the device."""
|
||||
data = CometBlueCoordinatorData()
|
||||
data: CometBlueCoordinatorData = 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 and battery are optional and should not trigger a retry
|
||||
# holiday is 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)",
|
||||
@@ -115,6 +110,7 @@ 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
|
||||
@@ -132,9 +128,5 @@ 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
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
"""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,17 +39,9 @@ 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,
|
||||
RESET_BREAKS_IN_HA_VERSION,
|
||||
EvoService,
|
||||
)
|
||||
from .const import ATTR_DURATION, ATTR_PERIOD, DOMAIN, EVOHOME_DATA, 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__)
|
||||
|
||||
@@ -193,11 +185,6 @@ 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(
|
||||
@@ -460,13 +447,6 @@ 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,9 +26,6 @@ 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):
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
"""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,16 +22,8 @@ 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,
|
||||
RESET_BREAKS_IN_HA_VERSION,
|
||||
EvoService,
|
||||
)
|
||||
from .const import ATTR_DURATION, ATTR_PERIOD, ATTR_SETPOINT, DOMAIN, 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]] = {
|
||||
@@ -166,13 +158,6 @@ 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,23 +19,9 @@
|
||||
"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 a zone to follow its schedule (deprecated).",
|
||||
"description": "Sets the zone to follow its schedule.",
|
||||
"name": "Clear zone override"
|
||||
},
|
||||
"refresh_system": {
|
||||
@@ -43,11 +29,11 @@
|
||||
"name": "Refresh system"
|
||||
},
|
||||
"reset_system": {
|
||||
"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.",
|
||||
"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).",
|
||||
"name": "Reset system"
|
||||
},
|
||||
"set_dhw_override": {
|
||||
"description": "Overrides a DHW's state, either indefinitely or for a specified duration, after which it will revert to following its schedule.",
|
||||
"description": "Overrides the DHW 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.",
|
||||
@@ -61,7 +47,7 @@
|
||||
"name": "Set DHW override"
|
||||
},
|
||||
"set_system_mode": {
|
||||
"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.",
|
||||
"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.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"description": "The duration in hours; used only with `AutoWithEco` mode (up to 24 hours).",
|
||||
@@ -79,7 +65,7 @@
|
||||
"name": "Set system mode"
|
||||
},
|
||||
"set_zone_override": {
|
||||
"description": "Overrides a zone's setpoint, either indefinitely or for a specified duration, after which it will revert to following its schedule.",
|
||||
"description": "Overrides the zone 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,6 +25,7 @@ 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,
|
||||
@@ -87,6 +88,7 @@ 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)
|
||||
|
||||
@@ -20,6 +20,7 @@ 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 (
|
||||
@@ -70,6 +71,7 @@ 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:
|
||||
@@ -77,6 +79,7 @@ def get_ffmpeg_manager(hass: HomeAssistant) -> FFmpegManager:
|
||||
return hass.data[DATA_FFMPEG]
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_image(
|
||||
hass: HomeAssistant,
|
||||
input_source: str,
|
||||
|
||||
@@ -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
|
||||
from homeassistant.loader import async_get_integration, bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .pr_download import download_pr_artifact
|
||||
@@ -354,6 +354,7 @@ class Panel:
|
||||
return response
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_register_built_in_panel(
|
||||
hass: HomeAssistant,
|
||||
@@ -392,6 +393,7 @@ 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
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
"disk_free": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
"disk_size": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
"disk_usage": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["glances_api"],
|
||||
"requirements": ["glances-api==0.10.0"]
|
||||
"requirements": ["glances-api==0.8.0"]
|
||||
}
|
||||
|
||||
@@ -49,14 +49,6 @@ SENSOR_TYPES = {
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
("fs", "disk_size"): GlancesSensorEntityDescription(
|
||||
key="disk_size",
|
||||
type="fs",
|
||||
translation_key="disk_size",
|
||||
native_unit_of_measurement=UnitOfInformation.GIBIBYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
("fs", "disk_free"): GlancesSensorEntityDescription(
|
||||
key="disk_free",
|
||||
type="fs",
|
||||
|
||||
@@ -50,9 +50,6 @@
|
||||
"disk_free": {
|
||||
"name": "{sensor_label} disk free"
|
||||
},
|
||||
"disk_size": {
|
||||
"name": "{sensor_label} disk size"
|
||||
},
|
||||
"disk_usage": {
|
||||
"name": "{sensor_label} disk usage"
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ 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
|
||||
@@ -102,6 +103,7 @@ 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:
|
||||
@@ -115,10 +117,11 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
|
||||
|
||||
# expand_entity_ids and get_entity_ids are for backwards compatibility only
|
||||
expand_entity_ids = _expand_entity_ids
|
||||
get_entity_ids = _get_entity_ids
|
||||
expand_entity_ids = bind_hass(_expand_entity_ids)
|
||||
get_entity_ids = bind_hass(_get_entity_ids)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]:
|
||||
"""Get all groups that contain this entity.
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ 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,
|
||||
@@ -73,6 +74,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
"""Return generic information from Supervisor.
|
||||
|
||||
@@ -82,6 +84,7 @@ 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.
|
||||
|
||||
@@ -91,6 +94,7 @@ 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.
|
||||
|
||||
@@ -100,6 +104,7 @@ 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.
|
||||
|
||||
@@ -109,6 +114,7 @@ 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.
|
||||
|
||||
@@ -118,6 +124,7 @@ 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.
|
||||
|
||||
@@ -136,6 +143,7 @@ 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.
|
||||
|
||||
@@ -145,6 +153,7 @@ 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.
|
||||
|
||||
@@ -154,6 +163,7 @@ def get_core_stats(hass: HomeAssistant) -> dict[str, Any]:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_supervisor_stats(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"""Return supervisor stats.
|
||||
|
||||
@@ -163,6 +173,7 @@ 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.
|
||||
|
||||
@@ -172,6 +183,7 @@ 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.
|
||||
|
||||
@@ -181,6 +193,7 @@ 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.
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ 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,
|
||||
@@ -174,6 +175,7 @@ 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,6 +24,7 @@ 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
|
||||
@@ -77,6 +78,7 @@ 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.
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ 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"
|
||||
|
||||
@@ -80,6 +81,7 @@ 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)
|
||||
|
||||
@@ -26,6 +26,7 @@ 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
|
||||
@@ -222,6 +223,7 @@ 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)
|
||||
|
||||
@@ -30,6 +30,7 @@ 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
|
||||
@@ -61,6 +62,7 @@ LOG_MESSAGE_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def log_entry(
|
||||
hass: HomeAssistant,
|
||||
name: str,
|
||||
@@ -74,6 +76,7 @@ def log_entry(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_log_entry(
|
||||
hass: HomeAssistant,
|
||||
name: str,
|
||||
|
||||
@@ -59,6 +59,7 @@ 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
|
||||
@@ -245,6 +246,7 @@ 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.
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
@@ -36,6 +37,7 @@ def _get_media_item(
|
||||
return item
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_browse_media(
|
||||
hass: HomeAssistant,
|
||||
media_content_id: str | None,
|
||||
@@ -69,6 +71,7 @@ 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
|
||||
) -> web.Response:
|
||||
) -> None:
|
||||
"""Handle a HEAD request.
|
||||
|
||||
This is sent by some DLNA renderers, like Samsung ones, prior to sending
|
||||
@@ -322,9 +322,7 @@ class LocalMediaView(http.HomeAssistantView):
|
||||
|
||||
Check whether the location exists or not.
|
||||
"""
|
||||
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)
|
||||
await self._validate_media_path(source_dir_id, location)
|
||||
|
||||
async def get(
|
||||
self, request: web.Request, source_dir_id: str, location: str
|
||||
|
||||
@@ -15,12 +15,8 @@ _MOTION_DOMAIN_SPECS = {
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"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
|
||||
),
|
||||
"is_detected": make_entity_state_condition(_MOTION_DOMAIN_SPECS, STATE_ON),
|
||||
"is_not_detected": make_entity_state_condition(_MOTION_DOMAIN_SPECS, STATE_OFF),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,11 +8,6 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_detected:
|
||||
fields: *condition_common_fields
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -11,9 +10,6 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::motion::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::motion::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Motion is detected"
|
||||
@@ -23,9 +19,6 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::motion::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::motion::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Motion is not detected"
|
||||
|
||||
@@ -45,6 +45,7 @@ from homeassistant.helpers.dispatcher import (
|
||||
from homeassistant.helpers.importlib import async_import_module
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.setup import SetupPhases, async_pause_setup
|
||||
from homeassistant.util.collection import chunked_or_all
|
||||
from homeassistant.util.logging import catch_log_exception, log_exception
|
||||
@@ -220,6 +221,7 @@ def async_on_subscribe_done(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_subscribe(
|
||||
hass: HomeAssistant,
|
||||
topic: str,
|
||||
@@ -271,6 +273,7 @@ def async_subscribe_internal(
|
||||
return client.async_subscribe(topic, msg_callback, qos, encoding, job_type)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def subscribe(
|
||||
hass: HomeAssistant,
|
||||
topic: str,
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import package
|
||||
|
||||
from . import util
|
||||
@@ -41,6 +42,7 @@ def _check_docker_without_host_networking() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||
"""Get the network adapter configuration."""
|
||||
network: Network = await async_get_network(hass)
|
||||
@@ -53,6 +55,7 @@ def async_get_loaded_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||
return async_get_loaded_network(hass).adapters
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_source_ip(
|
||||
hass: HomeAssistant, target_ip: str | UndefinedType = UNDEFINED
|
||||
) -> str:
|
||||
@@ -87,6 +90,7 @@ async def async_get_source_ip(
|
||||
return source_ip if source_ip in all_ipv4s else all_ipv4s[0]
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_enabled_source_ips(
|
||||
hass: HomeAssistant,
|
||||
) -> list[IPv4Address | IPv6Address]:
|
||||
@@ -124,6 +128,7 @@ def async_only_default_interface_enabled(adapters: list[Adapter]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_ipv4_broadcast_addresses(hass: HomeAssistant) -> set[IPv4Address]:
|
||||
"""Return a set of broadcast addresses."""
|
||||
broadcast_addresses: set[IPv4Address] = {IPv4Address(IPV4_BROADCAST_ADDR)}
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.service import async_set_service_schema
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.loader import async_get_integration
|
||||
from homeassistant.loader import async_get_integration, bind_hass
|
||||
from homeassistant.setup import (
|
||||
SetupPhases,
|
||||
async_prepare_setup_platform,
|
||||
@@ -159,6 +159,7 @@ def async_setup_legacy(
|
||||
]
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_reload(hass: HomeAssistant, integration_name: str) -> None:
|
||||
"""Register notify services for an integration."""
|
||||
if not _async_integration_has_notify_services(hass, integration_name):
|
||||
@@ -172,6 +173,7 @@ async def async_reload(hass: HomeAssistant, integration_name: str) -> None:
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None:
|
||||
"""Unregister notify services for an integration."""
|
||||
notify_discovery_dispatcher = hass.data.get(NOTIFY_DISCOVERY_DISPATCHER)
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from . import views
|
||||
from .const import (
|
||||
@@ -63,6 +64,7 @@ class OnboardingStorage(Store[OnboardingStoreData]):
|
||||
return old_data
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_is_onboarded(hass: HomeAssistant) -> bool:
|
||||
"""Return if Home Assistant has been onboarded."""
|
||||
@@ -70,6 +72,7 @@ def async_is_onboarded(hass: HomeAssistant) -> bool:
|
||||
return data is None or data.onboarded is True
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_is_user_onboarded(hass: HomeAssistant) -> bool:
|
||||
"""Return if a user has been created as part of onboarding."""
|
||||
|
||||
@@ -101,8 +101,7 @@ SENSOR_TYPES: tuple[OpenEVSESensorDescription, ...] = (
|
||||
OpenEVSESensorDescription(
|
||||
key="charging_current",
|
||||
translation_key="charging_current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
|
||||
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda ev: ev.charging_current,
|
||||
@@ -118,8 +117,7 @@ SENSOR_TYPES: tuple[OpenEVSESensorDescription, ...] = (
|
||||
OpenEVSESensorDescription(
|
||||
key="charging_power",
|
||||
translation_key="charging_power",
|
||||
native_unit_of_measurement=UnitOfPower.MILLIWATT,
|
||||
suggested_unit_of_measurement=UnitOfPower.WATT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda ev: ev.charging_power,
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.components import frontend
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -70,6 +71,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_register_panel(
|
||||
hass: HomeAssistant,
|
||||
# The url to serve the panel
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
from homeassistant.util.uuid import random_uuid_hex
|
||||
@@ -74,6 +75,7 @@ def async_register_callback(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def create(
|
||||
hass: HomeAssistant,
|
||||
message: str,
|
||||
@@ -84,12 +86,14 @@ def create(
|
||||
hass.add_job(async_create, hass, message, title, notification_id)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def dismiss(hass: HomeAssistant, notification_id: str) -> None:
|
||||
"""Remove a notification."""
|
||||
hass.add_job(async_dismiss, hass, notification_id)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_create(
|
||||
hass: HomeAssistant,
|
||||
message: str,
|
||||
@@ -123,6 +127,7 @@ def _async_get_or_create_notifications(hass: HomeAssistant) -> dict[str, Notific
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_dismiss(hass: HomeAssistant, notification_id: str) -> None:
|
||||
"""Remove a notification."""
|
||||
notifications = _async_get_or_create_notifications(hass)
|
||||
|
||||
@@ -52,6 +52,7 @@ from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.typing import ConfigType, VolDictType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -92,6 +93,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_create_person(
|
||||
hass: HomeAssistant,
|
||||
name: str,
|
||||
@@ -109,6 +111,7 @@ async def async_create_person(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_add_user_device_tracker(
|
||||
hass: HomeAssistant, user_id: str, device_tracker_entity_id: str
|
||||
) -> None:
|
||||
|
||||
@@ -35,6 +35,7 @@ from homeassistant.core import (
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.service import async_set_service_schema
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import dt as dt_util, raise_if_invalid_filename
|
||||
from homeassistant.util.yaml.loader import load_yaml_dict
|
||||
|
||||
@@ -194,6 +195,7 @@ def guarded_inplacevar(op: str, target: Any, operand: Any) -> Any:
|
||||
return op_fun(target, operand)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def execute_script(
|
||||
hass: HomeAssistant,
|
||||
name: str,
|
||||
@@ -208,6 +210,7 @@ def execute_script(
|
||||
return execute(hass, filename, source, data, return_response=return_response)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def execute(
|
||||
hass: HomeAssistant,
|
||||
filename: str,
|
||||
|
||||
@@ -25,6 +25,7 @@ from homeassistant.helpers.integration_platform import (
|
||||
)
|
||||
from homeassistant.helpers.recorder import DATA_INSTANCE
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.event_type import EventType
|
||||
|
||||
# Pre-import backup to avoid it being imported
|
||||
@@ -127,6 +128,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Check if an entity is being recorded.
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ 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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -72,6 +73,7 @@ REMOTE_SERVICE_ACTIVITY_SCHEMA = cv.make_entity_service_schema(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Return if the remote is on based on the statemachine."""
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
@@ -65,6 +65,7 @@ from homeassistant.helpers.script import (
|
||||
from homeassistant.helpers.service import async_set_service_schema
|
||||
from homeassistant.helpers.trace import trace_get, trace_path
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
from homeassistant.util.dt import parse_datetime
|
||||
|
||||
@@ -90,6 +91,7 @@ SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema(
|
||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Return if the script is on based on the statemachine."""
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Iterator
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from soco import SoCo, SoCoException
|
||||
from soco import SoCo
|
||||
from soco.alarms import Alarm, Alarms
|
||||
from soco.events_base import Event as SonosEvent
|
||||
|
||||
@@ -30,7 +30,6 @@ class SonosAlarms(SonosHouseholdCoordinator):
|
||||
super().__init__(*args)
|
||||
self.alarms: Alarms = Alarms()
|
||||
self.created_alarm_ids: set[str] = set()
|
||||
self._household_mismatch_logged = False
|
||||
|
||||
def __iter__(self) -> Iterator:
|
||||
"""Return an iterator for the known alarms."""
|
||||
@@ -77,40 +76,21 @@ class SonosAlarms(SonosHouseholdCoordinator):
|
||||
await self.async_update_entities(speaker.soco, event_id)
|
||||
|
||||
@soco_error()
|
||||
def update_cache(
|
||||
self,
|
||||
soco: SoCo,
|
||||
update_id: int | None = None,
|
||||
) -> bool:
|
||||
"""Update cache of known alarms and return whether any were seen."""
|
||||
try:
|
||||
self.alarms.update(soco)
|
||||
except SoCoException as err:
|
||||
err_msg = str(err)
|
||||
# Only catch the specific household mismatch error
|
||||
if "Alarm list UID" in err_msg and "does not match" in err_msg:
|
||||
if not self._household_mismatch_logged:
|
||||
_LOGGER.warning(
|
||||
"Sonos alarms for %s cannot be updated due to a household mismatch. "
|
||||
"This is a known limitation in setups with multiple households. "
|
||||
"You can safely ignore this warning, or to silence it, remove the "
|
||||
"affected household from your Sonos system. Error: %s",
|
||||
soco.player_name,
|
||||
err_msg,
|
||||
)
|
||||
self._household_mismatch_logged = True
|
||||
return False
|
||||
# Let all other exceptions bubble up to be handled by @soco_error()
|
||||
raise
|
||||
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
|
||||
"""Update cache of known alarms and return if cache has changed."""
|
||||
self.alarms.update(soco)
|
||||
|
||||
if update_id and self.alarms.last_id < update_id:
|
||||
# Skip updates if latest query result is outdated or lagging
|
||||
return False
|
||||
|
||||
if (
|
||||
self.last_processed_event_id
|
||||
and self.alarms.last_id <= self.last_processed_event_id
|
||||
):
|
||||
# Skip updates already processed
|
||||
return False
|
||||
|
||||
_LOGGER.debug(
|
||||
"Updating processed event %s from %s (was %s)",
|
||||
self.alarms.last_id,
|
||||
|
||||
@@ -8,6 +8,7 @@ from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.const import ATTR_TIME
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
from homeassistant.helpers.entity_platform import DATA_DOMAIN_PLATFORM_ENTITIES
|
||||
|
||||
from .const import ATTR_QUEUE_POSITION, DOMAIN
|
||||
from .media_player import SonosMediaPlayerEntity
|
||||
@@ -34,11 +35,25 @@ ATTR_WITH_GROUP = "with_group"
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register Sonos services."""
|
||||
|
||||
async def async_handle_snapshot_restore(
|
||||
entities: list[SonosMediaPlayerEntity], service_call: ServiceCall
|
||||
) -> None:
|
||||
"""Handle snapshot and restore services."""
|
||||
speakers = [entity.speaker for entity in entities]
|
||||
@service.verify_domain_control(DOMAIN)
|
||||
async def async_service_handle(service_call: ServiceCall) -> None:
|
||||
"""Handle dispatched services."""
|
||||
platform_entities = hass.data.get(DATA_DOMAIN_PLATFORM_ENTITIES, {}).get(
|
||||
(MEDIA_PLAYER_DOMAIN, DOMAIN), {}
|
||||
)
|
||||
|
||||
entities = await service.async_extract_entities(
|
||||
platform_entities.values(), service_call
|
||||
)
|
||||
|
||||
if not entities:
|
||||
return
|
||||
|
||||
speakers: list[SonosSpeaker] = []
|
||||
for entity in entities:
|
||||
assert isinstance(entity, SonosMediaPlayerEntity)
|
||||
speakers.append(entity.speaker)
|
||||
|
||||
config_entry = speakers[0].config_entry # All speakers share the same entry
|
||||
|
||||
if service_call.service == SERVICE_SNAPSHOT:
|
||||
@@ -50,22 +65,16 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
hass, config_entry, speakers, service_call.data[ATTR_WITH_GROUP]
|
||||
)
|
||||
|
||||
service.async_register_batched_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SNAPSHOT,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema={vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean},
|
||||
func=async_handle_snapshot_restore,
|
||||
join_unjoin_schema = cv.make_entity_service_schema(
|
||||
{vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean}
|
||||
)
|
||||
|
||||
service.async_register_batched_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_RESTORE,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema={vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean},
|
||||
func=async_handle_snapshot_restore,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SNAPSHOT, async_service_handle, join_unjoin_schema
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESTORE, async_service_handle, join_unjoin_schema
|
||||
)
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
snapshot:
|
||||
target:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
fields:
|
||||
entity_id:
|
||||
selector:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
with_group:
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
restore:
|
||||
target:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
fields:
|
||||
entity_id:
|
||||
selector:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
with_group:
|
||||
default: true
|
||||
selector:
|
||||
|
||||
@@ -173,6 +173,10 @@
|
||||
"restore": {
|
||||
"description": "Restores a snapshot of a media player.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Name of entity that will be restored.",
|
||||
"name": "Entity"
|
||||
},
|
||||
"with_group": {
|
||||
"description": "Whether the group layout and the state of other speakers in the group should also be restored.",
|
||||
"name": "[%key:component::sonos::services::snapshot::fields::with_group::name%]"
|
||||
@@ -193,6 +197,10 @@
|
||||
"snapshot": {
|
||||
"description": "Takes a snapshot of a media player.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Name of entity that will be snapshot.",
|
||||
"name": "Entity"
|
||||
},
|
||||
"with_group": {
|
||||
"description": "Whether the snapshot should include the group layout and the state of other speakers in the group.",
|
||||
"name": "With group"
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.core import HassJob, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo as _SsdpServiceInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import async_get_ssdp
|
||||
from homeassistant.loader import async_get_ssdp, bind_hass
|
||||
from homeassistant.util.logging import catch_log_exception
|
||||
|
||||
from . import websocket_api
|
||||
@@ -45,6 +45,7 @@ def _format_err(name: str, *args: Any) -> str:
|
||||
return f"Exception in SSDP callback {name}: {args}"
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_register_callback(
|
||||
hass: HomeAssistant,
|
||||
callback: Callable[
|
||||
@@ -67,6 +68,7 @@ async def async_register_callback(
|
||||
return await scanner.async_register_callback(job, match_dict)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_discovery_info_by_udn_st(
|
||||
hass: HomeAssistant, udn: str, st: str
|
||||
) -> _SsdpServiceInfo | None:
|
||||
@@ -75,6 +77,7 @@ async def async_get_discovery_info_by_udn_st(
|
||||
return await scanner.async_get_discovery_info_by_udn_st(udn, st)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_discovery_info_by_st(
|
||||
hass: HomeAssistant, st: str
|
||||
) -> list[_SsdpServiceInfo]:
|
||||
@@ -83,6 +86,7 @@ async def async_get_discovery_info_by_st(
|
||||
return await scanner.async_get_discovery_info_by_st(st)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_discovery_info_by_udn(
|
||||
hass: HomeAssistant, udn: str
|
||||
) -> list[_SsdpServiceInfo]:
|
||||
|
||||
@@ -21,6 +21,7 @@ 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 DOMAIN
|
||||
@@ -50,6 +51,7 @@ DEVICE_CLASSES = [cls.value for cls in SwitchDeviceClass]
|
||||
# mypy: disallow-any-generics
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Return if the switch is on based on the statemachine.
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ class SwitchbotDataUpdateCoordinator(ActiveBluetoothDataUpdateCoordinator[None])
|
||||
# and we actually have a way to connect to the device
|
||||
return (
|
||||
self.hass.state is CoreState.running
|
||||
and self.connectable
|
||||
and self.device.poll_needed(seconds_since_last_poll)
|
||||
and bool(
|
||||
bluetooth.async_ble_device_from_address(
|
||||
|
||||
@@ -121,15 +121,6 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"battery_range": {
|
||||
"default": "mdi:battery",
|
||||
"state": {
|
||||
"critical": "mdi:battery-alert-variant-outline",
|
||||
"high": "mdi:battery-80",
|
||||
"low": "mdi:battery-20",
|
||||
"medium": "mdi:battery-50"
|
||||
}
|
||||
},
|
||||
"light_level": {
|
||||
"default": "mdi:brightness-7",
|
||||
"state": {
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
import switchbot
|
||||
from switchbot import HumidifierWaterLevel, SwitchbotModel
|
||||
from switchbot import HumidifierWaterLevel
|
||||
from switchbot.const.air_purifier import AirQualityLevel
|
||||
|
||||
from homeassistant.components.bluetooth import async_last_service_info
|
||||
@@ -38,16 +35,8 @@ from .entity import SwitchbotEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SwitchBotSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes SwitchBot sensor entities with optional value transformation."""
|
||||
|
||||
value_fn: Callable[[str | int | None], str | int | None] = lambda v: v
|
||||
|
||||
|
||||
SENSOR_TYPES: dict[str, SwitchBotSensorEntityDescription] = {
|
||||
"rssi": SwitchBotSensorEntityDescription(
|
||||
SENSOR_TYPES: dict[str, SensorEntityDescription] = {
|
||||
"rssi": SensorEntityDescription(
|
||||
key="rssi",
|
||||
translation_key="bluetooth_signal",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
@@ -56,7 +45,7 @@ SENSOR_TYPES: dict[str, SwitchBotSensorEntityDescription] = {
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"wifi_rssi": SwitchBotSensorEntityDescription(
|
||||
"wifi_rssi": SensorEntityDescription(
|
||||
key="wifi_rssi",
|
||||
translation_key="wifi_signal",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
@@ -65,91 +54,78 @@ SENSOR_TYPES: dict[str, SwitchBotSensorEntityDescription] = {
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"battery": SwitchBotSensorEntityDescription(
|
||||
"battery": SensorEntityDescription(
|
||||
key="battery",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"co2": SwitchBotSensorEntityDescription(
|
||||
"co2": SensorEntityDescription(
|
||||
key="co2",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
),
|
||||
"lightLevel": SwitchBotSensorEntityDescription(
|
||||
"lightLevel": SensorEntityDescription(
|
||||
key="lightLevel",
|
||||
translation_key="light_level",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"humidity": SwitchBotSensorEntityDescription(
|
||||
"humidity": SensorEntityDescription(
|
||||
key="humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
),
|
||||
"illuminance": SwitchBotSensorEntityDescription(
|
||||
"illuminance": SensorEntityDescription(
|
||||
key="illuminance",
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
),
|
||||
"temperature": SwitchBotSensorEntityDescription(
|
||||
"temperature": SensorEntityDescription(
|
||||
key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
),
|
||||
"power": SwitchBotSensorEntityDescription(
|
||||
"power": SensorEntityDescription(
|
||||
key="power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
),
|
||||
"current": SwitchBotSensorEntityDescription(
|
||||
"current": SensorEntityDescription(
|
||||
key="current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
),
|
||||
"voltage": SwitchBotSensorEntityDescription(
|
||||
"voltage": SensorEntityDescription(
|
||||
key="voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
),
|
||||
"aqi_level": SwitchBotSensorEntityDescription(
|
||||
"aqi_level": SensorEntityDescription(
|
||||
key="aqi_level",
|
||||
translation_key="aqi_quality_level",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[member.name.lower() for member in AirQualityLevel],
|
||||
),
|
||||
"energy": SwitchBotSensorEntityDescription(
|
||||
"energy": SensorEntityDescription(
|
||||
key="energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
),
|
||||
"water_level": SwitchBotSensorEntityDescription(
|
||||
"water_level": SensorEntityDescription(
|
||||
key="water_level",
|
||||
translation_key="water_level",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=HumidifierWaterLevel.get_levels(),
|
||||
),
|
||||
"battery_range": SwitchBotSensorEntityDescription(
|
||||
key="battery_range",
|
||||
translation_key="battery_range",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["critical", "low", "medium", "high"],
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda v: {
|
||||
"<10%": "critical",
|
||||
"10-19%": "low",
|
||||
"20-59%": "medium",
|
||||
">=60%": "high",
|
||||
}.get(str(v)),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +136,6 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Switchbot sensor based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
parsed_data = coordinator.device.parsed_data
|
||||
sensor_entities: list[SensorEntity] = []
|
||||
if isinstance(coordinator.device, switchbot.SwitchbotRelaySwitch2PM):
|
||||
sensor_entities.extend(
|
||||
@@ -169,24 +144,10 @@ async def async_setup_entry(
|
||||
for sensor in coordinator.device.get_parsed_data(channel)
|
||||
if sensor in SENSOR_TYPES
|
||||
)
|
||||
elif coordinator.model == SwitchbotModel.PRESENCE_SENSOR:
|
||||
sensor_entities.extend(
|
||||
SwitchBotSensor(coordinator, sensor)
|
||||
for sensor in (
|
||||
*(
|
||||
s
|
||||
for s in parsed_data
|
||||
if s in SENSOR_TYPES and s not in ("battery", "battery_range")
|
||||
),
|
||||
"battery_range",
|
||||
)
|
||||
)
|
||||
if "battery" in parsed_data:
|
||||
sensor_entities.append(SwitchBotSensor(coordinator, "battery"))
|
||||
else:
|
||||
sensor_entities.extend(
|
||||
SwitchBotSensor(coordinator, sensor)
|
||||
for sensor in parsed_data
|
||||
for sensor in coordinator.device.parsed_data
|
||||
if sensor in SENSOR_TYPES
|
||||
)
|
||||
sensor_entities.append(SwitchbotRSSISensor(coordinator, "rssi"))
|
||||
@@ -196,8 +157,6 @@ async def async_setup_entry(
|
||||
class SwitchBotSensor(SwitchbotEntity, SensorEntity):
|
||||
"""Representation of a Switchbot sensor."""
|
||||
|
||||
entity_description: SwitchBotSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SwitchbotDataUpdateCoordinator,
|
||||
@@ -226,7 +185,7 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> str | int | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.parsed_data.get(self._sensor))
|
||||
return self.parsed_data[self._sensor]
|
||||
|
||||
|
||||
class SwitchbotRSSISensor(SwitchBotSensor):
|
||||
|
||||
@@ -291,15 +291,6 @@
|
||||
"unhealthy": "Unhealthy"
|
||||
}
|
||||
},
|
||||
"battery_range": {
|
||||
"name": "Battery range",
|
||||
"state": {
|
||||
"critical": "Critical",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"medium": "[%key:common::state::medium%]"
|
||||
}
|
||||
},
|
||||
"bluetooth_signal": {
|
||||
"name": "Bluetooth signal"
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.helpers import (
|
||||
integration_platform,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -39,6 +40,7 @@ class SystemHealthProtocol(Protocol):
|
||||
"""Register system health callbacks."""
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_register_info(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN, SENSOR_UNIQUE_ID_MIGRATION
|
||||
from .coordinator import TwenteMilieuConfigEntry, TwenteMilieuDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.CALENDAR, Platform.SENSOR]
|
||||
@@ -18,21 +14,6 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: TwenteMilieuConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Twente Milieu from a config entry."""
|
||||
old_prefix = f"{DOMAIN}_{entry.unique_id}_"
|
||||
|
||||
@callback
|
||||
def _migrate_unique_id(
|
||||
entity_entry: er.RegistryEntry,
|
||||
) -> dict[str, Any] | None:
|
||||
if not entity_entry.unique_id.startswith(old_prefix):
|
||||
return None
|
||||
old_key = entity_entry.unique_id.removeprefix(old_prefix)
|
||||
if (new_key := SENSOR_UNIQUE_ID_MIGRATION.get(old_key)) is None:
|
||||
return None
|
||||
return {"new_unique_id": f"{entry.unique_id}_{new_key}"}
|
||||
|
||||
await er.async_migrate_entries(hass, entry.entry_id, _migrate_unique_id)
|
||||
|
||||
coordinator = TwenteMilieuDataUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
@@ -22,11 +22,3 @@ WASTE_TYPE_TO_DESCRIPTION = {
|
||||
WasteType.PAPER: "Paper waste pickup",
|
||||
WasteType.TREE: "Christmas tree pickup",
|
||||
}
|
||||
|
||||
SENSOR_UNIQUE_ID_MIGRATION = {
|
||||
"tree": "tree",
|
||||
"Non-recyclable": "non_recyclable",
|
||||
"Organic": "organic",
|
||||
"Paper": "paper",
|
||||
"Plastic": "packages",
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TwenteMilieuConfigEntry
|
||||
from .entity import TwenteMilieuEntity
|
||||
|
||||
@@ -34,25 +36,25 @@ SENSORS: tuple[TwenteMilieuSensorDescription, ...] = (
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
),
|
||||
TwenteMilieuSensorDescription(
|
||||
key="non_recyclable",
|
||||
key="Non-recyclable",
|
||||
translation_key="non_recyclable_waste_pickup",
|
||||
waste_type=WasteType.NON_RECYCLABLE,
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
),
|
||||
TwenteMilieuSensorDescription(
|
||||
key="organic",
|
||||
key="Organic",
|
||||
translation_key="organic_waste_pickup",
|
||||
waste_type=WasteType.ORGANIC,
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
),
|
||||
TwenteMilieuSensorDescription(
|
||||
key="paper",
|
||||
key="Paper",
|
||||
translation_key="paper_waste_pickup",
|
||||
waste_type=WasteType.PAPER,
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
),
|
||||
TwenteMilieuSensorDescription(
|
||||
key="packages",
|
||||
key="Plastic",
|
||||
translation_key="packages_waste_pickup",
|
||||
waste_type=WasteType.PACKAGES,
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
@@ -84,7 +86,7 @@ class TwenteMilieuSensor(TwenteMilieuEntity, SensorEntity):
|
||||
"""Initialize the Twente Milieu entity."""
|
||||
super().__init__(entry)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{entry.unique_id}_{description.key}"
|
||||
self._attr_unique_id = f"{DOMAIN}_{entry.data[CONF_ID]}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> date | None:
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
Several config flow test improvements needed:
|
||||
- test_flow_works does not assert that the unique_id is set on the config entry.
|
||||
- test_flow_fails_user_credentials_faulty (and test_flow_fails_hub_unavailable)
|
||||
should also verify recovery by completing with CREATE_ENTRY; the two tests
|
||||
can be parametrized together.
|
||||
- aiounifi.Controller.login is patched at library level; it should be patched
|
||||
where it is actually used (homeassistant.components.unifi…).
|
||||
config-flow:
|
||||
status: todo
|
||||
comment: |
|
||||
The user step is missing data_description for port, site, username, password
|
||||
and verify_ssl fields, and the site step has no data_description at all.
|
||||
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:
|
||||
status: todo
|
||||
comment: |
|
||||
Not all entities have has_entity_name set to True. Requires migration
|
||||
with breaking change support.
|
||||
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: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations:
|
||||
status: todo
|
||||
comment: |
|
||||
Multiple entities still use name_fn to set the entity name instead of
|
||||
translation_key: button.py (Restart, Power Cycle, Regenerate Password),
|
||||
image.py (QR Code), sensor.py (RX, TX, Link speed, PoE Power, temperature,
|
||||
latency, Uptime), switch.py (firewall policy, outlet, port forward, traffic
|
||||
rule, traffic route, PoE, port, DPI restriction). Requires migration with
|
||||
breaking change support.
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: todo
|
||||
comment: |
|
||||
The user flow currently allows updating existing config entry data
|
||||
(host/credentials), which should be handled by a dedicated
|
||||
async_step_reconfigure instead.
|
||||
repair-issues: todo
|
||||
stale-devices:
|
||||
status: todo
|
||||
comment: |
|
||||
Only manual removal via async_remove_config_entry_device; no automatic
|
||||
cleanup of devices removed from the UniFi controller. Consider also
|
||||
whether device tracker clients should be split into their own device
|
||||
registry entries.
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
@@ -7,7 +7,6 @@ from dataclasses import dataclass
|
||||
from unifi_access_api import Door
|
||||
|
||||
from homeassistant.components.event import (
|
||||
DoorbellEventType,
|
||||
EventDeviceClass,
|
||||
EventEntity,
|
||||
EventEntityDescription,
|
||||
@@ -32,7 +31,7 @@ DOORBELL_EVENT_DESCRIPTION = UnifiAccessEventEntityDescription(
|
||||
key="doorbell",
|
||||
translation_key="doorbell",
|
||||
device_class=EventDeviceClass.DOORBELL,
|
||||
event_types=[DoorbellEventType.RING],
|
||||
event_types=["ring"],
|
||||
category="doorbell",
|
||||
)
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ from homeassistant.helpers.entity_platform import EntityPlatform
|
||||
from homeassistant.helpers.frame import ReportBehavior, report_usage
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import DATA_COMPONENT, DOMAIN, VacuumActivity, VacuumEntityFeature
|
||||
from .websocket import async_register_websocket_handlers
|
||||
@@ -70,6 +71,7 @@ _BATTERY_DEPRECATION_IGNORED_PLATFORMS = ("template",)
|
||||
# mypy: disallow-any-generics
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Return if the vacuum is on based on the statemachine."""
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.network import get_url, is_cloud_connection
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import network as network_util
|
||||
from homeassistant.util.aiohttp import MockRequest, MockStreamReader, serialize_response
|
||||
|
||||
@@ -35,6 +36,7 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_register(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
@@ -70,6 +72,7 @@ def async_register(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_unregister(hass: HomeAssistant, webhook_id: str) -> None:
|
||||
"""Remove a webhook."""
|
||||
handlers = hass.data.setdefault(DOMAIN, {})
|
||||
@@ -83,6 +86,7 @@ def async_generate_id() -> str:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_generate_url(
|
||||
hass: HomeAssistant,
|
||||
webhook_id: str,
|
||||
@@ -113,6 +117,7 @@ def async_generate_path(webhook_id: str) -> str:
|
||||
return URL_WEBHOOK_PATH.format(webhook_id=webhook_id)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_handle_webhook(
|
||||
hass: HomeAssistant, webhook_id: str, request: Request | MockRequest
|
||||
) -> Response:
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Final, cast
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType, VolSchemaType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from . import commands, connection, const, decorators, http, messages # noqa: F401
|
||||
from .connection import ActiveConnection, current_connection # noqa: F401
|
||||
@@ -46,6 +47,7 @@ DEPENDENCIES: Final[tuple[str]] = ("http",)
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_register_command(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -245,14 +245,6 @@ ENERGY_SENSORS = [
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda status: status.energy_in_defrost,
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="electricity_used_standby",
|
||||
key="electricity_used_standby",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda status: status.energy_in_standby,
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="energy_output_heating",
|
||||
key="energy_output_heating",
|
||||
|
||||
@@ -96,9 +96,6 @@
|
||||
"electricity_used_heating": {
|
||||
"name": "Electricity used heating"
|
||||
},
|
||||
"electricity_used_standby": {
|
||||
"name": "Electricity used standby"
|
||||
},
|
||||
"energy_output": {
|
||||
"name": "Total energy output"
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@ from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, instance_id
|
||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import async_get_homekit, async_get_zeroconf
|
||||
from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass
|
||||
from homeassistant.setup import async_when_setup_or_start
|
||||
|
||||
from . import websocket_api
|
||||
@@ -68,11 +68,13 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_instance(hass: HomeAssistant) -> HaZeroconf:
|
||||
"""Get or create the shared HaZeroconf instance."""
|
||||
return cast(HaZeroconf, (_async_get_instance(hass)).zeroconf)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_async_instance(hass: HomeAssistant) -> HaAsyncZeroconf:
|
||||
"""Get or create the shared HaAsyncZeroconf instance."""
|
||||
return _async_get_instance(hass)
|
||||
|
||||
@@ -46,6 +46,7 @@ from homeassistant.helpers import (
|
||||
storage,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, VolDictType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.location import distance
|
||||
|
||||
@@ -182,6 +183,7 @@ def async_in_zones(
|
||||
return (closest, [itm[0] for itm in zones])
|
||||
|
||||
|
||||
@bind_hass
|
||||
def async_active_zone(
|
||||
hass: HomeAssistant, latitude: float, longitude: float, radius: float = 0
|
||||
) -> State | None:
|
||||
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -142,7 +142,6 @@ FLOWS = {
|
||||
"deconz",
|
||||
"decora_wifi",
|
||||
"deluge",
|
||||
"denon_rs232",
|
||||
"denonavr",
|
||||
"devialet",
|
||||
"devolo_home_control",
|
||||
|
||||
@@ -1323,12 +1323,6 @@
|
||||
"iot_class": "local_push",
|
||||
"name": "Denon AVR Network Receivers"
|
||||
},
|
||||
"denon_rs232": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"name": "Denon RS232"
|
||||
},
|
||||
"heos": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
|
||||
@@ -24,6 +24,7 @@ from homeassistant import config_entries
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import ssl as ssl_util
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.json import json_loads
|
||||
@@ -213,6 +214,7 @@ class ChunkAsyncStreamIterator:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_get_clientsession(
|
||||
hass: HomeAssistant,
|
||||
verify_ssl: bool = True,
|
||||
@@ -242,6 +244,7 @@ def async_get_clientsession(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_create_clientsession(
|
||||
hass: HomeAssistant,
|
||||
verify_ssl: bool = True,
|
||||
@@ -315,6 +318,7 @@ def _async_create_clientsession(
|
||||
return clientsession
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_aiohttp_proxy_web(
|
||||
hass: HomeAssistant,
|
||||
request: web.BaseRequest,
|
||||
@@ -347,6 +351,7 @@ async def async_aiohttp_proxy_web(
|
||||
req.close()
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_aiohttp_proxy_stream(
|
||||
hass: HomeAssistant,
|
||||
request: web.BaseRequest,
|
||||
|
||||
@@ -345,16 +345,6 @@ ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR = (
|
||||
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class EntityConditionBase(Condition):
|
||||
"""Base class for entity conditions."""
|
||||
@@ -378,7 +368,6 @@ class EntityConditionBase(Condition):
|
||||
assert config.options
|
||||
self._target_selection = TargetSelection(config.target)
|
||||
self._behavior = config.options[ATTR_BEHAVIOR]
|
||||
self._duration: timedelta | None = config.options.get(CONF_FOR)
|
||||
|
||||
def entity_filter(self, entities: set[str]) -> set[str]:
|
||||
"""Filter entities matching any of the domain specs."""
|
||||
@@ -401,25 +390,11 @@ class EntityConditionBase(Condition):
|
||||
|
||||
def check_any_match_state(states: list[State]) -> bool:
|
||||
"""Test if any entity matches the state."""
|
||||
if not self._duration:
|
||||
# Skip duration check if duration is not specified or 0
|
||||
return any(self.is_valid_state(state) for state in states)
|
||||
duration = dt_util.utcnow() - self._duration
|
||||
return any(
|
||||
self.is_valid_state(state) and duration > state.last_changed
|
||||
for state in states
|
||||
)
|
||||
return any(self.is_valid_state(state) for state in states)
|
||||
|
||||
def check_all_match_state(states: list[State]) -> bool:
|
||||
"""Test if all entities match the state."""
|
||||
if not self._duration:
|
||||
# Skip duration check if duration is not specified or 0
|
||||
return all(self.is_valid_state(state) for state in states)
|
||||
duration = dt_util.utcnow() - self._duration
|
||||
return all(
|
||||
self.is_valid_state(state) and duration > state.last_changed
|
||||
for state in states
|
||||
)
|
||||
return all(self.is_valid_state(state) for state in states)
|
||||
|
||||
matcher: Callable[[list[State]], bool]
|
||||
if self._behavior == BEHAVIOR_ANY:
|
||||
@@ -469,8 +444,6 @@ def _normalize_domain_specs(
|
||||
def make_entity_state_condition(
|
||||
domain_specs: Mapping[str, DomainSpec] | str,
|
||||
states: str | bool | set[str | bool],
|
||||
*,
|
||||
support_duration: bool = False,
|
||||
) -> type[EntityStateConditionBase]:
|
||||
"""Create a condition for entity state changes to specific state(s).
|
||||
|
||||
@@ -488,11 +461,6 @@ def make_entity_state_condition(
|
||||
"""Condition for entity state."""
|
||||
|
||||
_domain_specs = specs
|
||||
_schema = (
|
||||
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR
|
||||
if support_duration
|
||||
else ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL
|
||||
)
|
||||
_states = states_set
|
||||
|
||||
return CustomCondition
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Any, TypedDict
|
||||
|
||||
from homeassistant import core, setup
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.signal_type import SignalTypeFormat
|
||||
|
||||
from .dispatcher import async_dispatcher_connect, async_dispatcher_send_internal
|
||||
@@ -35,6 +36,7 @@ class DiscoveryDict(TypedDict):
|
||||
|
||||
|
||||
@core.callback
|
||||
@bind_hass
|
||||
def async_listen(
|
||||
hass: core.HomeAssistant,
|
||||
service: str,
|
||||
@@ -60,6 +62,7 @@ def async_listen(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def discover(
|
||||
hass: core.HomeAssistant,
|
||||
service: str,
|
||||
@@ -74,6 +77,7 @@ def discover(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_discover(
|
||||
hass: core.HomeAssistant,
|
||||
service: str,
|
||||
@@ -96,6 +100,7 @@ async def async_discover(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def async_listen_platform(
|
||||
hass: core.HomeAssistant,
|
||||
component: str,
|
||||
@@ -122,6 +127,7 @@ def async_listen_platform(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def load_platform(
|
||||
hass: core.HomeAssistant,
|
||||
component: Platform | str,
|
||||
@@ -136,6 +142,7 @@ def load_platform(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_load_platform(
|
||||
hass: core.HomeAssistant,
|
||||
component: Platform | str,
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple, Self
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import CoreState, Event, HomeAssistant, callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.async_ import gather_with_limited_concurrency
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -36,6 +37,7 @@ class DiscoveryKey:
|
||||
return cls(domain=json_dict["domain"], key=key, version=json_dict["version"])
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_create_flow(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.core import (
|
||||
callback,
|
||||
get_hassjob_callable_job_type,
|
||||
)
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
from homeassistant.util.logging import catch_log_exception, log_exception
|
||||
|
||||
@@ -35,18 +36,21 @@ type _DispatcherDataType[*_Ts] = dict[
|
||||
|
||||
|
||||
@overload
|
||||
@bind_hass
|
||||
def dispatcher_connect[*_Ts](
|
||||
hass: HomeAssistant, signal: SignalType[*_Ts], target: Callable[[*_Ts], None]
|
||||
) -> Callable[[], None]: ...
|
||||
|
||||
|
||||
@overload
|
||||
@bind_hass
|
||||
def dispatcher_connect(
|
||||
hass: HomeAssistant, signal: str, target: Callable[..., None]
|
||||
) -> Callable[[], None]: ...
|
||||
|
||||
|
||||
def dispatcher_connect[*_Ts]( # type: ignore[misc]
|
||||
@bind_hass # type: ignore[misc] # workaround; exclude typing of 2 overload in func def
|
||||
def dispatcher_connect[*_Ts](
|
||||
hass: HomeAssistant,
|
||||
signal: SignalType[*_Ts],
|
||||
target: Callable[[*_Ts], None],
|
||||
@@ -85,6 +89,7 @@ def _async_remove_dispatcher[*_Ts](
|
||||
|
||||
@overload
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_dispatcher_connect[*_Ts](
|
||||
hass: HomeAssistant, signal: SignalType[*_Ts], target: Callable[[*_Ts], Any]
|
||||
) -> Callable[[], None]: ...
|
||||
@@ -92,12 +97,14 @@ def async_dispatcher_connect[*_Ts](
|
||||
|
||||
@overload
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_dispatcher_connect(
|
||||
hass: HomeAssistant, signal: str, target: Callable[..., Any]
|
||||
) -> Callable[[], None]: ...
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_dispatcher_connect[*_Ts](
|
||||
hass: HomeAssistant,
|
||||
signal: SignalType[*_Ts] | str,
|
||||
@@ -119,16 +126,19 @@ def async_dispatcher_connect[*_Ts](
|
||||
|
||||
|
||||
@overload
|
||||
@bind_hass
|
||||
def dispatcher_send[*_Ts](
|
||||
hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts
|
||||
) -> None: ...
|
||||
|
||||
|
||||
@overload
|
||||
@bind_hass
|
||||
def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: ...
|
||||
|
||||
|
||||
def dispatcher_send[*_Ts]( # type: ignore[misc]
|
||||
@bind_hass # type: ignore[misc] # workaround; exclude typing of 2 overload in func def
|
||||
def dispatcher_send[*_Ts](
|
||||
hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts
|
||||
) -> None:
|
||||
"""Send signal and data."""
|
||||
@@ -171,6 +181,7 @@ def _generate_job[*_Ts](
|
||||
|
||||
@overload
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_dispatcher_send[*_Ts](
|
||||
hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts
|
||||
) -> None: ...
|
||||
@@ -178,10 +189,12 @@ def async_dispatcher_send[*_Ts](
|
||||
|
||||
@overload
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: ...
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_dispatcher_send[*_Ts](
|
||||
hass: HomeAssistant, signal: SignalType[*_Ts] | str, *args: *_Ts
|
||||
) -> None:
|
||||
@@ -203,6 +216,7 @@ def async_dispatcher_send[*_Ts](
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_dispatcher_send_internal[*_Ts](
|
||||
hass: HomeAssistant, signal: SignalType[*_Ts] | str, *args: *_Ts
|
||||
) -> None:
|
||||
|
||||
@@ -50,7 +50,7 @@ from homeassistant.core import (
|
||||
)
|
||||
from homeassistant.core_config import DATA_CUSTOMIZE
|
||||
from homeassistant.exceptions import HomeAssistantError, NoEntitySpecifiedError
|
||||
from homeassistant.loader import async_suggest_report_issue
|
||||
from homeassistant.loader import async_suggest_report_issue, bind_hass
|
||||
from homeassistant.util import ensure_unique_string, slugify
|
||||
from homeassistant.util.frozen_dataclass_compat import FrozenOrThawed
|
||||
|
||||
@@ -91,6 +91,7 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
@singleton(DATA_ENTITY_SOURCE)
|
||||
def entity_sources(hass: HomeAssistant) -> dict[str, EntityInfo]:
|
||||
"""Get the entity sources.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine, Iterable, Mapping
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from types import ModuleType
|
||||
@@ -17,7 +17,6 @@ from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
EntityServiceResponse,
|
||||
Event,
|
||||
HassJobType,
|
||||
HomeAssistant,
|
||||
@@ -30,7 +29,7 @@ from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.loader import async_get_integration
|
||||
from homeassistant.loader import async_get_integration, bind_hass
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -42,6 +41,7 @@ DEFAULT_SCAN_INTERVAL = timedelta(seconds=15)
|
||||
DATA_INSTANCES: HassKey[dict[str, EntityComponent]] = HassKey("entity_components")
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None:
|
||||
"""Trigger an update for an entity."""
|
||||
domain = entity_id.partition(".")[0]
|
||||
@@ -96,7 +96,7 @@ class EntityComponent[_EntityT: entity.Entity = entity.Entity]:
|
||||
] = {domain: domain_platform}
|
||||
self.async_add_entities = domain_platform.async_add_entities
|
||||
self.add_entities = domain_platform.add_entities
|
||||
self._entities: dict[str, _EntityT] = domain_platform.domain_entities # type: ignore[assignment]
|
||||
self._entities: dict[str, entity.Entity] = domain_platform.domain_entities
|
||||
hass.data.setdefault(DATA_INSTANCES, {})[domain] = self # type: ignore[assignment]
|
||||
|
||||
@property
|
||||
@@ -107,11 +107,11 @@ class EntityComponent[_EntityT: entity.Entity = entity.Entity]:
|
||||
callers that iterate over this asynchronously should make a copy
|
||||
using list() before iterating.
|
||||
"""
|
||||
return self._entities.values()
|
||||
return self._entities.values() # type: ignore[return-value]
|
||||
|
||||
def get_entity(self, entity_id: str) -> _EntityT | None:
|
||||
"""Get an entity."""
|
||||
return self._entities.get(entity_id)
|
||||
return self._entities.get(entity_id) # type: ignore[return-value]
|
||||
|
||||
def register_shutdown(self) -> None:
|
||||
"""Register shutdown on Home Assistant STOP event.
|
||||
@@ -242,37 +242,6 @@ class EntityComponent[_EntityT: entity.Entity = entity.Entity]:
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_register_batched_entity_service(
|
||||
self,
|
||||
name: str,
|
||||
schema: VolDictType | VolSchemaType | None,
|
||||
func: Callable[
|
||||
[list[_EntityT], ServiceCall],
|
||||
Coroutine[Any, Any, EntityServiceResponse | None],
|
||||
],
|
||||
required_features: Iterable[int] | None = None,
|
||||
supports_response: SupportsResponse = SupportsResponse.NONE,
|
||||
*,
|
||||
description_placeholders: Mapping[str, str] | None = None,
|
||||
) -> None:
|
||||
"""Register a batched entity service.
|
||||
|
||||
A batched entity service calls the service function once with all
|
||||
matching entities as a list, instead of once per entity.
|
||||
"""
|
||||
service.async_register_batched_entity_service(
|
||||
self.hass,
|
||||
self.domain,
|
||||
name,
|
||||
entities=self._entities,
|
||||
func=func,
|
||||
required_features=required_features,
|
||||
schema=schema,
|
||||
supports_response=supports_response,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
async def async_setup_platform(
|
||||
self,
|
||||
platform_type: str,
|
||||
|
||||
@@ -37,6 +37,7 @@ from homeassistant.core import (
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, TemplateError
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
from homeassistant.util.event_type import EventType
|
||||
@@ -198,6 +199,7 @@ def threaded_listener_factory[**_P](
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_track_state_change(
|
||||
hass: HomeAssistant,
|
||||
entity_ids: str | Iterable[str],
|
||||
@@ -303,6 +305,7 @@ def async_track_state_change(
|
||||
track_state_change = threaded_listener_factory(async_track_state_change)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def async_track_state_change_event(
|
||||
hass: HomeAssistant,
|
||||
entity_ids: str | Iterable[str],
|
||||
@@ -381,6 +384,7 @@ _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def _async_track_state_change_event(
|
||||
hass: HomeAssistant,
|
||||
entity_ids: str | Iterable[str],
|
||||
@@ -533,6 +537,7 @@ _KEYED_TRACK_ENTITY_REGISTRY_UPDATED = _KeyedEventTracker(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_track_entity_registry_updated_event(
|
||||
hass: HomeAssistant,
|
||||
@@ -644,6 +649,7 @@ def _async_domain_added_filter(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def async_track_state_added_domain(
|
||||
hass: HomeAssistant,
|
||||
domains: str | Iterable[str],
|
||||
@@ -664,6 +670,7 @@ _KEYED_TRACK_STATE_ADDED_DOMAIN = _KeyedEventTracker(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def _async_track_state_added_domain(
|
||||
hass: HomeAssistant,
|
||||
domains: str | Iterable[str],
|
||||
@@ -700,6 +707,7 @@ _KEYED_TRACK_STATE_REMOVED_DOMAIN = _KeyedEventTracker(
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def async_track_state_removed_domain(
|
||||
hass: HomeAssistant,
|
||||
domains: str | Iterable[str],
|
||||
@@ -855,6 +863,7 @@ class _TrackStateChangeFiltered:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_track_state_change_filtered(
|
||||
hass: HomeAssistant,
|
||||
track_states: TrackStates,
|
||||
@@ -885,6 +894,7 @@ def async_track_state_change_filtered(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_track_template(
|
||||
hass: HomeAssistant,
|
||||
template: Template,
|
||||
@@ -1329,6 +1339,7 @@ type TrackTemplateResultListener = Callable[
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_track_template_result(
|
||||
hass: HomeAssistant,
|
||||
track_templates: Sequence[TrackTemplate],
|
||||
@@ -1381,6 +1392,7 @@ def async_track_template_result(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_track_same_state(
|
||||
hass: HomeAssistant,
|
||||
period: timedelta,
|
||||
@@ -1448,6 +1460,7 @@ track_same_state = threaded_listener_factory(async_track_same_state)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_track_point_in_time(
|
||||
hass: HomeAssistant,
|
||||
action: HassJob[[datetime], Coroutine[Any, Any, None] | None]
|
||||
@@ -1527,6 +1540,7 @@ class _TrackPointUTCTime:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_track_point_in_utc_time(
|
||||
hass: HomeAssistant,
|
||||
action: HassJob[[datetime], Coroutine[Any, Any, None] | None]
|
||||
@@ -1561,6 +1575,7 @@ def _run_async_call_action(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_call_at(
|
||||
hass: HomeAssistant,
|
||||
action: HassJob[[datetime], Coroutine[Any, Any, None] | None]
|
||||
@@ -1580,6 +1595,7 @@ def async_call_at(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_call_later(
|
||||
hass: HomeAssistant,
|
||||
delay: float | timedelta,
|
||||
@@ -1659,6 +1675,7 @@ class _TrackTimeInterval:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_track_time_interval(
|
||||
hass: HomeAssistant,
|
||||
action: Callable[[datetime], Coroutine[Any, Any, None] | None],
|
||||
@@ -1744,6 +1761,7 @@ class SunListener:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_track_sunrise(
|
||||
hass: HomeAssistant, action: Callable[[], None], offset: timedelta | None = None
|
||||
) -> CALLBACK_TYPE:
|
||||
@@ -1759,6 +1777,7 @@ track_sunrise = threaded_listener_factory(async_track_sunrise)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_track_sunset(
|
||||
hass: HomeAssistant, action: Callable[[], None], offset: timedelta | None = None
|
||||
) -> CALLBACK_TYPE:
|
||||
@@ -1834,6 +1853,7 @@ class _TrackUTCTimeChange:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_track_utc_time_change(
|
||||
hass: HomeAssistant,
|
||||
action: Callable[[datetime], Coroutine[Any, Any, None] | None],
|
||||
@@ -1881,6 +1901,7 @@ track_utc_time_change = threaded_listener_factory(async_track_utc_time_change)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_track_time_change(
|
||||
hass: HomeAssistant,
|
||||
action: Callable[[datetime], Coroutine[Any, Any, None] | None],
|
||||
|
||||
@@ -14,6 +14,7 @@ import httpx
|
||||
|
||||
from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.ssl import (
|
||||
SSL_ALPN_HTTP11,
|
||||
@@ -43,6 +44,7 @@ USER_AGENT = "User-Agent"
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_async_client(
|
||||
hass: HomeAssistant,
|
||||
verify_ssl: bool = True,
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.loader import (
|
||||
async_get_integrations,
|
||||
async_get_loaded_integration,
|
||||
async_register_preload_platform,
|
||||
bind_hass,
|
||||
)
|
||||
from homeassistant.setup import ATTR_COMPONENT, EventComponentLoaded
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
@@ -152,6 +153,7 @@ def _format_err(name: str, platform_name: str, *args: Any) -> str:
|
||||
return f"Exception in {name} when processing platform '{platform_name}': {args}"
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_process_integration_platforms(
|
||||
hass: HomeAssistant,
|
||||
platform_name: str,
|
||||
|
||||
@@ -23,6 +23,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Context, HomeAssistant, State, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from . import (
|
||||
@@ -71,6 +72,7 @@ SPEECH_TYPE_SSML = "ssml"
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_register(hass: HomeAssistant, handler: IntentHandler) -> None:
|
||||
"""Register an intent with Home Assistant."""
|
||||
if (intents := hass.data.get(DATA_KEY)) is None:
|
||||
@@ -88,6 +90,7 @@ def async_register(hass: HomeAssistant, handler: IntentHandler) -> None:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_remove(hass: HomeAssistant, intent_type: str) -> None:
|
||||
"""Remove an intent from Home Assistant."""
|
||||
if (intents := hass.data.get(DATA_KEY)) is None:
|
||||
@@ -102,6 +105,7 @@ def async_get(hass: HomeAssistant) -> Iterable[IntentHandler]:
|
||||
return hass.data.get(DATA_KEY, {}).values()
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_handle(
|
||||
hass: HomeAssistant,
|
||||
platform: str,
|
||||
@@ -770,6 +774,7 @@ def async_match_targets( # noqa: C901
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_match_states(
|
||||
hass: HomeAssistant,
|
||||
name: str | None = None,
|
||||
|
||||
@@ -12,6 +12,7 @@ import yarl
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.network import is_ip_address, is_loopback, normalize_url
|
||||
|
||||
from . import http
|
||||
@@ -26,6 +27,7 @@ class NoURLAvailableError(HomeAssistantError):
|
||||
"""An URL to the Home Assistant instance is not available."""
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_internal_request(hass: HomeAssistant) -> bool:
|
||||
"""Test if the current request is internal."""
|
||||
try:
|
||||
@@ -37,6 +39,7 @@ def is_internal_request(hass: HomeAssistant) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@bind_hass
|
||||
def get_supervisor_network_url(
|
||||
hass: HomeAssistant, *, allow_ssl: bool = False
|
||||
) -> str | None:
|
||||
@@ -111,6 +114,7 @@ def is_hass_url(hass: HomeAssistant, url: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
@bind_hass
|
||||
def get_url(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
@@ -225,6 +229,7 @@ def _get_request_host() -> str | None:
|
||||
return host
|
||||
|
||||
|
||||
@bind_hass
|
||||
def _get_internal_url(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
@@ -262,6 +267,7 @@ def _get_internal_url(
|
||||
raise NoURLAvailableError
|
||||
|
||||
|
||||
@bind_hass
|
||||
def _get_external_url(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
@@ -306,6 +312,7 @@ def _get_external_url(
|
||||
raise NoURLAvailableError
|
||||
|
||||
|
||||
@bind_hass
|
||||
def _get_cloud_url(hass: HomeAssistant, require_current_request: bool = False) -> str:
|
||||
"""Get external Home Assistant Cloud URL of this instance."""
|
||||
if "cloud" in hass.config.components:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine, Iterable, Mapping, Sequence
|
||||
from collections.abc import Callable, Coroutine, Iterable, Mapping
|
||||
import dataclasses
|
||||
from enum import Enum
|
||||
from functools import cache, partial
|
||||
@@ -48,7 +48,7 @@ from homeassistant.exceptions import (
|
||||
Unauthorized,
|
||||
UnknownUser,
|
||||
)
|
||||
from homeassistant.loader import Integration, async_get_integrations
|
||||
from homeassistant.loader import Integration, async_get_integrations, bind_hass
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.yaml import load_yaml_dict
|
||||
@@ -252,6 +252,7 @@ class SelectedEntities(target_helpers.SelectedEntities):
|
||||
super().log_missing(missing_entities, logger or _LOGGER)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def call_from_config(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
@@ -266,6 +267,7 @@ def call_from_config(
|
||||
).result()
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_call_from_config(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
@@ -288,6 +290,7 @@ async def async_call_from_config(
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_prepare_call_from_config(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
@@ -449,6 +452,7 @@ async def async_extract_entity_ids(
|
||||
"homeassistant.helpers.target.async_extract_referenced_entity_ids",
|
||||
breaks_in_ha_version="2026.8",
|
||||
)
|
||||
@bind_hass
|
||||
def async_extract_referenced_entity_ids(
|
||||
hass: HomeAssistant, service_call: ServiceCall, expand_group: bool = True
|
||||
) -> SelectedEntities:
|
||||
@@ -528,6 +532,7 @@ def async_get_cached_service_description(
|
||||
return hass.data.get(SERVICE_DESCRIPTION_CACHE, {}).get((domain, service))
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_all_descriptions(
|
||||
hass: HomeAssistant,
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
@@ -642,6 +647,7 @@ def remove_entity_service_fields(call: ServiceCall) -> dict[Any, Any]:
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_set_service_schema(
|
||||
hass: HomeAssistant, domain: str, service: str, schema: dict[str, Any]
|
||||
) -> None:
|
||||
@@ -673,7 +679,7 @@ def async_set_service_schema(
|
||||
|
||||
def _get_permissible_entity_candidates(
|
||||
call: ServiceCall,
|
||||
entities: Mapping[str, Entity],
|
||||
entities: dict[str, Entity],
|
||||
entity_perms: Callable[[str, str], bool] | None,
|
||||
target_all_entities: bool,
|
||||
all_referenced: set[str] | None,
|
||||
@@ -718,15 +724,22 @@ def _get_permissible_entity_candidates(
|
||||
return [entities[entity_id] for entity_id in all_referenced.intersection(entities)]
|
||||
|
||||
|
||||
async def _resolve_entity_service_call_entities(
|
||||
@bind_hass
|
||||
async def entity_service_call(
|
||||
hass: HomeAssistant,
|
||||
registered_entities: Mapping[str, Entity] | Callable[[], Mapping[str, Entity]],
|
||||
registered_entities: dict[str, Entity] | Callable[[], dict[str, Entity]],
|
||||
func: str | HassJob,
|
||||
call: ServiceCall,
|
||||
required_features: Iterable[int] | None = None,
|
||||
*,
|
||||
entity_device_classes: Iterable[str | None] | None = None,
|
||||
) -> list[Entity] | None:
|
||||
"""Resolve and filter entities for an entity service call."""
|
||||
) -> EntityServiceResponse | None:
|
||||
"""Handle an entity service call.
|
||||
|
||||
Calls all platforms simultaneously.
|
||||
"""
|
||||
entity_perms: Callable[[str, str], bool] | None = None
|
||||
return_response = call.return_response
|
||||
|
||||
if call.context.user_id:
|
||||
user = await hass.auth.async_get_user(call.context.user_id)
|
||||
@@ -748,6 +761,13 @@ async def _resolve_entity_service_call_entities(
|
||||
)
|
||||
all_referenced = referenced.referenced | referenced.indirectly_referenced
|
||||
|
||||
# If the service function is a string, we'll pass it the service call data
|
||||
if isinstance(func, str):
|
||||
data: dict | ServiceCall = remove_entity_service_fields(call)
|
||||
# If the service function is not a string, we pass the service call
|
||||
else:
|
||||
data = call
|
||||
|
||||
if callable(registered_entities):
|
||||
_registered_entities = registered_entities()
|
||||
else:
|
||||
@@ -802,96 +822,73 @@ async def _resolve_entity_service_call_entities(
|
||||
entities.append(entity)
|
||||
|
||||
if not entities:
|
||||
if call.return_response:
|
||||
if return_response:
|
||||
raise HomeAssistantError(
|
||||
"Service call requested response data but did not match any entities"
|
||||
)
|
||||
return None
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
async def _async_handle_entity_calls(
|
||||
entity_calls: list[tuple[Entity, Coroutine[Any, Any, ServiceResponse]]],
|
||||
*,
|
||||
context: Context,
|
||||
) -> EntityServiceResponse:
|
||||
"""Handle calls for entities."""
|
||||
|
||||
async def _with_context(
|
||||
entity: Entity, coro: Coroutine[Any, Any, ServiceResponse]
|
||||
) -> ServiceResponse:
|
||||
entity.async_set_context(context)
|
||||
return await coro
|
||||
|
||||
if len(entity_calls) == 1:
|
||||
if len(entities) == 1:
|
||||
# Single entity case avoids creating task
|
||||
entity, coro = entity_calls[0]
|
||||
single_result = await entity.async_request_call(_with_context(entity, coro))
|
||||
entity = entities[0]
|
||||
single_response = await entity.async_request_call(
|
||||
_handle_entity_call(hass, entity, func, data, call.context)
|
||||
)
|
||||
if entity.should_poll:
|
||||
# Context can expire, so set it again before we update
|
||||
entity.async_set_context(context)
|
||||
# Context expires if the turn on commands took a long time.
|
||||
# Set context again so it's there when we update
|
||||
entity.async_set_context(call.context)
|
||||
await entity.async_update_ha_state(True)
|
||||
return {entity.entity_id: single_result}
|
||||
return {entity.entity_id: single_response} if return_response else None
|
||||
|
||||
entities = [entity for entity, _ in entity_calls]
|
||||
# Use asyncio.gather here to ensure the returned results
|
||||
# are in the same order as the entities list
|
||||
results: list[ServiceResponse | BaseException] = await asyncio.gather(
|
||||
*[
|
||||
entity.async_request_call(_with_context(entity, coro))
|
||||
for entity, coro in entity_calls
|
||||
entity.async_request_call(
|
||||
_handle_entity_call(hass, entity, func, data, call.context)
|
||||
)
|
||||
for entity in entities
|
||||
],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
response_data: EntityServiceResponse = {}
|
||||
for entity, result in zip(entities, results, strict=True):
|
||||
for entity, result in zip(entities, results, strict=False):
|
||||
if isinstance(result, BaseException):
|
||||
raise result from None
|
||||
response_data[entity.entity_id] = result
|
||||
|
||||
tasks: list[asyncio.Task[None]] = []
|
||||
|
||||
for entity in entities:
|
||||
if not entity.should_poll:
|
||||
continue
|
||||
# Context can expire, so set it again before we update
|
||||
entity.async_set_context(context)
|
||||
|
||||
# Context expires if the turn on commands took a long time.
|
||||
# Set context again so it's there when we update
|
||||
entity.async_set_context(call.context)
|
||||
tasks.append(create_eager_task(entity.async_update_ha_state(True)))
|
||||
|
||||
if tasks:
|
||||
done, pending = await asyncio.wait(tasks)
|
||||
assert not pending
|
||||
for future in done:
|
||||
future.result()
|
||||
future.result() # pop exception if have
|
||||
|
||||
return response_data
|
||||
return response_data if return_response and response_data else None
|
||||
|
||||
|
||||
async def async_handle_entity_calls(
|
||||
func: str,
|
||||
entity_data: Sequence[tuple[Entity, dict[str, Any]]],
|
||||
*,
|
||||
context: Context,
|
||||
) -> EntityServiceResponse:
|
||||
"""Handle calls for multiple entities."""
|
||||
return await _async_handle_entity_calls(
|
||||
[
|
||||
(
|
||||
entity,
|
||||
getattr(entity, func)(**data),
|
||||
)
|
||||
for entity, data in entity_data
|
||||
],
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
async def _handle_single_entity_call(
|
||||
async def _handle_entity_call(
|
||||
hass: HomeAssistant,
|
||||
entity: Entity,
|
||||
func: str | HassJob,
|
||||
data: dict | ServiceCall,
|
||||
context: Context,
|
||||
) -> ServiceResponse:
|
||||
"""Handle calling service method."""
|
||||
entity.async_set_context(context)
|
||||
|
||||
task: asyncio.Future[ServiceResponse] | None
|
||||
if isinstance(func, str):
|
||||
job = HassJob(
|
||||
@@ -922,80 +919,6 @@ async def _handle_single_entity_call(
|
||||
return result
|
||||
|
||||
|
||||
async def entity_service_call(
|
||||
hass: HomeAssistant,
|
||||
registered_entities: Mapping[str, Entity] | Callable[[], Mapping[str, Entity]],
|
||||
func: str | HassJob,
|
||||
call: ServiceCall,
|
||||
required_features: Iterable[int] | None = None,
|
||||
*,
|
||||
entity_device_classes: Iterable[str | None] | None = None,
|
||||
) -> EntityServiceResponse | None:
|
||||
"""Handle an entity service call.
|
||||
|
||||
Calls all platforms simultaneously.
|
||||
"""
|
||||
entities = await _resolve_entity_service_call_entities(
|
||||
hass, registered_entities, call, required_features, entity_device_classes
|
||||
)
|
||||
if entities is None:
|
||||
return None
|
||||
|
||||
# If the service function is a string, we'll pass it the service call data
|
||||
if isinstance(func, str):
|
||||
data: dict | ServiceCall = remove_entity_service_fields(call)
|
||||
# If the service function is not a string, we pass the service call
|
||||
else:
|
||||
data = call
|
||||
|
||||
response_data = await _async_handle_entity_calls(
|
||||
[
|
||||
(entity, _handle_single_entity_call(hass, entity, func, data))
|
||||
for entity in entities
|
||||
],
|
||||
context=call.context,
|
||||
)
|
||||
|
||||
return response_data if call.return_response else None
|
||||
|
||||
|
||||
async def batched_entity_service_call(
|
||||
hass: HomeAssistant,
|
||||
registered_entities: Mapping[str, Entity] | Callable[[], Mapping[str, Entity]],
|
||||
func: Callable[
|
||||
[list[Entity], ServiceCall],
|
||||
Coroutine[Any, Any, EntityServiceResponse | None],
|
||||
],
|
||||
call: ServiceCall,
|
||||
required_features: Iterable[int] | None = None,
|
||||
) -> EntityServiceResponse | None:
|
||||
"""Handle a batched entity service call.
|
||||
|
||||
Calls the service function once with all matching entities as a list,
|
||||
instead of once per entity.
|
||||
"""
|
||||
entities = await _resolve_entity_service_call_entities(
|
||||
hass, registered_entities, call, required_features
|
||||
)
|
||||
if entities is None:
|
||||
return None
|
||||
|
||||
return_response = call.return_response
|
||||
|
||||
# Create a new ServiceCall without entity service fields.
|
||||
call = ServiceCall(
|
||||
hass,
|
||||
call.domain,
|
||||
call.service,
|
||||
remove_entity_service_fields(call),
|
||||
context=call.context,
|
||||
return_response=return_response,
|
||||
)
|
||||
result = await func(entities, call)
|
||||
|
||||
return result if return_response else None
|
||||
|
||||
|
||||
async def _async_admin_handler(
|
||||
hass: HomeAssistant,
|
||||
service_job: HassJob[
|
||||
@@ -1021,6 +944,7 @@ async def _async_admin_handler(
|
||||
return None
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_register_admin_service(
|
||||
hass: HomeAssistant,
|
||||
@@ -1199,7 +1123,7 @@ def async_register_entity_service(
|
||||
*,
|
||||
description_placeholders: Mapping[str, str] | None = None,
|
||||
entity_device_classes: Iterable[str | None] | None = None,
|
||||
entities: Mapping[str, Entity],
|
||||
entities: dict[str, Entity],
|
||||
func: str | Callable[..., Any],
|
||||
job_type: HassJobType | None,
|
||||
required_features: Iterable[int] | None = None,
|
||||
@@ -1235,65 +1159,6 @@ def async_register_entity_service(
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_batched_entity_service[_EntityT: Entity](
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
name: str,
|
||||
*,
|
||||
description_placeholders: Mapping[str, str] | None = None,
|
||||
entities: dict[str, _EntityT],
|
||||
func: Callable[
|
||||
[list[_EntityT], ServiceCall],
|
||||
Coroutine[Any, Any, EntityServiceResponse | None],
|
||||
],
|
||||
required_features: Iterable[int] | None = None,
|
||||
schema: VolDictType | VolSchemaType | None,
|
||||
supports_response: SupportsResponse = SupportsResponse.NONE,
|
||||
) -> None:
|
||||
"""Help registering a batched entity service.
|
||||
|
||||
This is called by EntityComponent.async_register_batched_entity_service
|
||||
and should not be called directly by integrations.
|
||||
|
||||
A batched entity service calls the service function once with all
|
||||
matching entities as a list, instead of once per entity.
|
||||
"""
|
||||
schema = _validate_entity_service_schema(schema, f"{domain}.{name}")
|
||||
|
||||
hass.services.async_register(
|
||||
domain,
|
||||
name,
|
||||
partial(
|
||||
batched_entity_service_call,
|
||||
hass,
|
||||
entities,
|
||||
func, # type: ignore[arg-type]
|
||||
required_features=required_features,
|
||||
),
|
||||
schema,
|
||||
supports_response,
|
||||
job_type=HassJobType.Coroutinefunction,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
|
||||
def _get_platform_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_domain: str,
|
||||
service_domain: str,
|
||||
) -> dict[str, Entity]:
|
||||
"""Get platform entities for a service domain."""
|
||||
from .entity_platform import DATA_DOMAIN_PLATFORM_ENTITIES # noqa: PLC0415
|
||||
|
||||
entities = hass.data.get(DATA_DOMAIN_PLATFORM_ENTITIES, {}).get(
|
||||
(entity_domain, service_domain)
|
||||
)
|
||||
if entities is None:
|
||||
return {}
|
||||
return entities
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_platform_entity_service(
|
||||
hass: HomeAssistant,
|
||||
@@ -1309,18 +1174,28 @@ def async_register_platform_entity_service(
|
||||
supports_response: SupportsResponse = SupportsResponse.NONE,
|
||||
) -> None:
|
||||
"""Help registering a platform entity service."""
|
||||
from .entity_platform import DATA_DOMAIN_PLATFORM_ENTITIES # noqa: PLC0415
|
||||
|
||||
schema = _validate_entity_service_schema(schema, f"{service_domain}.{service_name}")
|
||||
|
||||
service_func: str | HassJob[..., Any]
|
||||
service_func = func if isinstance(func, str) else HassJob(func)
|
||||
|
||||
def get_entities() -> dict[str, Entity]:
|
||||
entities = hass.data.get(DATA_DOMAIN_PLATFORM_ENTITIES, {}).get(
|
||||
(entity_domain, service_domain)
|
||||
)
|
||||
if entities is None:
|
||||
return {}
|
||||
return entities
|
||||
|
||||
hass.services.async_register(
|
||||
service_domain,
|
||||
service_name,
|
||||
partial(
|
||||
entity_service_call,
|
||||
hass,
|
||||
partial(_get_platform_entities, hass, entity_domain, service_domain),
|
||||
get_entities,
|
||||
service_func,
|
||||
entity_device_classes=entity_device_classes,
|
||||
required_features=required_features,
|
||||
@@ -1332,46 +1207,6 @@ def async_register_platform_entity_service(
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_batched_platform_entity_service[_EntityT: Entity](
|
||||
hass: HomeAssistant,
|
||||
service_domain: str,
|
||||
service_name: str,
|
||||
*,
|
||||
description_placeholders: Mapping[str, str] | None = None,
|
||||
entity_domain: str,
|
||||
func: Callable[
|
||||
[list[_EntityT], ServiceCall],
|
||||
Coroutine[Any, Any, EntityServiceResponse | None],
|
||||
],
|
||||
required_features: Iterable[int] | None = None,
|
||||
schema: VolDictType | VolSchemaType | None,
|
||||
supports_response: SupportsResponse = SupportsResponse.NONE,
|
||||
) -> None:
|
||||
"""Help registering a batched platform entity service.
|
||||
|
||||
A batched entity service calls the service function once with all
|
||||
matching entities as a list, instead of once per entity.
|
||||
"""
|
||||
schema = _validate_entity_service_schema(schema, f"{service_domain}.{service_name}")
|
||||
|
||||
hass.services.async_register(
|
||||
service_domain,
|
||||
service_name,
|
||||
partial(
|
||||
batched_entity_service_call,
|
||||
hass,
|
||||
partial(_get_platform_entities, hass, entity_domain, service_domain),
|
||||
func, # type: ignore[arg-type]
|
||||
required_features=required_features,
|
||||
),
|
||||
schema,
|
||||
supports_response,
|
||||
job_type=HassJobType.Coroutinefunction,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_config_entry(
|
||||
hass: HomeAssistant, domain: str, entry_id: str
|
||||
|
||||
@@ -22,5 +22,5 @@ class ESPHomeServiceInfo(BaseServiceInfo):
|
||||
"""Return the socket path to connect to the ESPHome device."""
|
||||
url = URL.build(scheme="esphome", host=self.ip_address, port=self.port)
|
||||
if self.noise_psk:
|
||||
url = url.with_query({"key": self.noise_psk})
|
||||
url = url.with_user(self.noise_psk)
|
||||
return str(url)
|
||||
|
||||
@@ -6,6 +6,7 @@ import signal
|
||||
|
||||
from homeassistant.const import RESTART_EXIT_CODE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -14,6 +15,7 @@ KEY_HA_STOP: HassKey[asyncio.Task[None]] = HassKey("homeassistant_stop")
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_register_signal_handling(hass: HomeAssistant) -> None:
|
||||
"""Register system signal handler for core."""
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import inspect
|
||||
from typing import Any, Literal, assert_type, cast, overload
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
type _FuncType[_T] = Callable[[HomeAssistant], _T]
|
||||
@@ -50,6 +51,7 @@ def singleton[_S, _T, _U](
|
||||
if not inspect.iscoroutinefunction(func):
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
@bind_hass
|
||||
@functools.wraps(func)
|
||||
def wrapped(hass: HomeAssistant) -> _U:
|
||||
if data_key not in hass.data:
|
||||
@@ -58,6 +60,7 @@ def singleton[_S, _T, _U](
|
||||
|
||||
return wrapped
|
||||
|
||||
@bind_hass
|
||||
@functools.wraps(func)
|
||||
async def async_wrapped(hass: HomeAssistant) -> _T:
|
||||
if data_key not in hass.data:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user