Compare commits

..

2 Commits

Author SHA1 Message Date
Ariel Ebersberger
973ddf3476 Address comments 2026-04-16 16:51:09 +02:00
Ariel Ebersberger
de6e8bd19d Fix test_cover_callbacks 2026-04-16 13:46:33 +02:00
159 changed files with 649 additions and 3631 deletions

2
CODEOWNERS generated
View File

@@ -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
View File

@@ -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 \

View File

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

View File

@@ -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:

View File

@@ -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.

View File

@@ -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:

View File

@@ -36,7 +36,7 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration
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."""

View File

@@ -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(

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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]

View File

@@ -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"]
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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"
}
}
}
}

View File

@@ -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)

View File

@@ -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,
),
),
}

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -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.",

View File

@@ -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)

View File

@@ -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,

View File

@@ -34,7 +34,7 @@ from homeassistant.helpers.json import json_dumps_sorted
from homeassistant.helpers.storage import Store
from homeassistant.helpers.translation import async_get_translations
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration
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

View File

@@ -13,9 +13,6 @@
"disk_free": {
"default": "mdi:harddisk"
},
"disk_size": {
"default": "mdi:harddisk"
},
"disk_usage": {
"default": "mdi:harddisk"
},

View File

@@ -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"]
}

View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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.

View File

@@ -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.

View File

@@ -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)

View File

@@ -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.

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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.

View File

@@ -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,

View File

@@ -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

View File

@@ -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),
}

View File

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

View File

@@ -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"

View File

@@ -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,

View File

@@ -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)}

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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,

View File

@@ -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.

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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(

View File

@@ -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:

View File

@@ -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"

View File

@@ -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]:

View File

@@ -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.

View File

@@ -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(

View File

@@ -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": {

View File

@@ -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):

View File

@@ -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"
},

View File

@@ -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,

View File

@@ -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()

View File

@@ -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",
}

View File

@@ -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:

View File

@@ -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

View File

@@ -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",
)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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,

View File

@@ -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",

View File

@@ -96,9 +96,6 @@
"electricity_used_heating": {
"name": "Electricity used heating"
},
"electricity_used_standby": {
"name": "Electricity used standby"
},
"energy_output": {
"name": "Total energy output"
},

View File

@@ -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)

View File

@@ -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:

View File

@@ -142,7 +142,6 @@ FLOWS = {
"deconz",
"decora_wifi",
"deluge",
"denon_rs232",
"denonavr",
"devialet",
"devolo_home_control",

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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:

View File

@@ -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.

View File

@@ -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,

View File

@@ -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],

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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