Compare commits

..

24 Commits

Author SHA1 Message Date
Stefan Agner d3b2be7e86 Deprecate legacy "homeassistant" entry in hassio backup/restore folders (#170317)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:35:06 +02:00
Franck Nijhof a2131c0d45 Add pylint plugin to detect name fields in config flow schemas (#168875)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: frenck <195327+frenck@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 12:30:30 +02:00
Markus Tuominen b179d71658 Add Ouman EH-800 heating controller integration (#169733) 2026-05-13 12:59:49 +03:00
Michael 070ef8f0b0 Remove advanced mode from FRITZ!Box Tools (#167815) 2026-05-13 11:50:07 +02:00
Rob Bierbooms aaeb55b132 Fix influxdb reconfigure for v1 configuration (#170448) 2026-05-13 10:46:51 +02:00
Erik Montnemery 1f5cb05f50 Add websocket command subscribe_condition (#170385) 2026-05-13 10:18:40 +02:00
dependabot[bot] cee87ed1f5 Bump sigstore/cosign-installer from 4.1.1 to 4.1.2 (#170466) 2026-05-13 09:20:19 +02:00
Marc Hörsken e2ae9c1b95 Bump pywmspro to 0.3.4 (#170454) 2026-05-13 08:55:04 +02:00
Jeef 8b257cdd6c Fix WeatherFlow Cloud empty observations (#170440) 2026-05-13 07:53:23 +02:00
tronikos f756392b6a Add nest.set_fan_timer service action (#170367) 2026-05-12 22:41:03 -07:00
Abílio Costa 894ee88033 Add agent instructions to prefer usefixtures (#170458) 2026-05-12 22:01:26 -04:00
J. Nick Koston d5d56e6e23 Bump aioharmony to 1.0.3 (#170459) 2026-05-12 18:38:57 -05:00
James Nimmo a19a1ec6e8 Bump pyintesishome to 1.8.7 (#170382) 2026-05-12 23:29:32 +02:00
mhuiskes b98015dc76 Add hardware and software version to Zeversolar device info (#170407) 2026-05-12 22:30:15 +02:00
zhangluofeng 4112b2af07 Add Xthings Cloud (#167885)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-05-12 22:07:39 +02:00
Ariel Ebersberger 944c0d7ed2 Drop Advanced mode dependency in generic camera config flow (#170427) 2026-05-12 19:11:09 +01:00
perceival a471f7059f risco: improve local reconnect/unload robustness (#165924)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:39:08 +02:00
J. Nick Koston cd1d4244ae Bump aioesphomeapi to 44.24.1 (#170428) 2026-05-12 12:17:06 -05:00
Lukas 573409dcbf bump pooldose api to 0.9.1 (#170434) 2026-05-12 19:16:07 +02:00
Øyvind Matheson Wergeland 6ec70734c1 Drop _spec_hub helper in nobo_hub init tests (#170147) 2026-05-12 19:05:33 +02:00
Åke Strandberg adf6213c9f Avoid stack traces on certain transient miele API errors (#170429) 2026-05-12 18:26:58 +02:00
Jan Bouwhuis e925672bb6 Fix duplicate doorbell events when entity becomes unavailable (#170354)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-12 18:13:54 +02:00
Jan Bouwhuis 15c5e257f5 Allow MQTT discovery to happen at QoS 0, 1 or 2 (#170178) 2026-05-12 18:02:40 +02:00
Richard Kroegel 8396964023 Update device class for eurotronic number (#170356) 2026-05-12 17:00:57 +02:00
153 changed files with 6531 additions and 224 deletions
+1
View File
@@ -30,6 +30,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- When writing or modifying tests, ensure all test function parameters have type annotations.
- Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`.
- Prefer `@pytest.mark.usefixtures` over arguments, if the argument is not going to be used.
- Avoid using conditions/branching in tests. Instead, either split tests or adjust the test parametrization to cover all cases without branching.
- If multiple tests share most of their code, use `pytest.mark.parametrize` to merge them into a single parameterized test instead of duplicating the body.
+1 -1
View File
@@ -338,7 +338,7 @@ jobs:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps:
- name: Install Cosign
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: "v2.5.3"
+1
View File
@@ -423,6 +423,7 @@ homeassistant.components.opower.*
homeassistant.components.oralb.*
homeassistant.components.otbr.*
homeassistant.components.otp.*
homeassistant.components.ouman_eh_800.*
homeassistant.components.overkiz.*
homeassistant.components.overseerr.*
homeassistant.components.p1_monitor.*
+1
View File
@@ -20,6 +20,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- When writing or modifying tests, ensure all test function parameters have type annotations.
- Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`.
- Prefer `@pytest.mark.usefixtures` over arguments, if the argument is not going to be used.
- Avoid using conditions/branching in tests. Instead, either split tests or adjust the test parametrization to cover all cases without branching.
- If multiple tests share most of their code, use `pytest.mark.parametrize` to merge them into a single parameterized test instead of duplicating the body.
Generated
+4
View File
@@ -1305,6 +1305,8 @@ CLAUDE.md @home-assistant/core
/tests/components/osoenergy/ @osohotwateriot
/homeassistant/components/otbr/ @home-assistant/core
/tests/components/otbr/ @home-assistant/core
/homeassistant/components/ouman_eh_800/ @Markus98
/tests/components/ouman_eh_800/ @Markus98
/homeassistant/components/ourgroceries/ @OnFreund
/tests/components/ourgroceries/ @OnFreund
/homeassistant/components/overkiz/ @imicknl
@@ -2026,6 +2028,8 @@ CLAUDE.md @home-assistant/core
/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG
/homeassistant/components/xiaomi_tv/ @simse
/homeassistant/components/xmpp/ @fabaff @flowolf
/homeassistant/components/xthings_cloud/ @XthingsJacobs
/tests/components/xthings_cloud/ @XthingsJacobs
/homeassistant/components/yale/ @bdraco
/tests/components/yale/ @bdraco
/homeassistant/components/yale_smart_alarm/ @gjohansson-ST
@@ -59,6 +59,8 @@ class AemetConfigFlow(ConfigFlow, domain=DOMAIN):
schema = vol.Schema(
{
vol.Required(CONF_API_KEY): str,
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
vol.Optional(
CONF_LATITUDE, default=self.hass.config.latitude
+36 -3
View File
@@ -2,6 +2,7 @@
from asyncio import timeout
from collections.abc import Mapping
from datetime import datetime, timedelta
from http import HTTPStatus
import json
import logging
@@ -11,7 +12,12 @@ from uuid import uuid4
import aiohttp
from homeassistant.components import event
from homeassistant.const import EVENT_STATE_CHANGED, STATE_ON
from homeassistant.const import (
EVENT_STATE_CHANGED,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import (
CALLBACK_TYPE,
Event,
@@ -51,6 +57,25 @@ DEFAULT_TIMEOUT = 10
TO_REDACT = {"correlationToken", "token"}
def valid_doorbell_timestamp(entity_id: str, event_state: str) -> bool:
"""Check if doorbell event timestamp is valid."""
if event_state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return False
try:
timestamp = datetime.fromisoformat(event_state)
except ValueError:
_LOGGER.debug(
"Unable to parse ISO timestamp from state for %s. Got %s",
entity_id,
event_state,
)
return False
else:
if (dt_util.utcnow() - timestamp) < timedelta(seconds=30):
return True
return False
class AlexaDirective:
"""An incoming Alexa directive."""
@@ -315,9 +340,17 @@ async def async_enable_proactive_mode(
if should_doorbell:
old_state = data["old_state"]
if new_state.domain == event.DOMAIN or (
if (
new_state.domain == event.DOMAIN
and valid_doorbell_timestamp(new_state.entity_id, new_state.state)
and (old_state is None or old_state.state != STATE_UNAVAILABLE)
and (old_state is None or old_state.state != new_state.state)
) or (
new_state.state == STATE_ON
and (old_state is None or old_state.state != STATE_ON)
and (
old_state is None
or old_state.state not in (STATE_ON, STATE_UNAVAILABLE)
)
):
await async_send_doorbell_event_message(
hass, smart_home_config, alexa_changed_entity
@@ -1,4 +1,5 @@
"""Config flow for the Bayesian integration."""
# pylint: disable=hass-config-flow-name-field # Name field is no longer allowed in config flow schemas
from collections.abc import Mapping
from enum import StrEnum
@@ -313,6 +313,8 @@ class BroadlinkFlowHandler(ConfigFlow, domain=DOMAIN):
},
)
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
data_schema = {vol.Required(CONF_NAME, default=device.name): str}
return self.async_show_form(
step_id="finish", data_schema=vol.Schema(data_schema), errors=errors
@@ -53,6 +53,8 @@ def _schema_with_defaults(
{
vol.Required(CONF_HOST, default=host): str,
**AUTH_VOL_DICT,
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(CONF_NAME, default=name): str,
}
)
@@ -47,6 +47,8 @@ class EmulatedRokuFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="user",
data_schema=vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=default_name): str,
vol.Required(CONF_LISTEN_PORT, default=default_port): vol.Coerce(
int
@@ -23,6 +23,8 @@ DATA_SCHEMA = vol.Schema(
)
),
vol.Required(CONF_HOST): str,
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=DOMAIN): str,
}
)
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==44.21.0",
"aioesphomeapi==44.24.1",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.7.3"
],
@@ -35,7 +35,7 @@ DESCRIPTIONS = [
key="offset",
cometblue_key="tempOffset",
translation_key="offset",
device_class=NumberDeviceClass.TEMPERATURE,
device_class=NumberDeviceClass.TEMPERATURE_DELTA,
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
set_fn=lambda x: x.set_temperature_async,
@@ -130,6 +130,8 @@ def get_model_selection_schema(
mode=SelectSelectorMode.DROPDOWN,
)
),
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(
CONF_NAME,
default=options.get(CONF_NAME) or vol.UNDEFINED,
+16 -18
View File
@@ -34,7 +34,6 @@ from homeassistant.helpers.service_info.ssdp import (
ATTR_UPNP_UDN,
SsdpServiceInfo,
)
from homeassistant.helpers.typing import VolDictType
from .const import (
CONF_FEATURE_DEVICE_TRACKING,
@@ -225,19 +224,12 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
self, errors: dict[str, str] | None = None
) -> ConfigFlowResult:
"""Show the setup form to the user."""
advanced_data_schema: VolDictType = {}
if self.show_advanced_options:
advanced_data_schema = {
vol.Optional(CONF_PORT): vol.Coerce(int),
}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Optional(CONF_HOST, default=DEFAULT_HOST): str,
**advanced_data_schema,
vol.Optional(CONF_PORT): vol.Coerce(int),
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
@@ -357,18 +349,14 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any], errors: dict[str, str] | None = None
) -> ConfigFlowResult:
"""Show the reconfigure form to the user."""
advanced_data_schema: VolDictType = {}
if self.show_advanced_options:
advanced_data_schema = {
vol.Optional(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce(int),
}
return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST, default=user_input[CONF_HOST]): str,
**advanced_data_schema,
vol.Optional(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce(
int
),
vol.Required(CONF_SSL, default=user_input[CONF_SSL]): bool,
}
),
@@ -382,11 +370,21 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle reconfigure flow."""
if user_input is None:
reconfigure_entry_data = self._get_reconfigure_entry().data
port = reconfigure_entry_data[CONF_PORT]
ssl = reconfigure_entry_data.get(CONF_SSL, DEFAULT_SSL)
if (port == DEFAULT_HTTP_PORT and not ssl) or (
port == DEFAULT_HTTPS_PORT and ssl
):
# don't show default ports in reconfigure flow, as they are determined by ssl value
# this allows the user to toggle ssl without having to change the port
port = vol.UNDEFINED
return self._show_setup_form_reconfigure(
{
CONF_HOST: reconfigure_entry_data[CONF_HOST],
CONF_PORT: reconfigure_entry_data[CONF_PORT],
CONF_SSL: reconfigure_entry_data.get(CONF_SSL, DEFAULT_SSL),
CONF_PORT: port,
CONF_SSL: ssl,
}
)
@@ -104,7 +104,6 @@ class InvalidStreamException(HomeAssistantError):
def build_schema(
is_options_flow: bool = False,
show_advanced_options: bool = False,
) -> vol.Schema:
"""Create schema for camera config setup."""
rtsp_options = [
@@ -141,8 +140,7 @@ def build_schema(
}
if is_options_flow:
advanced_section[vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE)] = bool
if show_advanced_options:
advanced_section[vol.Optional(CONF_USE_WALLCLOCK_AS_TIMESTAMPS)] = bool
advanced_section[vol.Optional(CONF_USE_WALLCLOCK_AS_TIMESTAMPS)] = bool
return vol.Schema(spec)
@@ -469,10 +467,7 @@ class GenericOptionsFlowHandler(OptionsFlow):
return self.async_show_form(
step_id="init",
data_schema=self.add_suggested_values_to_schema(
build_schema(
True,
self.show_advanced_options,
),
build_schema(True),
user_input or self.config_entry.options,
),
errors=errors,
@@ -40,8 +40,8 @@
"rtsp_transport": "RTSP transport protocol",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"description": "Advanced settings are only needed for special cases. Leave them unchanged unless you know what you are doing.",
"name": "Advanced settings"
"description": "These options are only needed for special cases.",
"name": "More options"
}
}
},
@@ -90,6 +90,8 @@ class GoalZeroFlowHandler(ConfigFlow, domain=DOMAIN):
vol.Required(
CONF_HOST, default=user_input.get(CONF_HOST) or ""
): str,
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(
CONF_NAME, default=user_input.get(CONF_NAME) or DEFAULT_NAME
): str,
@@ -69,6 +69,8 @@ def _get_location_schema(hass: HomeAssistant) -> vol.Schema:
"""Return the schema for a location with default values from the hass config."""
return vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=hass.config.location_name): str,
vol.Required(
CONF_LOCATION,
@@ -321,6 +321,8 @@ async def google_generative_ai_config_option_schema(
else:
default_name = DEFAULT_CONVERSATION_NAME
schema: dict[vol.Required | vol.Optional, Any] = {
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=default_name): str,
}
else:
@@ -67,6 +67,8 @@ RECONFIGURE_SCHEMA = vol.Schema(
CONFIG_SCHEMA = RECONFIGURE_SCHEMA.extend(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
@@ -70,6 +70,8 @@ def _get_location_schema(hass: HomeAssistant) -> vol.Schema:
"""Return the schema for a location with default values from the hass config."""
return vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=hass.config.location_name): str,
vol.Required(
CONF_LOCATION,
@@ -38,7 +38,10 @@ from .util import (
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}, extra=vol.ALLOW_EXTRA
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
{vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str},
extra=vol.ALLOW_EXTRA,
)
@@ -8,7 +8,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["aioharmony", "slixmpp"],
"requirements": ["aioharmony==0.5.3"],
"requirements": ["aioharmony==1.0.3"],
"ssdp": [
{
"deviceType": "urn:myharmony-com:device:harmony:1",
+1
View File
@@ -162,6 +162,7 @@ ISSUE_KEY_ADDON_PWNED = "issue_addon_pwned"
ISSUE_KEY_SYSTEM_FREE_SPACE = "issue_system_free_space"
ISSUE_KEY_ADDON_DEPRECATED = "issue_addon_deprecated_addon"
ISSUE_KEY_ADDON_DEPRECATED_ARCH = "issue_addon_deprecated_arch_addon"
ISSUE_KEY_LEGACY_HOMEASSISTANT_FOLDER = "legacy_homeassistant_folder"
ISSUE_MOUNT_MOUNT_FAILED = "issue_mount_mount_failed"
+8 -1
View File
@@ -8,7 +8,11 @@ from aiohasupervisor import SupervisorError
from aiohasupervisor.models import ContextType
import voluptuous as vol
from homeassistant.components.repairs import RepairsFlow, RepairsFlowResult
from homeassistant.components.repairs import (
ConfirmRepairFlow,
RepairsFlow,
RepairsFlowResult,
)
from homeassistant.const import ATTR_NAME
from homeassistant.core import HomeAssistant
@@ -21,6 +25,7 @@ from .const import (
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_PWNED,
ISSUE_KEY_LEGACY_HOMEASSISTANT_FOLDER,
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
PLACEHOLDER_KEY_ADDON,
PLACEHOLDER_KEY_ADDON_DOCUMENTATION,
@@ -226,6 +231,8 @@ async def async_create_fix_flow(
data: dict[str, str | int | float | None] | None,
) -> RepairsFlow:
"""Create flow."""
if issue_id == ISSUE_KEY_LEGACY_HOMEASSISTANT_FOLDER:
return ConfirmRepairFlow()
supervisor_issues = get_issues_info(hass)
issue = supervisor_issues and supervisor_issues.get_issue(issue_id)
if issue and issue.key == ISSUE_KEY_SYSTEM_DOCKER_CONFIG:
+17 -3
View File
@@ -28,6 +28,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
issue_registry as ir,
selector,
)
from homeassistant.helpers.service import async_register_admin_service
@@ -47,6 +48,7 @@ from .const import (
ATTR_PASSWORD,
ATTR_SLUG,
DOMAIN,
ISSUE_KEY_LEGACY_HOMEASSISTANT_FOLDER,
MAIN_COORDINATOR,
SupervisorEntityModel,
)
@@ -76,7 +78,9 @@ VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))
LEGACY_FOLDER_HOMEASSISTANT = "homeassistant"
def _normalize_partial_options_data(data: dict[str, Any]) -> dict[str, Any]:
def _normalize_partial_options_data(
hass: HomeAssistant, data: dict[str, Any]
) -> dict[str, Any]:
"""Map legacy aliases used by both partial backup and partial restore handlers."""
if ATTR_APPS in data:
data[ATTR_ADDONS] = data.pop(ATTR_APPS)
@@ -90,6 +94,16 @@ def _normalize_partial_options_data(data: dict[str, Any]) -> dict[str, Any]:
f"{LEGACY_FOLDER_HOMEASSISTANT!r} entry in {ATTR_FOLDERS}"
)
data[ATTR_HOMEASSISTANT] = True
ir.async_create_issue(
hass,
DOMAIN,
ISSUE_KEY_LEGACY_HOMEASSISTANT_FOLDER,
breaks_in_ha_version="2026.12.0",
is_fixable=True,
is_persistent=True,
severity=ir.IssueSeverity.WARNING,
translation_key=ISSUE_KEY_LEGACY_HOMEASSISTANT_FOLDER,
)
if folders:
data[ATTR_FOLDERS] = folders
else:
@@ -375,7 +389,7 @@ def async_register_backup_restore_services(
service: ServiceCall,
) -> ServiceResponse:
"""Handler for create partial backup service. Returns the new backup's ID."""
data = _normalize_partial_options_data(service.data.copy())
data = _normalize_partial_options_data(hass, service.data.copy())
options = PartialBackupOptions(**data)
try:
@@ -422,7 +436,7 @@ def async_register_backup_restore_services(
"""Handler for partial restore service."""
data = service.data.copy()
backup_slug = data.pop(ATTR_SLUG)
data = _normalize_partial_options_data(data)
data = _normalize_partial_options_data(hass, data)
options = PartialRestoreOptions(**data)
try:
@@ -203,6 +203,17 @@
},
"title": "Reboot required"
},
"legacy_homeassistant_folder": {
"fix_flow": {
"step": {
"confirm": {
"description": "An automation or script called the `hassio.backup_partial` or `hassio.restore_partial` action with `\"homeassistant\"` listed in `folders`. This is a legacy alias for the `homeassistant: true` option and will stop being accepted in a future release.\n\nUpdate the affected automations and scripts to set `homeassistant: true` and remove `\"homeassistant\"` from the `folders` list. When this is done, select **Submit** to mark this issue as resolved.",
"title": "Legacy \"homeassistant\" folder used in partial backup/restore"
}
}
},
"title": "Legacy \"homeassistant\" folder used in partial backup/restore"
},
"unhealthy": {
"description": "System is currently unhealthy due to {reason}. For troubleshooting information, select Learn more.",
"title": "Unhealthy system - {reason}"
@@ -88,6 +88,8 @@ def get_user_step_schema(data: Mapping[str, Any]) -> vol.Schema:
travel_mode = TRAVEL_MODE_PUBLIC
return vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(
CONF_NAME, default=data.get(CONF_NAME, DEFAULT_NAME)
): cv.string,
@@ -156,6 +156,8 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
schema = vol.Schema(
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
{vol.Optional(CONF_DEVICE_NAME, default=self.device_name): str}
)
return self.async_show_form(
@@ -1,4 +1,5 @@
"""Lutron Homeworks Series 4 and 8 config flow."""
# pylint: disable=hass-config-flow-name-field # Name field is no longer allowed in config flow schemas
from functools import partial
import logging
@@ -380,6 +380,8 @@ class HuaweiLteOptionsFlow(OptionsFlow):
data_schema = vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(
CONF_NAME,
default=self.config_entry.options.get(
@@ -290,7 +290,9 @@ class InfluxDBConfigFlow(ConfigFlow, domain=DOMAIN):
scheme="https" if entry.data.get(CONF_SSL) else "http",
host=entry.data.get(CONF_HOST, ""),
port=entry.data.get(CONF_PORT),
path=entry.data.get(CONF_PATH, ""),
path=""
if entry.data.get(CONF_PATH) is None
else entry.data[CONF_PATH],
)
)
@@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["pyintesishome"],
"quality_scale": "legacy",
"requirements": ["pyintesishome==1.8.0"]
"requirements": ["pyintesishome==1.8.7"]
}
@@ -79,6 +79,8 @@ class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="user",
data_schema=vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(CONF_NAME, default=NAME): TextSelector(),
vol.Required(
CONF_LOCATION, default=home_location
@@ -1,4 +1,5 @@
"""Config flow for konnected.io integration."""
# pylint: disable=hass-config-flow-name-field # Name field is no longer allowed in config flow schemas
import asyncio
import copy
@@ -42,6 +42,8 @@ DATA_SCHEMA_OPTIONS = vol.Schema(
)
DATA_SCHEMA_SETUP = vol.Schema(
{
# Approved exemption: user names the local file camera
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
}
).extend(DATA_SCHEMA_OPTIONS.schema)
@@ -110,6 +110,8 @@ class LoqedConfigFlow(ConfigFlow, domain=DOMAIN):
if self._host
else vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME): str,
vol.Required(CONF_API_TOKEN): str,
}
@@ -1,4 +1,5 @@
"""Config flow to configure Met component."""
# pylint: disable=hass-config-flow-name-field # Name field is no longer allowed in config flow schemas
from typing import Any
@@ -31,6 +31,8 @@ class MetEireannFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="user",
data_schema=vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=HOME_LOCATION_NAME): str,
vol.Required(
CONF_LATITUDE, default=self.hass.config.latitude
+5 -1
View File
@@ -1089,7 +1089,11 @@ class MieleStatusSensor(MieleSensor):
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return StateStatus(self.device.state_status).name
return (
StateStatus(self.device.state_status).name
if self._device_id in self.coordinator.data.devices
else None
)
@property
def available(self) -> bool:
+2 -2
View File
@@ -188,9 +188,9 @@ class MielePowerSwitch(MieleSwitch):
def available(self) -> bool:
"""Return the availability of the entity."""
return (
return super().available and (
self.action.power_off_enabled or self.action.power_on_enabled
) and super().available
)
async def async_turn_switch(self, mode: dict[str, str | int | bool]) -> None:
"""Set switch to mode."""
+2 -2
View File
@@ -179,9 +179,9 @@ class MieleVacuum(MieleEntity, StateVacuumEntity):
def available(self) -> bool:
"""Return the availability of the entity."""
return (
return super().available and (
self.action.power_off_enabled or self.action.power_on_enabled
) and super().available
)
async def send(self, device_id: str, action: dict[str, Any]) -> None:
"""Send action to the device."""
@@ -57,6 +57,8 @@ def async_get_schema(
if show_name:
schema = {
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=defaults.get(CONF_NAME)): str,
**schema,
}
@@ -190,6 +190,7 @@ from .const import (
CONF_DIRECTION_STATE_TOPIC,
CONF_DIRECTION_VALUE_TEMPLATE,
CONF_DISCOVERY_PREFIX,
CONF_DISCOVERY_QOS,
CONF_EFFECT_COMMAND_TEMPLATE,
CONF_EFFECT_COMMAND_TOPIC,
CONF_EFFECT_LIST,
@@ -4382,6 +4383,7 @@ class MQTTOptionsFlowHandler(OptionsFlow):
"bad_discovery_prefix",
valid_publish_topic,
)
options_config[CONF_DISCOVERY_QOS] = int(user_input[CONF_DISCOVERY_QOS])
if "birth_topic" in user_input:
_validate(
CONF_BIRTH_MESSAGE,
@@ -4415,6 +4417,7 @@ class MQTTOptionsFlowHandler(OptionsFlow):
}
discovery = options_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY)
discovery_prefix = options_config.get(CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX)
discovery_qos = options_config.get(CONF_DISCOVERY_QOS, DEFAULT_QOS)
# build form
fields: OrderedDict[vol.Marker, Any] = OrderedDict()
@@ -4422,6 +4425,7 @@ class MQTTOptionsFlowHandler(OptionsFlow):
fields[vol.Optional(CONF_DISCOVERY_PREFIX, default=discovery_prefix)] = (
PUBLISH_TOPIC_SELECTOR
)
fields[vol.Optional("discovery_qos", default=discovery_qos)] = QOS_SELECTOR
# Birth message is disabled if CONF_BIRTH_MESSAGE = {}
fields[
+1
View File
@@ -43,6 +43,7 @@ CONF_COMMAND_TOPIC = "command_topic"
CONF_CONTENT_TYPE = "content_type"
CONF_DEFAULT_ENTITY_ID = "default_entity_id"
CONF_DISCOVERY_PREFIX = "discovery_prefix"
CONF_DISCOVERY_QOS = "discovery_qos"
CONF_ENCODING = "encoding"
CONF_IMAGE_ENCODING = "image_encoding"
CONF_IMAGE_TOPIC = "image_topic"
+4 -2
View File
@@ -42,6 +42,7 @@ from .const import (
ATTR_DISCOVERY_TOPIC,
CONF_AVAILABILITY,
CONF_COMPONENTS,
CONF_DISCOVERY_QOS,
CONF_ORIGIN,
CONF_TOPIC,
DOMAIN,
@@ -598,12 +599,13 @@ async def async_start( # noqa: C901
hass, MQTT_DISCOVERY_DONE.format(*discovery_hash), None
)
discovery_qos: int = config_entry.options.get(CONF_DISCOVERY_QOS, 0)
mqtt_data.discovery_unsubscribe = [
async_subscribe_internal(
hass,
topic,
async_discovery_message_received,
0,
discovery_qos,
job_type=HassJobType.Callback,
)
# Subscribe first for platform discovery wildcard topics first,
@@ -708,7 +710,7 @@ async def async_start( # noqa: C901
hass,
topic,
functools.partial(async_integration_message_received, integration),
0,
discovery_qos,
job_type=HassJobType.Coroutinefunction,
)
for integration, topics in mqtt_integrations.items()
@@ -1229,6 +1229,7 @@
"birth_topic": "Birth message topic",
"discovery": "Enable discovery",
"discovery_prefix": "Discovery prefix",
"discovery_qos": "Discovery QoS",
"will_enable": "Enable will message",
"will_payload": "Will message payload",
"will_qos": "Will message QoS",
@@ -1243,6 +1244,7 @@
"birth_topic": "The MQTT topic where Home Assistant will publish a \"birth\" message.",
"discovery": "Option to enable MQTT automatic discovery.",
"discovery_prefix": "The prefix of configuration topics the MQTT integration will subscribe to.",
"discovery_qos": "The quality of service for discovery subscriptions to your MQTT broker.",
"will_enable": "When set, Home Assistant will ask your broker to publish a \"will\" message when MQTT is stopped or when it loses the connection to your broker.",
"will_payload": "The message your MQTT broker \"will\" publish when the MQTT integration is stopped or when the connection is lost.",
"will_qos": "The quality of service of the \"will\" message that is published by your MQTT broker.",
@@ -19,6 +19,8 @@ DEFAULT_NAME = "myStrom Device"
STEP_USER_DATA_SCHEMA = vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
vol.Required(CONF_HOST): str,
}
@@ -71,6 +71,7 @@ from .media_source import (
async_get_media_source_devices,
async_get_transcoder,
)
from .services import async_setup_services
from .types import DevicesAddedListener, NestConfigEntry, NestData
_LOGGER = logging.getLogger(__name__)
@@ -115,6 +116,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Nest components with dispatch between old/new flows."""
hass.http.register_view(NestEventMediaView(hass))
hass.http.register_view(NestEventMediaThumbnailView(hass))
async_setup_services(hass)
return True
+26 -1
View File
@@ -1,5 +1,6 @@
"""Support for Google Nest SDM climate devices."""
from datetime import timedelta
from typing import Any, cast
from google_nest_sdm.device import Device
@@ -67,7 +68,7 @@ FAN_MODE_MAP = {
FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()}
FAN_INV_MODES = list(FAN_INV_MODE_MAP)
MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API
MAX_FAN_DURATION = 43200 # 12 hours is the max in the SDM API
MIN_TEMP = 10
MAX_TEMP = 32
MIN_TEMP_RANGE = 1.66667
@@ -344,3 +345,27 @@ class ThermostatEntity(ClimateEntity):
raise HomeAssistantError(
f"Error setting {self.entity_id} fan mode to {fan_mode}: {err}"
) from err
async def async_set_fan_timer(self, duration: timedelta) -> None:
"""Set a short term fan timer."""
if not self.supported_features & ClimateEntityFeature.FAN_MODE:
raise HomeAssistantError(f"Entity {self.entity_id} does not support fan")
if self.hvac_mode == HVACMode.OFF:
raise HomeAssistantError(
f"Cannot turn on fan for {self.entity_id}, please set an HVAC mode (e.g. heat/cool) first"
)
seconds = int(duration.total_seconds())
if seconds <= 0 or seconds > MAX_FAN_DURATION:
raise ValueError(
f"Duration {seconds} for {self.entity_id} must be between 1 and {MAX_FAN_DURATION} seconds"
)
trait = self._device.traits[FanTrait.NAME]
try:
await trait.set_timer(FAN_INV_MODE_MAP[FAN_ON], duration=seconds)
except ApiException as err:
raise HomeAssistantError(
f"Error setting {self.entity_id} fan timer: {err}"
) from err
+7
View File
@@ -0,0 +1,7 @@
{
"services": {
"set_fan_timer": {
"service": "mdi:fan-clock"
}
}
}
+30
View File
@@ -0,0 +1,30 @@
"""Define services for the Nest integration."""
import voluptuous as vol
from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN,
ClimateEntityFeature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service
from .const import DOMAIN
SERVICE_SET_FAN_TIMER = "set_fan_timer"
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services for the Nest integration."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_SET_FAN_TIMER,
entity_domain=CLIMATE_DOMAIN,
schema={
vol.Required("duration"): cv.time_period,
},
func="async_set_fan_timer",
required_features=[ClimateEntityFeature.FAN_MODE],
)
@@ -0,0 +1,12 @@
set_fan_timer:
target:
entity:
domain: climate
integration: nest
supported_features:
- climate.ClimateEntityFeature.FAN_MODE
fields:
duration:
required: true
selector:
duration:
@@ -163,5 +163,17 @@
"create_new_topic": "Create new topic"
}
}
},
"services": {
"set_fan_timer": {
"description": "Sets the fan to run for a specific duration.",
"fields": {
"duration": {
"description": "The duration the fan should run for.",
"name": "Duration"
}
},
"name": "Set fan timer"
}
}
}
@@ -144,6 +144,8 @@ TOPIC_FILTER_SCHEMA = vol.Schema(
STEP_USER_TOPIC_SCHEMA = vol.Schema(
{
vol.Required(CONF_TOPIC): str,
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(CONF_NAME): str,
vol.Required(SECTION_FILTER): data_entry_flow.section(
TOPIC_FILTER_SCHEMA,
@@ -69,6 +69,8 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema = {
vol.Required(CONF_HOST): str,
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
vol.Optional(CONF_USERNAME): str,
vol.Optional(CONF_PASSWORD): str,
@@ -418,6 +418,8 @@ def ollama_config_option_schema(
default_name = DEFAULT_CONVERSATION_NAME
schema: dict = {
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=default_name): str,
}
else:
@@ -274,6 +274,8 @@ class OnvifFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="configure",
data_schema=vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=conf(CONF_NAME)): str,
vol.Required(CONF_HOST, default=conf(CONF_HOST)): str,
vol.Required(CONF_PORT, default=conf(CONF_PORT, DEFAULT_PORT)): int,
@@ -17,6 +17,8 @@ _LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME): str,
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
@@ -99,6 +99,8 @@ class OpenThermGwConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="init",
data_schema=vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME): str,
vol.Required(CONF_DEVICE): str,
vol.Optional(CONF_ID): str,
@@ -26,6 +26,8 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Optional(CONF_TOKEN): str,
vol.Optional(CONF_NEW_TOKEN): BooleanSelector(BooleanSelectorConfig()),
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
}
)
@@ -0,0 +1,29 @@
"""The Ouman EH-800 integration."""
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import OumanEh800ConfigEntry, OumanEh800Coordinator
_PLATFORMS: list[Platform] = [
Platform.SENSOR,
]
async def async_setup_entry(hass: HomeAssistant, entry: OumanEh800ConfigEntry) -> bool:
"""Set up Ouman EH-800 from a config entry."""
coordinator = OumanEh800Coordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
coordinator.sync_circuit_device_names()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: OumanEh800ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
@@ -0,0 +1,79 @@
"""Config flow for the Ouman EH-800 integration."""
import logging
from typing import Any
from ouman_eh_800_api import (
OumanClientAuthenticationError,
OumanClientCommunicationError,
OumanEh800Client,
)
import voluptuous as vol
from yarl import URL
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_URL): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
def _normalize_url(url: str) -> str:
"""Reduce URL to scheme://host[:port], discarding any path, query, or fragment."""
return str(URL(url.strip()).origin())
class OumanEh800ConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Ouman EH-800."""
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:
try:
user_input[CONF_URL] = _normalize_url(user_input[CONF_URL])
except ValueError:
errors[CONF_URL] = "invalid_url"
else:
self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]})
client = OumanEh800Client(
session=async_get_clientsession(self.hass),
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
address=user_input[CONF_URL],
)
try:
await client.login()
except OumanClientCommunicationError:
errors["base"] = "cannot_connect"
except OumanClientAuthenticationError:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title="Ouman EH-800", data=user_input
)
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, user_input
),
errors=errors,
)
@@ -0,0 +1,15 @@
"""Constants for the Ouman EH-800 integration."""
from enum import StrEnum
DOMAIN = "ouman_eh_800"
DEFAULT_SCAN_INTERVAL_SECONDS = 60
class OumanDevice(StrEnum):
"""Logical device that an entity belongs to."""
MAIN = "main"
L1 = "l1"
L2 = "l2"
@@ -0,0 +1,117 @@
"""Data update coordinator for the Ouman EH-800 integration."""
from datetime import timedelta
import logging
from ouman_eh_800_api import (
L1BaseEndpoints,
L2BaseEndpoints,
OumanClientAuthenticationError,
OumanClientCommunicationError,
OumanEh800Client,
OumanEndpoint,
OumanRegistrySet,
OumanValues,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_SCAN_INTERVAL_SECONDS, DOMAIN, OumanDevice
_LOGGER = logging.getLogger(__name__)
type OumanEh800ConfigEntry = ConfigEntry[OumanEh800Coordinator]
class OumanEh800Coordinator(DataUpdateCoordinator[dict[OumanEndpoint, OumanValues]]):
"""Ouman EH-800 data update coordinator."""
_registry_set: OumanRegistrySet
config_entry: OumanEh800ConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: OumanEh800ConfigEntry,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name="Ouman EH-800",
config_entry=config_entry,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL_SECONDS),
always_update=False,
)
self.client: OumanEh800Client = OumanEh800Client(
session=async_get_clientsession(hass),
username=config_entry.data[CONF_USERNAME],
password=config_entry.data[CONF_PASSWORD],
address=config_entry.data[CONF_URL],
)
entry_id = config_entry.entry_id
main_device_identifier = (DOMAIN, entry_id)
self.device_info: dict[OumanDevice, DeviceInfo] = {
OumanDevice.MAIN: DeviceInfo(
identifiers={main_device_identifier},
manufacturer="Ouman",
model="EH-800",
configuration_url=config_entry.data[CONF_URL],
),
OumanDevice.L1: DeviceInfo(
identifiers={(DOMAIN, f"{entry_id}_{OumanDevice.L1}")},
translation_key="heating_circuit",
translation_placeholders={"circuit_number": "1"},
via_device=main_device_identifier,
),
OumanDevice.L2: DeviceInfo(
identifiers={(DOMAIN, f"{entry_id}_{OumanDevice.L2}")},
translation_key="heating_circuit",
translation_placeholders={"circuit_number": "2"},
via_device=main_device_identifier,
),
}
async def _async_setup(self) -> None:
try:
# Even though not required to fetch values, perform login once
# at the start to verify that the credentials are valid.
await self.client.login()
self._registry_set = await self.client.get_active_registries()
except OumanClientAuthenticationError as err:
raise ConfigEntryError("Invalid credentials") from err
except OumanClientCommunicationError as err:
raise ConfigEntryNotReady("Error communicating with API") from err
async def _async_update_data(self) -> dict[OumanEndpoint, OumanValues]:
"""Fetch registry values from the device."""
try:
return await self.client.get_values(self._registry_set)
except OumanClientCommunicationError as err:
raise UpdateFailed("Error communicating with API") from err
def sync_circuit_device_names(self) -> None:
"""Set the device-reported circuit names for the L1/L2 sub-device names.
Should be called after the data update so that platforms register
L1/L2 devices with the resolved names.
"""
for device, endpoint, circuit_number in (
(OumanDevice.L1, L1BaseEndpoints.CIRCUIT_NAME, "1"),
(OumanDevice.L2, L2BaseEndpoints.CIRCUIT_NAME, "2"),
):
if circuit_name := self.data.get(endpoint):
assert isinstance(circuit_name, str)
device_info = self.device_info[device]
device_info["translation_key"] = "heating_circuit_with_name"
device_info["translation_placeholders"] = {
"circuit_number": circuit_number,
"circuit_name": circuit_name,
}
@@ -0,0 +1,42 @@
"""Base entity for Ouman EH-800."""
from dataclasses import dataclass
from ouman_eh_800_api import OumanEndpoint
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import OumanDevice
from .coordinator import OumanEh800Coordinator
@dataclass(frozen=True, kw_only=True)
class OumanEh800EntityDescription(EntityDescription):
"""Common Ouman EH-800 entity description fields."""
device: OumanDevice
class OumanEh800Entity(CoordinatorEntity[OumanEh800Coordinator]):
"""Base entity for Ouman EH-800."""
_attr_has_entity_name = True
entity_description: OumanEh800EntityDescription
def __init__(
self,
coordinator: OumanEh800Coordinator,
endpoint: OumanEndpoint,
description: OumanEh800EntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._endpoint = endpoint
self.entity_description = description
self._attr_unique_id = (
f"{coordinator.config_entry.entry_id}"
f"_{description.device}_{description.key}"
)
self._attr_device_info = coordinator.device_info[description.device]
@@ -0,0 +1,9 @@
{
"entity": {
"sensor": {
"valve_position": {
"default": "mdi:pipe-valve"
}
}
}
}
@@ -0,0 +1,11 @@
{
"domain": "ouman_eh_800",
"name": "Ouman EH-800",
"codeowners": ["@Markus98"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ouman_eh_800",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["ouman-eh-800-api==0.5.0"]
}
@@ -0,0 +1,76 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not provide actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not provide actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Integration does not use events.
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:
status: exempt
comment: Integration does not provide actions.
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: Integration is local polling only, no discovery.
discovery:
status: exempt
comment: Integration is local polling only, no discovery.
docs-data-update: done
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: Integration supports a single device per config entry.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices:
status: exempt
comment: Integration supports a single device per config entry.
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done
@@ -0,0 +1,186 @@
"""Sensor platform for the Ouman EH-800 integration."""
from dataclasses import dataclass
from ouman_eh_800_api import (
L1BaseEndpoints,
L1RoomSensor,
L2BaseEndpoints,
L2RoomSensor,
OumanEndpoint,
SystemEndpoints,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import OumanDevice
from .coordinator import OumanEh800ConfigEntry
from .entity import OumanEh800Entity, OumanEh800EntityDescription
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class OumanEh800SensorDescription(OumanEh800EntityDescription, SensorEntityDescription):
"""Sensor description with main/L1/L2 device assignment."""
def _temperature_sensor(
*,
device: OumanDevice,
key: str,
device_class: SensorDeviceClass = SensorDeviceClass.TEMPERATURE,
entity_category: EntityCategory | None = None,
enabled_by_default: bool = True,
) -> OumanEh800SensorDescription:
return OumanEh800SensorDescription(
device=device,
key=key,
translation_key=key,
device_class=device_class,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=1,
entity_category=entity_category,
entity_registry_enabled_default=enabled_by_default,
)
def _percentage_sensor(
*,
device: OumanDevice,
key: str,
) -> OumanEh800SensorDescription:
return OumanEh800SensorDescription(
device=device,
key=key,
translation_key=key,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=1,
)
SENSOR_DESCRIPTIONS: dict[OumanEndpoint, OumanEh800SensorDescription] = {
SystemEndpoints.OUTSIDE_TEMPERATURE: _temperature_sensor(
device=OumanDevice.MAIN, key="outside_temperature"
),
L1BaseEndpoints.SUPPLY_WATER_TEMPERATURE: _temperature_sensor(
device=OumanDevice.L1, key="supply_water_temperature"
),
L1BaseEndpoints.VALVE_POSITION: _percentage_sensor(
device=OumanDevice.L1, key="valve_position"
),
L1BaseEndpoints.SUPPLY_WATER_TEMPERATURE_SETPOINT: _temperature_sensor(
device=OumanDevice.L1,
key="supply_water_temperature_setpoint",
entity_category=EntityCategory.DIAGNOSTIC,
),
L1BaseEndpoints.CURVE_SUPPLY_WATER_TEMPERATURE: _temperature_sensor(
device=OumanDevice.L1,
key="curve_supply_water_temperature",
entity_category=EntityCategory.DIAGNOSTIC,
enabled_by_default=False,
),
L1BaseEndpoints.FINE_ADJUSTMENT_EFFECT: _temperature_sensor(
device=OumanDevice.L1,
key="fine_adjustment_effect",
device_class=SensorDeviceClass.TEMPERATURE_DELTA,
entity_category=EntityCategory.DIAGNOSTIC,
enabled_by_default=False,
),
L1RoomSensor.ROOM_TEMPERATURE: _temperature_sensor(
device=OumanDevice.L1, key="room_temperature"
),
L1RoomSensor.ROOM_TEMPERATURE_SETPOINT: _temperature_sensor(
device=OumanDevice.L1,
key="room_temperature_setpoint",
entity_category=EntityCategory.DIAGNOSTIC,
),
L1RoomSensor.DELAYED_ROOM_TEMPERATURE: _temperature_sensor(
device=OumanDevice.L1,
key="delayed_room_temperature",
entity_category=EntityCategory.DIAGNOSTIC,
enabled_by_default=False,
),
L1RoomSensor.ROOM_SENSOR_POTENTIOMETER: _temperature_sensor(
device=OumanDevice.L1,
key="room_sensor_potentiometer",
device_class=SensorDeviceClass.TEMPERATURE_DELTA,
entity_category=EntityCategory.DIAGNOSTIC,
enabled_by_default=False,
),
L2BaseEndpoints.SUPPLY_WATER_TEMPERATURE: _temperature_sensor(
device=OumanDevice.L2, key="supply_water_temperature"
),
L2BaseEndpoints.VALVE_POSITION: _percentage_sensor(
device=OumanDevice.L2, key="valve_position"
),
L2BaseEndpoints.SUPPLY_WATER_TEMPERATURE_SETPOINT: _temperature_sensor(
device=OumanDevice.L2,
key="supply_water_temperature_setpoint",
entity_category=EntityCategory.DIAGNOSTIC,
),
L2BaseEndpoints.CURVE_SUPPLY_WATER_TEMPERATURE: _temperature_sensor(
device=OumanDevice.L2,
key="curve_supply_water_temperature",
entity_category=EntityCategory.DIAGNOSTIC,
enabled_by_default=False,
),
L2BaseEndpoints.DELAYED_OUTDOOR_TEMPERATURE_EFFECT: _temperature_sensor(
device=OumanDevice.L2,
key="delayed_outdoor_temperature_effect",
device_class=SensorDeviceClass.TEMPERATURE_DELTA,
entity_category=EntityCategory.DIAGNOSTIC,
enabled_by_default=False,
),
L2RoomSensor.ROOM_TEMPERATURE: _temperature_sensor(
device=OumanDevice.L2, key="room_temperature"
),
L2RoomSensor.ROOM_TEMPERATURE_SETPOINT: _temperature_sensor(
device=OumanDevice.L2,
key="room_temperature_setpoint",
entity_category=EntityCategory.DIAGNOSTIC,
),
L2RoomSensor.DELAYED_ROOM_TEMPERATURE: _temperature_sensor(
device=OumanDevice.L2,
key="delayed_room_temperature",
entity_category=EntityCategory.DIAGNOSTIC,
enabled_by_default=False,
),
}
async def async_setup_entry(
hass: HomeAssistant,
entry: OumanEh800ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Ouman EH-800 sensors based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
OumanEh800SensorEntity(coordinator, endpoint, description)
for endpoint in coordinator.data
if (description := SENSOR_DESCRIPTIONS.get(endpoint)) is not None
)
class OumanEh800SensorEntity(OumanEh800Entity, SensorEntity):
"""Ouman EH-800 sensor entity."""
entity_description: OumanEh800SensorDescription
@property
def native_value(self) -> float | str:
"""Return the current sensor value."""
value = self.coordinator.data[self._endpoint]
assert isinstance(value, float | str)
return value
@@ -0,0 +1,54 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_url": "Invalid URL",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"url": "[%key:common::config_flow::data::url%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"password": "Password for the Ouman EH-800 web interface",
"url": "The URL of the Ouman EH-800 web interface",
"username": "Username for the Ouman EH-800 web interface"
}
}
}
},
"device": {
"heating_circuit": { "name": "Heating circuit {circuit_number}" },
"heating_circuit_with_name": {
"name": "Heating circuit {circuit_number} {circuit_name}"
}
},
"entity": {
"sensor": {
"curve_supply_water_temperature": {
"name": "Curve supply water temperature"
},
"delayed_outdoor_temperature_effect": {
"name": "Delayed outdoor temperature effect"
},
"delayed_room_temperature": { "name": "Delayed room temperature" },
"fine_adjustment_effect": { "name": "Fine adjustment effect" },
"outside_temperature": { "name": "Outside temperature" },
"room_sensor_potentiometer": { "name": "Room sensor potentiometer" },
"room_temperature": { "name": "Room temperature" },
"room_temperature_setpoint": { "name": "Room temperature setpoint" },
"supply_water_temperature": { "name": "Supply water temperature" },
"supply_water_temperature_setpoint": {
"name": "Supply water temperature setpoint"
},
"valve_position": { "name": "Valve position" }
}
}
}
@@ -93,6 +93,8 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN):
if self._data[CONF_HOST] is not None
else "",
): str,
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(
CONF_NAME,
default=self._data[CONF_NAME]
@@ -76,6 +76,8 @@ class PiHoleFlowHandler(ConfigFlow, domain=DOMAIN):
vol.Required(
CONF_PORT, default=user_input.get(CONF_PORT, 80)
): vol.Coerce(int),
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(
CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME)
): str,
@@ -59,6 +59,8 @@ class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="user",
data_schema=vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(
CONF_DEVICE_NAME,
default=self._init_info.get(CONF_DEVICE_NAME, None),
@@ -12,5 +12,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["python-pooldose==0.9.0"]
"requirements": ["python-pooldose==0.9.1"]
}
@@ -45,6 +45,8 @@ class ProwlConfigFlow(ConfigFlow, domain=DOMAIN):
vol.Schema(
{
vol.Required(CONF_API_KEY): str,
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME): str,
},
),
@@ -215,6 +215,8 @@ class PlayStation4FlowHandler(ConfigFlow, domain=DOMAIN):
link_schema[vol.Required(CONF_CODE)] = vol.All(
vol.Strip, vol.Length(max=PIN_LENGTH), vol.Coerce(int)
)
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
link_schema[vol.Required(CONF_NAME, default=DEFAULT_NAME)] = str
return self.async_show_form(
@@ -13,6 +13,8 @@ from .const import DEFAULT_NAME, DOMAIN
CONFIG_SCHEMA = vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(CONF_NAME, default=DEFAULT_NAME): selector.TextSelector(),
vol.Required(CONF_API_KEY): selector.TextSelector(),
}
@@ -14,6 +14,8 @@ from .const import CONF_USER_KEY, DEFAULT_NAME, DOMAIN
USER_SCHEMA = vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
vol.Required(CONF_API_KEY): str,
vol.Required(CONF_USER_KEY): str,
+43 -8
View File
@@ -1,8 +1,15 @@
"""The Risco integration."""
from asyncio import CancelledError
import logging
from pyrisco import CannotConnectError, RiscoCloud, RiscoLocal, UnauthorizedError
from pyrisco import (
CannotConnectError,
OperationError,
RiscoCloud,
RiscoLocal,
UnauthorizedError,
)
from pyrisco.common import Partition, System, Zone
from homeassistant.config_entries import ConfigEntry
@@ -23,6 +30,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_COMMUNICATION_DELAY,
CONF_CONCURRENCY,
DEFAULT_CONCURRENCY,
DOMAIN,
@@ -42,6 +50,8 @@ PLATFORMS = [
Platform.SWITCH,
]
_LOGGER = logging.getLogger(__name__)
# pyrisco exposes timeout context as message text for this case.
CLOCK_TIMEOUT_ERROR_FRAGMENT = "Timeout in command: CLOCK"
def is_local(entry: ConfigEntry) -> bool:
@@ -68,7 +78,11 @@ async def _async_setup_local_entry(
data = entry.data
concurrency = entry.options.get(CONF_CONCURRENCY, DEFAULT_CONCURRENCY)
risco = RiscoLocal(
data[CONF_HOST], data[CONF_PORT], data[CONF_PIN], concurrency=concurrency
data[CONF_HOST],
data[CONF_PORT],
data[CONF_PIN],
communication_delay=data.get(CONF_COMMUNICATION_DELAY, 0),
concurrency=concurrency,
)
try:
@@ -76,14 +90,26 @@ async def _async_setup_local_entry(
except CannotConnectError as error:
raise ConfigEntryNotReady from error
except UnauthorizedError:
_LOGGER.exception("Failed to login to Risco cloud")
_LOGGER.exception("Failed to authenticate with local Risco panel")
return False
async def _error(error: Exception) -> None:
_LOGGER.error("Error in Risco library", exc_info=error)
if isinstance(error, ConnectionResetError) and not hass.is_stopping:
_LOGGER.debug("Disconnected from panel. Reloading integration")
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
if isinstance(error, OperationError) and CLOCK_TIMEOUT_ERROR_FRAGMENT in str(
error
):
_LOGGER.warning(
"Risco keep-alive timeout for entry %s (host: %s)",
entry.title,
data.get(CONF_HOST, "unknown"),
)
else:
_LOGGER.error(
"Error in Risco library",
exc_info=error,
)
if isinstance(error, ConnectionResetError) and not hass.is_stopping:
_LOGGER.debug("Disconnected from panel. Reloading integration")
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
entry.async_on_unload(risco.add_error_handler(_error))
@@ -159,7 +185,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: RiscoConfigEntry) -> bo
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok and (local_data := entry.runtime_data.local_data):
await local_data.system.disconnect()
try:
await local_data.system.disconnect()
except CancelledError:
raise
except Exception:
_LOGGER.exception(
"Failed to disconnect from local Risco panel for entry %s (host: %s)",
entry.title,
entry.data.get(CONF_HOST, "unknown"),
)
return unload_ok
@@ -1,4 +1,5 @@
"""Config flow for Satel Integra."""
# pylint: disable=hass-config-flow-name-field # Name field is no longer allowed in config flow schemas
import logging
from typing import Any
@@ -189,6 +189,8 @@ SENSOR_SETTINGS = vol.Schema(
}
)
SENSOR_SETUP = vol.Schema(
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
{vol.Optional(CONF_NAME, default=DEFAULT_NAME): TextSelector()}
).extend(SENSOR_SETTINGS.schema)
@@ -67,6 +67,8 @@ class SimplePushFlowHandler(ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema(
{
vol.Required(CONF_DEVICE_KEY): str,
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
vol.Inclusive(CONF_PASSWORD, ATTR_ENCRYPTED): str,
vol.Inclusive(CONF_SALT, ATTR_ENCRYPTED): str,
@@ -114,6 +114,8 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="user",
data_schema=vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME): vol.In(
[
d.device.display_name
@@ -159,6 +159,8 @@ class SolarEdgeConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema_dict: dict[vol.Marker, Any] = {}
if self.source != SOURCE_RECONFIGURE:
data_schema_dict[
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME))
] = str
data_schema_dict[
@@ -1,4 +1,5 @@
"""Config flow for Splunk integration."""
# pylint: disable=hass-config-flow-name-field # Name field is no longer allowed in config flow schemas
from collections.abc import Mapping
import logging
@@ -84,6 +84,8 @@ OPTIONS_SCHEMA: vol.Schema = vol.Schema(
CONFIG_SCHEMA: vol.Schema = vol.Schema(
{
# Approved exemption: user names the SQL query sensor
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default="Select SQL Query"): selector.TextSelector(),
vol.Optional(CONF_DB_URL): selector.TextSelector(),
}
@@ -86,6 +86,8 @@ class SRPEnergyConfigFlow(ConfigFlow, domain=DOMAIN):
if self.source == SOURCE_USER
else self._get_reconfigure_entry().data[CONF_ID]
),
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(
CONF_NAME, default=self.hass.config.location_name
): str,
@@ -92,6 +92,8 @@ class SyncThruConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema(
{
vol.Required(CONF_URL, default=user_input.get(CONF_URL, "")): str,
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(CONF_NAME, default=user_input.get(CONF_NAME, "")): str,
}
),
@@ -167,6 +167,8 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
step_id="user",
data_schema=vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(
CONF_NAME, default=user_input.get(CONF_NAME, "")
): cv.string,
@@ -59,6 +59,8 @@ def _get_config_schema(input_dict: dict[str, Any] | None = None) -> vol.Schema:
return vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(
CONF_NAME, default=input_dict.get(CONF_NAME, DEFAULT_NAME)
): str,
@@ -101,6 +101,8 @@ OPTIONS_SCHEMA = vol.Schema(
CONFIG_SCHEMA = vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
vol.Required(CONF_ORIGIN): TextSelector(),
vol.Required(CONF_DESTINATION): TextSelector(),
@@ -485,9 +485,22 @@ class WeatherFlowCloudSensorREST(WeatherFlowSensorBase):
coordinator: WeatherFlowCloudUpdateCoordinatorREST
@property
def _observation(self) -> Observation | None:
"""Return the current station observation."""
observations = self.coordinator.data[self.station_id].observation.obs
if not observations:
return None
return observations[0]
@property
def available(self) -> bool:
"""Get if available."""
return super().available and self._observation is not None
@property
def native_value(self) -> StateType | datetime:
"""Return the native value."""
return self.entity_description.value_fn(
self.coordinator.data[self.station_id].observation.obs[0]
)
if (observation := self._observation) is None:
return None
return self.entity_description.value_fn(observation)
@@ -1,6 +1,7 @@
"""Commands part of Websocket API."""
from collections.abc import Callable
from datetime import datetime, timedelta
from functools import lru_cache, partial
import json
import logging
@@ -55,6 +56,7 @@ from homeassistant.helpers.event import (
TrackTemplate,
TrackTemplateResult,
async_track_template_result,
async_track_time_interval,
)
from homeassistant.helpers.json import (
JSON_DUMP,
@@ -124,6 +126,7 @@ def async_register_commands(
async_reg(hass, handle_ping)
async_reg(hass, handle_render_template)
async_reg(hass, handle_subscribe_bootstrap_integrations)
async_reg(hass, handle_subscribe_condition)
async_reg(hass, handle_subscribe_condition_platforms)
async_reg(hass, handle_subscribe_events)
async_reg(hass, handle_subscribe_trigger)
@@ -1035,6 +1038,55 @@ async def handle_test_condition(
condition.async_unload()
@decorators.websocket_command(
{
vol.Required("type"): "subscribe_condition",
vol.Required("condition"): cv.CONDITION_SCHEMA,
}
)
@decorators.require_admin
@decorators.async_response
async def handle_subscribe_condition(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle subscribe condition command."""
condition_config = await async_validate_condition_config(hass, msg["condition"])
condition = await async_condition_from_config(hass, condition_config)
event_data: dict[str, Any] = {}
@callback
def evaluate_condition(now: datetime | None) -> None:
"""Forward events to websocket."""
nonlocal event_data
new_event_data: dict[str, Any]
try:
new_event_data = {"result": condition.async_check()}
except HomeAssistantError as err:
new_event_data = {"error": str(err)}
if new_event_data == event_data:
return
event_data = new_event_data
connection.send_event(msg["id"], event_data)
@callback
def unsubscribe() -> None:
"""Unsubscribe from condition updates."""
condition.async_unload()
unsub()
unsub = async_track_time_interval(
hass,
evaluate_condition,
timedelta(seconds=1),
name="websocket_api_condition_subscription",
)
connection.subscriptions[msg["id"]] = unsubscribe
connection.send_result(msg["id"])
evaluate_condition(None)
@decorators.websocket_command(
{
vol.Required("type"): "execute_script",
@@ -14,5 +14,5 @@
"documentation": "https://www.home-assistant.io/integrations/wmspro",
"integration_type": "hub",
"iot_class": "local_polling",
"requirements": ["pywmspro==0.3.3"]
"requirements": ["pywmspro==0.3.4"]
}
@@ -241,6 +241,8 @@ class WorkdayConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="user",
data_schema=vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
vol.Optional(CONF_COUNTRY): CountrySelector(
CountrySelectorConfig(
@@ -53,6 +53,8 @@ async def get_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
)
return vol.Schema(
{
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
vol.Required(CONF_TIME_ZONE): SelectSelector(
SelectSelectorConfig(
@@ -40,6 +40,8 @@ GATEWAY_CONFIG_HOST = GATEWAY_CONFIG.extend(CONFIG_HOST)
GATEWAY_SETTINGS = vol.Schema(
{
vol.Optional(CONF_KEY): vol.All(str, vol.Length(min=16, max=16)),
# Name field is no longer allowed in config flow schemas
# pylint: disable-next=hass-config-flow-name-field
vol.Optional(CONF_NAME, default=DEFAULT_GATEWAY_NAME): str,
}
)

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