mirror of
https://github.com/home-assistant/core.git
synced 2026-05-14 10:01:46 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3b2be7e86 | |||
| a2131c0d45 | |||
| b179d71658 | |||
| 070ef8f0b0 | |||
| aaeb55b132 | |||
| 1f5cb05f50 | |||
| cee87ed1f5 | |||
| e2ae9c1b95 | |||
| 8b257cdd6c | |||
| f756392b6a | |||
| 894ee88033 | |||
| d5d56e6e23 | |||
| a19a1ec6e8 | |||
| b98015dc76 | |||
| 4112b2af07 | |||
| 944c0d7ed2 | |||
| a471f7059f | |||
| cd1d4244ae | |||
| 573409dcbf | |||
| 6ec70734c1 | |||
| adf6213c9f | |||
| e925672bb6 | |||
| 15c5e257f5 | |||
| 8396964023 |
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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[
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"services": {
|
||||
"set_fan_timer": {
|
||||
"service": "mdi:fan-clock"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user