Compare commits

..

39 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
619c7448ec Add ruff required-version to pyproject.toml and renovate regex manager
Agent-Logs-Url: https://github.com/home-assistant/core/sessions/30a85751-b0e7-4848-8228-2ad96b6c923b

Co-authored-by: edenhaus <26537646+edenhaus@users.noreply.github.com>
2026-04-19 08:38:17 +00:00
Martin Hjelmare
62bac02005 Delint 2026-04-17 10:40:40 +02:00
renovate[bot]
de02333888 Update ruff 2026-04-17 06:05:02 +00:00
Robert Resch
67458786a3 Use the python version from .pyton-version file for hassfest image (#168368) 2026-04-17 08:02:49 +02:00
Erik Montnemery
dfa911b2b3 Add tests asserting air_quality trigger features (#168377) 2026-04-16 23:52:16 +02:00
Maciej Bieniek
6da92a8be9 Add release_url for Shelly Wall Display devices (#168381)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-16 23:17:24 +02:00
Emily Love Watson
d5faf88c88 Add total disk size to glances as an entity (#168131) 2026-04-16 22:38:36 +02:00
Emily Love Watson
ad20b9798b Bump glances-api version (#168389) 2026-04-16 22:25:54 +02:00
Franck Nijhof
7c0ba4d250 Migrate Twente Milieu sensor unique IDs to snake_case and domainless (#168384) 2026-04-16 21:12:44 +02:00
Retha Runolfsson
6277ef5c21 Create a battery range sensor for switchbot presence sensor (#159096)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-16 18:52:10 +02:00
snek
b75263e486 Add heat/cool dmsr device support (#168279) 2026-04-16 18:41:22 +02:00
Paulus Schoutsen
2087906758 Add Denon rs232 integration (#166923)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-16 12:23:56 -04:00
Artur Pragacz
395d741324 Implement batched service call (#168175) 2026-04-16 18:10:43 +02:00
Richard Kroegel
2bcde89f5a Add sensor platform to eurotronic_cometblue (#168118) 2026-04-16 18:03:03 +02:00
Marc Mueller
74c62c34da Fix shelly test RuntimeWarnings (#168380) 2026-04-16 18:33:30 +03:00
Jan Bouwhuis
810672ea78 Improve scope discovery abbreviation checking for MQTT config options (#168302)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-16 17:15:18 +02:00
sanpo
afe3280aee Fix DLNA local file playback for Sony TA-AN1000 by returning content type for HEAD requests (#165807) 2026-04-16 17:14:24 +02:00
AlCalzone
fc573a0cf6 Fix Z-Wave connection string for encrypted ESPHome proxies (#168370) 2026-04-16 16:52:09 +02:00
Erik Montnemery
7b8978c7e5 Add duration to state based entity conditions (#168348) 2026-04-16 16:12:59 +02:00
renovate[bot]
d99d041e49 Update uv to 0.11.6 (#168237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 16:10:29 +02:00
Ariel Ebersberger
cd15261d1c Fix helper tests for Python 3.14.3 (#168355) 2026-04-16 15:56:50 +02:00
Raphael Hehl
5def2456f0 Unifi access doorbell event type (#168316)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-16 15:54:14 +02:00
David Bonnes
87742dbf4e Deprecate Evohome reset services and corresponding climate preset (#167975) 2026-04-16 15:51:48 +02:00
Artur Pragacz
f5fef37210 Remove bind_hass usage (#168369) 2026-04-16 15:38:59 +02:00
Colin
fa85d0d6c2 Fix openevse charging_current and charging_power units (#167863) 2026-04-16 15:29:32 +02:00
Raphael Hehl
0fa5927fc8 Add quality scale tracking for UniFi Network integration (#168125)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-16 15:00:03 +02:00
Joost Lekkerkerker
5335367493 Check if serialx is pinned (#168358) 2026-04-16 14:50:00 +02:00
Robert Resch
1f6e078d1d Extract dynamically package version at build time in hassfest image (#168347) 2026-04-16 14:40:13 +02:00
Marc Mueller
71d857b5e1 Update pydantic to 2.13.1 (#168311) 2026-04-16 14:34:30 +02:00
Barry vd. Heuvel
0de75a013b Add weheat standby electricity usage (#168363) 2026-04-16 14:33:36 +02:00
Robert Resch
f87ec0a7b8 Just copy explicit files in the Dockerfile (#168197) 2026-04-16 14:30:54 +02:00
Ariel Ebersberger
6d1bd15256 Fix synology_dsm test for Python 3.14.3 (#168359) 2026-04-16 13:23:09 +02:00
Jürgen
9fe9064884 Fix sonos availability (#161024)
Co-authored-by: Pete Sage <76050312+PeteRager@users.noreply.github.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-04-16 12:14:19 +01:00
Jamin
f9f57b00bb Fix VOIP blocking call in event loop (#168331) 2026-04-16 12:14:58 +02:00
johanzander
2b65b06003 Fix unit of measurement for SPH power sensors in growatt_server (#168251)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 12:14:13 +02:00
Leo Periou
206c498027 Bump pyaxencoapi to 1.0.7 (#168286) 2026-04-16 12:10:24 +02:00
renovate[bot]
0ac62b241e Update home-assistant-bluetooth to 2.0.0 (#168353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 12:06:34 +02:00
renovate[bot]
4ba123a1a8 Update PyTurboJPEG to 2.2.0 (#168354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 12:02:56 +02:00
Maciej Bieniek
8b8b39c1b7 Bump imgw-pib to 2.1.0 (#168319) 2026-04-16 11:27:44 +02:00
183 changed files with 3848 additions and 675 deletions

11
.github/renovate.json vendored
View File

@@ -6,6 +6,7 @@
"pep621",
"pip_requirements",
"pre-commit",
"regex",
"homeassistant-manifest"
],
@@ -26,6 +27,16 @@
]
},
"regexManagers": [
{
"description": "Update ruff required-version in pyproject.toml",
"managerFilePatterns": ["/^pyproject\\.toml$/"],
"matchStrings": ["required-version = \">=(?<currentValue>[\\d.]+)\""],
"depNameTemplate": "ruff",
"datasourceTemplate": "pypi"
}
],
"minimumReleaseAge": "7 days",
"prConcurrentLimit": 10,
"prHourlyLimit": 2,

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.1
rev: v0.15.10
hooks:
- id: ruff-check
args:

2
CODEOWNERS generated
View File

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

5
Dockerfile generated
View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
def get_serial_number_from_jid(jid: str) -> str:
"""Get serial number from Beolink JID."""
return jid.split(".")[2].split("@")[0]
return jid.split(".")[2].split("@", maxsplit=1)[0]
async def get_remotes(client: MozartClient) -> list[PairedRemote]:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -198,7 +198,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
def is_matching(self, other_flow: Self) -> bool:
"""Return True if other_flow is matching this flow."""
return other_flow._host == self._host # noqa: SLF001
return other_flow._host == self._host
async def async_step_confirm(
self, user_input: dict[str, Any] | None = None

View File

@@ -148,7 +148,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
def is_matching(self, other_flow: Self) -> bool:
"""Return True if other_flow is matching this flow."""
return other_flow._host == self._host # noqa: SLF001
return other_flow._host == self._host
async def async_step_confirm(
self, user_input: dict[str, Any] | None = None

View File

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

View File

@@ -13,6 +13,9 @@
"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.8.0"]
"requirements": ["glances-api==0.10.0"]
}

View File

@@ -49,6 +49,14 @@ 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,6 +50,9 @@
"disk_free": {
"name": "{sensor_label} disk free"
},
"disk_size": {
"name": "{sensor_label} disk size"
},
"disk_usage": {
"name": "{sensor_label} disk usage"
},

View File

@@ -75,7 +75,7 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN):
def is_matching(self, other_flow: Self) -> bool:
"""Return True if other_flow is matching this flow."""
return other_flow._ip_address == self._ip_address # noqa: SLF001
return other_flow._ip_address == self._ip_address
async def async_step_user(
self, user_input: dict[str, Any] | None = None

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -10,6 +11,9 @@
"fields": {
"behavior": {
"name": "[%key:component::motion::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::motion::common::condition_for_name%]"
}
},
"name": "Motion is detected"
@@ -19,6 +23,9 @@
"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,7 +45,6 @@ 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
@@ -221,7 +220,6 @@ def async_on_subscribe_done(
)
@bind_hass
async def async_subscribe(
hass: HomeAssistant,
topic: str,
@@ -273,7 +271,6 @@ 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

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["pyaxencoapi==1.0.6"]
"requirements": ["pyaxencoapi==1.0.7"]
}

View File

@@ -10,7 +10,6 @@ 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
@@ -42,7 +41,6 @@ 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)
@@ -55,7 +53,6 @@ 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:
@@ -90,7 +87,6 @@ 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]:
@@ -128,7 +124,6 @@ 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, bind_hass
from homeassistant.loader import async_get_integration
from homeassistant.setup import (
SetupPhases,
async_prepare_setup_platform,
@@ -159,7 +159,6 @@ 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):
@@ -173,7 +172,6 @@ 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,7 +10,6 @@ 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 (
@@ -64,7 +63,6 @@ class OnboardingStorage(Store[OnboardingStoreData]):
return old_data
@bind_hass
@callback
def async_is_onboarded(hass: HomeAssistant) -> bool:
"""Return if Home Assistant has been onboarded."""
@@ -72,7 +70,6 @@ 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,7 +101,8 @@ SENSOR_TYPES: tuple[OpenEVSESensorDescription, ...] = (
OpenEVSESensorDescription(
key="charging_current",
translation_key="charging_current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda ev: ev.charging_current,
@@ -117,7 +118,8 @@ SENSOR_TYPES: tuple[OpenEVSESensorDescription, ...] = (
OpenEVSESensorDescription(
key="charging_power",
translation_key="charging_power",
native_unit_of_measurement=UnitOfPower.WATT,
native_unit_of_measurement=UnitOfPower.MILLIWATT,
suggested_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda ev: ev.charging_power,

View File

@@ -10,7 +10,6 @@ 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__)
@@ -71,7 +70,6 @@ CONFIG_SCHEMA = vol.Schema(
)
@bind_hass
async def async_register_panel(
hass: HomeAssistant,
# The url to serve the panel

View File

@@ -19,7 +19,6 @@ 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
@@ -75,7 +74,6 @@ def async_register_callback(
)
@bind_hass
def create(
hass: HomeAssistant,
message: str,
@@ -86,14 +84,12 @@ 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,
@@ -127,7 +123,6 @@ 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,7 +52,6 @@ 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
@@ -93,7 +92,6 @@ CONFIG_SCHEMA = vol.Schema(
)
@bind_hass
async def async_create_person(
hass: HomeAssistant,
name: str,
@@ -111,7 +109,6 @@ 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,7 +35,6 @@ 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
@@ -195,7 +194,6 @@ def guarded_inplacevar(op: str, target: Any, operand: Any) -> Any:
return op_fun(target, operand)
@bind_hass
def execute_script(
hass: HomeAssistant,
name: str,
@@ -210,7 +208,6 @@ def execute_script(
return execute(hass, filename, source, data, return_response=return_response)
@bind_hass
def execute(
hass: HomeAssistant,
filename: str,

View File

@@ -25,7 +25,6 @@ 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
@@ -128,7 +127,6 @@ 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,7 +25,6 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.hass_dict import HassKey
_LOGGER = logging.getLogger(__name__)
@@ -73,7 +72,6 @@ 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

@@ -336,7 +336,7 @@ def _async_get_roomba_discovery() -> RoombaDiscovery:
@callback
def _async_blid_from_hostname(hostname: str) -> str:
"""Extract the blid from the hostname."""
return hostname.split("-")[1].split(".")[0].upper()
return hostname.split("-")[1].split(".", maxsplit=1)[0].upper()
async def _async_discover_roombas(

View File

@@ -65,7 +65,6 @@ 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
@@ -91,7 +90,6 @@ 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

@@ -26,6 +26,7 @@ from aioshelly.const import (
MODEL_VINTAGE_V2,
MODEL_WALL_DISPLAY,
MODEL_WALL_DISPLAY_X2,
MODEL_WALL_DISPLAY_X2I,
MODEL_WALL_DISPLAY_XL,
)
@@ -227,6 +228,7 @@ SHELLY_GAS_MODELS = [MODEL_GAS]
SHELLY_WALL_DISPLAY_MODELS = (
MODEL_WALL_DISPLAY,
MODEL_WALL_DISPLAY_X2,
MODEL_WALL_DISPLAY_X2I,
MODEL_WALL_DISPLAY_XL,
)
@@ -289,10 +291,8 @@ OTA_SUCCESS = "ota_success"
GEN1_RELEASE_URL = "https://shelly-api-docs.shelly.cloud/gen1/#changelog"
GEN2_RELEASE_URL = "https://shelly-api-docs.shelly.cloud/gen2/changelog/"
GEN2_BETA_RELEASE_URL = f"{GEN2_RELEASE_URL}#unreleased"
WALL_DISPLAY_RELEASE_URL = "https://github.com/ShellyGroup/Wall-Display-Changelog"
DEVICES_WITHOUT_FIRMWARE_CHANGELOG = (
MODEL_WALL_DISPLAY,
MODEL_WALL_DISPLAY_X2,
MODEL_WALL_DISPLAY_XL,
MODEL_MOTION,
MODEL_MOTION_2,
MODEL_VALVE,

View File

@@ -76,10 +76,12 @@ from .const import (
SHBTN_INPUTS_EVENTS_TYPES,
SHBTN_MODELS,
SHELLY_EMIT_EVENT_PATTERN,
SHELLY_WALL_DISPLAY_MODELS,
SHIX3_1_INPUTS_EVENTS_TYPES,
UPTIME_DEVIATION,
VIRTUAL_COMPONENTS,
VIRTUAL_COMPONENTS_MAP,
WALL_DISPLAY_RELEASE_URL,
All_LIGHT_TYPES,
)
@@ -588,6 +590,9 @@ def get_release_url(gen: int, model: str, beta: bool) -> str | None:
) or model in DEVICES_WITHOUT_FIRMWARE_CHANGELOG:
return None
if model in SHELLY_WALL_DISPLAY_MODELS:
return WALL_DISPLAY_RELEASE_URL
if beta:
return GEN2_BETA_RELEASE_URL

View File

@@ -6,7 +6,7 @@ from collections.abc import Iterator
import logging
from typing import TYPE_CHECKING, Any
from soco import SoCo
from soco import SoCo, SoCoException
from soco.alarms import Alarm, Alarms
from soco.events_base import Event as SonosEvent
@@ -30,6 +30,7 @@ class SonosAlarms(SonosHouseholdCoordinator):
super().__init__(*args)
self.alarms: Alarms = Alarms()
self.created_alarm_ids: set[str] = set()
self._household_mismatch_logged = False
def __iter__(self) -> Iterator:
"""Return an iterator for the known alarms."""
@@ -76,21 +77,40 @@ class SonosAlarms(SonosHouseholdCoordinator):
await self.async_update_entities(speaker.soco, event_id)
@soco_error()
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
"""Update cache of known alarms and return if cache has changed."""
self.alarms.update(soco)
def update_cache(
self,
soco: SoCo,
update_id: int | None = None,
) -> bool:
"""Update cache of known alarms and return whether any were seen."""
try:
self.alarms.update(soco)
except SoCoException as err:
err_msg = str(err)
# Only catch the specific household mismatch error
if "Alarm list UID" in err_msg and "does not match" in err_msg:
if not self._household_mismatch_logged:
_LOGGER.warning(
"Sonos alarms for %s cannot be updated due to a household mismatch. "
"This is a known limitation in setups with multiple households. "
"You can safely ignore this warning, or to silence it, remove the "
"affected household from your Sonos system. Error: %s",
soco.player_name,
err_msg,
)
self._household_mismatch_logged = True
return False
# Let all other exceptions bubble up to be handled by @soco_error()
raise
if update_id and self.alarms.last_id < update_id:
# Skip updates if latest query result is outdated or lagging
return False
if (
self.last_processed_event_id
and self.alarms.last_id <= self.last_processed_event_id
):
# Skip updates already processed
return False
_LOGGER.debug(
"Updating processed event %s from %s (was %s)",
self.alarms.last_id,

View File

@@ -8,7 +8,6 @@ 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
@@ -35,25 +34,11 @@ ATTR_WITH_GROUP = "with_group"
def async_setup_services(hass: HomeAssistant) -> None:
"""Register Sonos services."""
@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)
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]
config_entry = speakers[0].config_entry # All speakers share the same entry
if service_call.service == SERVICE_SNAPSHOT:
@@ -65,16 +50,22 @@ def async_setup_services(hass: HomeAssistant) -> None:
hass, config_entry, speakers, service_call.data[ATTR_WITH_GROUP]
)
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_SNAPSHOT,
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_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,
)
service.async_register_platform_entity_service(

View File

@@ -1,22 +1,20 @@
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,10 +173,6 @@
"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%]"
@@ -197,10 +193,6 @@
"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, bind_hass
from homeassistant.loader import async_get_ssdp
from homeassistant.util.logging import catch_log_exception
from . import websocket_api
@@ -45,7 +45,6 @@ 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[
@@ -68,7 +67,6 @@ 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:
@@ -77,7 +75,6 @@ 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]:
@@ -86,7 +83,6 @@ 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

@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["PyTurboJPEG==1.8.3", "av==16.0.1", "numpy==2.3.2"]
"requirements": ["PyTurboJPEG==2.2.0", "av==16.0.1", "numpy==2.3.2"]
}

View File

@@ -21,7 +21,6 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN
@@ -51,7 +50,6 @@ 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,6 +72,7 @@ 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,6 +121,15 @@
}
},
"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,8 +2,11 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import switchbot
from switchbot import HumidifierWaterLevel
from switchbot import HumidifierWaterLevel, SwitchbotModel
from switchbot.const.air_purifier import AirQualityLevel
from homeassistant.components.bluetooth import async_last_service_info
@@ -35,8 +38,16 @@ from .entity import SwitchbotEntity
PARALLEL_UPDATES = 0
SENSOR_TYPES: dict[str, SensorEntityDescription] = {
"rssi": SensorEntityDescription(
@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(
key="rssi",
translation_key="bluetooth_signal",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
@@ -45,7 +56,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = {
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"wifi_rssi": SensorEntityDescription(
"wifi_rssi": SwitchBotSensorEntityDescription(
key="wifi_rssi",
translation_key="wifi_signal",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
@@ -54,78 +65,91 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = {
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"battery": SensorEntityDescription(
"battery": SwitchBotSensorEntityDescription(
key="battery",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
"co2": SensorEntityDescription(
"co2": SwitchBotSensorEntityDescription(
key="co2",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CO2,
),
"lightLevel": SensorEntityDescription(
"lightLevel": SwitchBotSensorEntityDescription(
key="lightLevel",
translation_key="light_level",
state_class=SensorStateClass.MEASUREMENT,
),
"humidity": SensorEntityDescription(
"humidity": SwitchBotSensorEntityDescription(
key="humidity",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.HUMIDITY,
),
"illuminance": SensorEntityDescription(
"illuminance": SwitchBotSensorEntityDescription(
key="illuminance",
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.ILLUMINANCE,
),
"temperature": SensorEntityDescription(
"temperature": SwitchBotSensorEntityDescription(
key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
),
"power": SensorEntityDescription(
"power": SwitchBotSensorEntityDescription(
key="power",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
"current": SensorEntityDescription(
"current": SwitchBotSensorEntityDescription(
key="current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CURRENT,
),
"voltage": SensorEntityDescription(
"voltage": SwitchBotSensorEntityDescription(
key="voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
),
"aqi_level": SensorEntityDescription(
"aqi_level": SwitchBotSensorEntityDescription(
key="aqi_level",
translation_key="aqi_quality_level",
device_class=SensorDeviceClass.ENUM,
options=[member.name.lower() for member in AirQualityLevel],
),
"energy": SensorEntityDescription(
"energy": SwitchBotSensorEntityDescription(
key="energy",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
),
"water_level": SensorEntityDescription(
"water_level": SwitchBotSensorEntityDescription(
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)),
),
}
@@ -136,6 +160,7 @@ 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(
@@ -144,10 +169,24 @@ 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 coordinator.device.parsed_data
for sensor in parsed_data
if sensor in SENSOR_TYPES
)
sensor_entities.append(SwitchbotRSSISensor(coordinator, "rssi"))
@@ -157,6 +196,8 @@ async def async_setup_entry(
class SwitchBotSensor(SwitchbotEntity, SensorEntity):
"""Representation of a Switchbot sensor."""
entity_description: SwitchBotSensorEntityDescription
def __init__(
self,
coordinator: SwitchbotDataUpdateCoordinator,
@@ -185,7 +226,7 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity):
@property
def native_value(self) -> str | int | None:
"""Return the state of the sensor."""
return self.parsed_data[self._sensor]
return self.entity_description.value_fn(self.parsed_data.get(self._sensor))
class SwitchbotRSSISensor(SwitchBotSensor):

View File

@@ -291,6 +291,15 @@
"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,7 +20,6 @@ from homeassistant.helpers import (
integration_platform,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
_LOGGER = logging.getLogger(__name__)
@@ -40,7 +39,6 @@ class SystemHealthProtocol(Protocol):
"""Register system health callbacks."""
@bind_hass
@callback
def async_register_info(
hass: HomeAssistant,

View File

@@ -2,9 +2,13 @@
from __future__ import annotations
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from typing import Any
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from .const import DOMAIN, SENSOR_UNIQUE_ID_MIGRATION
from .coordinator import TwenteMilieuConfigEntry, TwenteMilieuDataUpdateCoordinator
PLATFORMS = [Platform.CALENDAR, Platform.SENSOR]
@@ -14,6 +18,21 @@ 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,3 +22,11 @@ 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,11 +12,9 @@ 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
@@ -36,25 +34,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="Plastic",
key="packages",
translation_key="packages_waste_pickup",
waste_type=WasteType.PACKAGES,
device_class=SensorDeviceClass.DATE,
@@ -86,7 +84,7 @@ class TwenteMilieuSensor(TwenteMilieuEntity, SensorEntity):
"""Initialize the Twente Milieu entity."""
super().__init__(entry)
self.entity_description = description
self._attr_unique_id = f"{DOMAIN}_{entry.data[CONF_ID]}_{description.key}"
self._attr_unique_id = f"{entry.unique_id}_{description.key}"
@property
def native_value(self) -> date | None:

View File

@@ -0,0 +1,96 @@
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,6 +7,7 @@ from dataclasses import dataclass
from unifi_access_api import Door
from homeassistant.components.event import (
DoorbellEventType,
EventDeviceClass,
EventEntity,
EventEntityDescription,
@@ -31,7 +32,7 @@ DOORBELL_EVENT_DESCRIPTION = UnifiAccessEventEntityDescription(
key="doorbell",
translation_key="doorbell",
device_class=EventDeviceClass.DOORBELL,
event_types=["ring"],
event_types=[DoorbellEventType.RING],
category="doorbell",
)

View File

@@ -31,7 +31,6 @@ 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
@@ -71,7 +70,6 @@ _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

@@ -150,11 +150,6 @@ class PreRecordMessageProtocol(RtpDatagramProtocol):
if self.transport is None:
return
if self._audio_bytes is None:
# 16Khz, 16-bit mono audio message
file_path = Path(__file__).parent / self.file_name
self._audio_bytes = file_path.read_bytes()
if self._audio_task is None:
self._audio_task = self.hass.async_create_background_task(
self._play_message(),
@@ -162,6 +157,11 @@ class PreRecordMessageProtocol(RtpDatagramProtocol):
)
async def _play_message(self) -> None:
if self._audio_bytes is None:
_LOGGER.debug("Loading audio from file %s", self.file_name)
self._audio_bytes = await self._load_audio()
_LOGGER.debug("Read %s bytes", len(self._audio_bytes))
await self.hass.async_add_executor_job(
partial(
self.send_audio,
@@ -175,3 +175,8 @@ class PreRecordMessageProtocol(RtpDatagramProtocol):
# Allow message to play again
self._audio_task = None
async def _load_audio(self) -> bytes:
# 16Khz, 16-bit mono audio message
file_path = Path(__file__).parent / self.file_name
return await self.hass.async_add_executor_job(file_path.read_bytes)

View File

@@ -20,7 +20,6 @@ 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
@@ -36,7 +35,6 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
@callback
@bind_hass
def async_register(
hass: HomeAssistant,
domain: str,
@@ -72,7 +70,6 @@ def async_register(
@callback
@bind_hass
def async_unregister(hass: HomeAssistant, webhook_id: str) -> None:
"""Remove a webhook."""
handlers = hass.data.setdefault(DOMAIN, {})
@@ -86,7 +83,6 @@ def async_generate_id() -> str:
@callback
@bind_hass
def async_generate_url(
hass: HomeAssistant,
webhook_id: str,
@@ -117,7 +113,6 @@ 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

@@ -136,7 +136,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
def is_matching(self, other_flow: Self) -> bool:
"""Return True if other_flow is matching this flow."""
return other_flow._host == self._host # noqa: SLF001
return other_flow._host == self._host
async def async_step_reauth(
self, entry_data: Mapping[str, Any]

View File

@@ -7,7 +7,6 @@ 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
@@ -47,7 +46,6 @@ DEPENDENCIES: Final[tuple[str]] = ("http",)
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
@bind_hass
@callback
def async_register_command(
hass: HomeAssistant,

View File

@@ -245,6 +245,14 @@ ENERGY_SENSORS = [
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda status: status.energy_in_defrost,
),
WeHeatSensorEntityDescription(
translation_key="electricity_used_standby",
key="electricity_used_standby",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda status: status.energy_in_standby,
),
WeHeatSensorEntityDescription(
translation_key="energy_output_heating",
key="energy_output_heating",

View File

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

View File

@@ -145,7 +145,7 @@ class YeelightConfigFlow(ConfigFlow, domain=DOMAIN):
def is_matching(self, other_flow: Self) -> bool:
"""Return True if other_flow is matching this flow."""
return other_flow._discovered_ip == self._discovered_ip # noqa: SLF001
return other_flow._discovered_ip == self._discovered_ip
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None

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, bind_hass
from homeassistant.loader import async_get_homekit, async_get_zeroconf
from homeassistant.setup import async_when_setup_or_start
from . import websocket_api
@@ -68,13 +68,11 @@ 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,7 +46,6 @@ 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
@@ -183,7 +182,6 @@ 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,6 +142,7 @@ FLOWS = {
"deconz",
"decora_wifi",
"deluge",
"denon_rs232",
"denonavr",
"devialet",
"devolo_home_control",

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