mirror of
https://github.com/home-assistant/core.git
synced 2025-08-05 05:35:11 +02:00
Merge branch 'dev' into aioautomower202541
This commit is contained in:
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -324,7 +324,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.8.1
|
uses: sigstore/cosign-installer@v3.8.2
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.2.3"
|
cosign-release: "v2.2.3"
|
||||||
|
|
||||||
|
@@ -363,6 +363,7 @@ homeassistant.components.no_ip.*
|
|||||||
homeassistant.components.nordpool.*
|
homeassistant.components.nordpool.*
|
||||||
homeassistant.components.notify.*
|
homeassistant.components.notify.*
|
||||||
homeassistant.components.notion.*
|
homeassistant.components.notion.*
|
||||||
|
homeassistant.components.ntfy.*
|
||||||
homeassistant.components.number.*
|
homeassistant.components.number.*
|
||||||
homeassistant.components.nut.*
|
homeassistant.components.nut.*
|
||||||
homeassistant.components.ohme.*
|
homeassistant.components.ohme.*
|
||||||
|
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1051,6 +1051,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/nsw_fuel_station/ @nickw444
|
/tests/components/nsw_fuel_station/ @nickw444
|
||||||
/homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte
|
/homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte
|
||||||
/tests/components/nsw_rural_fire_service_feed/ @exxamalte
|
/tests/components/nsw_rural_fire_service_feed/ @exxamalte
|
||||||
|
/homeassistant/components/ntfy/ @tr4nt0r
|
||||||
|
/tests/components/ntfy/ @tr4nt0r
|
||||||
/homeassistant/components/nuheat/ @tstabrawa
|
/homeassistant/components/nuheat/ @tstabrawa
|
||||||
/tests/components/nuheat/ @tstabrawa
|
/tests/components/nuheat/ @tstabrawa
|
||||||
/homeassistant/components/nuki/ @pschmitt @pvizeli @pree
|
/homeassistant/components/nuki/ @pschmitt @pvizeli @pree
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioairzone_cloud"],
|
"loggers": ["aioairzone_cloud"],
|
||||||
"requirements": ["aioairzone-cloud==0.6.11"]
|
"requirements": ["aioairzone-cloud==0.6.12"]
|
||||||
}
|
}
|
||||||
|
@@ -3,10 +3,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from asyncio import timeout
|
from asyncio import timeout
|
||||||
|
from collections.abc import Mapping
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from types import MappingProxyType
|
|
||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@@ -260,10 +260,10 @@ async def async_enable_proactive_mode(
|
|||||||
def extra_significant_check(
|
def extra_significant_check(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
old_state: str,
|
old_state: str,
|
||||||
old_attrs: dict[Any, Any] | MappingProxyType[Any, Any],
|
old_attrs: Mapping[Any, Any],
|
||||||
old_extra_arg: Any,
|
old_extra_arg: Any,
|
||||||
new_state: str,
|
new_state: str,
|
||||||
new_attrs: dict[str, Any] | MappingProxyType[Any, Any],
|
new_attrs: Mapping[Any, Any],
|
||||||
new_extra_arg: Any,
|
new_extra_arg: Any,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check if the serialized data has changed."""
|
"""Check if the serialized data has changed."""
|
||||||
|
@@ -3,12 +3,12 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"tracked_addons": "Addons",
|
"tracked_addons": "Add-ons",
|
||||||
"tracked_integrations": "Integrations",
|
"tracked_integrations": "Integrations",
|
||||||
"tracked_custom_integrations": "Custom integrations"
|
"tracked_custom_integrations": "Custom integrations"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"tracked_addons": "Select the addons you want to track",
|
"tracked_addons": "Select the add-ons you want to track",
|
||||||
"tracked_integrations": "Select the integrations you want to track",
|
"tracked_integrations": "Select the integrations you want to track",
|
||||||
"tracked_custom_integrations": "Select the custom integrations you want to track"
|
"tracked_custom_integrations": "Select the custom integrations you want to track"
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
@@ -175,7 +176,7 @@ class AnthropicOptionsFlow(OptionsFlow):
|
|||||||
|
|
||||||
def anthropic_config_option_schema(
|
def anthropic_config_option_schema(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
options: dict[str, Any] | MappingProxyType[str, Any],
|
options: Mapping[str, Any],
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Return a schema for Anthropic completion options."""
|
"""Return a schema for Anthropic completion options."""
|
||||||
hass_apis: list[SelectOptionDict] = [
|
hass_apis: list[SelectOptionDict] = [
|
||||||
|
@@ -265,21 +265,21 @@ async def _transform_stream(
|
|||||||
if current_block is None:
|
if current_block is None:
|
||||||
raise ValueError("Unexpected stop event without a current block")
|
raise ValueError("Unexpected stop event without a current block")
|
||||||
if current_block["type"] == "tool_use":
|
if current_block["type"] == "tool_use":
|
||||||
tool_block = cast(ToolUseBlockParam, current_block)
|
# tool block
|
||||||
tool_args = json.loads(current_tool_args) if current_tool_args else {}
|
tool_args = json.loads(current_tool_args) if current_tool_args else {}
|
||||||
tool_block["input"] = tool_args
|
current_block["input"] = tool_args
|
||||||
yield {
|
yield {
|
||||||
"tool_calls": [
|
"tool_calls": [
|
||||||
llm.ToolInput(
|
llm.ToolInput(
|
||||||
id=tool_block["id"],
|
id=current_block["id"],
|
||||||
tool_name=tool_block["name"],
|
tool_name=current_block["name"],
|
||||||
tool_args=tool_args,
|
tool_args=tool_args,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
elif current_block["type"] == "thinking":
|
elif current_block["type"] == "thinking":
|
||||||
thinking_block = cast(ThinkingBlockParam, current_block)
|
# thinking block
|
||||||
LOGGER.debug("Thinking: %s", thinking_block["thinking"])
|
LOGGER.debug("Thinking: %s", current_block["thinking"])
|
||||||
|
|
||||||
if current_message is None:
|
if current_message is None:
|
||||||
raise ValueError("Unexpected stop event without a current message")
|
raise ValueError("Unexpected stop event without a current message")
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
"entity": {
|
"entity": {
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"off_grid_status": {
|
"off_grid_status": {
|
||||||
"name": "Off grid status"
|
"name": "Off-grid status"
|
||||||
},
|
},
|
||||||
"dc_1_short_circuit_error_status": {
|
"dc_1_short_circuit_error_status": {
|
||||||
"name": "DC 1 short circuit error status"
|
"name": "DC 1 short circuit error status"
|
||||||
|
@@ -26,7 +26,7 @@
|
|||||||
"sensor": {
|
"sensor": {
|
||||||
"threshold": {
|
"threshold": {
|
||||||
"state": {
|
"state": {
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"green": "Green",
|
"green": "Green",
|
||||||
"yellow": "Yellow",
|
"yellow": "Yellow",
|
||||||
"red": "Red"
|
"red": "Red"
|
||||||
|
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable, Mapping
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from types import MappingProxyType
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pyasuswrt import AsusWrtError
|
from pyasuswrt import AsusWrtError
|
||||||
@@ -363,7 +362,7 @@ class AsusWrtRouter:
|
|||||||
"""Add a function to call when router is closed."""
|
"""Add a function to call when router is closed."""
|
||||||
self._on_close.append(func)
|
self._on_close.append(func)
|
||||||
|
|
||||||
def update_options(self, new_options: MappingProxyType[str, Any]) -> bool:
|
def update_options(self, new_options: Mapping[str, Any]) -> bool:
|
||||||
"""Update router options."""
|
"""Update router options."""
|
||||||
req_reload = False
|
req_reload = False
|
||||||
for name, new_opt in new_options.items():
|
for name, new_opt in new_options.items():
|
||||||
|
@@ -28,5 +28,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pubnub", "yalexs"],
|
"loggers": ["pubnub", "yalexs"],
|
||||||
"requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.7"]
|
"requirements": ["yalexs==8.10.0", "yalexs-ble==2.6.0"]
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
from types import MappingProxyType
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
@@ -88,7 +87,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
|||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
api = await get_axis_api(self.hass, MappingProxyType(user_input))
|
api = await get_axis_api(self.hass, user_input)
|
||||||
|
|
||||||
except AuthenticationRequired:
|
except AuthenticationRequired:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""Axis network device abstraction."""
|
"""Axis network device abstraction."""
|
||||||
|
|
||||||
from asyncio import timeout
|
from asyncio import timeout
|
||||||
from types import MappingProxyType
|
from collections.abc import Mapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import axis
|
import axis
|
||||||
@@ -23,7 +23,7 @@ from ..errors import AuthenticationRequired, CannotConnect
|
|||||||
|
|
||||||
async def get_axis_api(
|
async def get_axis_api(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: MappingProxyType[str, Any],
|
config: Mapping[str, Any],
|
||||||
) -> axis.AxisDevice:
|
) -> axis.AxisDevice:
|
||||||
"""Create a Axis device API."""
|
"""Create a Axis device API."""
|
||||||
session = get_async_client(hass, verify_ssl=False)
|
session = get_async_client(hass, verify_ssl=False)
|
||||||
|
@@ -3,11 +3,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable, Mapping
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from types import MappingProxyType
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from azure.eventhub import EventData, EventDataBatch
|
from azure.eventhub import EventData, EventDataBatch
|
||||||
@@ -179,7 +178,7 @@ class AzureEventHub:
|
|||||||
await self.async_send(None)
|
await self.async_send(None)
|
||||||
await self._queue.join()
|
await self._queue.join()
|
||||||
|
|
||||||
def update_options(self, new_options: MappingProxyType[str, Any]) -> None:
|
def update_options(self, new_options: Mapping[str, Any]) -> None:
|
||||||
"""Update options."""
|
"""Update options."""
|
||||||
self._send_interval = new_options[CONF_SEND_INTERVAL]
|
self._send_interval = new_options[CONF_SEND_INTERVAL]
|
||||||
|
|
||||||
|
@@ -30,7 +30,7 @@
|
|||||||
"available": "Available",
|
"available": "Available",
|
||||||
"charging": "[%key:common::state::charging%]",
|
"charging": "[%key:common::state::charging%]",
|
||||||
"unavailable": "Unavailable",
|
"unavailable": "Unavailable",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"offline": "Offline"
|
"offline": "Offline"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"vehicle_detected": "Detected",
|
"vehicle_detected": "Detected",
|
||||||
"ready": "Ready",
|
"ready": "Ready",
|
||||||
"no_power": "No power",
|
"no_power": "No power",
|
||||||
"vehicle_error": "Error"
|
"vehicle_error": "[%key:common::state::error%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"actual_v1": {
|
"actual_v1": {
|
||||||
|
@@ -139,7 +139,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"charging": "[%key:common::state::charging%]",
|
"charging": "[%key:common::state::charging%]",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"complete": "Complete",
|
"complete": "Complete",
|
||||||
"fully_charged": "Fully charged",
|
"fully_charged": "Fully charged",
|
||||||
"finished_fully_charged": "Finished, fully charged",
|
"finished_fully_charged": "Finished, fully charged",
|
||||||
|
@@ -8,46 +8,18 @@ from typing import Final
|
|||||||
|
|
||||||
from canary.api import Api
|
from canary.api import Api
|
||||||
from requests.exceptions import ConnectTimeout, HTTPError
|
from requests.exceptions import ConnectTimeout, HTTPError
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT
|
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME, Platform
|
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import config_validation as cv
|
|
||||||
from homeassistant.helpers.typing import ConfigType
|
|
||||||
|
|
||||||
from .const import (
|
from .const import CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_TIMEOUT
|
||||||
CONF_FFMPEG_ARGUMENTS,
|
|
||||||
DEFAULT_FFMPEG_ARGUMENTS,
|
|
||||||
DEFAULT_TIMEOUT,
|
|
||||||
DOMAIN,
|
|
||||||
)
|
|
||||||
from .coordinator import CanaryConfigEntry, CanaryDataUpdateCoordinator
|
from .coordinator import CanaryConfigEntry, CanaryDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES: Final = timedelta(seconds=30)
|
MIN_TIME_BETWEEN_UPDATES: Final = timedelta(seconds=30)
|
||||||
|
|
||||||
CONFIG_SCHEMA: Final = vol.Schema(
|
|
||||||
vol.All(
|
|
||||||
cv.deprecated(DOMAIN),
|
|
||||||
{
|
|
||||||
DOMAIN: vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_TIMEOUT, default=DEFAULT_TIMEOUT
|
|
||||||
): cv.positive_int,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
extra=vol.ALLOW_EXTRA,
|
|
||||||
)
|
|
||||||
|
|
||||||
PLATFORMS: Final[list[Platform]] = [
|
PLATFORMS: Final[list[Platform]] = [
|
||||||
Platform.ALARM_CONTROL_PANEL,
|
Platform.ALARM_CONTROL_PANEL,
|
||||||
Platform.CAMERA,
|
Platform.CAMERA,
|
||||||
@@ -55,37 +27,6 @@ PLATFORMS: Final[list[Platform]] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
||||||
"""Set up the Canary integration."""
|
|
||||||
if hass.config_entries.async_entries(DOMAIN):
|
|
||||||
return True
|
|
||||||
|
|
||||||
ffmpeg_arguments = DEFAULT_FFMPEG_ARGUMENTS
|
|
||||||
if CAMERA_DOMAIN in config:
|
|
||||||
camera_config = next(
|
|
||||||
(item for item in config[CAMERA_DOMAIN] if item["platform"] == DOMAIN),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
if camera_config:
|
|
||||||
ffmpeg_arguments = camera_config.get(
|
|
||||||
CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
|
|
||||||
)
|
|
||||||
|
|
||||||
if DOMAIN in config:
|
|
||||||
if ffmpeg_arguments != DEFAULT_FFMPEG_ARGUMENTS:
|
|
||||||
config[DOMAIN][CONF_FFMPEG_ARGUMENTS] = ffmpeg_arguments
|
|
||||||
|
|
||||||
hass.async_create_task(
|
|
||||||
hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": SOURCE_IMPORT},
|
|
||||||
data=config[DOMAIN],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: CanaryConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: CanaryConfigEntry) -> bool:
|
||||||
"""Set up Canary from a config entry."""
|
"""Set up Canary from a config entry."""
|
||||||
if not entry.options:
|
if not entry.options:
|
||||||
|
@@ -54,10 +54,6 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return CanaryOptionsFlowHandler()
|
return CanaryOptionsFlowHandler()
|
||||||
|
|
||||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
|
||||||
"""Handle a flow initiated by configuration file."""
|
|
||||||
return await self.async_step_user(import_data)
|
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/chacon_dio",
|
"documentation": "https://www.home-assistant.io/integrations/chacon_dio",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["dio_chacon_api"],
|
"loggers": ["dio_chacon_api"],
|
||||||
"requirements": ["dio-chacon-wifi-api==1.2.1"]
|
"requirements": ["dio-chacon-wifi-api==1.2.2"]
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Mapping
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from types import MappingProxyType
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from devolo_home_control_api.exceptions.gateway import GatewayOfflineError
|
from devolo_home_control_api.exceptions.gateway import GatewayOfflineError
|
||||||
@@ -97,7 +97,7 @@ async def async_remove_config_entry_device(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def configure_mydevolo(conf: dict[str, Any] | MappingProxyType[str, Any]) -> Mydevolo:
|
def configure_mydevolo(conf: Mapping[str, Any]) -> Mydevolo:
|
||||||
"""Configure mydevolo."""
|
"""Configure mydevolo."""
|
||||||
mydevolo = Mydevolo()
|
mydevolo = Mydevolo()
|
||||||
mydevolo.user = conf[CONF_USERNAME]
|
mydevolo.user = conf[CONF_USERNAME]
|
||||||
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from fnmatch import translate
|
from fnmatch import translate
|
||||||
from functools import lru_cache, partial
|
from functools import lru_cache, partial
|
||||||
@@ -66,13 +65,12 @@ from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo as _DhcpServ
|
|||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.loader import DHCPMatcher, async_get_dhcp
|
from homeassistant.loader import DHCPMatcher, async_get_dhcp
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import websocket_api
|
||||||
|
from .const import DOMAIN, HOSTNAME, IP_ADDRESS, MAC_ADDRESS
|
||||||
|
from .models import DATA_DHCP, DHCPAddressData, DHCPData, DhcpMatchers
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
HOSTNAME: Final = "hostname"
|
|
||||||
MAC_ADDRESS: Final = "macaddress"
|
|
||||||
IP_ADDRESS: Final = "ip"
|
|
||||||
REGISTERED_DEVICES: Final = "registered_devices"
|
REGISTERED_DEVICES: Final = "registered_devices"
|
||||||
SCAN_INTERVAL = timedelta(minutes=60)
|
SCAN_INTERVAL = timedelta(minutes=60)
|
||||||
|
|
||||||
@@ -87,15 +85,6 @@ _DEPRECATED_DhcpServiceInfo = DeprecatedConstant(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class DhcpMatchers:
|
|
||||||
"""Prepared info from dhcp entries."""
|
|
||||||
|
|
||||||
registered_devices_domains: set[str]
|
|
||||||
no_oui_matchers: dict[str, list[DHCPMatcher]]
|
|
||||||
oui_matchers: dict[str, list[DHCPMatcher]]
|
|
||||||
|
|
||||||
|
|
||||||
def async_index_integration_matchers(
|
def async_index_integration_matchers(
|
||||||
integration_matchers: list[DHCPMatcher],
|
integration_matchers: list[DHCPMatcher],
|
||||||
) -> DhcpMatchers:
|
) -> DhcpMatchers:
|
||||||
@@ -133,36 +122,34 @@ def async_index_integration_matchers(
|
|||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the dhcp component."""
|
"""Set up the dhcp component."""
|
||||||
watchers: list[WatcherBase] = []
|
|
||||||
address_data: dict[str, dict[str, str]] = {}
|
|
||||||
integration_matchers = async_index_integration_matchers(await async_get_dhcp(hass))
|
integration_matchers = async_index_integration_matchers(await async_get_dhcp(hass))
|
||||||
|
dhcp_data = DHCPData(integration_matchers=integration_matchers)
|
||||||
|
hass.data[DATA_DHCP] = dhcp_data
|
||||||
|
websocket_api.async_setup(hass)
|
||||||
|
watchers: list[WatcherBase] = []
|
||||||
# For the passive classes we need to start listening
|
# For the passive classes we need to start listening
|
||||||
# for state changes and connect the dispatchers before
|
# for state changes and connect the dispatchers before
|
||||||
# everything else starts up or we will miss events
|
# everything else starts up or we will miss events
|
||||||
device_watcher = DeviceTrackerWatcher(hass, address_data, integration_matchers)
|
device_watcher = DeviceTrackerWatcher(hass, dhcp_data)
|
||||||
device_watcher.async_start()
|
device_watcher.async_start()
|
||||||
watchers.append(device_watcher)
|
watchers.append(device_watcher)
|
||||||
|
|
||||||
device_tracker_registered_watcher = DeviceTrackerRegisteredWatcher(
|
device_tracker_registered_watcher = DeviceTrackerRegisteredWatcher(hass, dhcp_data)
|
||||||
hass, address_data, integration_matchers
|
|
||||||
)
|
|
||||||
device_tracker_registered_watcher.async_start()
|
device_tracker_registered_watcher.async_start()
|
||||||
watchers.append(device_tracker_registered_watcher)
|
watchers.append(device_tracker_registered_watcher)
|
||||||
|
|
||||||
async def _async_initialize(event: Event) -> None:
|
async def _async_initialize(event: Event) -> None:
|
||||||
await aiodhcpwatcher.async_init()
|
await aiodhcpwatcher.async_init()
|
||||||
|
|
||||||
network_watcher = NetworkWatcher(hass, address_data, integration_matchers)
|
network_watcher = NetworkWatcher(hass, dhcp_data)
|
||||||
network_watcher.async_start()
|
network_watcher.async_start()
|
||||||
watchers.append(network_watcher)
|
watchers.append(network_watcher)
|
||||||
|
|
||||||
dhcp_watcher = DHCPWatcher(hass, address_data, integration_matchers)
|
dhcp_watcher = DHCPWatcher(hass, dhcp_data)
|
||||||
await dhcp_watcher.async_start()
|
await dhcp_watcher.async_start()
|
||||||
watchers.append(dhcp_watcher)
|
watchers.append(dhcp_watcher)
|
||||||
|
|
||||||
rediscovery_watcher = RediscoveryWatcher(
|
rediscovery_watcher = RediscoveryWatcher(hass, dhcp_data)
|
||||||
hass, address_data, integration_matchers
|
|
||||||
)
|
|
||||||
rediscovery_watcher.async_start()
|
rediscovery_watcher.async_start()
|
||||||
watchers.append(rediscovery_watcher)
|
watchers.append(rediscovery_watcher)
|
||||||
|
|
||||||
@@ -180,18 +167,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
class WatcherBase:
|
class WatcherBase:
|
||||||
"""Base class for dhcp and device tracker watching."""
|
"""Base class for dhcp and device tracker watching."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, hass: HomeAssistant, dhcp_data: DHCPData) -> None:
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
address_data: dict[str, dict[str, str]],
|
|
||||||
integration_matchers: DhcpMatchers,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize class."""
|
"""Initialize class."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._integration_matchers = integration_matchers
|
self._callbacks = dhcp_data.callbacks
|
||||||
self._address_data = address_data
|
self._integration_matchers = dhcp_data.integration_matchers
|
||||||
|
self._address_data = dhcp_data.address_data
|
||||||
self._unsub: Callable[[], None] | None = None
|
self._unsub: Callable[[], None] | None = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -230,18 +212,18 @@ class WatcherBase:
|
|||||||
mac_address = formatted_mac.replace(":", "")
|
mac_address = formatted_mac.replace(":", "")
|
||||||
compressed_ip_address = made_ip_address.compressed
|
compressed_ip_address = made_ip_address.compressed
|
||||||
|
|
||||||
data = self._address_data.get(mac_address)
|
current_data = self._address_data.get(mac_address)
|
||||||
if (
|
if (
|
||||||
not force
|
not force
|
||||||
and data
|
and current_data
|
||||||
and data[IP_ADDRESS] == compressed_ip_address
|
and current_data[IP_ADDRESS] == compressed_ip_address
|
||||||
and data[HOSTNAME].startswith(hostname)
|
and current_data[HOSTNAME].startswith(hostname)
|
||||||
):
|
):
|
||||||
# If the address data is the same no need
|
# If the address data is the same no need
|
||||||
# to process it
|
# to process it
|
||||||
return
|
return
|
||||||
|
|
||||||
data = {IP_ADDRESS: compressed_ip_address, HOSTNAME: hostname}
|
data: DHCPAddressData = {IP_ADDRESS: compressed_ip_address, HOSTNAME: hostname}
|
||||||
self._address_data[mac_address] = data
|
self._address_data[mac_address] = data
|
||||||
|
|
||||||
lowercase_hostname = hostname.lower()
|
lowercase_hostname = hostname.lower()
|
||||||
@@ -287,9 +269,19 @@ class WatcherBase:
|
|||||||
_LOGGER.debug("Matched %s against %s", data, matcher)
|
_LOGGER.debug("Matched %s against %s", data, matcher)
|
||||||
matched_domains.add(domain)
|
matched_domains.add(domain)
|
||||||
|
|
||||||
if not matched_domains:
|
if self._callbacks:
|
||||||
return # avoid creating DiscoveryKey if there are no matches
|
address_data = {mac_address: data}
|
||||||
|
for callback_ in self._callbacks:
|
||||||
|
callback_(address_data)
|
||||||
|
|
||||||
|
service_info: _DhcpServiceInfo | None = None
|
||||||
|
if not matched_domains:
|
||||||
|
return
|
||||||
|
service_info = _DhcpServiceInfo(
|
||||||
|
ip=ip_address,
|
||||||
|
hostname=lowercase_hostname,
|
||||||
|
macaddress=mac_address,
|
||||||
|
)
|
||||||
discovery_key = DiscoveryKey(
|
discovery_key = DiscoveryKey(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
key=mac_address,
|
key=mac_address,
|
||||||
@@ -300,11 +292,7 @@ class WatcherBase:
|
|||||||
self.hass,
|
self.hass,
|
||||||
domain,
|
domain,
|
||||||
{"source": config_entries.SOURCE_DHCP},
|
{"source": config_entries.SOURCE_DHCP},
|
||||||
_DhcpServiceInfo(
|
service_info,
|
||||||
ip=ip_address,
|
|
||||||
hostname=lowercase_hostname,
|
|
||||||
macaddress=mac_address,
|
|
||||||
),
|
|
||||||
discovery_key=discovery_key,
|
discovery_key=discovery_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -315,11 +303,10 @@ class NetworkWatcher(WatcherBase):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
address_data: dict[str, dict[str, str]],
|
dhcp_data: DHCPData,
|
||||||
integration_matchers: DhcpMatchers,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize class."""
|
"""Initialize class."""
|
||||||
super().__init__(hass, address_data, integration_matchers)
|
super().__init__(hass, dhcp_data)
|
||||||
self._discover_hosts: DiscoverHosts | None = None
|
self._discover_hosts: DiscoverHosts | None = None
|
||||||
self._discover_task: asyncio.Task | None = None
|
self._discover_task: asyncio.Task | None = None
|
||||||
|
|
||||||
|
@@ -1,3 +1,8 @@
|
|||||||
"""Constants for the dhcp integration."""
|
"""Constants for the dhcp integration."""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
DOMAIN = "dhcp"
|
DOMAIN = "dhcp"
|
||||||
|
HOSTNAME: Final = "hostname"
|
||||||
|
MAC_ADDRESS: Final = "macaddress"
|
||||||
|
IP_ADDRESS: Final = "ip"
|
||||||
|
37
homeassistant/components/dhcp/helpers.py
Normal file
37
homeassistant/components/dhcp/helpers.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""The dhcp integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
|
|
||||||
|
from .models import DATA_DHCP, DHCPAddressData
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_register_dhcp_callback_internal(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
callback_: Callable[[dict[str, DHCPAddressData]], None],
|
||||||
|
) -> CALLBACK_TYPE:
|
||||||
|
"""Register a dhcp callback.
|
||||||
|
|
||||||
|
For internal use only.
|
||||||
|
This is not intended for use by integrations.
|
||||||
|
"""
|
||||||
|
callbacks = hass.data[DATA_DHCP].callbacks
|
||||||
|
callbacks.add(callback_)
|
||||||
|
return partial(callbacks.remove, callback_)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_address_data_internal(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> dict[str, DHCPAddressData]:
|
||||||
|
"""Get the address data.
|
||||||
|
|
||||||
|
For internal use only.
|
||||||
|
This is not intended for use by integrations.
|
||||||
|
"""
|
||||||
|
return hass.data[DATA_DHCP].address_data
|
43
homeassistant/components/dhcp/models.py
Normal file
43
homeassistant/components/dhcp/models.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""The dhcp integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
import dataclasses
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from homeassistant.loader import DHCPMatcher
|
||||||
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class DhcpMatchers:
|
||||||
|
"""Prepared info from dhcp entries."""
|
||||||
|
|
||||||
|
registered_devices_domains: set[str]
|
||||||
|
no_oui_matchers: dict[str, list[DHCPMatcher]]
|
||||||
|
oui_matchers: dict[str, list[DHCPMatcher]]
|
||||||
|
|
||||||
|
|
||||||
|
class DHCPAddressData(TypedDict):
|
||||||
|
"""Typed dict for DHCP address data."""
|
||||||
|
|
||||||
|
hostname: str
|
||||||
|
ip: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(slots=True)
|
||||||
|
class DHCPData:
|
||||||
|
"""Data for the dhcp component."""
|
||||||
|
|
||||||
|
integration_matchers: DhcpMatchers
|
||||||
|
callbacks: set[Callable[[dict[str, DHCPAddressData]], None]] = dataclasses.field(
|
||||||
|
default_factory=set
|
||||||
|
)
|
||||||
|
address_data: dict[str, DHCPAddressData] = dataclasses.field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
DATA_DHCP: HassKey[DHCPData] = HassKey(DOMAIN)
|
63
homeassistant/components/dhcp/websocket_api.py
Normal file
63
homeassistant/components/dhcp/websocket_api.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"""The dhcp integration websocket apis."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import websocket_api
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.json import json_bytes
|
||||||
|
|
||||||
|
from .const import HOSTNAME, IP_ADDRESS
|
||||||
|
from .helpers import (
|
||||||
|
async_get_address_data_internal,
|
||||||
|
async_register_dhcp_callback_internal,
|
||||||
|
)
|
||||||
|
from .models import DHCPAddressData
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_setup(hass: HomeAssistant) -> None:
|
||||||
|
"""Set up the DHCP websocket API."""
|
||||||
|
websocket_api.async_register_command(hass, ws_subscribe_discovery)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "dhcp/subscribe_discovery",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def ws_subscribe_discovery(
|
||||||
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Handle subscribe discovery websocket command."""
|
||||||
|
ws_msg_id: int = msg["id"]
|
||||||
|
|
||||||
|
def _async_send(address_data: dict[str, DHCPAddressData]) -> None:
|
||||||
|
connection.send_message(
|
||||||
|
json_bytes(
|
||||||
|
websocket_api.event_message(
|
||||||
|
ws_msg_id,
|
||||||
|
{
|
||||||
|
"add": [
|
||||||
|
{
|
||||||
|
"mac_address": dr.format_mac(mac_address).upper(),
|
||||||
|
"hostname": data[HOSTNAME],
|
||||||
|
"ip_address": data[IP_ADDRESS],
|
||||||
|
}
|
||||||
|
for mac_address, data in address_data.items()
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
unsub = async_register_dhcp_callback_internal(hass, _async_send)
|
||||||
|
connection.subscriptions[ws_msg_id] = unsub
|
||||||
|
connection.send_message(json_bytes(websocket_api.result_message(ws_msg_id)))
|
||||||
|
_async_send(async_get_address_data_internal(hass))
|
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable, Mapping
|
||||||
from types import MappingProxyType
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from dynalite_devices_lib.dynalite_devices import (
|
from dynalite_devices_lib.dynalite_devices import (
|
||||||
@@ -50,7 +49,7 @@ class DynaliteBridge:
|
|||||||
LOGGER.debug("Setting up bridge - host %s", self.host)
|
LOGGER.debug("Setting up bridge - host %s", self.host)
|
||||||
return await self.dynalite_devices.async_setup()
|
return await self.dynalite_devices.async_setup()
|
||||||
|
|
||||||
def reload_config(self, config: MappingProxyType[str, Any]) -> None:
|
def reload_config(self, config: Mapping[str, Any]) -> None:
|
||||||
"""Reconfigure a bridge when config changes."""
|
"""Reconfigure a bridge when config changes."""
|
||||||
LOGGER.debug("Reloading bridge - host %s, config %s", self.host, config)
|
LOGGER.debug("Reloading bridge - host %s, config %s", self.host, config)
|
||||||
self.dynalite_devices.configure(convert_config(config))
|
self.dynalite_devices.configure(convert_config(config))
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from types import MappingProxyType
|
from collections.abc import Mapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from dynalite_devices_lib import const as dyn_const
|
from dynalite_devices_lib import const as dyn_const
|
||||||
@@ -138,9 +138,7 @@ def convert_template(config: dict[str, Any]) -> dict[str, Any]:
|
|||||||
return convert_with_map(config, my_map)
|
return convert_with_map(config, my_map)
|
||||||
|
|
||||||
|
|
||||||
def convert_config(
|
def convert_config(config: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
config: dict[str, Any] | MappingProxyType[str, Any],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Convert a config dict by replacing component consts with library consts."""
|
"""Convert a config dict by replacing component consts with library consts."""
|
||||||
my_map = {
|
my_map = {
|
||||||
CONF_NAME: dyn_const.CONF_NAME,
|
CONF_NAME: dyn_const.CONF_NAME,
|
||||||
|
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
import logging
|
import logging
|
||||||
from types import MappingProxyType
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from elevenlabs import AsyncElevenLabs
|
from elevenlabs import AsyncElevenLabs
|
||||||
@@ -43,7 +43,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
def to_voice_settings(options: MappingProxyType[str, Any]) -> VoiceSettings:
|
def to_voice_settings(options: Mapping[str, Any]) -> VoiceSettings:
|
||||||
"""Return voice settings."""
|
"""Return voice settings."""
|
||||||
return VoiceSettings(
|
return VoiceSettings(
|
||||||
stability=options.get(CONF_STABILITY, DEFAULT_STABILITY),
|
stability=options.get(CONF_STABILITY, DEFAULT_STABILITY),
|
||||||
|
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from types import MappingProxyType
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from elkm1_lib.elements import Element
|
from elkm1_lib.elements import Element
|
||||||
@@ -235,7 +234,7 @@ def _async_find_matching_config_entry(
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ElkM1ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ElkM1ConfigEntry) -> bool:
|
||||||
"""Set up Elk-M1 Control from a config entry."""
|
"""Set up Elk-M1 Control from a config entry."""
|
||||||
conf: MappingProxyType[str, Any] = entry.data
|
conf = entry.data
|
||||||
|
|
||||||
host = hostname_from_url(entry.data[CONF_HOST])
|
host = hostname_from_url(entry.data[CONF_HOST])
|
||||||
|
|
||||||
|
@@ -293,9 +293,9 @@ async def ws_get_fossil_energy_consumption(
|
|||||||
if statistics_id not in statistic_ids:
|
if statistics_id not in statistic_ids:
|
||||||
continue
|
continue
|
||||||
for period in stat:
|
for period in stat:
|
||||||
if period["change"] is None:
|
if (change := period.get("change")) is None:
|
||||||
continue
|
continue
|
||||||
result[period["start"]] += period["change"]
|
result[period["start"]] += change
|
||||||
|
|
||||||
return {key: result[key] for key in sorted(result)}
|
return {key: result[key] for key in sorted(result)}
|
||||||
|
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyenphase"],
|
"loggers": ["pyenphase"],
|
||||||
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pyenphase==1.25.5"],
|
"requirements": ["pyenphase==1.25.5"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
|
@@ -1,31 +1,19 @@
|
|||||||
rules:
|
rules:
|
||||||
# Bronze
|
# Bronze
|
||||||
action-setup:
|
action-setup:
|
||||||
status: done
|
status: exempt
|
||||||
comment: only actions implemented are platform native ones.
|
comment: only actions implemented are platform native ones.
|
||||||
appropriate-polling:
|
appropriate-polling: done
|
||||||
status: done
|
|
||||||
comment: fixed 1 minute cycle based on Enphase Envoy device characteristics
|
|
||||||
brands: done
|
brands: done
|
||||||
common-modules: done
|
common-modules: done
|
||||||
config-flow-test-coverage: done
|
config-flow-test-coverage: done
|
||||||
config-flow: done
|
config-flow: done
|
||||||
dependency-transparency: done
|
dependency-transparency: done
|
||||||
docs-actions:
|
docs-actions: done
|
||||||
status: done
|
docs-high-level-description: done
|
||||||
comment: https://www.home-assistant.io/integrations/enphase_envoy/#actions
|
docs-installation-instructions: done
|
||||||
docs-high-level-description:
|
docs-removal-instructions: done
|
||||||
status: done
|
entity-event-setup: done
|
||||||
comment: https://www.home-assistant.io/integrations/enphase_envoy
|
|
||||||
docs-installation-instructions:
|
|
||||||
status: done
|
|
||||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#prerequisites
|
|
||||||
docs-removal-instructions:
|
|
||||||
status: done
|
|
||||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#removing-the-integration
|
|
||||||
entity-event-setup:
|
|
||||||
status: done
|
|
||||||
comment: no events used.
|
|
||||||
entity-unique-id: done
|
entity-unique-id: done
|
||||||
has-entity-name: done
|
has-entity-name: done
|
||||||
runtime-data: done
|
runtime-data: done
|
||||||
@@ -34,24 +22,14 @@ rules:
|
|||||||
unique-config-entry: done
|
unique-config-entry: done
|
||||||
|
|
||||||
# Silver
|
# Silver
|
||||||
action-exceptions:
|
action-exceptions: done
|
||||||
status: todo
|
|
||||||
comment: |
|
|
||||||
needs to raise appropriate error when exception occurs.
|
|
||||||
Pending https://github.com/pyenphase/pyenphase/pull/194
|
|
||||||
config-entry-unloading: done
|
config-entry-unloading: done
|
||||||
docs-configuration-parameters:
|
docs-configuration-parameters: done
|
||||||
status: done
|
docs-installation-parameters: done
|
||||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#configuration
|
|
||||||
docs-installation-parameters:
|
|
||||||
status: done
|
|
||||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#required-manual-input
|
|
||||||
entity-unavailable: done
|
entity-unavailable: done
|
||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
parallel-updates:
|
parallel-updates: done
|
||||||
status: done
|
|
||||||
comment: pending https://github.com/home-assistant/core/pull/132373
|
|
||||||
reauthentication-flow: done
|
reauthentication-flow: done
|
||||||
test-coverage: done
|
test-coverage: done
|
||||||
|
|
||||||
@@ -60,22 +38,14 @@ rules:
|
|||||||
diagnostics: done
|
diagnostics: done
|
||||||
discovery-update-info: done
|
discovery-update-info: done
|
||||||
discovery: done
|
discovery: done
|
||||||
docs-data-update:
|
docs-data-update: done
|
||||||
status: done
|
docs-examples: done
|
||||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#data-updates
|
docs-known-limitations: done
|
||||||
docs-examples:
|
docs-supported-devices: done
|
||||||
status: todo
|
docs-supported-functions: done
|
||||||
comment: add blue-print examples, if any
|
docs-troubleshooting: done
|
||||||
docs-known-limitations: todo
|
docs-use-cases: done
|
||||||
docs-supported-devices:
|
dynamic-devices: done
|
||||||
status: done
|
|
||||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#supported-devices
|
|
||||||
docs-supported-functions: todo
|
|
||||||
docs-troubleshooting:
|
|
||||||
status: done
|
|
||||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#troubleshooting
|
|
||||||
docs-use-cases: todo
|
|
||||||
dynamic-devices: todo
|
|
||||||
entity-category: done
|
entity-category: done
|
||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
entity-disabled-by-default: done
|
entity-disabled-by-default: done
|
||||||
@@ -86,7 +56,7 @@ rules:
|
|||||||
repair-issues:
|
repair-issues:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: no general issues or repair.py
|
comment: no general issues or repair.py
|
||||||
stale-devices: todo
|
stale-devices: done
|
||||||
|
|
||||||
# Platinum
|
# Platinum
|
||||||
async-dependency: done
|
async-dependency: done
|
||||||
|
@@ -336,6 +336,12 @@ class EsphomeAssistSatellite(
|
|||||||
"code": event.data["code"],
|
"code": event.data["code"],
|
||||||
"message": event.data["message"],
|
"message": event.data["message"],
|
||||||
}
|
}
|
||||||
|
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_START:
|
||||||
|
assert event.data is not None
|
||||||
|
if tts_output := event.data["tts_output"]:
|
||||||
|
path = tts_output["url"]
|
||||||
|
url = async_process_play_media_url(self.hass, path)
|
||||||
|
data_to_send = {"url": url}
|
||||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_END:
|
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_END:
|
||||||
if self._tts_streaming_task is None:
|
if self._tts_streaming_task is None:
|
||||||
# No TTS
|
# No TTS
|
||||||
|
@@ -57,6 +57,7 @@ ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk"
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ZERO_NOISE_PSK = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA="
|
ZERO_NOISE_PSK = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA="
|
||||||
|
DEFAULT_NAME = "ESPHome"
|
||||||
|
|
||||||
|
|
||||||
class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
@@ -117,8 +118,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
self._host = entry_data[CONF_HOST]
|
self._host = entry_data[CONF_HOST]
|
||||||
self._port = entry_data[CONF_PORT]
|
self._port = entry_data[CONF_PORT]
|
||||||
self._password = entry_data[CONF_PASSWORD]
|
self._password = entry_data[CONF_PASSWORD]
|
||||||
self._name = self._reauth_entry.title
|
|
||||||
self._device_name = entry_data.get(CONF_DEVICE_NAME)
|
self._device_name = entry_data.get(CONF_DEVICE_NAME)
|
||||||
|
self._name = self._reauth_entry.title
|
||||||
|
|
||||||
# Device without encryption allows fetching device info. We can then check
|
# Device without encryption allows fetching device info. We can then check
|
||||||
# if the device is no longer using a password. If we did try with a password,
|
# if the device is no longer using a password. If we did try with a password,
|
||||||
@@ -147,7 +148,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="reauth_encryption_removed_confirm",
|
step_id="reauth_encryption_removed_confirm",
|
||||||
description_placeholders={"name": self._name},
|
description_placeholders={"name": self._async_get_human_readable_name()},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
async def async_step_reauth_confirm(
|
||||||
@@ -172,7 +173,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
step_id="reauth_confirm",
|
step_id="reauth_confirm",
|
||||||
data_schema=vol.Schema({vol.Required(CONF_NOISE_PSK): str}),
|
data_schema=vol.Schema({vol.Required(CONF_NOISE_PSK): str}),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders={"name": self._name},
|
description_placeholders={"name": self._async_get_human_readable_name()},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_reconfigure(
|
async def async_step_reconfigure(
|
||||||
@@ -189,12 +190,14 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def _name(self) -> str:
|
def _name(self) -> str:
|
||||||
return self.__name or "ESPHome"
|
return self.__name or DEFAULT_NAME
|
||||||
|
|
||||||
@_name.setter
|
@_name.setter
|
||||||
def _name(self, value: str) -> None:
|
def _name(self, value: str) -> None:
|
||||||
self.__name = value
|
self.__name = value
|
||||||
self.context["title_placeholders"] = {"name": self._name}
|
self.context["title_placeholders"] = {
|
||||||
|
"name": self._async_get_human_readable_name()
|
||||||
|
}
|
||||||
|
|
||||||
async def _async_try_fetch_device_info(self) -> ConfigFlowResult:
|
async def _async_try_fetch_device_info(self) -> ConfigFlowResult:
|
||||||
"""Try to fetch device info and return any errors."""
|
"""Try to fetch device info and return any errors."""
|
||||||
@@ -254,7 +257,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return await self._async_try_fetch_device_info()
|
return await self._async_try_fetch_device_info()
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="discovery_confirm", description_placeholders={"name": self._name}
|
step_id="discovery_confirm",
|
||||||
|
description_placeholders={"name": self._async_get_human_readable_name()},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_zeroconf(
|
async def async_step_zeroconf(
|
||||||
@@ -274,8 +278,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
# Hostname is format: livingroom.local.
|
# Hostname is format: livingroom.local.
|
||||||
device_name = discovery_info.hostname.removesuffix(".local.")
|
device_name = discovery_info.hostname.removesuffix(".local.")
|
||||||
|
|
||||||
self._name = discovery_info.properties.get("friendly_name", device_name)
|
|
||||||
self._device_name = device_name
|
self._device_name = device_name
|
||||||
|
self._name = discovery_info.properties.get("friendly_name", device_name)
|
||||||
self._host = discovery_info.host
|
self._host = discovery_info.host
|
||||||
self._port = discovery_info.port
|
self._port = discovery_info.port
|
||||||
self._noise_required = bool(discovery_info.properties.get("api_encryption"))
|
self._noise_required = bool(discovery_info.properties.get("api_encryption"))
|
||||||
@@ -306,7 +310,32 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
updates[CONF_HOST] = host
|
updates[CONF_HOST] = host
|
||||||
if port is not None:
|
if port is not None:
|
||||||
updates[CONF_PORT] = port
|
updates[CONF_PORT] = port
|
||||||
self._abort_if_unique_id_configured(updates=updates)
|
self._abort_unique_id_configured_with_details(updates=updates)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _abort_unique_id_configured_with_details(self, updates: dict[str, Any]) -> None:
|
||||||
|
"""Abort if unique_id is already configured with details."""
|
||||||
|
assert self.unique_id is not None
|
||||||
|
if not (
|
||||||
|
conflict_entry := self.hass.config_entries.async_entry_for_domain_unique_id(
|
||||||
|
self.handler, self.unique_id
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
assert conflict_entry.unique_id is not None
|
||||||
|
if updates:
|
||||||
|
error = "already_configured_updates"
|
||||||
|
else:
|
||||||
|
error = "already_configured_detailed"
|
||||||
|
self._abort_if_unique_id_configured(
|
||||||
|
updates=updates,
|
||||||
|
error=error,
|
||||||
|
description_placeholders={
|
||||||
|
"title": conflict_entry.title,
|
||||||
|
"name": conflict_entry.data.get(CONF_DEVICE_NAME, "unknown"),
|
||||||
|
"mac": format_mac(conflict_entry.unique_id),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def async_step_mqtt(
|
async def async_step_mqtt(
|
||||||
self, discovery_info: MqttServiceInfo
|
self, discovery_info: MqttServiceInfo
|
||||||
@@ -341,7 +370,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
# Check if already configured
|
# Check if already configured
|
||||||
await self.async_set_unique_id(mac_address)
|
await self.async_set_unique_id(mac_address)
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_unique_id_configured_with_details(
|
||||||
updates={CONF_HOST: self._host, CONF_PORT: self._port}
|
updates={CONF_HOST: self._host, CONF_PORT: self._port}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -479,7 +508,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
data=self._reauth_entry.data | self._async_make_config_data(),
|
data=self._reauth_entry.data | self._async_make_config_data(),
|
||||||
)
|
)
|
||||||
assert self._host is not None
|
assert self._host is not None
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_unique_id_configured_with_details(
|
||||||
updates={
|
updates={
|
||||||
CONF_HOST: self._host,
|
CONF_HOST: self._host,
|
||||||
CONF_PORT: self._port,
|
CONF_PORT: self._port,
|
||||||
@@ -510,7 +539,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
if not (
|
if not (
|
||||||
unique_id_matches := (self.unique_id == self._reconfig_entry.unique_id)
|
unique_id_matches := (self.unique_id == self._reconfig_entry.unique_id)
|
||||||
):
|
):
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_unique_id_configured_with_details(
|
||||||
updates={
|
updates={
|
||||||
CONF_HOST: self._host,
|
CONF_HOST: self._host,
|
||||||
CONF_PORT: self._port,
|
CONF_PORT: self._port,
|
||||||
@@ -568,9 +597,30 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
step_id="encryption_key",
|
step_id="encryption_key",
|
||||||
data_schema=vol.Schema({vol.Required(CONF_NOISE_PSK): str}),
|
data_schema=vol.Schema({vol.Required(CONF_NOISE_PSK): str}),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders={"name": self._name},
|
description_placeholders={"name": self._async_get_human_readable_name()},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_get_human_readable_name(self) -> str:
|
||||||
|
"""Return a human readable name for the entry."""
|
||||||
|
entry: ConfigEntry | None = None
|
||||||
|
if self.source == SOURCE_REAUTH:
|
||||||
|
entry = self._reauth_entry
|
||||||
|
elif self.source == SOURCE_RECONFIGURE:
|
||||||
|
entry = self._reconfig_entry
|
||||||
|
friendly_name = self._name
|
||||||
|
device_name = self._device_name
|
||||||
|
if (
|
||||||
|
device_name
|
||||||
|
and friendly_name in (DEFAULT_NAME, device_name)
|
||||||
|
and entry
|
||||||
|
and entry.title != friendly_name
|
||||||
|
):
|
||||||
|
friendly_name = entry.title
|
||||||
|
if not device_name or friendly_name == device_name:
|
||||||
|
return friendly_name
|
||||||
|
return f"{friendly_name} ({device_name})"
|
||||||
|
|
||||||
async def async_step_authenticate(
|
async def async_step_authenticate(
|
||||||
self, user_input: dict[str, Any] | None = None, error: str | None = None
|
self, user_input: dict[str, Any] | None = None, error: str | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@@ -589,7 +639,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="authenticate",
|
step_id="authenticate",
|
||||||
data_schema=vol.Schema({vol.Required("password"): str}),
|
data_schema=vol.Schema({vol.Required("password"): str}),
|
||||||
description_placeholders={"name": self._name},
|
description_placeholders={"name": self._async_get_human_readable_name()},
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -612,9 +662,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
return ERROR_REQUIRES_ENCRYPTION_KEY
|
return ERROR_REQUIRES_ENCRYPTION_KEY
|
||||||
except InvalidEncryptionKeyAPIError as ex:
|
except InvalidEncryptionKeyAPIError as ex:
|
||||||
if ex.received_name:
|
if ex.received_name:
|
||||||
|
device_name_changed = self._device_name != ex.received_name
|
||||||
self._device_name = ex.received_name
|
self._device_name = ex.received_name
|
||||||
if ex.received_mac:
|
if ex.received_mac:
|
||||||
self._device_mac = format_mac(ex.received_mac)
|
self._device_mac = format_mac(ex.received_mac)
|
||||||
|
if not self._name or device_name_changed:
|
||||||
self._name = ex.received_name
|
self._name = ex.received_name
|
||||||
return ERROR_INVALID_ENCRYPTION_KEY
|
return ERROR_INVALID_ENCRYPTION_KEY
|
||||||
except ResolveAPIError:
|
except ResolveAPIError:
|
||||||
@@ -623,9 +675,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
return "connection_error"
|
return "connection_error"
|
||||||
finally:
|
finally:
|
||||||
await cli.disconnect(force=True)
|
await cli.disconnect(force=True)
|
||||||
self._name = self._device_info.friendly_name or self._device_info.name
|
|
||||||
self._device_name = self._device_info.name
|
|
||||||
self._device_mac = format_mac(self._device_info.mac_address)
|
self._device_mac = format_mac(self._device_info.mac_address)
|
||||||
|
self._device_name = self._device_info.name
|
||||||
|
self._name = self._device_info.friendly_name or self._device_info.name
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def fetch_device_info(self) -> str | None:
|
async def fetch_device_info(self) -> str | None:
|
||||||
@@ -640,7 +692,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
mac_address = format_mac(self._device_info.mac_address)
|
mac_address = format_mac(self._device_info.mac_address)
|
||||||
await self.async_set_unique_id(mac_address, raise_on_progress=False)
|
await self.async_set_unique_id(mac_address, raise_on_progress=False)
|
||||||
if self.source not in (SOURCE_REAUTH, SOURCE_RECONFIGURE):
|
if self.source not in (SOURCE_REAUTH, SOURCE_RECONFIGURE):
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_unique_id_configured_with_details(
|
||||||
updates={
|
updates={
|
||||||
CONF_HOST: self._host,
|
CONF_HOST: self._host,
|
||||||
CONF_PORT: self._port,
|
CONF_PORT: self._port,
|
||||||
|
@@ -224,7 +224,16 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
|||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||||
)
|
)
|
||||||
|
if entity_info.name:
|
||||||
self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}"
|
self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}"
|
||||||
|
else:
|
||||||
|
# https://github.com/home-assistant/core/issues/132532
|
||||||
|
# If name is not set, ESPHome will use the sanitized friendly name
|
||||||
|
# as the name, however we want to use the original object_id
|
||||||
|
# as the entity_id before it is sanitized since the sanitizer
|
||||||
|
# is not utf-8 aware. In this case, its always going to be
|
||||||
|
# an empty string so we drop the object_id.
|
||||||
|
self.entity_id = f"{domain}.{device_info.name}"
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
@@ -260,7 +269,12 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
|||||||
self._static_info = static_info
|
self._static_info = static_info
|
||||||
self._attr_unique_id = build_unique_id(device_info.mac_address, static_info)
|
self._attr_unique_id = build_unique_id(device_info.mac_address, static_info)
|
||||||
self._attr_entity_registry_enabled_default = not static_info.disabled_by_default
|
self._attr_entity_registry_enabled_default = not static_info.disabled_by_default
|
||||||
self._attr_name = static_info.name
|
# https://github.com/home-assistant/core/issues/132532
|
||||||
|
# If the name is "", we need to set it to None since otherwise
|
||||||
|
# the friendly_name will be "{friendly_name} " with a trailing
|
||||||
|
# space. ESPHome uses protobuf under the hood, and an empty field
|
||||||
|
# gets a default value of "".
|
||||||
|
self._attr_name = static_info.name if static_info.name else None
|
||||||
if entity_category := static_info.entity_category:
|
if entity_category := static_info.entity_category:
|
||||||
self._attr_entity_category = ENTITY_CATEGORIES.from_esphome(entity_category)
|
self._attr_entity_category = ENTITY_CATEGORIES.from_esphome(entity_category)
|
||||||
else:
|
else:
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"already_configured_detailed": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`.",
|
||||||
|
"already_configured_updates": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`; the existing configuration will be updated with the validated data.",
|
||||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
"mdns_missing_mac": "Missing MAC address in mDNS properties.",
|
"mdns_missing_mac": "Missing MAC address in mDNS properties.",
|
||||||
@@ -17,10 +19,11 @@
|
|||||||
"reconfigure_unique_id_changed": "**Reconfiguration of `{name}` was aborted** because the address `{host}` points to a different device: `{unexpected_device_name}` (MAC: `{unexpected_mac}`) instead of the expected one (MAC: `{expected_mac}`)."
|
"reconfigure_unique_id_changed": "**Reconfiguration of `{name}` was aborted** because the address `{host}` points to a different device: `{unexpected_device_name}` (MAC: `{unexpected_mac}`) instead of the expected one (MAC: `{expected_mac}`)."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address",
|
"resolve_error": "Unable to resolve the address of the ESPHome device. If this issue continues, consider setting a static IP address.",
|
||||||
"connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.",
|
"connection_error": "Unable to connect to the ESPHome device. Make sure the device’s YAML configuration includes an `api` section.",
|
||||||
|
"requires_encryption_key": "The ESPHome device requires an encryption key. Enter the key defined in the device’s YAML configuration under `api -> encryption -> key`.",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"invalid_psk": "The transport encryption key is invalid. Please ensure it matches what you have in your configuration"
|
"invalid_psk": "The encryption key is invalid. Make sure it matches the value in the device’s YAML configuration under `api -> encryption -> key`."
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
@@ -41,7 +44,7 @@
|
|||||||
"data_description": {
|
"data_description": {
|
||||||
"password": "Passwords are deprecated and will be removed in a future version. Please update your ESPHome device YAML configuration to use an encryption key instead."
|
"password": "Passwords are deprecated and will be removed in a future version. Please update your ESPHome device YAML configuration to use an encryption key instead."
|
||||||
},
|
},
|
||||||
"description": "Please enter the password you set in your ESPHome device YAML configuration for {name}."
|
"description": "Please enter the password you set in your ESPHome device YAML configuration for `{name}`."
|
||||||
},
|
},
|
||||||
"encryption_key": {
|
"encryption_key": {
|
||||||
"data": {
|
"data": {
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
"data_description": {
|
"data_description": {
|
||||||
"noise_psk": "The encryption key is used to encrypt the connection between Home Assistant and the ESPHome device. You can find this in the api: section of your ESPHome device YAML configuration."
|
"noise_psk": "The encryption key is used to encrypt the connection between Home Assistant and the ESPHome device. You can find this in the api: section of your ESPHome device YAML configuration."
|
||||||
},
|
},
|
||||||
"description": "Please enter the encryption key for {name}. You can find it in the ESPHome Dashboard or in your ESPHome device YAML configuration."
|
"description": "Please enter the encryption key for `{name}`. You can find it in the ESPHome Dashboard or in your ESPHome device YAML configuration."
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
"data": {
|
"data": {
|
||||||
@@ -59,10 +62,10 @@
|
|||||||
"data_description": {
|
"data_description": {
|
||||||
"noise_psk": "[%key:component::esphome::config::step::encryption_key::data_description::noise_psk%]"
|
"noise_psk": "[%key:component::esphome::config::step::encryption_key::data_description::noise_psk%]"
|
||||||
},
|
},
|
||||||
"description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your ESPHome device YAML configuration."
|
"description": "The ESPHome device `{name}` enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your ESPHome device YAML configuration."
|
||||||
},
|
},
|
||||||
"reauth_encryption_removed_confirm": {
|
"reauth_encryption_removed_confirm": {
|
||||||
"description": "The ESPHome device {name} disabled transport encryption. Please confirm that you want to remove the encryption key and allow unencrypted connections."
|
"description": "The ESPHome device `{name}` disabled transport encryption. Please confirm that you want to remove the encryption key and allow unencrypted connections."
|
||||||
},
|
},
|
||||||
"discovery_confirm": {
|
"discovery_confirm": {
|
||||||
"description": "Do you want to add the device `{name}` to Home Assistant?",
|
"description": "Do you want to add the device `{name}` to Home Assistant?",
|
||||||
|
@@ -72,7 +72,11 @@ async def get_hosts_list_if_supported(
|
|||||||
supports_hosts: bool = True
|
supports_hosts: bool = True
|
||||||
fbx_devices: list[dict[str, Any]] = []
|
fbx_devices: list[dict[str, Any]] = []
|
||||||
try:
|
try:
|
||||||
fbx_devices = await fbx_api.lan.get_hosts_list() or []
|
fbx_interfaces = await fbx_api.lan.get_interfaces() or []
|
||||||
|
for interface in fbx_interfaces:
|
||||||
|
fbx_devices.extend(
|
||||||
|
await fbx_api.lan.get_hosts_list(interface["name"]) or []
|
||||||
|
)
|
||||||
except HttpRequestError as err:
|
except HttpRequestError as err:
|
||||||
if (
|
if (
|
||||||
(matcher := re.search(r"Request failed \(APIResponse: (.+)\)", str(err)))
|
(matcher := re.search(r"Request failed \(APIResponse: (.+)\)", str(err)))
|
||||||
|
@@ -2,13 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable, ValuesView
|
from collections.abc import Callable, Mapping, ValuesView
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from types import MappingProxyType
|
|
||||||
from typing import Any, TypedDict, cast
|
from typing import Any, TypedDict, cast
|
||||||
|
|
||||||
from fritzconnection import FritzConnection
|
from fritzconnection import FritzConnection
|
||||||
@@ -187,7 +186,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._devices: dict[str, FritzDevice] = {}
|
self._devices: dict[str, FritzDevice] = {}
|
||||||
self._options: MappingProxyType[str, Any] | None = None
|
self._options: Mapping[str, Any] | None = None
|
||||||
self._unique_id: str | None = None
|
self._unique_id: str | None = None
|
||||||
self.connection: FritzConnection = None
|
self.connection: FritzConnection = None
|
||||||
self.fritz_guest_wifi: FritzGuestWLAN = None
|
self.fritz_guest_wifi: FritzGuestWLAN = None
|
||||||
@@ -213,9 +212,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
|||||||
str, Callable[[FritzStatus, StateType], Any]
|
str, Callable[[FritzStatus, StateType], Any]
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
async def async_setup(
|
async def async_setup(self, options: Mapping[str, Any] | None = None) -> None:
|
||||||
self, options: MappingProxyType[str, Any] | None = None
|
|
||||||
) -> None:
|
|
||||||
"""Wrap up FritzboxTools class setup."""
|
"""Wrap up FritzboxTools class setup."""
|
||||||
self._options = options
|
self._options = options
|
||||||
await self.hass.async_add_executor_job(self.setup)
|
await self.hass.async_add_executor_job(self.setup)
|
||||||
|
@@ -53,8 +53,11 @@ MAX_TEMPERATURE = 28
|
|||||||
# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome)
|
# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome)
|
||||||
ON_API_TEMPERATURE = 127.0
|
ON_API_TEMPERATURE = 127.0
|
||||||
OFF_API_TEMPERATURE = 126.5
|
OFF_API_TEMPERATURE = 126.5
|
||||||
ON_REPORT_SET_TEMPERATURE = 30.0
|
PRESET_API_HKR_STATE_MAPPING = {
|
||||||
OFF_REPORT_SET_TEMPERATURE = 0.0
|
PRESET_COMFORT: "comfort",
|
||||||
|
PRESET_BOOST: "on",
|
||||||
|
PRESET_ECO: "eco",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -128,29 +131,28 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
|||||||
return self.data.actual_temperature # type: ignore [no-any-return]
|
return self.data.actual_temperature # type: ignore [no-any-return]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self) -> float:
|
def target_temperature(self) -> float | None:
|
||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
if self.data.target_temperature == ON_API_TEMPERATURE:
|
if self.data.target_temperature in [ON_API_TEMPERATURE, OFF_API_TEMPERATURE]:
|
||||||
return ON_REPORT_SET_TEMPERATURE
|
return None
|
||||||
if self.data.target_temperature == OFF_API_TEMPERATURE:
|
|
||||||
return OFF_REPORT_SET_TEMPERATURE
|
|
||||||
return self.data.target_temperature # type: ignore [no-any-return]
|
return self.data.target_temperature # type: ignore [no-any-return]
|
||||||
|
|
||||||
|
async def async_set_hkr_state(self, hkr_state: str) -> None:
|
||||||
|
"""Set the state of the climate."""
|
||||||
|
await self.hass.async_add_executor_job(self.data.set_hkr_state, hkr_state, True)
|
||||||
|
await self.coordinator.async_refresh()
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is HVACMode.OFF:
|
if kwargs.get(ATTR_HVAC_MODE) is HVACMode.OFF:
|
||||||
await self.async_set_hvac_mode(hvac_mode)
|
await self.async_set_hkr_state("off")
|
||||||
elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||||
if target_temp == OFF_API_TEMPERATURE:
|
|
||||||
target_temp = OFF_REPORT_SET_TEMPERATURE
|
|
||||||
elif target_temp == ON_API_TEMPERATURE:
|
|
||||||
target_temp = ON_REPORT_SET_TEMPERATURE
|
|
||||||
await self.hass.async_add_executor_job(
|
await self.hass.async_add_executor_job(
|
||||||
self.data.set_target_temperature, target_temp, True
|
self.data.set_target_temperature, target_temp, True
|
||||||
)
|
)
|
||||||
|
await self.coordinator.async_refresh()
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
await self.coordinator.async_refresh()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self) -> HVACMode:
|
def hvac_mode(self) -> HVACMode:
|
||||||
@@ -159,10 +161,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
|||||||
return HVACMode.HEAT
|
return HVACMode.HEAT
|
||||||
if self.data.summer_active:
|
if self.data.summer_active:
|
||||||
return HVACMode.OFF
|
return HVACMode.OFF
|
||||||
if self.data.target_temperature in (
|
if self.data.target_temperature == OFF_API_TEMPERATURE:
|
||||||
OFF_REPORT_SET_TEMPERATURE,
|
|
||||||
OFF_API_TEMPERATURE,
|
|
||||||
):
|
|
||||||
return HVACMode.OFF
|
return HVACMode.OFF
|
||||||
|
|
||||||
return HVACMode.HEAT
|
return HVACMode.HEAT
|
||||||
@@ -180,7 +179,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
if hvac_mode is HVACMode.OFF:
|
if hvac_mode is HVACMode.OFF:
|
||||||
await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
|
await self.async_set_hkr_state("off")
|
||||||
else:
|
else:
|
||||||
if value_scheduled_preset(self.data) == PRESET_ECO:
|
if value_scheduled_preset(self.data) == PRESET_ECO:
|
||||||
target_temp = self.data.eco_temperature
|
target_temp = self.data.eco_temperature
|
||||||
@@ -210,12 +209,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
|||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="change_preset_while_active_mode",
|
translation_key="change_preset_while_active_mode",
|
||||||
)
|
)
|
||||||
if preset_mode == PRESET_COMFORT:
|
await self.async_set_hkr_state(PRESET_API_HKR_STATE_MAPPING[preset_mode])
|
||||||
await self.async_set_temperature(temperature=self.data.comfort_temperature)
|
|
||||||
elif preset_mode == PRESET_ECO:
|
|
||||||
await self.async_set_temperature(temperature=self.data.eco_temperature)
|
|
||||||
elif preset_mode == PRESET_BOOST:
|
|
||||||
await self.async_set_temperature(temperature=ON_REPORT_SET_TEMPERATURE)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self) -> ClimateExtraAttributes:
|
def extra_state_attributes(self) -> ClimateExtraAttributes:
|
||||||
|
@@ -184,7 +184,7 @@
|
|||||||
"running": "Running",
|
"running": "Running",
|
||||||
"standby": "[%key:common::state::standby%]",
|
"standby": "[%key:common::state::standby%]",
|
||||||
"bootloading": "Bootloading",
|
"bootloading": "Bootloading",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"idle": "[%key:common::state::idle%]",
|
"idle": "[%key:common::state::idle%]",
|
||||||
"ready": "Ready",
|
"ready": "Ready",
|
||||||
"sleeping": "Sleeping"
|
"sleeping": "Sleeping"
|
||||||
|
@@ -36,7 +36,7 @@
|
|||||||
"name": "Inverter operation mode",
|
"name": "Inverter operation mode",
|
||||||
"state": {
|
"state": {
|
||||||
"general": "General mode",
|
"general": "General mode",
|
||||||
"off_grid": "Off grid mode",
|
"off_grid": "Off-grid mode",
|
||||||
"backup": "Backup mode",
|
"backup": "Backup mode",
|
||||||
"eco": "Eco mode",
|
"eco": "Eco mode",
|
||||||
"peak_shaving": "Peak shaving mode",
|
"peak_shaving": "Peak shaving mode",
|
||||||
|
119
homeassistant/components/google/quality_scale.yaml
Normal file
119
homeassistant/components/google/quality_scale.yaml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
config-flow:
|
||||||
|
status: todo
|
||||||
|
comment: Some fields missing data_description in the option flow.
|
||||||
|
brands: done
|
||||||
|
dependency-transparency:
|
||||||
|
status: todo
|
||||||
|
comment: |
|
||||||
|
This depends on the legacy (deprecated) oauth libraries for device
|
||||||
|
auth (no longer recommended auth). Google publishes to pypi using
|
||||||
|
an internal build system. We need to either revisit approach or
|
||||||
|
revisit our stance on this.
|
||||||
|
common-modules: done
|
||||||
|
has-entity-name: done
|
||||||
|
action-setup:
|
||||||
|
status: todo
|
||||||
|
comment: |
|
||||||
|
Actions are current setup in `async_setup_entry` and need to be moved
|
||||||
|
to `async_setup`.
|
||||||
|
appropriate-polling: done
|
||||||
|
test-before-configure: done
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not subscribe to events.
|
||||||
|
unique-config-entry: done
|
||||||
|
entity-unique-id: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: todo
|
||||||
|
test-before-setup:
|
||||||
|
status: todo
|
||||||
|
comment: |
|
||||||
|
The integration does not test the connection in `async_setup_entry` but
|
||||||
|
instead does this in the calendar platform only, which can be improved.
|
||||||
|
docs-high-level-description: done
|
||||||
|
config-flow-test-coverage:
|
||||||
|
status: todo
|
||||||
|
comment: |
|
||||||
|
The config flow has 100% test coverage, however there are opportunities
|
||||||
|
to increase functionality such as checking for the specific contents
|
||||||
|
of a unique id assigned to a config entry.
|
||||||
|
docs-actions: done
|
||||||
|
runtime-data:
|
||||||
|
status: todo
|
||||||
|
comment: |
|
||||||
|
The integration stores config entry data in `hass.data` and should be
|
||||||
|
updated to use `runtime_data`.
|
||||||
|
|
||||||
|
# Silver
|
||||||
|
log-when-unavailable: done
|
||||||
|
config-entry-unloading: done
|
||||||
|
reauthentication-flow:
|
||||||
|
status: todo
|
||||||
|
comment: |
|
||||||
|
The integration supports reauthentication, however the config flow test
|
||||||
|
coverage can be improved on reauth corner cases.
|
||||||
|
action-exceptions: done
|
||||||
|
docs-installation-parameters: todo
|
||||||
|
integration-owner: done
|
||||||
|
parallel-updates: todo
|
||||||
|
test-coverage:
|
||||||
|
status: todo
|
||||||
|
comment: One module needs an additional line of coverage to be above the bar
|
||||||
|
docs-configuration-parameters: todo
|
||||||
|
entity-unavailable: done
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
docs-examples: done
|
||||||
|
discovery-update-info:
|
||||||
|
status: exempt
|
||||||
|
comment: Google calendar does not support discovery
|
||||||
|
entity-device-class: todo
|
||||||
|
entity-translations: todo
|
||||||
|
docs-data-update: todo
|
||||||
|
entity-disabled-by-default: done
|
||||||
|
discovery:
|
||||||
|
status: exempt
|
||||||
|
comment: Google calendar does not support discovery
|
||||||
|
exception-translations: todo
|
||||||
|
devices: todo
|
||||||
|
docs-supported-devices: done
|
||||||
|
icon-translations:
|
||||||
|
status: exempt
|
||||||
|
comment: Google calendar does not have any icons
|
||||||
|
docs-known-limitations: todo
|
||||||
|
stale-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: Google calendar does not have devices
|
||||||
|
docs-supported-functions: done
|
||||||
|
repair-issues:
|
||||||
|
status: todo
|
||||||
|
comment: There are some warnings/deprecations that should be repair issues
|
||||||
|
reconfiguration-flow:
|
||||||
|
status: exempt
|
||||||
|
comment: There is nothing to configure in the configuration flow
|
||||||
|
entity-category:
|
||||||
|
status: exempt
|
||||||
|
comment: The entities in google calendar do not support categories
|
||||||
|
dynamic-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: Google calendar does not have devices
|
||||||
|
docs-troubleshooting: todo
|
||||||
|
diagnostics: todo
|
||||||
|
docs-use-cases: todo
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency:
|
||||||
|
status: done
|
||||||
|
comment: |
|
||||||
|
The main client `gcal_sync` library is async. The primary authentication
|
||||||
|
used in config flow is handled by built in async OAuth code. The
|
||||||
|
integration still supports legacy OAuth credentials setup in the
|
||||||
|
configuration flow, which is no longer recommended or described in the
|
||||||
|
documentation for new users. This legacy config flow uses oauth2client
|
||||||
|
which is not natively async.
|
||||||
|
strict-typing:
|
||||||
|
status: todo
|
||||||
|
comment: Dependency oauth2client does not confirm to PEP 561
|
||||||
|
inject-websession: done
|
@@ -208,7 +208,7 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
|
|||||||
|
|
||||||
async def google_generative_ai_config_option_schema(
|
async def google_generative_ai_config_option_schema(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
options: dict[str, Any] | MappingProxyType[str, Any],
|
options: Mapping[str, Any],
|
||||||
genai_client: genai.Client,
|
genai_client: genai.Client,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Return a schema for Google Generative AI completion options."""
|
"""Return a schema for Google Generative AI completion options."""
|
||||||
|
@@ -157,9 +157,6 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
|
|||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
if not self.is_on or not kwargs:
|
|
||||||
await self.coordinator.turn_on(self._device)
|
|
||||||
|
|
||||||
if ATTR_BRIGHTNESS in kwargs:
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
brightness: int = int((float(kwargs[ATTR_BRIGHTNESS]) / 255.0) * 100.0)
|
brightness: int = int((float(kwargs[ATTR_BRIGHTNESS]) / 255.0) * 100.0)
|
||||||
await self.coordinator.set_brightness(self._device, brightness)
|
await self.coordinator.set_brightness(self._device, brightness)
|
||||||
@@ -187,6 +184,9 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
|
|||||||
self._save_last_color_state()
|
self._save_last_color_state()
|
||||||
await self.coordinator.set_scene(self._device, effect)
|
await self.coordinator.set_scene(self._device, effect)
|
||||||
|
|
||||||
|
if not self.is_on or not kwargs:
|
||||||
|
await self.coordinator.turn_on(self._device)
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Create Group",
|
"title": "Create group",
|
||||||
"description": "Groups allow you to create a new entity that represents multiple entities of the same type.",
|
"description": "Groups allow you to create a new entity that represents multiple entities of the same type.",
|
||||||
"menu_options": {
|
"menu_options": {
|
||||||
"binary_sensor": "Binary sensor group",
|
"binary_sensor": "Binary sensor group",
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
"round_digits": "Round value to number of decimals",
|
"round_digits": "Round value to number of decimals",
|
||||||
"device_class": "Device class",
|
"device_class": "Device class",
|
||||||
"state_class": "State class",
|
"state_class": "State class",
|
||||||
"unit_of_measurement": "Unit of Measurement"
|
"unit_of_measurement": "Unit of measurement"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
|
@@ -51,7 +51,6 @@ from homeassistant.helpers.hassio import (
|
|||||||
get_supervisor_ip as _get_supervisor_ip,
|
get_supervisor_ip as _get_supervisor_ip,
|
||||||
is_hassio as _is_hassio,
|
is_hassio as _is_hassio,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
|
||||||
from homeassistant.helpers.service_info.hassio import (
|
from homeassistant.helpers.service_info.hassio import (
|
||||||
HassioServiceInfo as _HassioServiceInfo,
|
HassioServiceInfo as _HassioServiceInfo,
|
||||||
)
|
)
|
||||||
@@ -160,7 +159,6 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
SERVICE_ADDON_START = "addon_start"
|
SERVICE_ADDON_START = "addon_start"
|
||||||
SERVICE_ADDON_STOP = "addon_stop"
|
SERVICE_ADDON_STOP = "addon_stop"
|
||||||
SERVICE_ADDON_RESTART = "addon_restart"
|
SERVICE_ADDON_RESTART = "addon_restart"
|
||||||
SERVICE_ADDON_UPDATE = "addon_update"
|
|
||||||
SERVICE_ADDON_STDIN = "addon_stdin"
|
SERVICE_ADDON_STDIN = "addon_stdin"
|
||||||
SERVICE_HOST_SHUTDOWN = "host_shutdown"
|
SERVICE_HOST_SHUTDOWN = "host_shutdown"
|
||||||
SERVICE_HOST_REBOOT = "host_reboot"
|
SERVICE_HOST_REBOOT = "host_reboot"
|
||||||
@@ -241,7 +239,6 @@ MAP_SERVICE_API = {
|
|||||||
SERVICE_ADDON_START: APIEndpointSettings("/addons/{addon}/start", SCHEMA_ADDON),
|
SERVICE_ADDON_START: APIEndpointSettings("/addons/{addon}/start", SCHEMA_ADDON),
|
||||||
SERVICE_ADDON_STOP: APIEndpointSettings("/addons/{addon}/stop", SCHEMA_ADDON),
|
SERVICE_ADDON_STOP: APIEndpointSettings("/addons/{addon}/stop", SCHEMA_ADDON),
|
||||||
SERVICE_ADDON_RESTART: APIEndpointSettings("/addons/{addon}/restart", SCHEMA_ADDON),
|
SERVICE_ADDON_RESTART: APIEndpointSettings("/addons/{addon}/restart", SCHEMA_ADDON),
|
||||||
SERVICE_ADDON_UPDATE: APIEndpointSettings("/addons/{addon}/update", SCHEMA_ADDON),
|
|
||||||
SERVICE_ADDON_STDIN: APIEndpointSettings(
|
SERVICE_ADDON_STDIN: APIEndpointSettings(
|
||||||
"/addons/{addon}/stdin", SCHEMA_ADDON_STDIN
|
"/addons/{addon}/stdin", SCHEMA_ADDON_STDIN
|
||||||
),
|
),
|
||||||
@@ -411,16 +408,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
|||||||
|
|
||||||
async def async_service_handler(service: ServiceCall) -> None:
|
async def async_service_handler(service: ServiceCall) -> None:
|
||||||
"""Handle service calls for Hass.io."""
|
"""Handle service calls for Hass.io."""
|
||||||
if service.service == SERVICE_ADDON_UPDATE:
|
|
||||||
async_create_issue(
|
|
||||||
hass,
|
|
||||||
DOMAIN,
|
|
||||||
"update_service_deprecated",
|
|
||||||
breaks_in_ha_version="2025.5",
|
|
||||||
is_fixable=False,
|
|
||||||
severity=IssueSeverity.WARNING,
|
|
||||||
translation_key="update_service_deprecated",
|
|
||||||
)
|
|
||||||
api_endpoint = MAP_SERVICE_API[service.service]
|
api_endpoint = MAP_SERVICE_API[service.service]
|
||||||
|
|
||||||
data = service.data.copy()
|
data = service.data.copy()
|
||||||
|
@@ -22,9 +22,6 @@
|
|||||||
"addon_stop": {
|
"addon_stop": {
|
||||||
"service": "mdi:stop"
|
"service": "mdi:stop"
|
||||||
},
|
},
|
||||||
"addon_update": {
|
|
||||||
"service": "mdi:update"
|
|
||||||
},
|
|
||||||
"host_reboot": {
|
"host_reboot": {
|
||||||
"service": "mdi:restart"
|
"service": "mdi:restart"
|
||||||
},
|
},
|
||||||
|
@@ -30,14 +30,6 @@ addon_stop:
|
|||||||
selector:
|
selector:
|
||||||
addon:
|
addon:
|
||||||
|
|
||||||
addon_update:
|
|
||||||
fields:
|
|
||||||
addon:
|
|
||||||
required: true
|
|
||||||
example: core_ssh
|
|
||||||
selector:
|
|
||||||
addon:
|
|
||||||
|
|
||||||
host_reboot:
|
host_reboot:
|
||||||
host_shutdown:
|
host_shutdown:
|
||||||
backup_full:
|
backup_full:
|
||||||
|
@@ -225,10 +225,6 @@
|
|||||||
"unsupported_virtualization_image": {
|
"unsupported_virtualization_image": {
|
||||||
"title": "Unsupported system - Incorrect OS image for virtualization",
|
"title": "Unsupported system - Incorrect OS image for virtualization",
|
||||||
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this."
|
||||||
},
|
|
||||||
"update_service_deprecated": {
|
|
||||||
"title": "Deprecated update add-on action",
|
|
||||||
"description": "The update add-on action has been deprecated and will be removed in 2025.5. Please use the update entity and the respective action to update the add-on instead."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
@@ -313,16 +309,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"addon_update": {
|
|
||||||
"name": "Update add-on",
|
|
||||||
"description": "Updates an add-on. This action should be used with caution since add-on updates can contain breaking changes. It is highly recommended that you review release notes/change logs before updating an add-on.",
|
|
||||||
"fields": {
|
|
||||||
"addon": {
|
|
||||||
"name": "[%key:component::hassio::services::addon_start::fields::addon::name%]",
|
|
||||||
"description": "The add-on to update."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"host_reboot": {
|
"host_reboot": {
|
||||||
"name": "Reboot the host system",
|
"name": "Reboot the host system",
|
||||||
"description": "Reboots the host system."
|
"description": "Reboots the host system."
|
||||||
|
@@ -54,7 +54,7 @@ class HistoryStats:
|
|||||||
self._period = (MIN_TIME_UTC, MIN_TIME_UTC)
|
self._period = (MIN_TIME_UTC, MIN_TIME_UTC)
|
||||||
self._state: HistoryStatsState = HistoryStatsState(None, None, self._period)
|
self._state: HistoryStatsState = HistoryStatsState(None, None, self._period)
|
||||||
self._history_current_period: list[HistoryState] = []
|
self._history_current_period: list[HistoryState] = []
|
||||||
self._previous_run_before_start = False
|
self._has_recorder_data = False
|
||||||
self._entity_states = set(entity_states)
|
self._entity_states = set(entity_states)
|
||||||
self._duration = duration
|
self._duration = duration
|
||||||
self._start = start
|
self._start = start
|
||||||
@@ -88,20 +88,20 @@ class HistoryStats:
|
|||||||
if current_period_start_timestamp > now_timestamp:
|
if current_period_start_timestamp > now_timestamp:
|
||||||
# History cannot tell the future
|
# History cannot tell the future
|
||||||
self._history_current_period = []
|
self._history_current_period = []
|
||||||
self._previous_run_before_start = True
|
self._has_recorder_data = False
|
||||||
self._state = HistoryStatsState(None, None, self._period)
|
self._state = HistoryStatsState(None, None, self._period)
|
||||||
return self._state
|
return self._state
|
||||||
#
|
#
|
||||||
# We avoid querying the database if the below did NOT happen:
|
# We avoid querying the database if the below did NOT happen:
|
||||||
#
|
#
|
||||||
# - The previous run happened before the start time
|
# - No previous run occurred (uninitialized)
|
||||||
# - The start time changed
|
# - The start time moved back in time
|
||||||
# - The period shrank in size
|
# - The end time moved back in time
|
||||||
# - The previous period ended before now
|
# - The previous period ended before now
|
||||||
#
|
#
|
||||||
if (
|
if (
|
||||||
not self._previous_run_before_start
|
self._has_recorder_data
|
||||||
and current_period_start_timestamp == previous_period_start_timestamp
|
and current_period_start_timestamp >= previous_period_start_timestamp
|
||||||
and (
|
and (
|
||||||
current_period_end_timestamp == previous_period_end_timestamp
|
current_period_end_timestamp == previous_period_end_timestamp
|
||||||
or (
|
or (
|
||||||
@@ -110,6 +110,12 @@ class HistoryStats:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
|
start_changed = (
|
||||||
|
current_period_start_timestamp != previous_period_start_timestamp
|
||||||
|
)
|
||||||
|
if start_changed:
|
||||||
|
self._prune_history_cache(current_period_start_timestamp)
|
||||||
|
|
||||||
new_data = False
|
new_data = False
|
||||||
if event and (new_state := event.data["new_state"]) is not None:
|
if event and (new_state := event.data["new_state"]) is not None:
|
||||||
if (
|
if (
|
||||||
@@ -121,7 +127,11 @@ class HistoryStats:
|
|||||||
HistoryState(new_state.state, new_state.last_changed_timestamp)
|
HistoryState(new_state.state, new_state.last_changed_timestamp)
|
||||||
)
|
)
|
||||||
new_data = True
|
new_data = True
|
||||||
if not new_data and current_period_end_timestamp < now_timestamp:
|
if (
|
||||||
|
not new_data
|
||||||
|
and current_period_end_timestamp < now_timestamp
|
||||||
|
and not start_changed
|
||||||
|
):
|
||||||
# If period has not changed and current time after the period end...
|
# If period has not changed and current time after the period end...
|
||||||
# Don't compute anything as the value cannot have changed
|
# Don't compute anything as the value cannot have changed
|
||||||
return self._state
|
return self._state
|
||||||
@@ -139,7 +149,7 @@ class HistoryStats:
|
|||||||
HistoryState(new_state.state, new_state.last_changed_timestamp)
|
HistoryState(new_state.state, new_state.last_changed_timestamp)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._previous_run_before_start = False
|
self._has_recorder_data = True
|
||||||
|
|
||||||
seconds_matched, match_count = self._async_compute_seconds_and_changes(
|
seconds_matched, match_count = self._async_compute_seconds_and_changes(
|
||||||
now_timestamp,
|
now_timestamp,
|
||||||
@@ -223,3 +233,18 @@ class HistoryStats:
|
|||||||
# Save value in seconds
|
# Save value in seconds
|
||||||
seconds_matched = elapsed
|
seconds_matched = elapsed
|
||||||
return seconds_matched, match_count
|
return seconds_matched, match_count
|
||||||
|
|
||||||
|
def _prune_history_cache(self, start_timestamp: float) -> None:
|
||||||
|
"""Remove unnecessary old data from the history state cache from previous runs.
|
||||||
|
|
||||||
|
Update the timestamp of the last record from before the start to the current start time.
|
||||||
|
"""
|
||||||
|
trim_count = 0
|
||||||
|
for i, history_state in enumerate(self._history_current_period):
|
||||||
|
if history_state.last_changed >= start_timestamp:
|
||||||
|
break
|
||||||
|
history_state.last_changed = start_timestamp
|
||||||
|
if i > 0:
|
||||||
|
trim_count += 1
|
||||||
|
if trim_count: # Don't slice if no data was removed
|
||||||
|
self._history_current_period = self._history_current_period[trim_count:]
|
||||||
|
@@ -47,8 +47,8 @@ from .utils import get_dict_from_home_connect_error
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAX_EXECUTIONS_TIME_WINDOW = 15 * 60 # 15 minutes
|
MAX_EXECUTIONS_TIME_WINDOW = 60 * 60 # 1 hour
|
||||||
MAX_EXECUTIONS = 5
|
MAX_EXECUTIONS = 8
|
||||||
|
|
||||||
type HomeConnectConfigEntry = ConfigEntry[HomeConnectCoordinator]
|
type HomeConnectConfigEntry = ConfigEntry[HomeConnectCoordinator]
|
||||||
|
|
||||||
|
@@ -1536,7 +1536,7 @@
|
|||||||
"pause": "[%key:common::state::paused%]",
|
"pause": "[%key:common::state::paused%]",
|
||||||
"actionrequired": "Action required",
|
"actionrequired": "Action required",
|
||||||
"finished": "Finished",
|
"finished": "Finished",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"aborting": "Aborting"
|
"aborting": "Aborting"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1587,7 +1587,7 @@
|
|||||||
"streaminglocal": "Streaming local",
|
"streaminglocal": "Streaming local",
|
||||||
"streamingcloud": "Streaming cloud",
|
"streamingcloud": "Streaming cloud",
|
||||||
"streaminglocal_and_cloud": "Streaming local and cloud",
|
"streaminglocal_and_cloud": "Streaming local and cloud",
|
||||||
"error": "Error"
|
"error": "[%key:common::state::error%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"last_selected_map": {
|
"last_selected_map": {
|
||||||
|
@@ -61,7 +61,7 @@ reload_config_entry:
|
|||||||
required: false
|
required: false
|
||||||
example: 8955375327824e14ba89e4b29cc3ec9a
|
example: 8955375327824e14ba89e4b29cc3ec9a
|
||||||
selector:
|
selector:
|
||||||
text:
|
config_entry:
|
||||||
|
|
||||||
save_persistent_states:
|
save_persistent_states:
|
||||||
|
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
"""The Homee number platform."""
|
"""The Homee number platform."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from pyHomee.const import AttributeType
|
from pyHomee.const import AttributeType
|
||||||
from pyHomee.model import HomeeAttribute
|
from pyHomee.model import HomeeAttribute
|
||||||
|
|
||||||
@@ -8,7 +11,7 @@ from homeassistant.components.number import (
|
|||||||
NumberEntity,
|
NumberEntity,
|
||||||
NumberEntityDescription,
|
NumberEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory, UnitOfSpeed
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
@@ -18,69 +21,89 @@ from .entity import HomeeEntity
|
|||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class HomeeNumberEntityDescription(NumberEntityDescription):
|
||||||
|
"""A class that describes Homee number entities."""
|
||||||
|
|
||||||
|
native_value_fn: Callable[[float], float] = lambda value: value
|
||||||
|
set_native_value_fn: Callable[[float], float] = lambda value: value
|
||||||
|
|
||||||
|
|
||||||
NUMBER_DESCRIPTIONS = {
|
NUMBER_DESCRIPTIONS = {
|
||||||
AttributeType.DOWN_POSITION: NumberEntityDescription(
|
AttributeType.DOWN_POSITION: HomeeNumberEntityDescription(
|
||||||
key="down_position",
|
key="down_position",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.DOWN_SLAT_POSITION: NumberEntityDescription(
|
AttributeType.DOWN_SLAT_POSITION: HomeeNumberEntityDescription(
|
||||||
key="down_slat_position",
|
key="down_slat_position",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.DOWN_TIME: NumberEntityDescription(
|
AttributeType.DOWN_TIME: HomeeNumberEntityDescription(
|
||||||
key="down_time",
|
key="down_time",
|
||||||
device_class=NumberDeviceClass.DURATION,
|
device_class=NumberDeviceClass.DURATION,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.ENDPOSITION_CONFIGURATION: NumberEntityDescription(
|
AttributeType.ENDPOSITION_CONFIGURATION: HomeeNumberEntityDescription(
|
||||||
key="endposition_configuration",
|
key="endposition_configuration",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.MOTION_ALARM_CANCELATION_DELAY: NumberEntityDescription(
|
AttributeType.MOTION_ALARM_CANCELATION_DELAY: HomeeNumberEntityDescription(
|
||||||
key="motion_alarm_cancelation_delay",
|
key="motion_alarm_cancelation_delay",
|
||||||
device_class=NumberDeviceClass.DURATION,
|
device_class=NumberDeviceClass.DURATION,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.OPEN_WINDOW_DETECTION_SENSIBILITY: NumberEntityDescription(
|
AttributeType.OPEN_WINDOW_DETECTION_SENSIBILITY: HomeeNumberEntityDescription(
|
||||||
key="open_window_detection_sensibility",
|
key="open_window_detection_sensibility",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.POLLING_INTERVAL: NumberEntityDescription(
|
AttributeType.POLLING_INTERVAL: HomeeNumberEntityDescription(
|
||||||
key="polling_interval",
|
key="polling_interval",
|
||||||
device_class=NumberDeviceClass.DURATION,
|
device_class=NumberDeviceClass.DURATION,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.SHUTTER_SLAT_TIME: NumberEntityDescription(
|
AttributeType.SHUTTER_SLAT_TIME: HomeeNumberEntityDescription(
|
||||||
key="shutter_slat_time",
|
key="shutter_slat_time",
|
||||||
device_class=NumberDeviceClass.DURATION,
|
device_class=NumberDeviceClass.DURATION,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.SLAT_MAX_ANGLE: NumberEntityDescription(
|
AttributeType.SLAT_MAX_ANGLE: HomeeNumberEntityDescription(
|
||||||
key="slat_max_angle",
|
key="slat_max_angle",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.SLAT_MIN_ANGLE: NumberEntityDescription(
|
AttributeType.SLAT_MIN_ANGLE: HomeeNumberEntityDescription(
|
||||||
key="slat_min_angle",
|
key="slat_min_angle",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.SLAT_STEPS: NumberEntityDescription(
|
AttributeType.SLAT_STEPS: HomeeNumberEntityDescription(
|
||||||
key="slat_steps",
|
key="slat_steps",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.TEMPERATURE_OFFSET: NumberEntityDescription(
|
AttributeType.TEMPERATURE_OFFSET: HomeeNumberEntityDescription(
|
||||||
key="temperature_offset",
|
key="temperature_offset",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.UP_TIME: NumberEntityDescription(
|
AttributeType.UP_TIME: HomeeNumberEntityDescription(
|
||||||
key="up_time",
|
key="up_time",
|
||||||
device_class=NumberDeviceClass.DURATION,
|
device_class=NumberDeviceClass.DURATION,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
AttributeType.WAKE_UP_INTERVAL: NumberEntityDescription(
|
AttributeType.WAKE_UP_INTERVAL: HomeeNumberEntityDescription(
|
||||||
key="wake_up_interval",
|
key="wake_up_interval",
|
||||||
device_class=NumberDeviceClass.DURATION,
|
device_class=NumberDeviceClass.DURATION,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
|
AttributeType.WIND_MONITORING_STATE: HomeeNumberEntityDescription(
|
||||||
|
key="wind_monitoring_state",
|
||||||
|
device_class=NumberDeviceClass.WIND_SPEED,
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
native_min_value=0,
|
||||||
|
native_max_value=22.5,
|
||||||
|
native_step=2.5,
|
||||||
|
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
|
||||||
|
native_value_fn=lambda value: value * 2.5,
|
||||||
|
set_native_value_fn=lambda value: value / 2.5,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -102,20 +125,25 @@ async def async_setup_entry(
|
|||||||
class HomeeNumber(HomeeEntity, NumberEntity):
|
class HomeeNumber(HomeeEntity, NumberEntity):
|
||||||
"""Representation of a Homee number."""
|
"""Representation of a Homee number."""
|
||||||
|
|
||||||
|
entity_description: HomeeNumberEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
attribute: HomeeAttribute,
|
attribute: HomeeAttribute,
|
||||||
entry: HomeeConfigEntry,
|
entry: HomeeConfigEntry,
|
||||||
description: NumberEntityDescription,
|
description: HomeeNumberEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a Homee number entity."""
|
"""Initialize a Homee number entity."""
|
||||||
super().__init__(attribute, entry)
|
super().__init__(attribute, entry)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_translation_key = description.key
|
self._attr_translation_key = description.key
|
||||||
self._attr_native_unit_of_measurement = HOMEE_UNIT_TO_HA_UNIT[attribute.unit]
|
self._attr_native_unit_of_measurement = (
|
||||||
self._attr_native_min_value = attribute.minimum
|
description.native_unit_of_measurement
|
||||||
self._attr_native_max_value = attribute.maximum
|
or HOMEE_UNIT_TO_HA_UNIT[attribute.unit]
|
||||||
self._attr_native_step = attribute.step_value
|
)
|
||||||
|
self._attr_native_min_value = description.native_min_value or attribute.minimum
|
||||||
|
self._attr_native_max_value = description.native_max_value or attribute.maximum
|
||||||
|
self._attr_native_step = description.native_step or attribute.step_value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
@@ -123,10 +151,12 @@ class HomeeNumber(HomeeEntity, NumberEntity):
|
|||||||
return super().available and self._attribute.editable
|
return super().available and self._attribute.editable
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> int:
|
def native_value(self) -> float | None:
|
||||||
"""Return the native value of the number."""
|
"""Return the native value of the number."""
|
||||||
return int(self._attribute.current_value)
|
return self.entity_description.native_value_fn(self._attribute.current_value)
|
||||||
|
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
"""Set the selected value."""
|
"""Set the selected value."""
|
||||||
await self.async_set_homee_value(value)
|
await self.async_set_homee_value(
|
||||||
|
self.entity_description.set_native_value_fn(value)
|
||||||
|
)
|
||||||
|
@@ -189,6 +189,9 @@
|
|||||||
},
|
},
|
||||||
"wake_up_interval": {
|
"wake_up_interval": {
|
||||||
"name": "Wake-up interval"
|
"name": "Wake-up interval"
|
||||||
|
},
|
||||||
|
"wind_monitoring_state": {
|
||||||
|
"name": "Threshold for wind trigger"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"select": {
|
"select": {
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
"loggers": ["pyhap"],
|
"loggers": ["pyhap"],
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"HAP-python==4.9.2",
|
"HAP-python==4.9.2",
|
||||||
"fnv-hash-fast==1.4.0",
|
"fnv-hash-fast==1.5.0",
|
||||||
"PyQRCode==1.2.1",
|
"PyQRCode==1.2.1",
|
||||||
"base36==0.1.1"
|
"base36==0.1.1"
|
||||||
],
|
],
|
||||||
|
@@ -14,6 +14,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aiohomekit", "commentjson"],
|
"loggers": ["aiohomekit", "commentjson"],
|
||||||
"requirements": ["aiohomekit==3.2.13"],
|
"requirements": ["aiohomekit==3.2.14"],
|
||||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -34,7 +34,7 @@
|
|||||||
},
|
},
|
||||||
"select": {
|
"select": {
|
||||||
"preferred_network_mode": {
|
"preferred_network_mode": {
|
||||||
"default": "mdi:transmission-tower"
|
"default": "mdi:antenna"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["huawei_lte_api.Session"],
|
"loggers": ["huawei_lte_api.Session"],
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"huawei-lte-api==1.10.0",
|
"huawei-lte-api==1.11.0",
|
||||||
"stringcase==1.2.0",
|
"stringcase==1.2.0",
|
||||||
"url-normalize==2.2.0"
|
"url-normalize==2.2.0"
|
||||||
],
|
],
|
||||||
|
@@ -181,7 +181,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
|||||||
"cell_id": HuaweiSensorEntityDescription(
|
"cell_id": HuaweiSensorEntityDescription(
|
||||||
key="cell_id",
|
key="cell_id",
|
||||||
translation_key="cell_id",
|
translation_key="cell_id",
|
||||||
icon="mdi:transmission-tower",
|
icon="mdi:antenna",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
"cqi0": HuaweiSensorEntityDescription(
|
"cqi0": HuaweiSensorEntityDescription(
|
||||||
@@ -230,6 +230,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
|||||||
"enodeb_id": HuaweiSensorEntityDescription(
|
"enodeb_id": HuaweiSensorEntityDescription(
|
||||||
key="enodeb_id",
|
key="enodeb_id",
|
||||||
translation_key="enodeb_id",
|
translation_key="enodeb_id",
|
||||||
|
icon="mdi:antenna",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
"lac": HuaweiSensorEntityDescription(
|
"lac": HuaweiSensorEntityDescription(
|
||||||
@@ -364,7 +365,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
|||||||
"pci": HuaweiSensorEntityDescription(
|
"pci": HuaweiSensorEntityDescription(
|
||||||
key="pci",
|
key="pci",
|
||||||
translation_key="pci",
|
translation_key="pci",
|
||||||
icon="mdi:transmission-tower",
|
icon="mdi:antenna",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
"plmn": HuaweiSensorEntityDescription(
|
"plmn": HuaweiSensorEntityDescription(
|
||||||
|
@@ -26,6 +26,10 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]"
|
"username": "[%key:common::config_flow::data::username%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"password": "[%key:component::huawei_lte::config::step::user::data_description::password%]",
|
||||||
|
"username": "[%key:component::huawei_lte::config::step::user::data_description::username%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
@@ -35,6 +39,12 @@
|
|||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||||
},
|
},
|
||||||
|
"data_description": {
|
||||||
|
"password": "Password for accessing the router's API. Typically, the same as the one used for the router's web interface.",
|
||||||
|
"url": "Base URL to the API of the router. Typically, something like `http://192.168.X.1`. This is the beginning of the location shown in a browser when accessing the router's web interface.",
|
||||||
|
"username": "Username for accessing the router's API. Typically, the same as the one used for the router's web interface. Usually, either `admin`, or left empty (recommended if that works).",
|
||||||
|
"verify_ssl": "Whether to verify the SSL certificate of the router when accessing it. Applicable only if the router is accessed via HTTPS."
|
||||||
|
},
|
||||||
"description": "Enter device access details.",
|
"description": "Enter device access details.",
|
||||||
"title": "Configure Huawei LTE"
|
"title": "Configure Huawei LTE"
|
||||||
}
|
}
|
||||||
@@ -48,6 +58,12 @@
|
|||||||
"recipient": "SMS notification recipients",
|
"recipient": "SMS notification recipients",
|
||||||
"track_wired_clients": "Track wired network clients",
|
"track_wired_clients": "Track wired network clients",
|
||||||
"unauthenticated_mode": "Unauthenticated mode (change requires reload)"
|
"unauthenticated_mode": "Unauthenticated mode (change requires reload)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"name": "Used to distinguish between notification services in case there are multiple Huawei LTE devices configured. Changes to this option value take effect after Home Assistant restart.",
|
||||||
|
"recipient": "Comma separated list of default recipient SMS phone numbers for the notification service, used in case the notification sender does not specify any.",
|
||||||
|
"track_wired_clients": "Whether the device tracker entities track also clients attached to the router's wired Ethernet network, in addition to wireless clients.",
|
||||||
|
"unauthenticated_mode": "Whether to run in unauthenticated mode. Unauthenticated mode provides a limited set of features, but may help in case there are problems accessing the router's web interface from a browser while the integration is active. Changes to this option value take effect after integration reload."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -57,7 +57,7 @@
|
|||||||
"3": "[%key:component::hue::device_automation::trigger_subtype::button_3%]",
|
"3": "[%key:component::hue::device_automation::trigger_subtype::button_3%]",
|
||||||
"4": "[%key:component::hue::device_automation::trigger_subtype::button_4%]",
|
"4": "[%key:component::hue::device_automation::trigger_subtype::button_4%]",
|
||||||
"clock_wise": "Rotation clockwise",
|
"clock_wise": "Rotation clockwise",
|
||||||
"counter_clock_wise": "Rotation counter-clockwise"
|
"counter_clock_wise": "Rotation counterclockwise"
|
||||||
},
|
},
|
||||||
"trigger_type": {
|
"trigger_type": {
|
||||||
"remote_button_long_release": "\"{subtype}\" released after long press",
|
"remote_button_long_release": "\"{subtype}\" released after long press",
|
||||||
|
@@ -61,6 +61,15 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
|||||||
self._zones_last_update: dict[str, set[str]] = {}
|
self._zones_last_update: dict[str, set[str]] = {}
|
||||||
self._areas_last_update: dict[str, set[int]] = {}
|
self._areas_last_update: dict[str, set[int]] = {}
|
||||||
|
|
||||||
|
def _async_add_remove_devices_and_entities(self, data: MowerDictionary) -> None:
|
||||||
|
"""Add/remove devices and dynamic entities, when amount of devices changed."""
|
||||||
|
self._async_add_remove_devices(data)
|
||||||
|
for mower_id in data:
|
||||||
|
if data[mower_id].capabilities.stay_out_zones:
|
||||||
|
self._async_add_remove_stay_out_zones(data)
|
||||||
|
if data[mower_id].capabilities.work_areas:
|
||||||
|
self._async_add_remove_work_areas(data)
|
||||||
|
|
||||||
async def _async_update_data(self) -> MowerDictionary:
|
async def _async_update_data(self) -> MowerDictionary:
|
||||||
"""Subscribe for websocket and poll data from the API."""
|
"""Subscribe for websocket and poll data from the API."""
|
||||||
if not self.ws_connected:
|
if not self.ws_connected:
|
||||||
@@ -73,20 +82,14 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
|||||||
raise UpdateFailed(err) from err
|
raise UpdateFailed(err) from err
|
||||||
except AuthError as err:
|
except AuthError as err:
|
||||||
raise ConfigEntryAuthFailed(err) from err
|
raise ConfigEntryAuthFailed(err) from err
|
||||||
|
self._async_add_remove_devices_and_entities(data)
|
||||||
self._async_add_remove_devices(data)
|
|
||||||
for mower_id in data:
|
|
||||||
if data[mower_id].capabilities.stay_out_zones:
|
|
||||||
self._async_add_remove_stay_out_zones(data)
|
|
||||||
for mower_id in data:
|
|
||||||
if data[mower_id].capabilities.work_areas:
|
|
||||||
self._async_add_remove_work_areas(data)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def callback(self, ws_data: MowerDictionary) -> None:
|
def callback(self, ws_data: MowerDictionary) -> None:
|
||||||
"""Process websocket callbacks and write them to the DataUpdateCoordinator."""
|
"""Process websocket callbacks and write them to the DataUpdateCoordinator."""
|
||||||
self.async_set_updated_data(ws_data)
|
self.async_set_updated_data(ws_data)
|
||||||
|
self._async_add_remove_devices_and_entities(ws_data)
|
||||||
|
|
||||||
async def client_listen(
|
async def client_listen(
|
||||||
self,
|
self,
|
||||||
|
@@ -40,8 +40,7 @@ PARALLEL_UPDATES = 0
|
|||||||
|
|
||||||
ATTR_WORK_AREA_ID_ASSIGNMENT = "work_area_id_assignment"
|
ATTR_WORK_AREA_ID_ASSIGNMENT = "work_area_id_assignment"
|
||||||
|
|
||||||
ERROR_KEY_LIST = [
|
ERROR_KEYS = [
|
||||||
"no_error",
|
|
||||||
"alarm_mower_in_motion",
|
"alarm_mower_in_motion",
|
||||||
"alarm_mower_lifted",
|
"alarm_mower_lifted",
|
||||||
"alarm_mower_stopped",
|
"alarm_mower_stopped",
|
||||||
@@ -50,13 +49,11 @@ ERROR_KEY_LIST = [
|
|||||||
"alarm_outside_geofence",
|
"alarm_outside_geofence",
|
||||||
"angular_sensor_problem",
|
"angular_sensor_problem",
|
||||||
"battery_problem",
|
"battery_problem",
|
||||||
"battery_problem",
|
|
||||||
"battery_restriction_due_to_ambient_temperature",
|
"battery_restriction_due_to_ambient_temperature",
|
||||||
"can_error",
|
"can_error",
|
||||||
"charging_current_too_high",
|
"charging_current_too_high",
|
||||||
"charging_station_blocked",
|
"charging_station_blocked",
|
||||||
"charging_system_problem",
|
"charging_system_problem",
|
||||||
"charging_system_problem",
|
|
||||||
"collision_sensor_defect",
|
"collision_sensor_defect",
|
||||||
"collision_sensor_error",
|
"collision_sensor_error",
|
||||||
"collision_sensor_problem_front",
|
"collision_sensor_problem_front",
|
||||||
@@ -67,24 +64,18 @@ ERROR_KEY_LIST = [
|
|||||||
"connection_changed",
|
"connection_changed",
|
||||||
"connection_not_changed",
|
"connection_not_changed",
|
||||||
"connectivity_problem",
|
"connectivity_problem",
|
||||||
"connectivity_problem",
|
|
||||||
"connectivity_problem",
|
|
||||||
"connectivity_problem",
|
|
||||||
"connectivity_problem",
|
|
||||||
"connectivity_problem",
|
|
||||||
"connectivity_settings_restored",
|
"connectivity_settings_restored",
|
||||||
"cutting_drive_motor_1_defect",
|
"cutting_drive_motor_1_defect",
|
||||||
"cutting_drive_motor_2_defect",
|
"cutting_drive_motor_2_defect",
|
||||||
"cutting_drive_motor_3_defect",
|
"cutting_drive_motor_3_defect",
|
||||||
"cutting_height_blocked",
|
"cutting_height_blocked",
|
||||||
"cutting_height_problem",
|
|
||||||
"cutting_height_problem_curr",
|
"cutting_height_problem_curr",
|
||||||
"cutting_height_problem_dir",
|
"cutting_height_problem_dir",
|
||||||
"cutting_height_problem_drive",
|
"cutting_height_problem_drive",
|
||||||
|
"cutting_height_problem",
|
||||||
"cutting_motor_problem",
|
"cutting_motor_problem",
|
||||||
"cutting_stopped_slope_too_steep",
|
"cutting_stopped_slope_too_steep",
|
||||||
"cutting_system_blocked",
|
"cutting_system_blocked",
|
||||||
"cutting_system_blocked",
|
|
||||||
"cutting_system_imbalance_warning",
|
"cutting_system_imbalance_warning",
|
||||||
"cutting_system_major_imbalance",
|
"cutting_system_major_imbalance",
|
||||||
"destination_not_reachable",
|
"destination_not_reachable",
|
||||||
@@ -92,13 +83,9 @@ ERROR_KEY_LIST = [
|
|||||||
"docking_sensor_defect",
|
"docking_sensor_defect",
|
||||||
"electronic_problem",
|
"electronic_problem",
|
||||||
"empty_battery",
|
"empty_battery",
|
||||||
MowerStates.ERROR.lower(),
|
|
||||||
MowerStates.ERROR_AT_POWER_UP.lower(),
|
|
||||||
MowerStates.FATAL_ERROR.lower(),
|
|
||||||
"folding_cutting_deck_sensor_defect",
|
"folding_cutting_deck_sensor_defect",
|
||||||
"folding_sensor_activated",
|
"folding_sensor_activated",
|
||||||
"geofence_problem",
|
"geofence_problem",
|
||||||
"geofence_problem",
|
|
||||||
"gps_navigation_problem",
|
"gps_navigation_problem",
|
||||||
"guide_1_not_found",
|
"guide_1_not_found",
|
||||||
"guide_2_not_found",
|
"guide_2_not_found",
|
||||||
@@ -116,7 +103,6 @@ ERROR_KEY_LIST = [
|
|||||||
"lift_sensor_defect",
|
"lift_sensor_defect",
|
||||||
"lifted",
|
"lifted",
|
||||||
"limited_cutting_height_range",
|
"limited_cutting_height_range",
|
||||||
"limited_cutting_height_range",
|
|
||||||
"loop_sensor_defect",
|
"loop_sensor_defect",
|
||||||
"loop_sensor_problem_front",
|
"loop_sensor_problem_front",
|
||||||
"loop_sensor_problem_left",
|
"loop_sensor_problem_left",
|
||||||
@@ -129,6 +115,7 @@ ERROR_KEY_LIST = [
|
|||||||
"no_accurate_position_from_satellites",
|
"no_accurate_position_from_satellites",
|
||||||
"no_confirmed_position",
|
"no_confirmed_position",
|
||||||
"no_drive",
|
"no_drive",
|
||||||
|
"no_error",
|
||||||
"no_loop_signal",
|
"no_loop_signal",
|
||||||
"no_power_in_charging_station",
|
"no_power_in_charging_station",
|
||||||
"no_response_from_charger",
|
"no_response_from_charger",
|
||||||
@@ -139,9 +126,6 @@ ERROR_KEY_LIST = [
|
|||||||
"safety_function_faulty",
|
"safety_function_faulty",
|
||||||
"settings_restored",
|
"settings_restored",
|
||||||
"sim_card_locked",
|
"sim_card_locked",
|
||||||
"sim_card_locked",
|
|
||||||
"sim_card_locked",
|
|
||||||
"sim_card_locked",
|
|
||||||
"sim_card_not_found",
|
"sim_card_not_found",
|
||||||
"sim_card_requires_pin",
|
"sim_card_requires_pin",
|
||||||
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern",
|
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern",
|
||||||
@@ -151,13 +135,6 @@ ERROR_KEY_LIST = [
|
|||||||
"stuck_in_charging_station",
|
"stuck_in_charging_station",
|
||||||
"switch_cord_problem",
|
"switch_cord_problem",
|
||||||
"temporary_battery_problem",
|
"temporary_battery_problem",
|
||||||
"temporary_battery_problem",
|
|
||||||
"temporary_battery_problem",
|
|
||||||
"temporary_battery_problem",
|
|
||||||
"temporary_battery_problem",
|
|
||||||
"temporary_battery_problem",
|
|
||||||
"temporary_battery_problem",
|
|
||||||
"temporary_battery_problem",
|
|
||||||
"tilt_sensor_problem",
|
"tilt_sensor_problem",
|
||||||
"too_high_discharge_current",
|
"too_high_discharge_current",
|
||||||
"too_high_internal_current",
|
"too_high_internal_current",
|
||||||
@@ -189,11 +166,19 @@ ERROR_KEY_LIST = [
|
|||||||
"zone_generator_problem",
|
"zone_generator_problem",
|
||||||
]
|
]
|
||||||
|
|
||||||
ERROR_STATES = {
|
ERROR_STATES = [
|
||||||
MowerStates.ERROR,
|
|
||||||
MowerStates.ERROR_AT_POWER_UP,
|
MowerStates.ERROR_AT_POWER_UP,
|
||||||
|
MowerStates.ERROR,
|
||||||
MowerStates.FATAL_ERROR,
|
MowerStates.FATAL_ERROR,
|
||||||
}
|
MowerStates.OFF,
|
||||||
|
MowerStates.STOPPED,
|
||||||
|
MowerStates.WAIT_POWER_UP,
|
||||||
|
MowerStates.WAIT_UPDATING,
|
||||||
|
]
|
||||||
|
|
||||||
|
ERROR_KEY_LIST = list(
|
||||||
|
dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES])
|
||||||
|
)
|
||||||
|
|
||||||
RESTRICTED_REASONS: list = [
|
RESTRICTED_REASONS: list = [
|
||||||
RestrictedReasons.ALL_WORK_AREAS_COMPLETED,
|
RestrictedReasons.ALL_WORK_AREAS_COMPLETED,
|
||||||
@@ -292,6 +277,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
key="cutting_blade_usage_time",
|
key="cutting_blade_usage_time",
|
||||||
translation_key="cutting_blade_usage_time",
|
translation_key="cutting_blade_usage_time",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
device_class=SensorDeviceClass.DURATION,
|
device_class=SensorDeviceClass.DURATION,
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
@@ -302,6 +288,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
key="downtime",
|
key="downtime",
|
||||||
translation_key="downtime",
|
translation_key="downtime",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
device_class=SensorDeviceClass.DURATION,
|
device_class=SensorDeviceClass.DURATION,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@@ -386,6 +373,7 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
key="uptime",
|
key="uptime",
|
||||||
translation_key="uptime",
|
translation_key="uptime",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
device_class=SensorDeviceClass.DURATION,
|
device_class=SensorDeviceClass.DURATION,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
|
@@ -106,10 +106,10 @@
|
|||||||
"cutting_drive_motor_2_defect": "Cutting drive motor 2 defect",
|
"cutting_drive_motor_2_defect": "Cutting drive motor 2 defect",
|
||||||
"cutting_drive_motor_3_defect": "Cutting drive motor 3 defect",
|
"cutting_drive_motor_3_defect": "Cutting drive motor 3 defect",
|
||||||
"cutting_height_blocked": "Cutting height blocked",
|
"cutting_height_blocked": "Cutting height blocked",
|
||||||
"cutting_height_problem": "Cutting height problem",
|
|
||||||
"cutting_height_problem_curr": "Cutting height problem, curr",
|
"cutting_height_problem_curr": "Cutting height problem, curr",
|
||||||
"cutting_height_problem_dir": "Cutting height problem, dir",
|
"cutting_height_problem_dir": "Cutting height problem, dir",
|
||||||
"cutting_height_problem_drive": "Cutting height problem, drive",
|
"cutting_height_problem_drive": "Cutting height problem, drive",
|
||||||
|
"cutting_height_problem": "Cutting height problem",
|
||||||
"cutting_motor_problem": "Cutting motor problem",
|
"cutting_motor_problem": "Cutting motor problem",
|
||||||
"cutting_stopped_slope_too_steep": "Cutting stopped - slope too steep",
|
"cutting_stopped_slope_too_steep": "Cutting stopped - slope too steep",
|
||||||
"cutting_system_blocked": "Cutting system blocked",
|
"cutting_system_blocked": "Cutting system blocked",
|
||||||
@@ -120,8 +120,8 @@
|
|||||||
"docking_sensor_defect": "Docking sensor defect",
|
"docking_sensor_defect": "Docking sensor defect",
|
||||||
"electronic_problem": "Electronic problem",
|
"electronic_problem": "Electronic problem",
|
||||||
"empty_battery": "Empty battery",
|
"empty_battery": "Empty battery",
|
||||||
"error": "Error",
|
|
||||||
"error_at_power_up": "Error at power up",
|
"error_at_power_up": "Error at power up",
|
||||||
|
"error": "[%key:common::state::error%]",
|
||||||
"fatal_error": "Fatal error",
|
"fatal_error": "Fatal error",
|
||||||
"folding_cutting_deck_sensor_defect": "Folding cutting deck sensor defect",
|
"folding_cutting_deck_sensor_defect": "Folding cutting deck sensor defect",
|
||||||
"folding_sensor_activated": "Folding sensor activated",
|
"folding_sensor_activated": "Folding sensor activated",
|
||||||
@@ -159,6 +159,7 @@
|
|||||||
"no_loop_signal": "No loop signal",
|
"no_loop_signal": "No loop signal",
|
||||||
"no_power_in_charging_station": "No power in charging station",
|
"no_power_in_charging_station": "No power in charging station",
|
||||||
"no_response_from_charger": "No response from charger",
|
"no_response_from_charger": "No response from charger",
|
||||||
|
"off": "[%key:common::state::off%]",
|
||||||
"outside_working_area": "Outside working area",
|
"outside_working_area": "Outside working area",
|
||||||
"poor_signal_quality": "Poor signal quality",
|
"poor_signal_quality": "Poor signal quality",
|
||||||
"reference_station_communication_problem": "Reference station communication problem",
|
"reference_station_communication_problem": "Reference station communication problem",
|
||||||
@@ -172,6 +173,7 @@
|
|||||||
"slope_too_steep": "Slope too steep",
|
"slope_too_steep": "Slope too steep",
|
||||||
"sms_could_not_be_sent": "SMS could not be sent",
|
"sms_could_not_be_sent": "SMS could not be sent",
|
||||||
"stop_button_problem": "STOP button problem",
|
"stop_button_problem": "STOP button problem",
|
||||||
|
"stopped": "[%key:common::state::stopped%]",
|
||||||
"stuck_in_charging_station": "Stuck in charging station",
|
"stuck_in_charging_station": "Stuck in charging station",
|
||||||
"switch_cord_problem": "Switch cord problem",
|
"switch_cord_problem": "Switch cord problem",
|
||||||
"temporary_battery_problem": "Temporary battery problem",
|
"temporary_battery_problem": "Temporary battery problem",
|
||||||
@@ -187,6 +189,8 @@
|
|||||||
"unexpected_cutting_height_adj": "Unexpected cutting height adjustment",
|
"unexpected_cutting_height_adj": "Unexpected cutting height adjustment",
|
||||||
"unexpected_error": "Unexpected error",
|
"unexpected_error": "Unexpected error",
|
||||||
"upside_down": "Upside down",
|
"upside_down": "Upside down",
|
||||||
|
"wait_power_up": "Wait power up",
|
||||||
|
"wait_updating": "Wait updating",
|
||||||
"weak_gps_signal": "Weak GPS signal",
|
"weak_gps_signal": "Weak GPS signal",
|
||||||
"wheel_drive_problem_left": "Left wheel drive problem",
|
"wheel_drive_problem_left": "Left wheel drive problem",
|
||||||
"wheel_drive_problem_rear_left": "Rear left wheel drive problem",
|
"wheel_drive_problem_rear_left": "Rear left wheel drive problem",
|
||||||
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
@@ -22,9 +23,6 @@ from homeassistant.helpers.dispatcher import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_INSTANCE_CLIENTS,
|
|
||||||
CONF_ON_UNLOAD,
|
|
||||||
CONF_ROOT_CLIENT,
|
|
||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
HYPERION_RELEASES_URL,
|
HYPERION_RELEASES_URL,
|
||||||
@@ -52,15 +50,15 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
# The get_hyperion_unique_id method will create a per-entity unique id when given the
|
# The get_hyperion_unique_id method will create a per-entity unique id when given the
|
||||||
# server id, an instance number and a name.
|
# server id, an instance number and a name.
|
||||||
|
|
||||||
# hass.data format
|
type HyperionConfigEntry = ConfigEntry[HyperionData]
|
||||||
# ================
|
|
||||||
#
|
|
||||||
# hass.data[DOMAIN] = {
|
@dataclass
|
||||||
# <config_entry.entry_id>: {
|
class HyperionData:
|
||||||
# "ROOT_CLIENT": <Hyperion Client>,
|
"""Hyperion runtime data."""
|
||||||
# "ON_UNLOAD": [<callable>, ...],
|
|
||||||
# }
|
root_client: client.HyperionClient
|
||||||
# }
|
instance_clients: dict[int, client.HyperionClient]
|
||||||
|
|
||||||
|
|
||||||
def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str:
|
def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str:
|
||||||
@@ -107,29 +105,29 @@ async def async_create_connect_hyperion_client(
|
|||||||
@callback
|
@callback
|
||||||
def listen_for_instance_updates(
|
def listen_for_instance_updates(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
entry: HyperionConfigEntry,
|
||||||
add_func: Callable,
|
add_func: Callable[[int, str], None],
|
||||||
remove_func: Callable,
|
remove_func: Callable[[int], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Listen for instance additions/removals."""
|
"""Listen for instance additions/removals."""
|
||||||
|
|
||||||
hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].extend(
|
entry.async_on_unload(
|
||||||
[
|
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
hass,
|
hass,
|
||||||
SIGNAL_INSTANCE_ADD.format(config_entry.entry_id),
|
SIGNAL_INSTANCE_ADD.format(entry.entry_id),
|
||||||
add_func,
|
add_func,
|
||||||
),
|
)
|
||||||
|
)
|
||||||
|
entry.async_on_unload(
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
hass,
|
hass,
|
||||||
SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id),
|
SIGNAL_INSTANCE_REMOVE.format(entry.entry_id),
|
||||||
remove_func,
|
remove_func,
|
||||||
),
|
)
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: HyperionConfigEntry) -> bool:
|
||||||
"""Set up Hyperion from a config entry."""
|
"""Set up Hyperion from a config entry."""
|
||||||
host = entry.data[CONF_HOST]
|
host = entry.data[CONF_HOST]
|
||||||
port = entry.data[CONF_PORT]
|
port = entry.data[CONF_PORT]
|
||||||
@@ -185,12 +183,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
# We need 1 root client (to manage instances being removed/added) and then 1 client
|
# We need 1 root client (to manage instances being removed/added) and then 1 client
|
||||||
# per Hyperion server instance which is shared for all entities associated with
|
# per Hyperion server instance which is shared for all entities associated with
|
||||||
# that instance.
|
# that instance.
|
||||||
hass.data.setdefault(DOMAIN, {})
|
entry.runtime_data = HyperionData(
|
||||||
hass.data[DOMAIN][entry.entry_id] = {
|
root_client=hyperion_client,
|
||||||
CONF_ROOT_CLIENT: hyperion_client,
|
instance_clients={},
|
||||||
CONF_INSTANCE_CLIENTS: {},
|
)
|
||||||
CONF_ON_UNLOAD: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
async def async_instances_to_clients(response: dict[str, Any]) -> None:
|
async def async_instances_to_clients(response: dict[str, Any]) -> None:
|
||||||
"""Convert instances to Hyperion clients."""
|
"""Convert instances to Hyperion clients."""
|
||||||
@@ -203,7 +199,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
running_instances: set[int] = set()
|
running_instances: set[int] = set()
|
||||||
stopped_instances: set[int] = set()
|
stopped_instances: set[int] = set()
|
||||||
existing_instances = hass.data[DOMAIN][entry.entry_id][CONF_INSTANCE_CLIENTS]
|
existing_instances = entry.runtime_data.instance_clients
|
||||||
server_id = cast(str, entry.unique_id)
|
server_id = cast(str, entry.unique_id)
|
||||||
|
|
||||||
# In practice, an instance can be in 3 states as seen by this function:
|
# In practice, an instance can be in 3 states as seen by this function:
|
||||||
@@ -270,39 +266,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
assert hyperion_client
|
assert hyperion_client
|
||||||
if hyperion_client.instances is not None:
|
if hyperion_client.instances is not None:
|
||||||
await async_instances_to_clients_raw(hyperion_client.instances)
|
await async_instances_to_clients_raw(hyperion_client.instances)
|
||||||
hass.data[DOMAIN][entry.entry_id][CONF_ON_UNLOAD].append(
|
entry.async_on_unload(entry.add_update_listener(_async_entry_updated))
|
||||||
entry.add_update_listener(_async_entry_updated)
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def _async_entry_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
async def _async_entry_updated(hass: HomeAssistant, entry: HyperionConfigEntry) -> None:
|
||||||
"""Handle entry updates."""
|
"""Handle entry updates."""
|
||||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: HyperionConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
config_entry, PLATFORMS
|
if unload_ok:
|
||||||
)
|
|
||||||
if unload_ok and config_entry.entry_id in hass.data[DOMAIN]:
|
|
||||||
config_data = hass.data[DOMAIN].pop(config_entry.entry_id)
|
|
||||||
for func in config_data[CONF_ON_UNLOAD]:
|
|
||||||
func()
|
|
||||||
|
|
||||||
# Disconnect the shared instance clients.
|
# Disconnect the shared instance clients.
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*(
|
*(
|
||||||
config_data[CONF_INSTANCE_CLIENTS][
|
inst.async_client_disconnect()
|
||||||
instance_num
|
for inst in entry.runtime_data.instance_clients.values()
|
||||||
].async_client_disconnect()
|
|
||||||
for instance_num in config_data[CONF_INSTANCE_CLIENTS]
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Disconnect the root client.
|
# Disconnect the root client.
|
||||||
root_client = config_data[CONF_ROOT_CLIENT]
|
root_client = entry.runtime_data.root_client
|
||||||
await root_client.async_client_disconnect()
|
await root_client.async_client_disconnect()
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
@@ -25,7 +25,6 @@ from homeassistant.components.camera import (
|
|||||||
Camera,
|
Camera,
|
||||||
async_get_still_stream,
|
async_get_still_stream,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
@@ -35,12 +34,12 @@ from homeassistant.helpers.dispatcher import (
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
|
HyperionConfigEntry,
|
||||||
get_hyperion_device_id,
|
get_hyperion_device_id,
|
||||||
get_hyperion_unique_id,
|
get_hyperion_unique_id,
|
||||||
listen_for_instance_updates,
|
listen_for_instance_updates,
|
||||||
)
|
)
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_INSTANCE_CLIENTS,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
HYPERION_MANUFACTURER_NAME,
|
HYPERION_MANUFACTURER_NAME,
|
||||||
HYPERION_MODEL_NAME,
|
HYPERION_MODEL_NAME,
|
||||||
@@ -53,12 +52,11 @@ IMAGE_STREAM_JPG_SENTINEL = "data:image/jpg;base64,"
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
entry: HyperionConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a Hyperion platform from config entry."""
|
"""Set up a Hyperion platform from config entry."""
|
||||||
entry_data = hass.data[DOMAIN][config_entry.entry_id]
|
server_id = entry.unique_id
|
||||||
server_id = config_entry.unique_id
|
|
||||||
|
|
||||||
def camera_unique_id(instance_num: int) -> str:
|
def camera_unique_id(instance_num: int) -> str:
|
||||||
"""Return the camera unique_id."""
|
"""Return the camera unique_id."""
|
||||||
@@ -75,7 +73,7 @@ async def async_setup_entry(
|
|||||||
server_id,
|
server_id,
|
||||||
instance_num,
|
instance_num,
|
||||||
instance_name,
|
instance_name,
|
||||||
entry_data[CONF_INSTANCE_CLIENTS][instance_num],
|
entry.runtime_data.instance_clients[instance_num],
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -91,7 +89,7 @@ async def async_setup_entry(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
listen_for_instance_updates(hass, config_entry, instance_add, instance_remove)
|
listen_for_instance_updates(hass, entry, instance_add, instance_remove)
|
||||||
|
|
||||||
|
|
||||||
# A note on Hyperion streaming semantics:
|
# A note on Hyperion streaming semantics:
|
||||||
|
@@ -3,10 +3,7 @@
|
|||||||
CONF_AUTH_ID = "auth_id"
|
CONF_AUTH_ID = "auth_id"
|
||||||
CONF_CREATE_TOKEN = "create_token"
|
CONF_CREATE_TOKEN = "create_token"
|
||||||
CONF_INSTANCE = "instance"
|
CONF_INSTANCE = "instance"
|
||||||
CONF_INSTANCE_CLIENTS = "INSTANCE_CLIENTS"
|
|
||||||
CONF_ON_UNLOAD = "ON_UNLOAD"
|
|
||||||
CONF_PRIORITY = "priority"
|
CONF_PRIORITY = "priority"
|
||||||
CONF_ROOT_CLIENT = "ROOT_CLIENT"
|
|
||||||
CONF_EFFECT_HIDE_LIST = "effect_hide_list"
|
CONF_EFFECT_HIDE_LIST = "effect_hide_list"
|
||||||
CONF_EFFECT_SHOW_LIST = "effect_show_list"
|
CONF_EFFECT_SHOW_LIST = "effect_show_list"
|
||||||
|
|
||||||
|
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable, Mapping, Sequence
|
from collections.abc import Callable, Mapping, Sequence
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
from types import MappingProxyType
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from hyperion import client, const
|
from hyperion import client, const
|
||||||
@@ -18,7 +17,6 @@ from homeassistant.components.light import (
|
|||||||
LightEntity,
|
LightEntity,
|
||||||
LightEntityFeature,
|
LightEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
@@ -29,13 +27,13 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
from homeassistant.util import color as color_util
|
from homeassistant.util import color as color_util
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
|
HyperionConfigEntry,
|
||||||
get_hyperion_device_id,
|
get_hyperion_device_id,
|
||||||
get_hyperion_unique_id,
|
get_hyperion_unique_id,
|
||||||
listen_for_instance_updates,
|
listen_for_instance_updates,
|
||||||
)
|
)
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_EFFECT_HIDE_LIST,
|
CONF_EFFECT_HIDE_LIST,
|
||||||
CONF_INSTANCE_CLIENTS,
|
|
||||||
CONF_PRIORITY,
|
CONF_PRIORITY,
|
||||||
DEFAULT_ORIGIN,
|
DEFAULT_ORIGIN,
|
||||||
DEFAULT_PRIORITY,
|
DEFAULT_PRIORITY,
|
||||||
@@ -75,28 +73,26 @@ ICON_EFFECT = "mdi:lava-lamp"
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
entry: HyperionConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a Hyperion platform from config entry."""
|
"""Set up a Hyperion platform from config entry."""
|
||||||
|
|
||||||
entry_data = hass.data[DOMAIN][config_entry.entry_id]
|
server_id = entry.unique_id
|
||||||
server_id = config_entry.unique_id
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def instance_add(instance_num: int, instance_name: str) -> None:
|
def instance_add(instance_num: int, instance_name: str) -> None:
|
||||||
"""Add entities for a new Hyperion instance."""
|
"""Add entities for a new Hyperion instance."""
|
||||||
assert server_id
|
assert server_id
|
||||||
args = (
|
async_add_entities(
|
||||||
|
[
|
||||||
|
HyperionLight(
|
||||||
server_id,
|
server_id,
|
||||||
instance_num,
|
instance_num,
|
||||||
instance_name,
|
instance_name,
|
||||||
config_entry.options,
|
entry.options,
|
||||||
entry_data[CONF_INSTANCE_CLIENTS][instance_num],
|
entry.runtime_data.instance_clients[instance_num],
|
||||||
)
|
),
|
||||||
async_add_entities(
|
|
||||||
[
|
|
||||||
HyperionLight(*args),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -111,7 +107,7 @@ async def async_setup_entry(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
listen_for_instance_updates(hass, config_entry, instance_add, instance_remove)
|
listen_for_instance_updates(hass, entry, instance_add, instance_remove)
|
||||||
|
|
||||||
|
|
||||||
class HyperionLight(LightEntity):
|
class HyperionLight(LightEntity):
|
||||||
@@ -129,7 +125,7 @@ class HyperionLight(LightEntity):
|
|||||||
server_id: str,
|
server_id: str,
|
||||||
instance_num: int,
|
instance_num: int,
|
||||||
instance_name: str,
|
instance_name: str,
|
||||||
options: MappingProxyType[str, Any],
|
options: Mapping[str, Any],
|
||||||
hyperion_client: client.HyperionClient,
|
hyperion_client: client.HyperionClient,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
|
@@ -19,7 +19,6 @@ from hyperion.const import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
@@ -29,12 +28,12 @@ from homeassistant.helpers.dispatcher import (
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
|
HyperionConfigEntry,
|
||||||
get_hyperion_device_id,
|
get_hyperion_device_id,
|
||||||
get_hyperion_unique_id,
|
get_hyperion_unique_id,
|
||||||
listen_for_instance_updates,
|
listen_for_instance_updates,
|
||||||
)
|
)
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_INSTANCE_CLIENTS,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
HYPERION_MANUFACTURER_NAME,
|
HYPERION_MANUFACTURER_NAME,
|
||||||
HYPERION_MODEL_NAME,
|
HYPERION_MODEL_NAME,
|
||||||
@@ -62,12 +61,11 @@ def _sensor_unique_id(server_id: str, instance_num: int, suffix: str) -> str:
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
entry: HyperionConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a Hyperion platform from config entry."""
|
"""Set up a Hyperion platform from config entry."""
|
||||||
entry_data = hass.data[DOMAIN][config_entry.entry_id]
|
server_id = entry.unique_id
|
||||||
server_id = config_entry.unique_id
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def instance_add(instance_num: int, instance_name: str) -> None:
|
def instance_add(instance_num: int, instance_name: str) -> None:
|
||||||
@@ -78,7 +76,7 @@ async def async_setup_entry(
|
|||||||
server_id,
|
server_id,
|
||||||
instance_num,
|
instance_num,
|
||||||
instance_name,
|
instance_name,
|
||||||
entry_data[CONF_INSTANCE_CLIENTS][instance_num],
|
entry.runtime_data.instance_clients[instance_num],
|
||||||
PRIORITY_SENSOR_DESCRIPTION,
|
PRIORITY_SENSOR_DESCRIPTION,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -98,7 +96,7 @@ async def async_setup_entry(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
listen_for_instance_updates(hass, config_entry, instance_add, instance_remove)
|
listen_for_instance_updates(hass, entry, instance_add, instance_remove)
|
||||||
|
|
||||||
|
|
||||||
class HyperionSensor(SensorEntity):
|
class HyperionSensor(SensorEntity):
|
||||||
|
@@ -26,7 +26,6 @@ from hyperion.const import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
@@ -38,12 +37,12 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
|
HyperionConfigEntry,
|
||||||
get_hyperion_device_id,
|
get_hyperion_device_id,
|
||||||
get_hyperion_unique_id,
|
get_hyperion_unique_id,
|
||||||
listen_for_instance_updates,
|
listen_for_instance_updates,
|
||||||
)
|
)
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_INSTANCE_CLIENTS,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
HYPERION_MANUFACTURER_NAME,
|
HYPERION_MANUFACTURER_NAME,
|
||||||
HYPERION_MODEL_NAME,
|
HYPERION_MODEL_NAME,
|
||||||
@@ -89,12 +88,11 @@ def _component_to_translation_key(component: str) -> str:
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
entry: HyperionConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a Hyperion platform from config entry."""
|
"""Set up a Hyperion platform from config entry."""
|
||||||
entry_data = hass.data[DOMAIN][config_entry.entry_id]
|
server_id = entry.unique_id
|
||||||
server_id = config_entry.unique_id
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def instance_add(instance_num: int, instance_name: str) -> None:
|
def instance_add(instance_num: int, instance_name: str) -> None:
|
||||||
@@ -106,7 +104,7 @@ async def async_setup_entry(
|
|||||||
instance_num,
|
instance_num,
|
||||||
instance_name,
|
instance_name,
|
||||||
component,
|
component,
|
||||||
entry_data[CONF_INSTANCE_CLIENTS][instance_num],
|
entry.runtime_data.instance_clients[instance_num],
|
||||||
)
|
)
|
||||||
for component in COMPONENT_SWITCHES
|
for component in COMPONENT_SWITCHES
|
||||||
)
|
)
|
||||||
@@ -123,7 +121,7 @@ async def async_setup_entry(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
listen_for_instance_updates(hass, config_entry, instance_add, instance_remove)
|
listen_for_instance_updates(hass, entry, instance_add, instance_remove)
|
||||||
|
|
||||||
|
|
||||||
class HyperionComponentSwitch(SwitchEntity):
|
class HyperionComponentSwitch(SwitchEntity):
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
"description": "Select fireplace by serial number:"
|
"description": "Select fireplace by serial number:"
|
||||||
},
|
},
|
||||||
"cloud_api": {
|
"cloud_api": {
|
||||||
"description": "Authenticate against IntelliFire Cloud",
|
"description": "Authenticate against IntelliFire cloud",
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"username": "Your IntelliFire app username",
|
"username": "Your IntelliFire app username",
|
||||||
"password": "Your IntelliFire app password"
|
"password": "Your IntelliFire app password"
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"name": "Pilot flame error"
|
"name": "Pilot flame error"
|
||||||
},
|
},
|
||||||
"flame_error": {
|
"flame_error": {
|
||||||
"name": "Flame Error"
|
"name": "Flame error"
|
||||||
},
|
},
|
||||||
"fan_delay_error": {
|
"fan_delay_error": {
|
||||||
"name": "Fan delay error"
|
"name": "Fan delay error"
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
"name": "Target temperature"
|
"name": "Target temperature"
|
||||||
},
|
},
|
||||||
"fan_speed": {
|
"fan_speed": {
|
||||||
"name": "Fan Speed"
|
"name": "Fan speed"
|
||||||
},
|
},
|
||||||
"timer_end_timestamp": {
|
"timer_end_timestamp": {
|
||||||
"name": "Timer end"
|
"name": "Timer end"
|
||||||
|
@@ -22,6 +22,7 @@ from pynecil import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers.debounce import Debouncer
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
@@ -83,7 +84,11 @@ class IronOSBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
|||||||
self.device_info = await self.device.get_device_info()
|
self.device_info = await self.device.get_device_info()
|
||||||
|
|
||||||
except CommunicationError as e:
|
except CommunicationError as e:
|
||||||
raise UpdateFailed("Cannot connect to device") from e
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="cannot_connect",
|
||||||
|
translation_placeholders={CONF_NAME: self.config_entry.title},
|
||||||
|
) from e
|
||||||
|
|
||||||
self.v223_features = AwesomeVersion(self.device_info.build) >= V223
|
self.v223_features = AwesomeVersion(self.device_info.build) >= V223
|
||||||
|
|
||||||
@@ -108,7 +113,11 @@ class IronOSLiveDataCoordinator(IronOSBaseCoordinator[LiveDataResponse]):
|
|||||||
return await self.device.get_live_data()
|
return await self.device.get_live_data()
|
||||||
|
|
||||||
except CommunicationError as e:
|
except CommunicationError as e:
|
||||||
raise UpdateFailed("Cannot connect to device") from e
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="cannot_connect",
|
||||||
|
translation_placeholders={CONF_NAME: self.config_entry.title},
|
||||||
|
) from e
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_tip(self) -> bool:
|
def has_tip(self) -> bool:
|
||||||
@@ -187,4 +196,7 @@ class IronOSFirmwareUpdateCoordinator(DataUpdateCoordinator[LatestRelease]):
|
|||||||
try:
|
try:
|
||||||
return await self.github.latest_release()
|
return await self.github.latest_release()
|
||||||
except UpdateException as e:
|
except UpdateException as e:
|
||||||
raise UpdateFailed("Failed to check for latest IronOS update") from e
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="update_check_failed",
|
||||||
|
) from e
|
||||||
|
@@ -284,6 +284,12 @@
|
|||||||
},
|
},
|
||||||
"submit_setting_failed": {
|
"submit_setting_failed": {
|
||||||
"message": "Failed to submit setting to device, try again later"
|
"message": "Failed to submit setting to device, try again later"
|
||||||
|
},
|
||||||
|
"cannot_connect": {
|
||||||
|
"message": "Cannot connect to device {name}"
|
||||||
|
},
|
||||||
|
"update_check_failed": {
|
||||||
|
"message": "Failed to check for latest IronOS update"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,11 +4,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerError
|
from pyecotrend_ista import PyEcotrendIsta
|
||||||
|
|
||||||
|
from homeassistant.components.recorder import get_instance
|
||||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import IstaConfigEntry, IstaCoordinator
|
from .coordinator import IstaConfigEntry, IstaCoordinator
|
||||||
@@ -25,19 +25,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: IstaConfigEntry) -> bool
|
|||||||
entry.data[CONF_PASSWORD],
|
entry.data[CONF_PASSWORD],
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
await hass.async_add_executor_job(ista.login)
|
|
||||||
except ServerError as e:
|
|
||||||
raise ConfigEntryNotReady(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="connection_exception",
|
|
||||||
) from e
|
|
||||||
except (LoginError, KeycloakError) as e:
|
|
||||||
raise ConfigEntryAuthFailed(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="authentication_exception",
|
|
||||||
translation_placeholders={CONF_EMAIL: entry.data[CONF_EMAIL]},
|
|
||||||
) from e
|
|
||||||
|
|
||||||
coordinator = IstaCoordinator(hass, entry, ista)
|
coordinator = IstaCoordinator(hass, entry, ista)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
@@ -52,3 +39,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: IstaConfigEntry) -> bool
|
|||||||
async def async_unload_entry(hass: HomeAssistant, entry: IstaConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: IstaConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_remove_entry(hass: HomeAssistant, entry: IstaConfigEntry) -> None:
|
||||||
|
"""Handle removal of an entry."""
|
||||||
|
statistic_ids = [f"{DOMAIN}:{name}" for name in entry.options.values()]
|
||||||
|
get_instance(hass).async_clear_statistics(statistic_ids)
|
||||||
|
@@ -100,8 +100,19 @@ class IstaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
user_input[CONF_PASSWORD],
|
user_input[CONF_PASSWORD],
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_consumption_units() -> set[str]:
|
||||||
|
ista.login()
|
||||||
|
consumption_units = ista.get_consumption_unit_details()[
|
||||||
|
"consumptionUnits"
|
||||||
|
]
|
||||||
|
return {unit["id"] for unit in consumption_units}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.hass.async_add_executor_job(ista.login)
|
consumption_units = await self.hass.async_add_executor_job(
|
||||||
|
get_consumption_units
|
||||||
|
)
|
||||||
|
|
||||||
except ServerError:
|
except ServerError:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except (LoginError, KeycloakError):
|
except (LoginError, KeycloakError):
|
||||||
@@ -110,6 +121,8 @@ class IstaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
else:
|
else:
|
||||||
|
if reauth_entry.unique_id not in consumption_units:
|
||||||
|
return self.async_abort(reason="unique_id_mismatch")
|
||||||
return self.async_update_reload_and_abort(reauth_entry, data=user_input)
|
return self.async_update_reload_and_abort(reauth_entry, data=user_input)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
|
@@ -11,7 +11,7 @@ from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerErr
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_EMAIL
|
from homeassistant.const import CONF_EMAIL
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@@ -25,6 +25,7 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
"""Ista EcoTrend data update coordinator."""
|
"""Ista EcoTrend data update coordinator."""
|
||||||
|
|
||||||
config_entry: IstaConfigEntry
|
config_entry: IstaConfigEntry
|
||||||
|
details: dict[str, Any]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hass: HomeAssistant, config_entry: IstaConfigEntry, ista: PyEcotrendIsta
|
self, hass: HomeAssistant, config_entry: IstaConfigEntry, ista: PyEcotrendIsta
|
||||||
@@ -38,22 +39,35 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
update_interval=timedelta(days=1),
|
update_interval=timedelta(days=1),
|
||||||
)
|
)
|
||||||
self.ista = ista
|
self.ista = ista
|
||||||
self.details: dict[str, Any] = {}
|
|
||||||
|
async def _async_setup(self) -> None:
|
||||||
|
"""Set up the ista EcoTrend coordinator."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.details = await self.hass.async_add_executor_job(self.get_details)
|
||||||
|
except ServerError as e:
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="connection_exception",
|
||||||
|
) from e
|
||||||
|
except (LoginError, KeycloakError) as e:
|
||||||
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="authentication_exception",
|
||||||
|
translation_placeholders={
|
||||||
|
CONF_EMAIL: self.config_entry.data[CONF_EMAIL]
|
||||||
|
},
|
||||||
|
) from e
|
||||||
|
|
||||||
async def _async_update_data(self):
|
async def _async_update_data(self):
|
||||||
"""Fetch ista EcoTrend data."""
|
"""Fetch ista EcoTrend data."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.hass.async_add_executor_job(self.ista.login)
|
|
||||||
|
|
||||||
if not self.details:
|
|
||||||
self.details = await self.async_get_details()
|
|
||||||
|
|
||||||
return await self.hass.async_add_executor_job(self.get_consumption_data)
|
return await self.hass.async_add_executor_job(self.get_consumption_data)
|
||||||
|
|
||||||
except ServerError as e:
|
except ServerError as e:
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
"Unable to connect and retrieve data from ista EcoTrend, try again later"
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="connection_exception",
|
||||||
) from e
|
) from e
|
||||||
except (LoginError, KeycloakError) as e:
|
except (LoginError, KeycloakError) as e:
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryAuthFailed(
|
||||||
@@ -67,17 +81,17 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
def get_consumption_data(self) -> dict[str, Any]:
|
def get_consumption_data(self) -> dict[str, Any]:
|
||||||
"""Get raw json data for all consumption units."""
|
"""Get raw json data for all consumption units."""
|
||||||
|
|
||||||
|
self.ista.login()
|
||||||
return {
|
return {
|
||||||
consumption_unit: self.ista.get_consumption_data(consumption_unit)
|
consumption_unit: self.ista.get_consumption_data(consumption_unit)
|
||||||
for consumption_unit in self.ista.get_uuids()
|
for consumption_unit in self.ista.get_uuids()
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_get_details(self) -> dict[str, Any]:
|
def get_details(self) -> dict[str, Any]:
|
||||||
"""Retrieve details of consumption units."""
|
"""Retrieve details of consumption units."""
|
||||||
|
|
||||||
result = await self.hass.async_add_executor_job(
|
self.ista.login()
|
||||||
self.ista.get_consumption_unit_details
|
result = self.ista.get_consumption_unit_details()
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
consumption_unit: next(
|
consumption_unit: next(
|
||||||
|
33
homeassistant/components/ista_ecotrend/diagnostics.py
Normal file
33
homeassistant/components/ista_ecotrend/diagnostics.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""Diagnostics platform for ista EcoTrend integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import IstaConfigEntry
|
||||||
|
|
||||||
|
TO_REDACT = {
|
||||||
|
"firstName",
|
||||||
|
"lastName",
|
||||||
|
"street",
|
||||||
|
"houseNumber",
|
||||||
|
"documentNumber",
|
||||||
|
"postalCode",
|
||||||
|
"city",
|
||||||
|
"propertyNumber",
|
||||||
|
"idAtCustomerUser",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_config_entry_diagnostics(
|
||||||
|
hass: HomeAssistant, config_entry: IstaConfigEntry
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return diagnostics for a config entry."""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"details": async_redact_data(config_entry.runtime_data.details, TO_REDACT),
|
||||||
|
"data": async_redact_data(config_entry.runtime_data.data, TO_REDACT),
|
||||||
|
}
|
@@ -5,12 +5,8 @@ rules:
|
|||||||
comment: The integration registers no actions.
|
comment: The integration registers no actions.
|
||||||
appropriate-polling: done
|
appropriate-polling: done
|
||||||
brands: done
|
brands: done
|
||||||
common-modules:
|
common-modules: done
|
||||||
status: todo
|
config-flow-test-coverage: done
|
||||||
comment: Group the 3 different executor jobs as one executor job
|
|
||||||
config-flow-test-coverage:
|
|
||||||
status: todo
|
|
||||||
comment: test_form/docstrings outdated, test already_configuret, test abort conditions in reauth,
|
|
||||||
config-flow: done
|
config-flow: done
|
||||||
dependency-transparency: done
|
dependency-transparency: done
|
||||||
docs-actions:
|
docs-actions:
|
||||||
@@ -47,7 +43,7 @@ rules:
|
|||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
devices: done
|
devices: done
|
||||||
diagnostics: todo
|
diagnostics: done
|
||||||
discovery-update-info:
|
discovery-update-info:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: The integration is a web service, there are no discoverable devices.
|
comment: The integration is a web service, there are no discoverable devices.
|
||||||
|
@@ -2,7 +2,8 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
|
"unique_id_mismatch": "The login details correspond to a different account. Please re-authenticate to the previously configured account."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
@@ -73,7 +73,7 @@ async def build_root_response(
|
|||||||
children = [
|
children = [
|
||||||
await item_payload(hass, client, user_id, folder)
|
await item_payload(hass, client, user_id, folder)
|
||||||
for folder in folders["Items"]
|
for folder in folders["Items"]
|
||||||
if folder["CollectionType"] in SUPPORTED_COLLECTION_TYPES
|
if folder.get("CollectionType") in SUPPORTED_COLLECTION_TYPES
|
||||||
]
|
]
|
||||||
|
|
||||||
return BrowseMedia(
|
return BrowseMedia(
|
||||||
|
@@ -56,7 +56,7 @@
|
|||||||
"on": "[%key:common::state::on%]",
|
"on": "[%key:common::state::on%]",
|
||||||
"warming": "Warming",
|
"warming": "Warming",
|
||||||
"cooling": "Cooling",
|
"cooling": "Cooling",
|
||||||
"error": "Error"
|
"error": "[%key:common::state::error%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
|||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
SETTINGS_UPDATE_INTERVAL = timedelta(hours=1)
|
SETTINGS_UPDATE_INTERVAL = timedelta(hours=1)
|
||||||
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5)
|
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -82,9 +82,14 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||||||
class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
||||||
"""Class to handle fetching data from the La Marzocco API centrally."""
|
"""Class to handle fetching data from the La Marzocco API centrally."""
|
||||||
|
|
||||||
async def _async_connect_websocket(self) -> None:
|
async def _internal_async_update_data(self) -> None:
|
||||||
"""Set up the coordinator."""
|
"""Fetch data from API endpoint."""
|
||||||
if not self.device.websocket.connected:
|
|
||||||
|
if self.device.websocket.connected:
|
||||||
|
return
|
||||||
|
await self.device.get_dashboard()
|
||||||
|
_LOGGER.debug("Current status: %s", self.device.dashboard.to_dict())
|
||||||
|
|
||||||
_LOGGER.debug("Init WebSocket in background task")
|
_LOGGER.debug("Init WebSocket in background task")
|
||||||
|
|
||||||
self.config_entry.async_create_background_task(
|
self.config_entry.async_create_background_task(
|
||||||
@@ -100,18 +105,10 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
|||||||
await self.device.websocket.disconnect()
|
await self.device.websocket.disconnect()
|
||||||
|
|
||||||
self.config_entry.async_on_unload(
|
self.config_entry.async_on_unload(
|
||||||
self.hass.bus.async_listen_once(
|
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, websocket_close)
|
||||||
EVENT_HOMEASSISTANT_STOP, websocket_close
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.config_entry.async_on_unload(websocket_close)
|
self.config_entry.async_on_unload(websocket_close)
|
||||||
|
|
||||||
async def _internal_async_update_data(self) -> None:
|
|
||||||
"""Fetch data from API endpoint."""
|
|
||||||
await self.device.get_dashboard()
|
|
||||||
_LOGGER.debug("Current status: %s", self.device.dashboard.to_dict())
|
|
||||||
await self._async_connect_websocket()
|
|
||||||
|
|
||||||
|
|
||||||
class LaMarzoccoSettingsUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
class LaMarzoccoSettingsUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
||||||
"""Coordinator for La Marzocco settings."""
|
"""Coordinator for La Marzocco settings."""
|
||||||
|
@@ -76,6 +76,9 @@
|
|||||||
"coffee_boiler_ready_time": {
|
"coffee_boiler_ready_time": {
|
||||||
"default": "mdi:av-timer"
|
"default": "mdi:av-timer"
|
||||||
},
|
},
|
||||||
|
"last_cleaning_time": {
|
||||||
|
"default": "mdi:spray-bottle"
|
||||||
|
},
|
||||||
"steam_boiler_ready_time": {
|
"steam_boiler_ready_time": {
|
||||||
"default": "mdi:av-timer"
|
"default": "mdi:av-timer"
|
||||||
}
|
}
|
||||||
|
@@ -37,5 +37,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pylamarzocco"],
|
"loggers": ["pylamarzocco"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pylamarzocco==2.0.0b1"]
|
"requirements": ["pylamarzocco==2.0.0b3"]
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ from typing import cast
|
|||||||
|
|
||||||
from pylamarzocco.const import ModelName, WidgetType
|
from pylamarzocco.const import ModelName, WidgetType
|
||||||
from pylamarzocco.models import (
|
from pylamarzocco.models import (
|
||||||
|
BackFlush,
|
||||||
BaseWidgetOutput,
|
BaseWidgetOutput,
|
||||||
CoffeeBoiler,
|
CoffeeBoiler,
|
||||||
SteamBoilerLevel,
|
SteamBoilerLevel,
|
||||||
@@ -84,6 +85,17 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
|
|||||||
in (ModelName.GS3_AV, ModelName.GS3_MP, ModelName.LINEA_MINI)
|
in (ModelName.GS3_AV, ModelName.GS3_MP, ModelName.LINEA_MINI)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
LaMarzoccoSensorEntityDescription(
|
||||||
|
key="last_cleaning_time",
|
||||||
|
translation_key="last_cleaning_time",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
value_fn=(
|
||||||
|
lambda config: cast(
|
||||||
|
BackFlush, config[WidgetType.CM_BACK_FLUSH]
|
||||||
|
).last_cleaning_start_time
|
||||||
|
),
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -146,6 +146,9 @@
|
|||||||
},
|
},
|
||||||
"steam_boiler_ready_time": {
|
"steam_boiler_ready_time": {
|
||||||
"name": "Steam boiler ready time"
|
"name": "Steam boiler ready time"
|
||||||
|
},
|
||||||
|
"last_cleaning_time": {
|
||||||
|
"name": "Last cleaning time"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
|
@@ -24,12 +24,17 @@ from homeassistant.const import (
|
|||||||
CONF_IP_ADDRESS,
|
CONF_IP_ADDRESS,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
|
CONF_RESOURCE,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
from homeassistant.helpers import (
|
||||||
|
config_validation as cv,
|
||||||
|
device_registry as dr,
|
||||||
|
entity_registry as er,
|
||||||
|
)
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
@@ -38,6 +43,7 @@ from .const import (
|
|||||||
CONF_DIM_MODE,
|
CONF_DIM_MODE,
|
||||||
CONF_DOMAIN_DATA,
|
CONF_DOMAIN_DATA,
|
||||||
CONF_SK_NUM_TRIES,
|
CONF_SK_NUM_TRIES,
|
||||||
|
CONF_TARGET_VALUE_LOCKED,
|
||||||
CONF_TRANSITION,
|
CONF_TRANSITION,
|
||||||
CONNECTION,
|
CONNECTION,
|
||||||
DEVICE_CONNECTIONS,
|
DEVICE_CONNECTIONS,
|
||||||
@@ -155,6 +161,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
if config_entry.minor_version < 2:
|
if config_entry.minor_version < 2:
|
||||||
new_data[CONF_ACKNOWLEDGE] = False
|
new_data[CONF_ACKNOWLEDGE] = False
|
||||||
|
|
||||||
|
if config_entry.version < 2:
|
||||||
# update to 2.1 (fix transitions for lights and switches)
|
# update to 2.1 (fix transitions for lights and switches)
|
||||||
new_entities_data = [*new_data[CONF_ENTITIES]]
|
new_entities_data = [*new_data[CONF_ENTITIES]]
|
||||||
for entity in new_entities_data:
|
for entity in new_entities_data:
|
||||||
@@ -164,8 +171,19 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
entity[CONF_DOMAIN_DATA][CONF_TRANSITION] /= 1000.0
|
entity[CONF_DOMAIN_DATA][CONF_TRANSITION] /= 1000.0
|
||||||
new_data[CONF_ENTITIES] = new_entities_data
|
new_data[CONF_ENTITIES] = new_entities_data
|
||||||
|
|
||||||
|
if config_entry.version < 3:
|
||||||
|
# update to 3.1 (remove resource parameter, add climate target lock value parameter)
|
||||||
|
for entity in new_data[CONF_ENTITIES]:
|
||||||
|
entity.pop(CONF_RESOURCE, None)
|
||||||
|
|
||||||
|
if entity[CONF_DOMAIN] == Platform.CLIMATE:
|
||||||
|
entity[CONF_DOMAIN_DATA].setdefault(CONF_TARGET_VALUE_LOCKED, -1)
|
||||||
|
|
||||||
|
# migrate climate and scene unique ids
|
||||||
|
await async_migrate_entities(hass, config_entry)
|
||||||
|
|
||||||
hass.config_entries.async_update_entry(
|
hass.config_entries.async_update_entry(
|
||||||
config_entry, data=new_data, minor_version=1, version=2
|
config_entry, data=new_data, minor_version=1, version=3
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@@ -176,6 +194,29 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entities(
|
||||||
|
hass: HomeAssistant, config_entry: ConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Migrate entity registry."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None:
|
||||||
|
"""Update unique ID of entity entry."""
|
||||||
|
# fix unique entity ids for climate and scene
|
||||||
|
if "." in entity_entry.unique_id:
|
||||||
|
if entity_entry.domain == Platform.CLIMATE:
|
||||||
|
setpoint = entity_entry.unique_id.split(".")[-1]
|
||||||
|
return {
|
||||||
|
"new_unique_id": entity_entry.unique_id.rsplit("-", 1)[0]
|
||||||
|
+ f"-{setpoint}"
|
||||||
|
}
|
||||||
|
if entity_entry.domain == Platform.SCENE:
|
||||||
|
return {"new_unique_id": entity_entry.unique_id.replace(".", "")}
|
||||||
|
return None
|
||||||
|
|
||||||
|
await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||||
"""Close connection to PCHK host represented by config_entry."""
|
"""Close connection to PCHK host represented by config_entry."""
|
||||||
# forward unloading to platforms
|
# forward unloading to platforms
|
||||||
|
@@ -110,7 +110,7 @@ async def validate_connection(data: ConfigType) -> str | None:
|
|||||||
class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a LCN config flow."""
|
"""Handle a LCN config flow."""
|
||||||
|
|
||||||
VERSION = 2
|
VERSION = 3
|
||||||
MINOR_VERSION = 1
|
MINOR_VERSION = 1
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
|
@@ -3,18 +3,19 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_RESOURCE
|
from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_NAME
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import CONF_DOMAIN_DATA, DOMAIN
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
AddressType,
|
AddressType,
|
||||||
DeviceConnectionType,
|
DeviceConnectionType,
|
||||||
InputType,
|
InputType,
|
||||||
generate_unique_id,
|
generate_unique_id,
|
||||||
get_device_connection,
|
get_device_connection,
|
||||||
|
get_resource,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +49,11 @@ class LcnEntity(Entity):
|
|||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
"""Return a unique ID."""
|
"""Return a unique ID."""
|
||||||
return generate_unique_id(
|
return generate_unique_id(
|
||||||
self.config_entry.entry_id, self.address, self.config[CONF_RESOURCE]
|
self.config_entry.entry_id,
|
||||||
|
self.address,
|
||||||
|
get_resource(
|
||||||
|
self.config[CONF_DOMAIN], self.config[CONF_DOMAIN_DATA]
|
||||||
|
).lower(),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
|
@@ -19,7 +19,6 @@ from homeassistant.const import (
|
|||||||
CONF_ENTITIES,
|
CONF_ENTITIES,
|
||||||
CONF_LIGHTS,
|
CONF_LIGHTS,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_RESOURCE,
|
|
||||||
CONF_SENSORS,
|
CONF_SENSORS,
|
||||||
CONF_SWITCHES,
|
CONF_SWITCHES,
|
||||||
)
|
)
|
||||||
@@ -29,6 +28,7 @@ from homeassistant.helpers.typing import ConfigType
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_CLIMATES,
|
CONF_CLIMATES,
|
||||||
|
CONF_DOMAIN_DATA,
|
||||||
CONF_HARDWARE_SERIAL,
|
CONF_HARDWARE_SERIAL,
|
||||||
CONF_HARDWARE_TYPE,
|
CONF_HARDWARE_TYPE,
|
||||||
CONF_SCENES,
|
CONF_SCENES,
|
||||||
@@ -79,9 +79,9 @@ def get_resource(domain_name: str, domain_data: ConfigType) -> str:
|
|||||||
if domain_name == "cover":
|
if domain_name == "cover":
|
||||||
return cast(str, domain_data["motor"])
|
return cast(str, domain_data["motor"])
|
||||||
if domain_name == "climate":
|
if domain_name == "climate":
|
||||||
return f"{domain_data['source']}.{domain_data['setpoint']}"
|
return cast(str, domain_data["setpoint"])
|
||||||
if domain_name == "scene":
|
if domain_name == "scene":
|
||||||
return f"{domain_data['register']}.{domain_data['scene']}"
|
return f"{domain_data['register']}{domain_data['scene']}"
|
||||||
raise ValueError("Unknown domain")
|
raise ValueError("Unknown domain")
|
||||||
|
|
||||||
|
|
||||||
@@ -115,7 +115,9 @@ def purge_entity_registry(
|
|||||||
references_entry_data = set()
|
references_entry_data = set()
|
||||||
for entity_data in imported_entry_data[CONF_ENTITIES]:
|
for entity_data in imported_entry_data[CONF_ENTITIES]:
|
||||||
entity_unique_id = generate_unique_id(
|
entity_unique_id = generate_unique_id(
|
||||||
entry_id, entity_data[CONF_ADDRESS], entity_data[CONF_RESOURCE]
|
entry_id,
|
||||||
|
entity_data[CONF_ADDRESS],
|
||||||
|
get_resource(entity_data[CONF_DOMAIN], entity_data[CONF_DOMAIN_DATA]),
|
||||||
)
|
)
|
||||||
entity_id = entity_registry.async_get_entity_id(
|
entity_id = entity_registry.async_get_entity_id(
|
||||||
entity_data[CONF_DOMAIN], DOMAIN, entity_unique_id
|
entity_data[CONF_DOMAIN], DOMAIN, entity_unique_id
|
||||||
|
@@ -8,5 +8,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/lcn",
|
"documentation": "https://www.home-assistant.io/integrations/lcn",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pypck"],
|
"loggers": ["pypck"],
|
||||||
"requirements": ["pypck==0.8.5", "lcn-frontend==0.2.3"]
|
"requirements": ["pypck==0.8.5", "lcn-frontend==0.2.4"]
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,6 @@ from homeassistant.const import (
|
|||||||
CONF_DOMAIN,
|
CONF_DOMAIN,
|
||||||
CONF_ENTITIES,
|
CONF_ENTITIES,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_RESOURCE,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
@@ -343,7 +342,6 @@ async def websocket_add_entity(
|
|||||||
entity_config = {
|
entity_config = {
|
||||||
CONF_ADDRESS: msg[CONF_ADDRESS],
|
CONF_ADDRESS: msg[CONF_ADDRESS],
|
||||||
CONF_NAME: msg[CONF_NAME],
|
CONF_NAME: msg[CONF_NAME],
|
||||||
CONF_RESOURCE: resource,
|
|
||||||
CONF_DOMAIN: domain_name,
|
CONF_DOMAIN: domain_name,
|
||||||
CONF_DOMAIN_DATA: domain_data,
|
CONF_DOMAIN_DATA: domain_data,
|
||||||
}
|
}
|
||||||
@@ -371,7 +369,15 @@ async def websocket_add_entity(
|
|||||||
vol.Required("entry_id"): cv.string,
|
vol.Required("entry_id"): cv.string,
|
||||||
vol.Required(CONF_ADDRESS): ADDRESS_SCHEMA,
|
vol.Required(CONF_ADDRESS): ADDRESS_SCHEMA,
|
||||||
vol.Required(CONF_DOMAIN): cv.string,
|
vol.Required(CONF_DOMAIN): cv.string,
|
||||||
vol.Required(CONF_RESOURCE): cv.string,
|
vol.Required(CONF_DOMAIN_DATA): vol.Any(
|
||||||
|
DOMAIN_DATA_BINARY_SENSOR,
|
||||||
|
DOMAIN_DATA_SENSOR,
|
||||||
|
DOMAIN_DATA_SWITCH,
|
||||||
|
DOMAIN_DATA_LIGHT,
|
||||||
|
DOMAIN_DATA_CLIMATE,
|
||||||
|
DOMAIN_DATA_COVER,
|
||||||
|
DOMAIN_DATA_SCENE,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
@@ -390,7 +396,10 @@ async def websocket_delete_entity(
|
|||||||
if (
|
if (
|
||||||
tuple(entity_config[CONF_ADDRESS]) == msg[CONF_ADDRESS]
|
tuple(entity_config[CONF_ADDRESS]) == msg[CONF_ADDRESS]
|
||||||
and entity_config[CONF_DOMAIN] == msg[CONF_DOMAIN]
|
and entity_config[CONF_DOMAIN] == msg[CONF_DOMAIN]
|
||||||
and entity_config[CONF_RESOURCE] == msg[CONF_RESOURCE]
|
and get_resource(
|
||||||
|
entity_config[CONF_DOMAIN], entity_config[CONF_DOMAIN_DATA]
|
||||||
|
)
|
||||||
|
== get_resource(msg[CONF_DOMAIN], msg[CONF_DOMAIN_DATA])
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
|
@@ -88,7 +88,7 @@
|
|||||||
"available": "Available",
|
"available": "Available",
|
||||||
"charging": "[%key:common::state::charging%]",
|
"charging": "[%key:common::state::charging%]",
|
||||||
"connected": "[%key:common::state::connected%]",
|
"connected": "[%key:common::state::connected%]",
|
||||||
"error": "Error",
|
"error": "[%key:common::state::error%]",
|
||||||
"locked": "[%key:common::state::locked%]",
|
"locked": "[%key:common::state::locked%]",
|
||||||
"need_auth": "Waiting for authentication",
|
"need_auth": "Waiting for authentication",
|
||||||
"paused": "[%key:common::state::paused%]",
|
"paused": "[%key:common::state::paused%]",
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
"ocpp": "OCPP",
|
"ocpp": "OCPP",
|
||||||
"overtemperature": "Overtemperature",
|
"overtemperature": "Overtemperature",
|
||||||
"switching_phases": "Switching phases",
|
"switching_phases": "Switching phases",
|
||||||
"1p_charging_disabled": "1p charging disabled"
|
"1p_charging_disabled": "1P charging disabled"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"breaker_current": {
|
"breaker_current": {
|
||||||
|
@@ -7,6 +7,6 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["linkplay"],
|
"loggers": ["linkplay"],
|
||||||
"requirements": ["python-linkplay==0.2.3"],
|
"requirements": ["python-linkplay==0.2.4"],
|
||||||
"zeroconf": ["_linkplay._tcp.local."]
|
"zeroconf": ["_linkplay._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -120,6 +121,8 @@ SERVICE_PLAY_PRESET_SCHEMA = cv.make_entity_service_schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
RETRY_POLL_MAXIMUM = 3
|
RETRY_POLL_MAXIMUM = 3
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=5)
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@@ -7,38 +7,19 @@ import mimetypes
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.camera import (
|
from homeassistant.components.camera import Camera
|
||||||
PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA,
|
from homeassistant.config_entries import ConfigEntry
|
||||||
Camera,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
|
||||||
from homeassistant.const import CONF_FILE_PATH, CONF_NAME
|
from homeassistant.const import CONF_FILE_PATH, CONF_NAME
|
||||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
config_validation as cv,
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
entity_platform,
|
|
||||||
issue_registry as ir,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.entity_platform import (
|
|
||||||
AddConfigEntryEntitiesCallback,
|
|
||||||
AddEntitiesCallback,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|
||||||
from homeassistant.util import slugify
|
|
||||||
|
|
||||||
from .const import DEFAULT_NAME, DOMAIN, SERVICE_UPDATE_FILE_PATH
|
from .const import SERVICE_UPDATE_FILE_PATH
|
||||||
from .util import check_file_path_access
|
from .util import check_file_path_access
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_FILE_PATH): cv.string,
|
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -67,57 +48,6 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
config: ConfigType,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the Camera that works with local files."""
|
|
||||||
file_path: str = config[CONF_FILE_PATH]
|
|
||||||
file_path_slug = slugify(file_path)
|
|
||||||
|
|
||||||
if not await hass.async_add_executor_job(check_file_path_access, file_path):
|
|
||||||
ir.async_create_issue(
|
|
||||||
hass,
|
|
||||||
DOMAIN,
|
|
||||||
f"no_access_path_{file_path_slug}",
|
|
||||||
breaks_in_ha_version="2025.5.0",
|
|
||||||
is_fixable=False,
|
|
||||||
learn_more_url="https://www.home-assistant.io/integrations/local_file/",
|
|
||||||
severity=ir.IssueSeverity.WARNING,
|
|
||||||
translation_key="no_access_path",
|
|
||||||
translation_placeholders={
|
|
||||||
"file_path": file_path_slug,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
ir.async_create_issue(
|
|
||||||
hass,
|
|
||||||
HOMEASSISTANT_DOMAIN,
|
|
||||||
f"deprecated_yaml_{DOMAIN}",
|
|
||||||
breaks_in_ha_version="2025.5.0",
|
|
||||||
is_fixable=False,
|
|
||||||
issue_domain=DOMAIN,
|
|
||||||
learn_more_url="https://www.home-assistant.io/integrations/local_file/",
|
|
||||||
severity=ir.IssueSeverity.WARNING,
|
|
||||||
translation_key="deprecated_yaml",
|
|
||||||
translation_placeholders={
|
|
||||||
"domain": DOMAIN,
|
|
||||||
"integration_title": "Local file",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.async_create_task(
|
|
||||||
hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": SOURCE_IMPORT},
|
|
||||||
data=config,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LocalFile(Camera):
|
class LocalFile(Camera):
|
||||||
"""Representation of a local file camera."""
|
"""Representation of a local file camera."""
|
||||||
|
|
||||||
|
@@ -50,18 +50,12 @@ DATA_SCHEMA_SETUP = vol.Schema(
|
|||||||
|
|
||||||
CONFIG_FLOW = {
|
CONFIG_FLOW = {
|
||||||
"user": SchemaFlowFormStep(
|
"user": SchemaFlowFormStep(
|
||||||
schema=DATA_SCHEMA_SETUP,
|
schema=DATA_SCHEMA_SETUP, validate_user_input=validate_options
|
||||||
validate_user_input=validate_options,
|
)
|
||||||
),
|
|
||||||
"import": SchemaFlowFormStep(
|
|
||||||
schema=DATA_SCHEMA_SETUP,
|
|
||||||
validate_user_input=validate_options,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
OPTIONS_FLOW = {
|
OPTIONS_FLOW = {
|
||||||
"init": SchemaFlowFormStep(
|
"init": SchemaFlowFormStep(
|
||||||
DATA_SCHEMA_OPTIONS,
|
DATA_SCHEMA_OPTIONS, validate_user_input=validate_options
|
||||||
validate_user_input=validate_options,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -53,11 +53,5 @@
|
|||||||
"file_path_not_accessible": {
|
"file_path_not_accessible": {
|
||||||
"message": "Path {file_path} is not accessible"
|
"message": "Path {file_path} is not accessible"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"issues": {
|
|
||||||
"no_access_path": {
|
|
||||||
"title": "Incorrect file path",
|
|
||||||
"description": "While trying to import your configuration the provided file path {file_path} could not be read.\nPlease update your configuration to a correct file path and restart to fix this issue."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user