forked from home-assistant/core
Merge branch 'dev' into mqtt-subentry-entity_category
This commit is contained in:
5
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
5
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,15 +1,14 @@
|
|||||||
name: Report an issue with Home Assistant Core
|
name: Report an issue with Home Assistant Core
|
||||||
description: Report an issue with Home Assistant Core.
|
description: Report an issue with Home Assistant Core.
|
||||||
type: Bug
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
This issue form is for reporting bugs only!
|
This issue form is for reporting bugs only!
|
||||||
|
|
||||||
If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
|
If you have a feature or enhancement request, please [request them here instead][fr].
|
||||||
|
|
||||||
[fr]: https://community.home-assistant.io/c/feature-requests
|
[fr]: https://github.com/orgs/home-assistant/discussions
|
||||||
- type: textarea
|
- type: textarea
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -10,8 +10,8 @@ contact_links:
|
|||||||
url: https://www.home-assistant.io/help
|
url: https://www.home-assistant.io/help
|
||||||
about: We use GitHub for tracking bugs, check our website for resources on getting help.
|
about: We use GitHub for tracking bugs, check our website for resources on getting help.
|
||||||
- name: Feature Request
|
- name: Feature Request
|
||||||
url: https://community.home-assistant.io/c/feature-requests
|
url: https://github.com/orgs/home-assistant/discussions
|
||||||
about: Please use our Community Forum for making feature requests.
|
about: Please use this link to request new features or enhancements to existing features.
|
||||||
- name: I'm unsure where to go
|
- name: I'm unsure where to go
|
||||||
url: https://www.home-assistant.io/join-chat
|
url: https://www.home-assistant.io/join-chat
|
||||||
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
||||||
|
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -108,7 +108,7 @@ jobs:
|
|||||||
uses: dawidd6/action-download-artifact@v11
|
uses: dawidd6/action-download-artifact@v11
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
repo: home-assistant/intents-package
|
repo: OHF-Voice/intents-package
|
||||||
branch: main
|
branch: main
|
||||||
workflow: nightly.yaml
|
workflow: nightly.yaml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
|
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CACHE_VERSION: 2
|
CACHE_VERSION: 3
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2025.7"
|
HA_SHORT_VERSION: "2025.7"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.11.12
|
rev: v0.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
args:
|
args:
|
||||||
|
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -57,6 +57,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/aemet/ @Noltari
|
/tests/components/aemet/ @Noltari
|
||||||
/homeassistant/components/agent_dvr/ @ispysoftware
|
/homeassistant/components/agent_dvr/ @ispysoftware
|
||||||
/tests/components/agent_dvr/ @ispysoftware
|
/tests/components/agent_dvr/ @ispysoftware
|
||||||
|
/homeassistant/components/ai_task/ @home-assistant/core
|
||||||
|
/tests/components/ai_task/ @home-assistant/core
|
||||||
/homeassistant/components/air_quality/ @home-assistant/core
|
/homeassistant/components/air_quality/ @home-assistant/core
|
||||||
/tests/components/air_quality/ @home-assistant/core
|
/tests/components/air_quality/ @home-assistant/core
|
||||||
/homeassistant/components/airgradient/ @airgradienthq @joostlek
|
/homeassistant/components/airgradient/ @airgradienthq @joostlek
|
||||||
|
@@ -38,8 +38,7 @@ def validate_python() -> None:
|
|||||||
|
|
||||||
def ensure_config_path(config_dir: str) -> None:
|
def ensure_config_path(config_dir: str) -> None:
|
||||||
"""Validate the configuration directory."""
|
"""Validate the configuration directory."""
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from . import config as config_util # noqa: PLC0415
|
||||||
from . import config as config_util
|
|
||||||
|
|
||||||
lib_dir = os.path.join(config_dir, "deps")
|
lib_dir = os.path.join(config_dir, "deps")
|
||||||
|
|
||||||
@@ -80,8 +79,7 @@ def ensure_config_path(config_dir: str) -> None:
|
|||||||
|
|
||||||
def get_arguments() -> argparse.Namespace:
|
def get_arguments() -> argparse.Namespace:
|
||||||
"""Get parsed passed in arguments."""
|
"""Get parsed passed in arguments."""
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from . import config as config_util # noqa: PLC0415
|
||||||
from . import config as config_util
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Home Assistant: Observe, Control, Automate.",
|
description="Home Assistant: Observe, Control, Automate.",
|
||||||
@@ -177,8 +175,7 @@ def main() -> int:
|
|||||||
validate_os()
|
validate_os()
|
||||||
|
|
||||||
if args.script is not None:
|
if args.script is not None:
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from . import scripts # noqa: PLC0415
|
||||||
from . import scripts
|
|
||||||
|
|
||||||
return scripts.run(args.script)
|
return scripts.run(args.script)
|
||||||
|
|
||||||
@@ -188,8 +185,7 @@ def main() -> int:
|
|||||||
|
|
||||||
ensure_config_path(config_dir)
|
ensure_config_path(config_dir)
|
||||||
|
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from . import config, runner # noqa: PLC0415
|
||||||
from . import config, runner
|
|
||||||
|
|
||||||
safe_mode = config.safe_mode_enabled(config_dir)
|
safe_mode = config.safe_mode_enabled(config_dir)
|
||||||
|
|
||||||
|
@@ -52,28 +52,28 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def _generate_secret() -> str:
|
def _generate_secret() -> str:
|
||||||
"""Generate a secret."""
|
"""Generate a secret."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
return str(pyotp.random_base32())
|
return str(pyotp.random_base32())
|
||||||
|
|
||||||
|
|
||||||
def _generate_random() -> int:
|
def _generate_random() -> int:
|
||||||
"""Generate a 32 digit number."""
|
"""Generate a 32 digit number."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
return int(pyotp.random_base32(length=32, chars=list("1234567890")))
|
return int(pyotp.random_base32(length=32, chars=list("1234567890")))
|
||||||
|
|
||||||
|
|
||||||
def _generate_otp(secret: str, count: int) -> str:
|
def _generate_otp(secret: str, count: int) -> str:
|
||||||
"""Generate one time password."""
|
"""Generate one time password."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
return str(pyotp.HOTP(secret).at(count))
|
return str(pyotp.HOTP(secret).at(count))
|
||||||
|
|
||||||
|
|
||||||
def _verify_otp(secret: str, otp: str, count: int) -> bool:
|
def _verify_otp(secret: str, otp: str, count: int) -> bool:
|
||||||
"""Verify one time password."""
|
"""Verify one time password."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
return bool(pyotp.HOTP(secret).verify(otp, count))
|
return bool(pyotp.HOTP(secret).verify(otp, count))
|
||||||
|
|
||||||
|
@@ -37,7 +37,7 @@ DUMMY_SECRET = "FPPTH34D4E3MI2HG"
|
|||||||
|
|
||||||
def _generate_qr_code(data: str) -> str:
|
def _generate_qr_code(data: str) -> str:
|
||||||
"""Generate a base64 PNG string represent QR Code image of data."""
|
"""Generate a base64 PNG string represent QR Code image of data."""
|
||||||
import pyqrcode # pylint: disable=import-outside-toplevel
|
import pyqrcode # noqa: PLC0415
|
||||||
|
|
||||||
qr_code = pyqrcode.create(data)
|
qr_code = pyqrcode.create(data)
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ def _generate_qr_code(data: str) -> str:
|
|||||||
|
|
||||||
def _generate_secret_and_qr_code(username: str) -> tuple[str, str, str]:
|
def _generate_secret_and_qr_code(username: str) -> tuple[str, str, str]:
|
||||||
"""Generate a secret, url, and QR code."""
|
"""Generate a secret, url, and QR code."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
ota_secret = pyotp.random_base32()
|
ota_secret = pyotp.random_base32()
|
||||||
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
|
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
|
||||||
@@ -107,7 +107,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||||||
|
|
||||||
def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str:
|
def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str:
|
||||||
"""Create a ota_secret for user."""
|
"""Create a ota_secret for user."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
ota_secret: str = secret or pyotp.random_base32()
|
ota_secret: str = secret or pyotp.random_base32()
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||||||
|
|
||||||
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
||||||
"""Validate two factor authentication code."""
|
"""Validate two factor authentication code."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
if (ota_secret := self._users.get(user_id)) is None: # type: ignore[union-attr]
|
if (ota_secret := self._users.get(user_id)) is None: # type: ignore[union-attr]
|
||||||
# even we cannot find user, we still do verify
|
# even we cannot find user, we still do verify
|
||||||
@@ -196,7 +196,7 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]):
|
|||||||
Return self.async_show_form(step_id='init') if user_input is None.
|
Return self.async_show_form(step_id='init') if user_input is None.
|
||||||
Return self.async_create_entry(data={'result': result}) if finish.
|
Return self.async_create_entry(data={'result': result}) if finish.
|
||||||
"""
|
"""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
@@ -394,7 +394,7 @@ async def async_setup_hass(
|
|||||||
|
|
||||||
def open_hass_ui(hass: core.HomeAssistant) -> None:
|
def open_hass_ui(hass: core.HomeAssistant) -> None:
|
||||||
"""Open the UI."""
|
"""Open the UI."""
|
||||||
import webbrowser # pylint: disable=import-outside-toplevel
|
import webbrowser # noqa: PLC0415
|
||||||
|
|
||||||
if hass.config.api is None or "frontend" not in hass.config.components:
|
if hass.config.api is None or "frontend" not in hass.config.components:
|
||||||
_LOGGER.warning("Cannot launch the UI because frontend not loaded")
|
_LOGGER.warning("Cannot launch the UI because frontend not loaded")
|
||||||
@@ -561,8 +561,7 @@ async def async_enable_logging(
|
|||||||
|
|
||||||
if not log_no_color:
|
if not log_no_color:
|
||||||
try:
|
try:
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from colorlog import ColoredFormatter # noqa: PLC0415
|
||||||
from colorlog import ColoredFormatter
|
|
||||||
|
|
||||||
# basicConfig must be called after importing colorlog in order to
|
# basicConfig must be called after importing colorlog in order to
|
||||||
# ensure that the handlers it sets up wraps the correct streams.
|
# ensure that the handlers it sets up wraps the correct streams.
|
||||||
@@ -606,7 +605,7 @@ async def async_enable_logging(
|
|||||||
)
|
)
|
||||||
threading.excepthook = lambda args: logging.getLogger().exception(
|
threading.excepthook = lambda args: logging.getLogger().exception(
|
||||||
"Uncaught thread exception",
|
"Uncaught thread exception",
|
||||||
exc_info=( # type: ignore[arg-type]
|
exc_info=( # type: ignore[arg-type] # noqa: LOG014
|
||||||
args.exc_type,
|
args.exc_type,
|
||||||
args.exc_value,
|
args.exc_value,
|
||||||
args.exc_traceback,
|
args.exc_traceback,
|
||||||
@@ -1060,5 +1059,5 @@ async def _async_setup_multi_components(
|
|||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Error setting up integration %s - received exception",
|
"Error setting up integration %s - received exception",
|
||||||
domain,
|
domain,
|
||||||
exc_info=(type(result), result, result.__traceback__),
|
exc_info=(type(result), result, result.__traceback__), # noqa: LOG014
|
||||||
)
|
)
|
||||||
|
125
homeassistant/components/ai_task/__init__.py
Normal file
125
homeassistant/components/ai_task/__init__.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"""Integration to offer AI tasks to Home Assistant."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import (
|
||||||
|
HassJobType,
|
||||||
|
HomeAssistant,
|
||||||
|
ServiceCall,
|
||||||
|
ServiceResponse,
|
||||||
|
SupportsResponse,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers import config_validation as cv, storage
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
||||||
|
|
||||||
|
from .const import DATA_COMPONENT, DATA_PREFERENCES, DOMAIN, AITaskEntityFeature
|
||||||
|
from .entity import AITaskEntity
|
||||||
|
from .http import async_setup as async_setup_conversation_http
|
||||||
|
from .task import GenTextTask, GenTextTaskResult, async_generate_text
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DOMAIN",
|
||||||
|
"AITaskEntity",
|
||||||
|
"AITaskEntityFeature",
|
||||||
|
"GenTextTask",
|
||||||
|
"GenTextTaskResult",
|
||||||
|
"async_generate_text",
|
||||||
|
"async_setup",
|
||||||
|
"async_setup_entry",
|
||||||
|
"async_unload_entry",
|
||||||
|
]
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Register the process service."""
|
||||||
|
entity_component = EntityComponent[AITaskEntity](_LOGGER, DOMAIN, hass)
|
||||||
|
hass.data[DATA_COMPONENT] = entity_component
|
||||||
|
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
|
||||||
|
await hass.data[DATA_PREFERENCES].async_load()
|
||||||
|
async_setup_conversation_http(hass)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
"generate_text",
|
||||||
|
async_service_generate_text,
|
||||||
|
schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required("task_name"): cv.string,
|
||||||
|
vol.Optional("entity_id"): cv.entity_id,
|
||||||
|
vol.Required("instructions"): cv.string,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
supports_response=SupportsResponse.ONLY,
|
||||||
|
job_type=HassJobType.Coroutinefunction,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_service_generate_text(call: ServiceCall) -> ServiceResponse:
|
||||||
|
"""Run the run task service."""
|
||||||
|
result = await async_generate_text(hass=call.hass, **call.data)
|
||||||
|
return result.as_dict() # type: ignore[return-value]
|
||||||
|
|
||||||
|
|
||||||
|
class AITaskPreferences:
|
||||||
|
"""AI Task preferences."""
|
||||||
|
|
||||||
|
KEYS = ("gen_text_entity_id",)
|
||||||
|
|
||||||
|
gen_text_entity_id: str | None = None
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize the preferences."""
|
||||||
|
self._store: storage.Store[dict[str, str | None]] = storage.Store(
|
||||||
|
hass, 1, DOMAIN
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_load(self) -> None:
|
||||||
|
"""Load the data from the store."""
|
||||||
|
data = await self._store.async_load()
|
||||||
|
if data is None:
|
||||||
|
return
|
||||||
|
for key in self.KEYS:
|
||||||
|
setattr(self, key, data[key])
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_set_preferences(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
gen_text_entity_id: str | None | UndefinedType = UNDEFINED,
|
||||||
|
) -> None:
|
||||||
|
"""Set the preferences."""
|
||||||
|
changed = False
|
||||||
|
for key, value in (("gen_text_entity_id", gen_text_entity_id),):
|
||||||
|
if value is not UNDEFINED:
|
||||||
|
if getattr(self, key) != value:
|
||||||
|
setattr(self, key, value)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._store.async_delay_save(self.as_dict, 10)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def as_dict(self) -> dict[str, str | None]:
|
||||||
|
"""Get the current preferences."""
|
||||||
|
return {key: getattr(self, key) for key in self.KEYS}
|
29
homeassistant/components/ai_task/const.py
Normal file
29
homeassistant/components/ai_task/const.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
"""Constants for the AI Task integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import IntFlag
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
|
||||||
|
from . import AITaskPreferences
|
||||||
|
from .entity import AITaskEntity
|
||||||
|
|
||||||
|
DOMAIN = "ai_task"
|
||||||
|
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
|
||||||
|
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
|
||||||
|
|
||||||
|
DEFAULT_SYSTEM_PROMPT = (
|
||||||
|
"You are a Home Assistant expert and help users with their tasks."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AITaskEntityFeature(IntFlag):
|
||||||
|
"""Supported features of the AI task entity."""
|
||||||
|
|
||||||
|
GENERATE_TEXT = 1
|
||||||
|
"""Generate text based on instructions."""
|
103
homeassistant/components/ai_task/entity.py
Normal file
103
homeassistant/components/ai_task/entity.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"""Entity for the AI Task integration."""
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
import contextlib
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
from propcache.api import cached_property
|
||||||
|
|
||||||
|
from homeassistant.components.conversation import (
|
||||||
|
ChatLog,
|
||||||
|
UserContent,
|
||||||
|
async_get_chat_log,
|
||||||
|
)
|
||||||
|
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||||
|
from homeassistant.helpers import llm
|
||||||
|
from homeassistant.helpers.chat_session import async_get_chat_session
|
||||||
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from .const import DEFAULT_SYSTEM_PROMPT, DOMAIN, AITaskEntityFeature
|
||||||
|
from .task import GenTextTask, GenTextTaskResult
|
||||||
|
|
||||||
|
|
||||||
|
class AITaskEntity(RestoreEntity):
|
||||||
|
"""Entity that supports conversations."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
_attr_supported_features = AITaskEntityFeature(0)
|
||||||
|
__last_activity: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
@final
|
||||||
|
def state(self) -> str | None:
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
if self.__last_activity is None:
|
||||||
|
return None
|
||||||
|
return self.__last_activity
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def supported_features(self) -> AITaskEntityFeature:
|
||||||
|
"""Flag supported features."""
|
||||||
|
return self._attr_supported_features
|
||||||
|
|
||||||
|
async def async_internal_added_to_hass(self) -> None:
|
||||||
|
"""Call when the entity is added to hass."""
|
||||||
|
await super().async_internal_added_to_hass()
|
||||||
|
state = await self.async_get_last_state()
|
||||||
|
if (
|
||||||
|
state is not None
|
||||||
|
and state.state is not None
|
||||||
|
and state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||||
|
):
|
||||||
|
self.__last_activity = state.state
|
||||||
|
|
||||||
|
@final
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def _async_get_ai_task_chat_log(
|
||||||
|
self,
|
||||||
|
task: GenTextTask,
|
||||||
|
) -> AsyncGenerator[ChatLog]:
|
||||||
|
"""Context manager used to manage the ChatLog used during an AI Task."""
|
||||||
|
# pylint: disable-next=contextmanager-generator-missing-cleanup
|
||||||
|
with (
|
||||||
|
async_get_chat_session(self.hass) as session,
|
||||||
|
async_get_chat_log(
|
||||||
|
self.hass,
|
||||||
|
session,
|
||||||
|
None,
|
||||||
|
) as chat_log,
|
||||||
|
):
|
||||||
|
await chat_log.async_provide_llm_data(
|
||||||
|
llm.LLMContext(
|
||||||
|
platform=self.platform.domain,
|
||||||
|
context=None,
|
||||||
|
language=None,
|
||||||
|
assistant=DOMAIN,
|
||||||
|
device_id=None,
|
||||||
|
),
|
||||||
|
user_llm_prompt=DEFAULT_SYSTEM_PROMPT,
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_log.async_add_user_content(UserContent(task.instructions))
|
||||||
|
|
||||||
|
yield chat_log
|
||||||
|
|
||||||
|
@final
|
||||||
|
async def internal_async_generate_text(
|
||||||
|
self,
|
||||||
|
task: GenTextTask,
|
||||||
|
) -> GenTextTaskResult:
|
||||||
|
"""Run a gen text task."""
|
||||||
|
self.__last_activity = dt_util.utcnow().isoformat()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
async with self._async_get_ai_task_chat_log(task) as chat_log:
|
||||||
|
return await self._async_generate_text(task, chat_log)
|
||||||
|
|
||||||
|
async def _async_generate_text(
|
||||||
|
self,
|
||||||
|
task: GenTextTask,
|
||||||
|
chat_log: ChatLog,
|
||||||
|
) -> GenTextTaskResult:
|
||||||
|
"""Handle a gen text task."""
|
||||||
|
raise NotImplementedError
|
54
homeassistant/components/ai_task/http.py
Normal file
54
homeassistant/components/ai_task/http.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""HTTP endpoint for AI Task integration."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import websocket_api
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from .const import DATA_PREFERENCES
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_setup(hass: HomeAssistant) -> None:
|
||||||
|
"""Set up the HTTP API for the conversation integration."""
|
||||||
|
websocket_api.async_register_command(hass, websocket_get_preferences)
|
||||||
|
websocket_api.async_register_command(hass, websocket_set_preferences)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "ai_task/preferences/get",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@callback
|
||||||
|
def websocket_get_preferences(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Get AI task preferences."""
|
||||||
|
preferences = hass.data[DATA_PREFERENCES]
|
||||||
|
connection.send_result(msg["id"], preferences.as_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "ai_task/preferences/set",
|
||||||
|
vol.Optional("gen_text_entity_id"): vol.Any(str, None),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@callback
|
||||||
|
def websocket_set_preferences(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Set AI task preferences."""
|
||||||
|
preferences = hass.data[DATA_PREFERENCES]
|
||||||
|
msg.pop("type")
|
||||||
|
msg_id = msg.pop("id")
|
||||||
|
preferences.async_set_preferences(**msg)
|
||||||
|
connection.send_result(msg_id, preferences.as_dict())
|
7
homeassistant/components/ai_task/icons.json
Normal file
7
homeassistant/components/ai_task/icons.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"services": {
|
||||||
|
"generate_text": {
|
||||||
|
"service": "mdi:file-star-four-points-outline"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
homeassistant/components/ai_task/manifest.json
Normal file
9
homeassistant/components/ai_task/manifest.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"domain": "ai_task",
|
||||||
|
"name": "AI Task",
|
||||||
|
"codeowners": ["@home-assistant/core"],
|
||||||
|
"dependencies": ["conversation"],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/ai_task",
|
||||||
|
"integration_type": "system",
|
||||||
|
"quality_scale": "internal"
|
||||||
|
}
|
19
homeassistant/components/ai_task/services.yaml
Normal file
19
homeassistant/components/ai_task/services.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
generate_text:
|
||||||
|
fields:
|
||||||
|
task_name:
|
||||||
|
example: "home summary"
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
instructions:
|
||||||
|
example: "Generate a funny notification that garage door was left open"
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
entity_id:
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain: ai_task
|
||||||
|
supported_features:
|
||||||
|
- ai_task.AITaskEntityFeature.GENERATE_TEXT
|
22
homeassistant/components/ai_task/strings.json
Normal file
22
homeassistant/components/ai_task/strings.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"services": {
|
||||||
|
"generate_text": {
|
||||||
|
"name": "Generate text",
|
||||||
|
"description": "Use AI to run a task that generates text.",
|
||||||
|
"fields": {
|
||||||
|
"task_name": {
|
||||||
|
"name": "Task Name",
|
||||||
|
"description": "Name of the task."
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"name": "Instructions",
|
||||||
|
"description": "Instructions on what needs to be done."
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"name": "Entity ID",
|
||||||
|
"description": "Entity ID to run the task on. If not provided, the preferred entity will be used."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
homeassistant/components/ai_task/task.py
Normal file
71
homeassistant/components/ai_task/task.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
"""AI tasks to be handled by agents."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
|
||||||
|
|
||||||
|
|
||||||
|
async def async_generate_text(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
*,
|
||||||
|
task_name: str,
|
||||||
|
entity_id: str | None = None,
|
||||||
|
instructions: str,
|
||||||
|
) -> GenTextTaskResult:
|
||||||
|
"""Run a task in the AI Task integration."""
|
||||||
|
if entity_id is None:
|
||||||
|
entity_id = hass.data[DATA_PREFERENCES].gen_text_entity_id
|
||||||
|
|
||||||
|
if entity_id is None:
|
||||||
|
raise ValueError("No entity_id provided and no preferred entity set")
|
||||||
|
|
||||||
|
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
|
||||||
|
if entity is None:
|
||||||
|
raise ValueError(f"AI Task entity {entity_id} not found")
|
||||||
|
|
||||||
|
if AITaskEntityFeature.GENERATE_TEXT not in entity.supported_features:
|
||||||
|
raise ValueError(f"AI Task entity {entity_id} does not support generating text")
|
||||||
|
|
||||||
|
return await entity.internal_async_generate_text(
|
||||||
|
GenTextTask(
|
||||||
|
name=task_name,
|
||||||
|
instructions=instructions,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class GenTextTask:
|
||||||
|
"""Gen text task to be processed."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
"""Name of the task."""
|
||||||
|
|
||||||
|
instructions: str
|
||||||
|
"""Instructions on what needs to be done."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return task as a string."""
|
||||||
|
return f"<GenTextTask {self.name}: {id(self)}>"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class GenTextTaskResult:
|
||||||
|
"""Result of gen text task."""
|
||||||
|
|
||||||
|
conversation_id: str
|
||||||
|
"""Unique identifier for the conversation."""
|
||||||
|
|
||||||
|
text: str
|
||||||
|
"""Generated text."""
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, str]:
|
||||||
|
"""Return result as a dict."""
|
||||||
|
return {
|
||||||
|
"conversation_id": self.conversation_id,
|
||||||
|
"text": self.text,
|
||||||
|
}
|
@@ -39,14 +39,14 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
try:
|
try:
|
||||||
location_point_valid = await test_location(
|
location_point_valid = await check_location(
|
||||||
websession,
|
websession,
|
||||||
user_input["api_key"],
|
user_input["api_key"],
|
||||||
user_input["latitude"],
|
user_input["latitude"],
|
||||||
user_input["longitude"],
|
user_input["longitude"],
|
||||||
)
|
)
|
||||||
if not location_point_valid:
|
if not location_point_valid:
|
||||||
location_nearest_valid = await test_location(
|
location_nearest_valid = await check_location(
|
||||||
websession,
|
websession,
|
||||||
user_input["api_key"],
|
user_input["api_key"],
|
||||||
user_input["latitude"],
|
user_input["latitude"],
|
||||||
@@ -88,7 +88,7 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_location(
|
async def check_location(
|
||||||
client: ClientSession,
|
client: ClientSession,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
latitude: float,
|
latitude: float,
|
||||||
|
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["aioamazondevices==3.1.12"]
|
"requirements": ["aioamazondevices==3.1.14"]
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,23 @@
|
|||||||
"""Base class for assist satellite entities."""
|
"""Base class for assist satellite entities."""
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from hassil.util import (
|
||||||
|
PUNCTUATION_END,
|
||||||
|
PUNCTUATION_END_WORD,
|
||||||
|
PUNCTUATION_START,
|
||||||
|
PUNCTUATION_START_WORD,
|
||||||
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.http import StaticPathConfig
|
from homeassistant.components.http import StaticPathConfig
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
@@ -23,6 +33,7 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .entity import (
|
from .entity import (
|
||||||
AssistSatelliteAnnouncement,
|
AssistSatelliteAnnouncement,
|
||||||
|
AssistSatelliteAnswer,
|
||||||
AssistSatelliteConfiguration,
|
AssistSatelliteConfiguration,
|
||||||
AssistSatelliteEntity,
|
AssistSatelliteEntity,
|
||||||
AssistSatelliteEntityDescription,
|
AssistSatelliteEntityDescription,
|
||||||
@@ -34,6 +45,7 @@ from .websocket_api import async_register_websocket_api
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"DOMAIN",
|
"DOMAIN",
|
||||||
"AssistSatelliteAnnouncement",
|
"AssistSatelliteAnnouncement",
|
||||||
|
"AssistSatelliteAnswer",
|
||||||
"AssistSatelliteConfiguration",
|
"AssistSatelliteConfiguration",
|
||||||
"AssistSatelliteEntity",
|
"AssistSatelliteEntity",
|
||||||
"AssistSatelliteEntityDescription",
|
"AssistSatelliteEntityDescription",
|
||||||
@@ -86,6 +98,62 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
"async_internal_start_conversation",
|
"async_internal_start_conversation",
|
||||||
[AssistSatelliteEntityFeature.START_CONVERSATION],
|
[AssistSatelliteEntityFeature.START_CONVERSATION],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def handle_ask_question(call: ServiceCall) -> dict[str, Any]:
|
||||||
|
"""Handle a Show View service call."""
|
||||||
|
satellite_entity_id: str = call.data[ATTR_ENTITY_ID]
|
||||||
|
satellite_entity: AssistSatelliteEntity | None = component.get_entity(
|
||||||
|
satellite_entity_id
|
||||||
|
)
|
||||||
|
if satellite_entity is None:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
f"Invalid Assist satellite entity id: {satellite_entity_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
ask_question_args = {
|
||||||
|
"question": call.data.get("question"),
|
||||||
|
"question_media_id": call.data.get("question_media_id"),
|
||||||
|
"preannounce": call.data.get("preannounce", False),
|
||||||
|
"answers": call.data.get("answers"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if preannounce_media_id := call.data.get("preannounce_media_id"):
|
||||||
|
ask_question_args["preannounce_media_id"] = preannounce_media_id
|
||||||
|
|
||||||
|
answer = await satellite_entity.async_internal_ask_question(**ask_question_args)
|
||||||
|
|
||||||
|
if answer is None:
|
||||||
|
raise HomeAssistantError("No answer from satellite")
|
||||||
|
|
||||||
|
return asdict(answer)
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
domain=DOMAIN,
|
||||||
|
service="ask_question",
|
||||||
|
service_func=handle_ask_question,
|
||||||
|
schema=vol.All(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_ENTITY_ID): cv.entity_domain(DOMAIN),
|
||||||
|
vol.Optional("question"): str,
|
||||||
|
vol.Optional("question_media_id"): str,
|
||||||
|
vol.Optional("preannounce"): bool,
|
||||||
|
vol.Optional("preannounce_media_id"): str,
|
||||||
|
vol.Optional("answers"): [
|
||||||
|
{
|
||||||
|
vol.Required("id"): str,
|
||||||
|
vol.Required("sentences"): vol.All(
|
||||||
|
cv.ensure_list,
|
||||||
|
[cv.string],
|
||||||
|
has_one_non_empty_item,
|
||||||
|
has_no_punctuation,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cv.has_at_least_one_key("question", "question_media_id"),
|
||||||
|
),
|
||||||
|
supports_response=SupportsResponse.ONLY,
|
||||||
|
)
|
||||||
hass.data[CONNECTION_TEST_DATA] = {}
|
hass.data[CONNECTION_TEST_DATA] = {}
|
||||||
async_register_websocket_api(hass)
|
async_register_websocket_api(hass)
|
||||||
hass.http.register_view(ConnectionTestView())
|
hass.http.register_view(ConnectionTestView())
|
||||||
@@ -110,3 +178,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
def has_no_punctuation(value: list[str]) -> list[str]:
|
||||||
|
"""Validate result does not contain punctuation."""
|
||||||
|
for sentence in value:
|
||||||
|
if (
|
||||||
|
PUNCTUATION_START.search(sentence)
|
||||||
|
or PUNCTUATION_END.search(sentence)
|
||||||
|
or PUNCTUATION_START_WORD.search(sentence)
|
||||||
|
or PUNCTUATION_END_WORD.search(sentence)
|
||||||
|
):
|
||||||
|
raise vol.Invalid("sentence should not contain punctuation")
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def has_one_non_empty_item(value: list[str]) -> list[str]:
|
||||||
|
"""Validate result has at least one item."""
|
||||||
|
if len(value) < 1:
|
||||||
|
raise vol.Invalid("at least one sentence is required")
|
||||||
|
|
||||||
|
for sentence in value:
|
||||||
|
if not sentence:
|
||||||
|
raise vol.Invalid("sentences cannot be empty")
|
||||||
|
|
||||||
|
return value
|
||||||
|
@@ -4,12 +4,16 @@ from abc import abstractmethod
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import AsyncIterable
|
from collections.abc import AsyncIterable
|
||||||
import contextlib
|
import contextlib
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Any, Literal, final
|
from typing import Any, Literal, final
|
||||||
|
|
||||||
|
from hassil import Intents, recognize
|
||||||
|
from hassil.expression import Expression, ListReference, Sequence
|
||||||
|
from hassil.intents import WildcardSlotList
|
||||||
|
|
||||||
from homeassistant.components import conversation, media_source, stt, tts
|
from homeassistant.components import conversation, media_source, stt, tts
|
||||||
from homeassistant.components.assist_pipeline import (
|
from homeassistant.components.assist_pipeline import (
|
||||||
OPTION_PREFERRED,
|
OPTION_PREFERRED,
|
||||||
@@ -105,6 +109,20 @@ class AssistSatelliteAnnouncement:
|
|||||||
"""Media ID to be played before announcement."""
|
"""Media ID to be played before announcement."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AssistSatelliteAnswer:
|
||||||
|
"""Answer to a question."""
|
||||||
|
|
||||||
|
id: str | None
|
||||||
|
"""Matched answer id or None if no answer was matched."""
|
||||||
|
|
||||||
|
sentence: str
|
||||||
|
"""Raw sentence text from user response."""
|
||||||
|
|
||||||
|
slots: dict[str, Any] = field(default_factory=dict)
|
||||||
|
"""Matched slots from answer."""
|
||||||
|
|
||||||
|
|
||||||
class AssistSatelliteEntity(entity.Entity):
|
class AssistSatelliteEntity(entity.Entity):
|
||||||
"""Entity encapsulating the state and functionality of an Assist satellite."""
|
"""Entity encapsulating the state and functionality of an Assist satellite."""
|
||||||
|
|
||||||
@@ -122,6 +140,7 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
_wake_word_intercept_future: asyncio.Future[str | None] | None = None
|
_wake_word_intercept_future: asyncio.Future[str | None] | None = None
|
||||||
_attr_tts_options: dict[str, Any] | None = None
|
_attr_tts_options: dict[str, Any] | None = None
|
||||||
_pipeline_task: asyncio.Task | None = None
|
_pipeline_task: asyncio.Task | None = None
|
||||||
|
_ask_question_future: asyncio.Future[str | None] | None = None
|
||||||
|
|
||||||
__assist_satellite_state = AssistSatelliteState.IDLE
|
__assist_satellite_state = AssistSatelliteState.IDLE
|
||||||
|
|
||||||
@@ -309,6 +328,112 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
"""Start a conversation from the satellite."""
|
"""Start a conversation from the satellite."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def async_internal_ask_question(
|
||||||
|
self,
|
||||||
|
question: str | None = None,
|
||||||
|
question_media_id: str | None = None,
|
||||||
|
preannounce: bool = True,
|
||||||
|
preannounce_media_id: str = PREANNOUNCE_URL,
|
||||||
|
answers: list[dict[str, Any]] | None = None,
|
||||||
|
) -> AssistSatelliteAnswer | None:
|
||||||
|
"""Ask a question and get a user's response from the satellite.
|
||||||
|
|
||||||
|
If question_media_id is not provided, question is synthesized to audio
|
||||||
|
with the selected pipeline.
|
||||||
|
|
||||||
|
If question_media_id is provided, it is played directly. It is possible
|
||||||
|
to omit the message and the satellite will not show any text.
|
||||||
|
|
||||||
|
If preannounce is True, a sound is played before the start message or media.
|
||||||
|
If preannounce_media_id is provided, it overrides the default sound.
|
||||||
|
|
||||||
|
Calls async_start_conversation.
|
||||||
|
"""
|
||||||
|
await self._cancel_running_pipeline()
|
||||||
|
|
||||||
|
if question is None:
|
||||||
|
question = ""
|
||||||
|
|
||||||
|
announcement = await self._resolve_announcement_media_id(
|
||||||
|
question,
|
||||||
|
question_media_id,
|
||||||
|
preannounce_media_id=preannounce_media_id if preannounce else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._is_announcing:
|
||||||
|
raise SatelliteBusyError
|
||||||
|
|
||||||
|
self._is_announcing = True
|
||||||
|
self._set_state(AssistSatelliteState.RESPONDING)
|
||||||
|
self._ask_question_future = asyncio.Future()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Wait for announcement to finish
|
||||||
|
await self.async_start_conversation(announcement)
|
||||||
|
|
||||||
|
# Wait for response text
|
||||||
|
response_text = await self._ask_question_future
|
||||||
|
if response_text is None:
|
||||||
|
raise HomeAssistantError("No answer from question")
|
||||||
|
|
||||||
|
if not answers:
|
||||||
|
return AssistSatelliteAnswer(id=None, sentence=response_text)
|
||||||
|
|
||||||
|
return self._question_response_to_answer(response_text, answers)
|
||||||
|
finally:
|
||||||
|
self._is_announcing = False
|
||||||
|
self._set_state(AssistSatelliteState.IDLE)
|
||||||
|
self._ask_question_future = None
|
||||||
|
|
||||||
|
def _question_response_to_answer(
|
||||||
|
self, response_text: str, answers: list[dict[str, Any]]
|
||||||
|
) -> AssistSatelliteAnswer:
|
||||||
|
"""Match text to a pre-defined set of answers."""
|
||||||
|
|
||||||
|
# Build intents and match
|
||||||
|
intents = Intents.from_dict(
|
||||||
|
{
|
||||||
|
"language": self.hass.config.language,
|
||||||
|
"intents": {
|
||||||
|
"QuestionIntent": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"sentences": answer["sentences"],
|
||||||
|
"metadata": {"answer_id": answer["id"]},
|
||||||
|
}
|
||||||
|
for answer in answers
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assume slot list references are wildcards
|
||||||
|
wildcard_names: set[str] = set()
|
||||||
|
for intent in intents.intents.values():
|
||||||
|
for intent_data in intent.data:
|
||||||
|
for sentence in intent_data.sentences:
|
||||||
|
_collect_list_references(sentence, wildcard_names)
|
||||||
|
|
||||||
|
for wildcard_name in wildcard_names:
|
||||||
|
intents.slot_lists[wildcard_name] = WildcardSlotList(wildcard_name)
|
||||||
|
|
||||||
|
# Match response text
|
||||||
|
result = recognize(response_text, intents)
|
||||||
|
if result is None:
|
||||||
|
# No match
|
||||||
|
return AssistSatelliteAnswer(id=None, sentence=response_text)
|
||||||
|
|
||||||
|
assert result.intent_metadata
|
||||||
|
return AssistSatelliteAnswer(
|
||||||
|
id=result.intent_metadata["answer_id"],
|
||||||
|
sentence=response_text,
|
||||||
|
slots={
|
||||||
|
entity_name: entity.value
|
||||||
|
for entity_name, entity in result.entities.items()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def async_accept_pipeline_from_satellite(
|
async def async_accept_pipeline_from_satellite(
|
||||||
self,
|
self,
|
||||||
audio_stream: AsyncIterable[bytes],
|
audio_stream: AsyncIterable[bytes],
|
||||||
@@ -351,6 +476,11 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
self._internal_on_pipeline_event(PipelineEvent(PipelineEventType.RUN_END))
|
self._internal_on_pipeline_event(PipelineEvent(PipelineEventType.RUN_END))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if (self._ask_question_future is not None) and (
|
||||||
|
start_stage == PipelineStage.STT
|
||||||
|
):
|
||||||
|
end_stage = PipelineStage.STT
|
||||||
|
|
||||||
device_id = self.registry_entry.device_id if self.registry_entry else None
|
device_id = self.registry_entry.device_id if self.registry_entry else None
|
||||||
|
|
||||||
# Refresh context if necessary
|
# Refresh context if necessary
|
||||||
@@ -433,6 +563,16 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
self._set_state(AssistSatelliteState.IDLE)
|
self._set_state(AssistSatelliteState.IDLE)
|
||||||
elif event.type is PipelineEventType.STT_START:
|
elif event.type is PipelineEventType.STT_START:
|
||||||
self._set_state(AssistSatelliteState.LISTENING)
|
self._set_state(AssistSatelliteState.LISTENING)
|
||||||
|
elif event.type is PipelineEventType.STT_END:
|
||||||
|
# Intercepting text for ask question
|
||||||
|
if (
|
||||||
|
(self._ask_question_future is not None)
|
||||||
|
and (not self._ask_question_future.done())
|
||||||
|
and event.data
|
||||||
|
):
|
||||||
|
self._ask_question_future.set_result(
|
||||||
|
event.data.get("stt_output", {}).get("text")
|
||||||
|
)
|
||||||
elif event.type is PipelineEventType.INTENT_START:
|
elif event.type is PipelineEventType.INTENT_START:
|
||||||
self._set_state(AssistSatelliteState.PROCESSING)
|
self._set_state(AssistSatelliteState.PROCESSING)
|
||||||
elif event.type is PipelineEventType.TTS_START:
|
elif event.type is PipelineEventType.TTS_START:
|
||||||
@@ -443,6 +583,12 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
if not self._run_has_tts:
|
if not self._run_has_tts:
|
||||||
self._set_state(AssistSatelliteState.IDLE)
|
self._set_state(AssistSatelliteState.IDLE)
|
||||||
|
|
||||||
|
if (self._ask_question_future is not None) and (
|
||||||
|
not self._ask_question_future.done()
|
||||||
|
):
|
||||||
|
# No text for ask question
|
||||||
|
self._ask_question_future.set_result(None)
|
||||||
|
|
||||||
self.on_pipeline_event(event)
|
self.on_pipeline_event(event)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -577,3 +723,15 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
media_id_source=media_id_source,
|
media_id_source=media_id_source,
|
||||||
preannounce_media_id=preannounce_media_id,
|
preannounce_media_id=preannounce_media_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
|
||||||
|
"""Collect list reference names recursively."""
|
||||||
|
if isinstance(expression, Sequence):
|
||||||
|
seq: Sequence = expression
|
||||||
|
for item in seq.items:
|
||||||
|
_collect_list_references(item, list_names)
|
||||||
|
elif isinstance(expression, ListReference):
|
||||||
|
# {list}
|
||||||
|
list_ref: ListReference = expression
|
||||||
|
list_names.add(list_ref.slot_name)
|
||||||
|
@@ -10,6 +10,9 @@
|
|||||||
},
|
},
|
||||||
"start_conversation": {
|
"start_conversation": {
|
||||||
"service": "mdi:forum"
|
"service": "mdi:forum"
|
||||||
|
},
|
||||||
|
"ask_question": {
|
||||||
|
"service": "mdi:microphone-question"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,5 +5,6 @@
|
|||||||
"dependencies": ["assist_pipeline", "http", "stt", "tts"],
|
"dependencies": ["assist_pipeline", "http", "stt", "tts"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
||||||
"integration_type": "entity",
|
"integration_type": "entity",
|
||||||
"quality_scale": "internal"
|
"quality_scale": "internal",
|
||||||
|
"requirements": ["hassil==2.2.3"]
|
||||||
}
|
}
|
||||||
|
@@ -54,3 +54,35 @@ start_conversation:
|
|||||||
required: false
|
required: false
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
ask_question:
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain: assist_satellite
|
||||||
|
supported_features:
|
||||||
|
- assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION
|
||||||
|
question:
|
||||||
|
required: false
|
||||||
|
example: "What kind of music would you like to play?"
|
||||||
|
default: ""
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
question_media_id:
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
preannounce:
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
selector:
|
||||||
|
boolean:
|
||||||
|
preannounce_media_id:
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
answers:
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
object:
|
||||||
|
@@ -59,6 +59,36 @@
|
|||||||
"description": "Custom media ID to play before the start message or media."
|
"description": "Custom media ID to play before the start message or media."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ask_question": {
|
||||||
|
"name": "Ask question",
|
||||||
|
"description": "Asks a question and gets the user's response.",
|
||||||
|
"fields": {
|
||||||
|
"entity_id": {
|
||||||
|
"name": "Entity",
|
||||||
|
"description": "Assist satellite entity to ask the question on."
|
||||||
|
},
|
||||||
|
"question": {
|
||||||
|
"name": "Question",
|
||||||
|
"description": "The question to ask."
|
||||||
|
},
|
||||||
|
"question_media_id": {
|
||||||
|
"name": "Question media ID",
|
||||||
|
"description": "The media ID of the question to use instead of text-to-speech."
|
||||||
|
},
|
||||||
|
"preannounce": {
|
||||||
|
"name": "Preannounce",
|
||||||
|
"description": "Play a sound before the start message or media."
|
||||||
|
},
|
||||||
|
"preannounce_media_id": {
|
||||||
|
"name": "Preannounce media ID",
|
||||||
|
"description": "Custom media ID to play before the start message or media."
|
||||||
|
},
|
||||||
|
"answers": {
|
||||||
|
"name": "Answers",
|
||||||
|
"description": "Possible answers to the question."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@ DATA_BLUEPRINTS = "automation_blueprints"
|
|||||||
|
|
||||||
def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool:
|
def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool:
|
||||||
"""Return True if any automation references the blueprint."""
|
"""Return True if any automation references the blueprint."""
|
||||||
from . import automations_with_blueprint # pylint: disable=import-outside-toplevel
|
from . import automations_with_blueprint # noqa: PLC0415
|
||||||
|
|
||||||
return len(automations_with_blueprint(hass, blueprint_path)) > 0
|
return len(automations_with_blueprint(hass, blueprint_path)) > 0
|
||||||
|
|
||||||
@@ -28,8 +28,7 @@ async def _reload_blueprint_automations(
|
|||||||
@callback
|
@callback
|
||||||
def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints:
|
def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints:
|
||||||
"""Get automation blueprints."""
|
"""Get automation blueprints."""
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from .config import AUTOMATION_BLUEPRINT_SCHEMA # noqa: PLC0415
|
||||||
from .config import AUTOMATION_BLUEPRINT_SCHEMA
|
|
||||||
|
|
||||||
return blueprint.DomainBlueprints(
|
return blueprint.DomainBlueprints(
|
||||||
hass,
|
hass,
|
||||||
|
@@ -94,8 +94,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
if not with_hassio:
|
if not with_hassio:
|
||||||
reader_writer = CoreBackupReaderWriter(hass)
|
reader_writer = CoreBackupReaderWriter(hass)
|
||||||
else:
|
else:
|
||||||
# pylint: disable-next=import-outside-toplevel, hass-component-root-import
|
# pylint: disable-next=hass-component-root-import
|
||||||
from homeassistant.components.hassio.backup import SupervisorBackupReaderWriter
|
from homeassistant.components.hassio.backup import ( # noqa: PLC0415
|
||||||
|
SupervisorBackupReaderWriter,
|
||||||
|
)
|
||||||
|
|
||||||
reader_writer = SupervisorBackupReaderWriter(hass)
|
reader_writer = SupervisorBackupReaderWriter(hass)
|
||||||
|
|
||||||
|
@@ -240,6 +240,10 @@ async def _async_get_stream_image(
|
|||||||
height: int | None = None,
|
height: int | None = None,
|
||||||
wait_for_next_keyframe: bool = False,
|
wait_for_next_keyframe: bool = False,
|
||||||
) -> bytes | None:
|
) -> bytes | None:
|
||||||
|
if (provider := camera._webrtc_provider) and ( # noqa: SLF001
|
||||||
|
image := await provider.async_get_image(camera, width=width, height=height)
|
||||||
|
) is not None:
|
||||||
|
return image
|
||||||
if not camera.stream and CameraEntityFeature.STREAM in camera.supported_features:
|
if not camera.stream and CameraEntityFeature.STREAM in camera.supported_features:
|
||||||
camera.stream = await camera.async_create_stream()
|
camera.stream = await camera.async_create_stream()
|
||||||
if camera.stream:
|
if camera.stream:
|
||||||
|
@@ -156,6 +156,15 @@ class CameraWebRTCProvider(ABC):
|
|||||||
"""Close the session."""
|
"""Close the session."""
|
||||||
return ## This is an optional method so we need a default here.
|
return ## This is an optional method so we need a default here.
|
||||||
|
|
||||||
|
async def async_get_image(
|
||||||
|
self,
|
||||||
|
camera: Camera,
|
||||||
|
width: int | None = None,
|
||||||
|
height: int | None = None,
|
||||||
|
) -> bytes | None:
|
||||||
|
"""Get an image from the camera."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_register_webrtc_provider(
|
def async_register_webrtc_provider(
|
||||||
|
@@ -13,6 +13,6 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||||
"requirements": ["hass-nabucasa==0.102.0"],
|
"requirements": ["hass-nabucasa==0.103.0"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@@ -54,10 +54,10 @@ class Control4RuntimeData:
|
|||||||
type Control4ConfigEntry = ConfigEntry[Control4RuntimeData]
|
type Control4ConfigEntry = ConfigEntry[Control4RuntimeData]
|
||||||
|
|
||||||
|
|
||||||
async def call_c4_api_retry(func, *func_args):
|
async def call_c4_api_retry(func, *func_args): # noqa: RET503
|
||||||
"""Call C4 API function and retry on failure."""
|
"""Call C4 API function and retry on failure."""
|
||||||
# Ruff doesn't understand this loop - the exception is always raised after the retries
|
# Ruff doesn't understand this loop - the exception is always raised after the retries
|
||||||
for i in range(API_RETRY_TIMES): # noqa: RET503
|
for i in range(API_RETRY_TIMES):
|
||||||
try:
|
try:
|
||||||
return await func(*func_args)
|
return await func(*func_args)
|
||||||
except client_exceptions.ClientError as exception:
|
except client_exceptions.ClientError as exception:
|
||||||
|
@@ -271,7 +271,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Temporary migration. We can remove this in 2024.10
|
# Temporary migration. We can remove this in 2024.10
|
||||||
from homeassistant.components.assist_pipeline import ( # pylint: disable=import-outside-toplevel
|
from homeassistant.components.assist_pipeline import ( # noqa: PLC0415
|
||||||
async_migrate_engine,
|
async_migrate_engine,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -18,7 +18,7 @@ from homeassistant.core import Event, HomeAssistant
|
|||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers.device_registry import DeviceEntry
|
from homeassistant.helpers.device_registry import DeviceEntry
|
||||||
|
|
||||||
from .const import GATEWAY_SERIAL_PATTERN, PLATFORMS
|
from .const import DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS
|
||||||
|
|
||||||
type DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]]
|
type DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]]
|
||||||
|
|
||||||
@@ -32,10 +32,16 @@ async def async_setup_entry(
|
|||||||
credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid)
|
credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid)
|
||||||
|
|
||||||
if not credentials_valid:
|
if not credentials_valid:
|
||||||
raise ConfigEntryAuthFailed
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="invalid_auth",
|
||||||
|
)
|
||||||
|
|
||||||
if await hass.async_add_executor_job(mydevolo.maintenance):
|
if await hass.async_add_executor_job(mydevolo.maintenance):
|
||||||
raise ConfigEntryNotReady
|
raise ConfigEntryNotReady(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="maintenance",
|
||||||
|
)
|
||||||
|
|
||||||
gateway_ids = await hass.async_add_executor_job(mydevolo.get_gateway_ids)
|
gateway_ids = await hass.async_add_executor_job(mydevolo.get_gateway_ids)
|
||||||
|
|
||||||
@@ -69,7 +75,11 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
except GatewayOfflineError as err:
|
except GatewayOfflineError as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="connection_failed",
|
||||||
|
translation_placeholders={"gateway_id": gateway_id},
|
||||||
|
) from err
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
@@ -45,5 +45,16 @@
|
|||||||
"name": "Brightness"
|
"name": "Brightness"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"connection_failed": {
|
||||||
|
"message": "Failed to connect to devolo Home Control central unit {gateway_id}."
|
||||||
|
},
|
||||||
|
"invalid_auth": {
|
||||||
|
"message": "Authentication failed. Please re-authenticaticate with your mydevolo account."
|
||||||
|
},
|
||||||
|
"maintenance": {
|
||||||
|
"message": "devolo Home Control is currently in maintenance mode."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -108,8 +108,7 @@ def download_file(service: ServiceCall) -> None:
|
|||||||
_LOGGER.debug("%s -> %s", url, final_path)
|
_LOGGER.debug("%s -> %s", url, final_path)
|
||||||
|
|
||||||
with open(final_path, "wb") as fil:
|
with open(final_path, "wb") as fil:
|
||||||
for chunk in req.iter_content(1024):
|
fil.writelines(req.iter_content(1024))
|
||||||
fil.write(chunk)
|
|
||||||
|
|
||||||
_LOGGER.debug("Downloading of %s done", url)
|
_LOGGER.debug("Downloading of %s done", url)
|
||||||
service.hass.bus.fire(
|
service.hass.bus.fire(
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Generic
|
|
||||||
|
|
||||||
from deebot_client.capabilities import CapabilityEvent
|
from deebot_client.capabilities import CapabilityEvent
|
||||||
|
from deebot_client.events.base import Event
|
||||||
from deebot_client.events.water_info import MopAttachedEvent
|
from deebot_client.events.water_info import MopAttachedEvent
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
@@ -16,15 +16,14 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import EcovacsConfigEntry
|
from . import EcovacsConfigEntry
|
||||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
|
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity
|
||||||
from .util import get_supported_entities
|
from .util import get_supported_entities
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True, frozen=True)
|
@dataclass(kw_only=True, frozen=True)
|
||||||
class EcovacsBinarySensorEntityDescription(
|
class EcovacsBinarySensorEntityDescription[EventT: Event](
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
EcovacsCapabilityEntityDescription,
|
EcovacsCapabilityEntityDescription,
|
||||||
Generic[EventT],
|
|
||||||
):
|
):
|
||||||
"""Class describing Deebot binary sensor entity."""
|
"""Class describing Deebot binary sensor entity."""
|
||||||
|
|
||||||
@@ -55,7 +54,7 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EcovacsBinarySensor(
|
class EcovacsBinarySensor[EventT: Event](
|
||||||
EcovacsDescriptionEntity[CapabilityEvent[EventT]],
|
EcovacsDescriptionEntity[CapabilityEvent[EventT]],
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
):
|
):
|
||||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable, Coroutine
|
from collections.abc import Callable, Coroutine
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Generic, TypeVar
|
from typing import Any
|
||||||
|
|
||||||
from deebot_client.capabilities import Capabilities
|
from deebot_client.capabilities import Capabilities
|
||||||
from deebot_client.device import Device
|
from deebot_client.device import Device
|
||||||
@@ -18,11 +18,8 @@ from homeassistant.helpers.entity import Entity, EntityDescription
|
|||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
CapabilityEntity = TypeVar("CapabilityEntity")
|
|
||||||
EventT = TypeVar("EventT", bound=Event)
|
|
||||||
|
|
||||||
|
class EcovacsEntity[CapabilityEntityT](Entity):
|
||||||
class EcovacsEntity(Entity, Generic[CapabilityEntity]):
|
|
||||||
"""Ecovacs entity."""
|
"""Ecovacs entity."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
@@ -32,7 +29,7 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device: Device,
|
device: Device,
|
||||||
capability: CapabilityEntity,
|
capability: CapabilityEntityT,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize entity."""
|
"""Initialize entity."""
|
||||||
@@ -80,7 +77,7 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]):
|
|||||||
|
|
||||||
self._subscribe(AvailabilityEvent, on_available)
|
self._subscribe(AvailabilityEvent, on_available)
|
||||||
|
|
||||||
def _subscribe(
|
def _subscribe[EventT: Event](
|
||||||
self,
|
self,
|
||||||
event_type: type[EventT],
|
event_type: type[EventT],
|
||||||
callback: Callable[[EventT], Coroutine[Any, Any, None]],
|
callback: Callable[[EventT], Coroutine[Any, Any, None]],
|
||||||
@@ -98,13 +95,13 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]):
|
|||||||
self._device.events.request_refresh(event_type)
|
self._device.events.request_refresh(event_type)
|
||||||
|
|
||||||
|
|
||||||
class EcovacsDescriptionEntity(EcovacsEntity[CapabilityEntity]):
|
class EcovacsDescriptionEntity[CapabilityEntityT](EcovacsEntity[CapabilityEntityT]):
|
||||||
"""Ecovacs entity."""
|
"""Ecovacs entity."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device: Device,
|
device: Device,
|
||||||
capability: CapabilityEntity,
|
capability: CapabilityEntityT,
|
||||||
entity_description: EntityDescription,
|
entity_description: EntityDescription,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -114,13 +111,12 @@ class EcovacsDescriptionEntity(EcovacsEntity[CapabilityEntity]):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True, frozen=True)
|
@dataclass(kw_only=True, frozen=True)
|
||||||
class EcovacsCapabilityEntityDescription(
|
class EcovacsCapabilityEntityDescription[CapabilityEntityT](
|
||||||
EntityDescription,
|
EntityDescription,
|
||||||
Generic[CapabilityEntity],
|
|
||||||
):
|
):
|
||||||
"""Ecovacs entity description."""
|
"""Ecovacs entity description."""
|
||||||
|
|
||||||
capability_fn: Callable[[Capabilities], CapabilityEntity | None]
|
capability_fn: Callable[[Capabilities], CapabilityEntityT | None]
|
||||||
|
|
||||||
|
|
||||||
class EcovacsLegacyEntity(Entity):
|
class EcovacsLegacyEntity(Entity):
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"]
|
"requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"]
|
||||||
}
|
}
|
||||||
|
@@ -4,10 +4,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Generic
|
|
||||||
|
|
||||||
from deebot_client.capabilities import CapabilitySet
|
from deebot_client.capabilities import CapabilitySet
|
||||||
from deebot_client.events import CleanCountEvent, CutDirectionEvent, VolumeEvent
|
from deebot_client.events import CleanCountEvent, CutDirectionEvent, VolumeEvent
|
||||||
|
from deebot_client.events.base import Event
|
||||||
|
|
||||||
from homeassistant.components.number import (
|
from homeassistant.components.number import (
|
||||||
NumberEntity,
|
NumberEntity,
|
||||||
@@ -23,16 +23,14 @@ from .entity import (
|
|||||||
EcovacsCapabilityEntityDescription,
|
EcovacsCapabilityEntityDescription,
|
||||||
EcovacsDescriptionEntity,
|
EcovacsDescriptionEntity,
|
||||||
EcovacsEntity,
|
EcovacsEntity,
|
||||||
EventT,
|
|
||||||
)
|
)
|
||||||
from .util import get_supported_entities
|
from .util import get_supported_entities
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True, frozen=True)
|
@dataclass(kw_only=True, frozen=True)
|
||||||
class EcovacsNumberEntityDescription(
|
class EcovacsNumberEntityDescription[EventT: Event](
|
||||||
NumberEntityDescription,
|
NumberEntityDescription,
|
||||||
EcovacsCapabilityEntityDescription,
|
EcovacsCapabilityEntityDescription,
|
||||||
Generic[EventT],
|
|
||||||
):
|
):
|
||||||
"""Ecovacs number entity description."""
|
"""Ecovacs number entity description."""
|
||||||
|
|
||||||
@@ -94,7 +92,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class EcovacsNumberEntity(
|
class EcovacsNumberEntity[EventT: Event](
|
||||||
EcovacsDescriptionEntity[CapabilitySet[EventT, [int]]],
|
EcovacsDescriptionEntity[CapabilitySet[EventT, [int]]],
|
||||||
NumberEntity,
|
NumberEntity,
|
||||||
):
|
):
|
||||||
|
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Generic
|
from typing import Any
|
||||||
|
|
||||||
from deebot_client.capabilities import CapabilitySetTypes
|
from deebot_client.capabilities import CapabilitySetTypes
|
||||||
from deebot_client.device import Device
|
from deebot_client.device import Device
|
||||||
from deebot_client.events import WorkModeEvent
|
from deebot_client.events import WorkModeEvent
|
||||||
|
from deebot_client.events.base import Event
|
||||||
from deebot_client.events.water_info import WaterAmountEvent
|
from deebot_client.events.water_info import WaterAmountEvent
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||||
@@ -15,15 +16,14 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import EcovacsConfigEntry
|
from . import EcovacsConfigEntry
|
||||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
|
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity
|
||||||
from .util import get_name_key, get_supported_entities
|
from .util import get_name_key, get_supported_entities
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True, frozen=True)
|
@dataclass(kw_only=True, frozen=True)
|
||||||
class EcovacsSelectEntityDescription(
|
class EcovacsSelectEntityDescription[EventT: Event](
|
||||||
SelectEntityDescription,
|
SelectEntityDescription,
|
||||||
EcovacsCapabilityEntityDescription,
|
EcovacsCapabilityEntityDescription,
|
||||||
Generic[EventT],
|
|
||||||
):
|
):
|
||||||
"""Ecovacs select entity description."""
|
"""Ecovacs select entity description."""
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class EcovacsSelectEntity(
|
class EcovacsSelectEntity[EventT: Event](
|
||||||
EcovacsDescriptionEntity[CapabilitySetTypes[EventT, [str], str]],
|
EcovacsDescriptionEntity[CapabilitySetTypes[EventT, [str], str]],
|
||||||
SelectEntity,
|
SelectEntity,
|
||||||
):
|
):
|
||||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Generic
|
from typing import Any
|
||||||
|
|
||||||
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
|
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
|
||||||
from deebot_client.device import Device
|
from deebot_client.device import Device
|
||||||
@@ -46,16 +46,14 @@ from .entity import (
|
|||||||
EcovacsDescriptionEntity,
|
EcovacsDescriptionEntity,
|
||||||
EcovacsEntity,
|
EcovacsEntity,
|
||||||
EcovacsLegacyEntity,
|
EcovacsLegacyEntity,
|
||||||
EventT,
|
|
||||||
)
|
)
|
||||||
from .util import get_name_key, get_options, get_supported_entities
|
from .util import get_name_key, get_options, get_supported_entities
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True, frozen=True)
|
@dataclass(kw_only=True, frozen=True)
|
||||||
class EcovacsSensorEntityDescription(
|
class EcovacsSensorEntityDescription[EventT: Event](
|
||||||
EcovacsCapabilityEntityDescription,
|
EcovacsCapabilityEntityDescription,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
Generic[EventT],
|
|
||||||
):
|
):
|
||||||
"""Ecovacs sensor entity description."""
|
"""Ecovacs sensor entity description."""
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Generic, TypeVar, override
|
from typing import Any, override
|
||||||
|
|
||||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||||
from eheimdigital.device import EheimDigitalDevice
|
from eheimdigital.device import EheimDigitalDevice
|
||||||
@@ -30,16 +30,16 @@ from .entity import EheimDigitalEntity, exception_handler
|
|||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class EheimDigitalNumberDescription(NumberEntityDescription, Generic[_DeviceT_co]):
|
class EheimDigitalNumberDescription[_DeviceT: EheimDigitalDevice](
|
||||||
|
NumberEntityDescription
|
||||||
|
):
|
||||||
"""Class describing EHEIM Digital sensor entities."""
|
"""Class describing EHEIM Digital sensor entities."""
|
||||||
|
|
||||||
value_fn: Callable[[_DeviceT_co], float | None]
|
value_fn: Callable[[_DeviceT], float | None]
|
||||||
set_value_fn: Callable[[_DeviceT_co, float], Awaitable[None]]
|
set_value_fn: Callable[[_DeviceT, float], Awaitable[None]]
|
||||||
uom_fn: Callable[[_DeviceT_co], str] | None = None
|
uom_fn: Callable[[_DeviceT], str] | None = None
|
||||||
|
|
||||||
|
|
||||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||||
@@ -136,7 +136,7 @@ async def async_setup_entry(
|
|||||||
device_address: dict[str, EheimDigitalDevice],
|
device_address: dict[str, EheimDigitalDevice],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the number entities for one or multiple devices."""
|
"""Set up the number entities for one or multiple devices."""
|
||||||
entities: list[EheimDigitalNumber[EheimDigitalDevice]] = []
|
entities: list[EheimDigitalNumber[Any]] = []
|
||||||
for device in device_address.values():
|
for device in device_address.values():
|
||||||
if isinstance(device, EheimDigitalClassicVario):
|
if isinstance(device, EheimDigitalClassicVario):
|
||||||
entities.extend(
|
entities.extend(
|
||||||
@@ -163,18 +163,18 @@ async def async_setup_entry(
|
|||||||
async_setup_device_entities(coordinator.hub.devices)
|
async_setup_device_entities(coordinator.hub.devices)
|
||||||
|
|
||||||
|
|
||||||
class EheimDigitalNumber(
|
class EheimDigitalNumber[_DeviceT: EheimDigitalDevice](
|
||||||
EheimDigitalEntity[_DeviceT_co], NumberEntity, Generic[_DeviceT_co]
|
EheimDigitalEntity[_DeviceT], NumberEntity
|
||||||
):
|
):
|
||||||
"""Represent a EHEIM Digital number entity."""
|
"""Represent a EHEIM Digital number entity."""
|
||||||
|
|
||||||
entity_description: EheimDigitalNumberDescription[_DeviceT_co]
|
entity_description: EheimDigitalNumberDescription[_DeviceT]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: EheimDigitalUpdateCoordinator,
|
coordinator: EheimDigitalUpdateCoordinator,
|
||||||
device: _DeviceT_co,
|
device: _DeviceT,
|
||||||
description: EheimDigitalNumberDescription[_DeviceT_co],
|
description: EheimDigitalNumberDescription[_DeviceT],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an EHEIM Digital number entity."""
|
"""Initialize an EHEIM Digital number entity."""
|
||||||
super().__init__(coordinator, device)
|
super().__init__(coordinator, device)
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Generic, TypeVar, override
|
from typing import Any, override
|
||||||
|
|
||||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||||
from eheimdigital.device import EheimDigitalDevice
|
from eheimdigital.device import EheimDigitalDevice
|
||||||
@@ -17,15 +17,15 @@ from .entity import EheimDigitalEntity, exception_handler
|
|||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class EheimDigitalSelectDescription(SelectEntityDescription, Generic[_DeviceT_co]):
|
class EheimDigitalSelectDescription[_DeviceT: EheimDigitalDevice](
|
||||||
|
SelectEntityDescription
|
||||||
|
):
|
||||||
"""Class describing EHEIM Digital select entities."""
|
"""Class describing EHEIM Digital select entities."""
|
||||||
|
|
||||||
value_fn: Callable[[_DeviceT_co], str | None]
|
value_fn: Callable[[_DeviceT], str | None]
|
||||||
set_value_fn: Callable[[_DeviceT_co, str], Awaitable[None]]
|
set_value_fn: Callable[[_DeviceT, str], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||||
@@ -59,7 +59,7 @@ async def async_setup_entry(
|
|||||||
device_address: dict[str, EheimDigitalDevice],
|
device_address: dict[str, EheimDigitalDevice],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the number entities for one or multiple devices."""
|
"""Set up the number entities for one or multiple devices."""
|
||||||
entities: list[EheimDigitalSelect[EheimDigitalDevice]] = []
|
entities: list[EheimDigitalSelect[Any]] = []
|
||||||
for device in device_address.values():
|
for device in device_address.values():
|
||||||
if isinstance(device, EheimDigitalClassicVario):
|
if isinstance(device, EheimDigitalClassicVario):
|
||||||
entities.extend(
|
entities.extend(
|
||||||
@@ -75,18 +75,18 @@ async def async_setup_entry(
|
|||||||
async_setup_device_entities(coordinator.hub.devices)
|
async_setup_device_entities(coordinator.hub.devices)
|
||||||
|
|
||||||
|
|
||||||
class EheimDigitalSelect(
|
class EheimDigitalSelect[_DeviceT: EheimDigitalDevice](
|
||||||
EheimDigitalEntity[_DeviceT_co], SelectEntity, Generic[_DeviceT_co]
|
EheimDigitalEntity[_DeviceT], SelectEntity
|
||||||
):
|
):
|
||||||
"""Represent an EHEIM Digital select entity."""
|
"""Represent an EHEIM Digital select entity."""
|
||||||
|
|
||||||
entity_description: EheimDigitalSelectDescription[_DeviceT_co]
|
entity_description: EheimDigitalSelectDescription[_DeviceT]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: EheimDigitalUpdateCoordinator,
|
coordinator: EheimDigitalUpdateCoordinator,
|
||||||
device: _DeviceT_co,
|
device: _DeviceT,
|
||||||
description: EheimDigitalSelectDescription[_DeviceT_co],
|
description: EheimDigitalSelectDescription[_DeviceT],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an EHEIM Digital select entity."""
|
"""Initialize an EHEIM Digital select entity."""
|
||||||
super().__init__(coordinator, device)
|
super().__init__(coordinator, device)
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Generic, TypeVar, override
|
from typing import Any, override
|
||||||
|
|
||||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||||
from eheimdigital.device import EheimDigitalDevice
|
from eheimdigital.device import EheimDigitalDevice
|
||||||
@@ -20,14 +20,14 @@ from .entity import EheimDigitalEntity
|
|||||||
# Coordinator is used to centralize the data updates
|
# Coordinator is used to centralize the data updates
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class EheimDigitalSensorDescription(SensorEntityDescription, Generic[_DeviceT_co]):
|
class EheimDigitalSensorDescription[_DeviceT: EheimDigitalDevice](
|
||||||
|
SensorEntityDescription
|
||||||
|
):
|
||||||
"""Class describing EHEIM Digital sensor entities."""
|
"""Class describing EHEIM Digital sensor entities."""
|
||||||
|
|
||||||
value_fn: Callable[[_DeviceT_co], float | str | None]
|
value_fn: Callable[[_DeviceT], float | str | None]
|
||||||
|
|
||||||
|
|
||||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||||
@@ -75,7 +75,7 @@ async def async_setup_entry(
|
|||||||
device_address: dict[str, EheimDigitalDevice],
|
device_address: dict[str, EheimDigitalDevice],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the light entities for one or multiple devices."""
|
"""Set up the light entities for one or multiple devices."""
|
||||||
entities: list[EheimDigitalSensor[EheimDigitalDevice]] = []
|
entities: list[EheimDigitalSensor[Any]] = []
|
||||||
for device in device_address.values():
|
for device in device_address.values():
|
||||||
if isinstance(device, EheimDigitalClassicVario):
|
if isinstance(device, EheimDigitalClassicVario):
|
||||||
entities += [
|
entities += [
|
||||||
@@ -91,18 +91,18 @@ async def async_setup_entry(
|
|||||||
async_setup_device_entities(coordinator.hub.devices)
|
async_setup_device_entities(coordinator.hub.devices)
|
||||||
|
|
||||||
|
|
||||||
class EheimDigitalSensor(
|
class EheimDigitalSensor[_DeviceT: EheimDigitalDevice](
|
||||||
EheimDigitalEntity[_DeviceT_co], SensorEntity, Generic[_DeviceT_co]
|
EheimDigitalEntity[_DeviceT], SensorEntity
|
||||||
):
|
):
|
||||||
"""Represent a EHEIM Digital sensor entity."""
|
"""Represent a EHEIM Digital sensor entity."""
|
||||||
|
|
||||||
entity_description: EheimDigitalSensorDescription[_DeviceT_co]
|
entity_description: EheimDigitalSensorDescription[_DeviceT]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: EheimDigitalUpdateCoordinator,
|
coordinator: EheimDigitalUpdateCoordinator,
|
||||||
device: _DeviceT_co,
|
device: _DeviceT,
|
||||||
description: EheimDigitalSensorDescription[_DeviceT_co],
|
description: EheimDigitalSensorDescription[_DeviceT],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an EHEIM Digital number entity."""
|
"""Initialize an EHEIM Digital number entity."""
|
||||||
super().__init__(coordinator, device)
|
super().__init__(coordinator, device)
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import time
|
from datetime import time
|
||||||
from typing import Generic, TypeVar, final, override
|
from typing import Any, final, override
|
||||||
|
|
||||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||||
from eheimdigital.device import EheimDigitalDevice
|
from eheimdigital.device import EheimDigitalDevice
|
||||||
@@ -19,15 +19,13 @@ from .entity import EheimDigitalEntity, exception_handler
|
|||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class EheimDigitalTimeDescription(TimeEntityDescription, Generic[_DeviceT_co]):
|
class EheimDigitalTimeDescription[_DeviceT: EheimDigitalDevice](TimeEntityDescription):
|
||||||
"""Class describing EHEIM Digital time entities."""
|
"""Class describing EHEIM Digital time entities."""
|
||||||
|
|
||||||
value_fn: Callable[[_DeviceT_co], time | None]
|
value_fn: Callable[[_DeviceT], time | None]
|
||||||
set_value_fn: Callable[[_DeviceT_co, time], Awaitable[None]]
|
set_value_fn: Callable[[_DeviceT, time], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||||
@@ -79,7 +77,7 @@ async def async_setup_entry(
|
|||||||
device_address: dict[str, EheimDigitalDevice],
|
device_address: dict[str, EheimDigitalDevice],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the time entities for one or multiple devices."""
|
"""Set up the time entities for one or multiple devices."""
|
||||||
entities: list[EheimDigitalTime[EheimDigitalDevice]] = []
|
entities: list[EheimDigitalTime[Any]] = []
|
||||||
for device in device_address.values():
|
for device in device_address.values():
|
||||||
if isinstance(device, EheimDigitalClassicVario):
|
if isinstance(device, EheimDigitalClassicVario):
|
||||||
entities.extend(
|
entities.extend(
|
||||||
@@ -103,18 +101,18 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
class EheimDigitalTime(
|
class EheimDigitalTime[_DeviceT: EheimDigitalDevice](
|
||||||
EheimDigitalEntity[_DeviceT_co], TimeEntity, Generic[_DeviceT_co]
|
EheimDigitalEntity[_DeviceT], TimeEntity
|
||||||
):
|
):
|
||||||
"""Represent an EHEIM Digital time entity."""
|
"""Represent an EHEIM Digital time entity."""
|
||||||
|
|
||||||
entity_description: EheimDigitalTimeDescription[_DeviceT_co]
|
entity_description: EheimDigitalTimeDescription[_DeviceT]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: EheimDigitalUpdateCoordinator,
|
coordinator: EheimDigitalUpdateCoordinator,
|
||||||
device: _DeviceT_co,
|
device: _DeviceT,
|
||||||
description: EheimDigitalTimeDescription[_DeviceT_co],
|
description: EheimDigitalTimeDescription[_DeviceT],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an EHEIM Digital time entity."""
|
"""Initialize an EHEIM Digital time entity."""
|
||||||
super().__init__(coordinator, device)
|
super().__init__(coordinator, device)
|
||||||
|
@@ -60,6 +60,7 @@ _VOICE_ASSISTANT_EVENT_TYPES: EsphomeEnumMapper[
|
|||||||
VoiceAssistantEventType.VOICE_ASSISTANT_STT_START: PipelineEventType.STT_START,
|
VoiceAssistantEventType.VOICE_ASSISTANT_STT_START: PipelineEventType.STT_START,
|
||||||
VoiceAssistantEventType.VOICE_ASSISTANT_STT_END: PipelineEventType.STT_END,
|
VoiceAssistantEventType.VOICE_ASSISTANT_STT_END: PipelineEventType.STT_END,
|
||||||
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_START: PipelineEventType.INTENT_START,
|
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_START: PipelineEventType.INTENT_START,
|
||||||
|
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS: PipelineEventType.INTENT_PROGRESS,
|
||||||
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END: PipelineEventType.INTENT_END,
|
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END: PipelineEventType.INTENT_END,
|
||||||
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_START: PipelineEventType.TTS_START,
|
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_START: PipelineEventType.TTS_START,
|
||||||
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: PipelineEventType.TTS_END,
|
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: PipelineEventType.TTS_END,
|
||||||
@@ -282,6 +283,12 @@ class EsphomeAssistSatellite(
|
|||||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_END:
|
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_END:
|
||||||
assert event.data is not None
|
assert event.data is not None
|
||||||
data_to_send = {"text": event.data["stt_output"]["text"]}
|
data_to_send = {"text": event.data["stt_output"]["text"]}
|
||||||
|
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS:
|
||||||
|
data_to_send = {
|
||||||
|
"tts_start_streaming": "1"
|
||||||
|
if (event.data and event.data.get("tts_start_streaming"))
|
||||||
|
else "0",
|
||||||
|
}
|
||||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END:
|
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END:
|
||||||
assert event.data is not None
|
assert event.data is not None
|
||||||
data_to_send = {
|
data_to_send = {
|
||||||
|
@@ -63,9 +63,7 @@ class ESPHomeDashboardManager:
|
|||||||
if not (data := self._data) or not (info := data.get("info")):
|
if not (data := self._data) or not (info := data.get("info")):
|
||||||
return
|
return
|
||||||
if is_hassio(self._hass):
|
if is_hassio(self._hass):
|
||||||
from homeassistant.components.hassio import ( # pylint: disable=import-outside-toplevel
|
from homeassistant.components.hassio import get_addons_info # noqa: PLC0415
|
||||||
get_addons_info,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (addons := get_addons_info(self._hass)) is not None and info[
|
if (addons := get_addons_info(self._hass)) is not None and info[
|
||||||
"addon_slug"
|
"addon_slug"
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
"mqtt": ["esphome/discover/#"],
|
"mqtt": ["esphome/discover/#"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aioesphomeapi==32.2.1",
|
"aioesphomeapi==32.2.4",
|
||||||
"esphome-dashboard-api==1.3.0",
|
"esphome-dashboard-api==1.3.0",
|
||||||
"bleak-esphome==2.16.0"
|
"bleak-esphome==2.16.0"
|
||||||
],
|
],
|
||||||
|
@@ -364,8 +364,7 @@ def _frontend_root(dev_repo_path: str | None) -> pathlib.Path:
|
|||||||
if dev_repo_path is not None:
|
if dev_repo_path is not None:
|
||||||
return pathlib.Path(dev_repo_path) / "hass_frontend"
|
return pathlib.Path(dev_repo_path) / "hass_frontend"
|
||||||
# Keep import here so that we can import frontend without installing reqs
|
# Keep import here so that we can import frontend without installing reqs
|
||||||
# pylint: disable-next=import-outside-toplevel
|
import hass_frontend # noqa: PLC0415
|
||||||
import hass_frontend
|
|
||||||
|
|
||||||
return hass_frontend.where()
|
return hass_frontend.where()
|
||||||
|
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
"""The go2rtc component."""
|
"""The go2rtc component."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from aiohttp import ClientSession
|
||||||
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
from go2rtc_client import Go2RtcRestClient
|
from go2rtc_client import Go2RtcRestClient
|
||||||
@@ -32,7 +35,7 @@ from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOM
|
|||||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||||
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import Event, HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
config_validation as cv,
|
config_validation as cv,
|
||||||
discovery_flow,
|
discovery_flow,
|
||||||
@@ -98,6 +101,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
|
|
||||||
_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN)
|
_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN)
|
||||||
_RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError)
|
_RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError)
|
||||||
|
type Go2RtcConfigEntry = ConfigEntry[WebRTCProvider]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
@@ -151,13 +155,14 @@ async def _remove_go2rtc_entries(hass: HomeAssistant) -> None:
|
|||||||
await hass.config_entries.async_remove(entry.entry_id)
|
await hass.config_entries.async_remove(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: Go2RtcConfigEntry) -> bool:
|
||||||
"""Set up go2rtc from a config entry."""
|
"""Set up go2rtc from a config entry."""
|
||||||
url = hass.data[_DATA_GO2RTC]
|
|
||||||
|
|
||||||
|
url = hass.data[_DATA_GO2RTC]
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
client = Go2RtcRestClient(session, url)
|
||||||
# Validate the server URL
|
# Validate the server URL
|
||||||
try:
|
try:
|
||||||
client = Go2RtcRestClient(async_get_clientsession(hass), url)
|
|
||||||
version = await client.validate_server_version()
|
version = await client.validate_server_version()
|
||||||
if version < AwesomeVersion(RECOMMENDED_VERSION):
|
if version < AwesomeVersion(RECOMMENDED_VERSION):
|
||||||
ir.async_create_issue(
|
ir.async_create_issue(
|
||||||
@@ -188,13 +193,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
|
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
provider = WebRTCProvider(hass, url)
|
provider = entry.runtime_data = WebRTCProvider(hass, url, session, client)
|
||||||
async_register_webrtc_provider(hass, provider)
|
entry.async_on_unload(async_register_webrtc_provider(hass, provider))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: Go2RtcConfigEntry) -> bool:
|
||||||
"""Unload a go2rtc config entry."""
|
"""Unload a go2rtc config entry."""
|
||||||
|
await entry.runtime_data.teardown()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -206,12 +212,18 @@ async def _get_binary(hass: HomeAssistant) -> str | None:
|
|||||||
class WebRTCProvider(CameraWebRTCProvider):
|
class WebRTCProvider(CameraWebRTCProvider):
|
||||||
"""WebRTC provider."""
|
"""WebRTC provider."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, url: str) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
url: str,
|
||||||
|
session: ClientSession,
|
||||||
|
rest_client: Go2RtcRestClient,
|
||||||
|
) -> None:
|
||||||
"""Initialize the WebRTC provider."""
|
"""Initialize the WebRTC provider."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._url = url
|
self._url = url
|
||||||
self._session = async_get_clientsession(hass)
|
self._session = session
|
||||||
self._rest_client = Go2RtcRestClient(self._session, url)
|
self._rest_client = rest_client
|
||||||
self._sessions: dict[str, Go2RtcWsClient] = {}
|
self._sessions: dict[str, Go2RtcWsClient] = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -232,30 +244,14 @@ class WebRTCProvider(CameraWebRTCProvider):
|
|||||||
send_message: WebRTCSendMessage,
|
send_message: WebRTCSendMessage,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle the WebRTC offer and return the answer via the provided callback."""
|
"""Handle the WebRTC offer and return the answer via the provided callback."""
|
||||||
self._sessions[session_id] = ws_client = Go2RtcWsClient(
|
try:
|
||||||
self._session, self._url, source=camera.entity_id
|
await self._update_stream_source(camera)
|
||||||
)
|
except HomeAssistantError as err:
|
||||||
|
send_message(WebRTCError("go2rtc_webrtc_offer_failed", str(err)))
|
||||||
if not (stream_source := await camera.stream_source()):
|
|
||||||
send_message(
|
|
||||||
WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source")
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
streams = await self._rest_client.streams.list()
|
self._sessions[session_id] = ws_client = Go2RtcWsClient(
|
||||||
|
self._session, self._url, source=camera.entity_id
|
||||||
if (stream := streams.get(camera.entity_id)) is None or not any(
|
|
||||||
stream_source == producer.url for producer in stream.producers
|
|
||||||
):
|
|
||||||
await self._rest_client.streams.add(
|
|
||||||
camera.entity_id,
|
|
||||||
[
|
|
||||||
stream_source,
|
|
||||||
# We are setting any ffmpeg rtsp related logs to debug
|
|
||||||
# Connection problems to the camera will be logged by the first stream
|
|
||||||
# Therefore setting it to debug will not hide any important logs
|
|
||||||
f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -291,3 +287,48 @@ class WebRTCProvider(CameraWebRTCProvider):
|
|||||||
"""Close the session."""
|
"""Close the session."""
|
||||||
ws_client = self._sessions.pop(session_id)
|
ws_client = self._sessions.pop(session_id)
|
||||||
self._hass.async_create_task(ws_client.close())
|
self._hass.async_create_task(ws_client.close())
|
||||||
|
|
||||||
|
async def async_get_image(
|
||||||
|
self,
|
||||||
|
camera: Camera,
|
||||||
|
width: int | None = None,
|
||||||
|
height: int | None = None,
|
||||||
|
) -> bytes | None:
|
||||||
|
"""Get an image from the camera."""
|
||||||
|
await self._update_stream_source(camera)
|
||||||
|
return await self._rest_client.get_jpeg_snapshot(
|
||||||
|
camera.entity_id, width, height
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _update_stream_source(self, camera: Camera) -> None:
|
||||||
|
"""Update the stream source in go2rtc config if needed."""
|
||||||
|
if not (stream_source := await camera.stream_source()):
|
||||||
|
await self.teardown()
|
||||||
|
raise HomeAssistantError("Camera has no stream source")
|
||||||
|
|
||||||
|
if not self.async_is_supported(stream_source):
|
||||||
|
await self.teardown()
|
||||||
|
raise HomeAssistantError("Stream source is not supported by go2rtc")
|
||||||
|
|
||||||
|
streams = await self._rest_client.streams.list()
|
||||||
|
|
||||||
|
if (stream := streams.get(camera.entity_id)) is None or not any(
|
||||||
|
stream_source == producer.url for producer in stream.producers
|
||||||
|
):
|
||||||
|
await self._rest_client.streams.add(
|
||||||
|
camera.entity_id,
|
||||||
|
[
|
||||||
|
stream_source,
|
||||||
|
# We are setting any ffmpeg rtsp related logs to debug
|
||||||
|
# Connection problems to the camera will be logged by the first stream
|
||||||
|
# Therefore setting it to debug will not hide any important logs
|
||||||
|
f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
|
||||||
|
f"ffmpeg:{camera.entity_id}#video=mjpeg",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def teardown(self) -> None:
|
||||||
|
"""Tear down the provider."""
|
||||||
|
for ws_client in self._sessions.values():
|
||||||
|
await ws_client.close()
|
||||||
|
self._sessions.clear()
|
||||||
|
@@ -212,8 +212,7 @@ class AbstractConfig(ABC):
|
|||||||
def async_enable_report_state(self) -> None:
|
def async_enable_report_state(self) -> None:
|
||||||
"""Enable proactive mode."""
|
"""Enable proactive mode."""
|
||||||
# Circular dep
|
# Circular dep
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from .report_state import async_enable_report_state # noqa: PLC0415
|
||||||
from .report_state import async_enable_report_state
|
|
||||||
|
|
||||||
if self._unsub_report_state is None:
|
if self._unsub_report_state is None:
|
||||||
self._unsub_report_state = async_enable_report_state(self.hass, self)
|
self._unsub_report_state = async_enable_report_state(self.hass, self)
|
||||||
@@ -395,8 +394,7 @@ class AbstractConfig(ABC):
|
|||||||
async def _handle_local_webhook(self, hass, webhook_id, request):
|
async def _handle_local_webhook(self, hass, webhook_id, request):
|
||||||
"""Handle an incoming local SDK message."""
|
"""Handle an incoming local SDK message."""
|
||||||
# Circular dep
|
# Circular dep
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from . import smart_home # noqa: PLC0415
|
||||||
from . import smart_home
|
|
||||||
|
|
||||||
self._local_last_active = utcnow()
|
self._local_last_active = utcnow()
|
||||||
|
|
||||||
@@ -655,8 +653,9 @@ class GoogleEntity:
|
|||||||
if "matter" in self.hass.config.components and any(
|
if "matter" in self.hass.config.components and any(
|
||||||
x for x in device_entry.identifiers if x[0] == "matter"
|
x for x in device_entry.identifiers if x[0] == "matter"
|
||||||
):
|
):
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from homeassistant.components.matter import ( # noqa: PLC0415
|
||||||
from homeassistant.components.matter import get_matter_device_info
|
get_matter_device_info,
|
||||||
|
)
|
||||||
|
|
||||||
# Import matter can block the event loop for multiple seconds
|
# Import matter can block the event loop for multiple seconds
|
||||||
# so we import it here to avoid blocking the event loop during
|
# so we import it here to avoid blocking the event loop during
|
||||||
|
@@ -29,8 +29,7 @@ async def update_addon(
|
|||||||
client = get_supervisor_client(hass)
|
client = get_supervisor_client(hass)
|
||||||
|
|
||||||
if backup:
|
if backup:
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from .backup import backup_addon_before_update # noqa: PLC0415
|
||||||
from .backup import backup_addon_before_update
|
|
||||||
|
|
||||||
await backup_addon_before_update(hass, addon, addon_name, installed_version)
|
await backup_addon_before_update(hass, addon, addon_name, installed_version)
|
||||||
|
|
||||||
@@ -50,8 +49,7 @@ async def update_core(hass: HomeAssistant, version: str | None, backup: bool) ->
|
|||||||
client = get_supervisor_client(hass)
|
client = get_supervisor_client(hass)
|
||||||
|
|
||||||
if backup:
|
if backup:
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from .backup import backup_core_before_update # noqa: PLC0415
|
||||||
from .backup import backup_core_before_update
|
|
||||||
|
|
||||||
await backup_core_before_update(hass)
|
await backup_core_before_update(hass)
|
||||||
|
|
||||||
@@ -71,8 +69,7 @@ async def update_os(hass: HomeAssistant, version: str | None, backup: bool) -> N
|
|||||||
client = get_supervisor_client(hass)
|
client = get_supervisor_client(hass)
|
||||||
|
|
||||||
if backup:
|
if backup:
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from .backup import backup_core_before_update # noqa: PLC0415
|
||||||
from .backup import backup_core_before_update
|
|
||||||
|
|
||||||
await backup_core_before_update(hass)
|
await backup_core_before_update(hass)
|
||||||
|
|
||||||
|
@@ -100,9 +100,11 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
|||||||
try:
|
try:
|
||||||
response = await self._api.route(
|
response = await self._api.route(
|
||||||
transport_mode=TransportMode(params.travel_mode),
|
transport_mode=TransportMode(params.travel_mode),
|
||||||
origin=here_routing.Place(params.origin[0], params.origin[1]),
|
origin=here_routing.Place(
|
||||||
|
float(params.origin[0]), float(params.origin[1])
|
||||||
|
),
|
||||||
destination=here_routing.Place(
|
destination=here_routing.Place(
|
||||||
params.destination[0], params.destination[1]
|
float(params.destination[0]), float(params.destination[1])
|
||||||
),
|
),
|
||||||
routing_mode=params.route_mode,
|
routing_mode=params.route_mode,
|
||||||
arrival_time=params.arrival,
|
arrival_time=params.arrival,
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/here_travel_time",
|
"documentation": "https://www.home-assistant.io/integrations/here_travel_time",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"],
|
"loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"],
|
||||||
"requirements": ["here-routing==1.0.1", "here-transit==1.2.1"]
|
"requirements": ["here-routing==1.2.0", "here-transit==1.2.1"]
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,8 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from here_routing import RoutingMode
|
||||||
|
|
||||||
|
|
||||||
class HERETravelTimeData(TypedDict):
|
class HERETravelTimeData(TypedDict):
|
||||||
"""Routing information."""
|
"""Routing information."""
|
||||||
@@ -27,6 +29,6 @@ class HERETravelTimeAPIParams:
|
|||||||
destination: list[str]
|
destination: list[str]
|
||||||
origin: list[str]
|
origin: list[str]
|
||||||
travel_mode: str
|
travel_mode: str
|
||||||
route_mode: str
|
route_mode: RoutingMode
|
||||||
arrival: datetime | None
|
arrival: datetime | None
|
||||||
departure: datetime | None
|
departure: datetime | None
|
||||||
|
@@ -22,6 +22,6 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aiohomeconnect"],
|
"loggers": ["aiohomeconnect"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aiohomeconnect==0.18.0"],
|
"requirements": ["aiohomeconnect==0.18.1"],
|
||||||
"zeroconf": ["_homeconnect._tcp.local."]
|
"zeroconf": ["_homeconnect._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -309,8 +309,7 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
|||||||
|
|
||||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||||
"""Set up the options flow."""
|
"""Set up the options flow."""
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||||
from homeassistant.components.zha.radio_manager import (
|
|
||||||
ZhaMultiPANMigrationHelper,
|
ZhaMultiPANMigrationHelper,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -451,16 +450,11 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
|||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Configure the Silicon Labs Multiprotocol add-on."""
|
"""Configure the Silicon Labs Multiprotocol add-on."""
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
|
||||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN
|
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||||
|
|
||||||
# pylint: disable-next=import-outside-toplevel
|
|
||||||
from homeassistant.components.zha.radio_manager import (
|
|
||||||
ZhaMultiPANMigrationHelper,
|
ZhaMultiPANMigrationHelper,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.zha.silabs_multiprotocol import ( # noqa: PLC0415
|
||||||
# pylint: disable-next=import-outside-toplevel
|
|
||||||
from homeassistant.components.zha.silabs_multiprotocol import (
|
|
||||||
async_get_channel as async_get_zha_channel,
|
async_get_channel as async_get_zha_channel,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -747,11 +741,8 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
|||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Perform initial backup and reconfigure ZHA."""
|
"""Perform initial backup and reconfigure ZHA."""
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
|
||||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN
|
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||||
|
|
||||||
# pylint: disable-next=import-outside-toplevel
|
|
||||||
from homeassistant.components.zha.radio_manager import (
|
|
||||||
ZhaMultiPANMigrationHelper,
|
ZhaMultiPANMigrationHelper,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeeConfigEntry) -> boo
|
|||||||
entry.runtime_data = homee
|
entry.runtime_data = homee
|
||||||
entry.async_on_unload(homee.disconnect)
|
entry.async_on_unload(homee.disconnect)
|
||||||
|
|
||||||
def _connection_update_callback(connected: bool) -> None:
|
async def _connection_update_callback(connected: bool) -> None:
|
||||||
"""Call when the device is notified of changes."""
|
"""Call when the device is notified of changes."""
|
||||||
if connected:
|
if connected:
|
||||||
_LOGGER.warning("Reconnected to Homee at %s", entry.data[CONF_HOST])
|
_LOGGER.warning("Reconnected to Homee at %s", entry.data[CONF_HOST])
|
||||||
|
@@ -28,6 +28,7 @@ class HomeeEntity(Entity):
|
|||||||
self._entry = entry
|
self._entry = entry
|
||||||
node = entry.runtime_data.get_node_by_id(attribute.node_id)
|
node = entry.runtime_data.get_node_by_id(attribute.node_id)
|
||||||
# Homee hub itself has node-id -1
|
# Homee hub itself has node-id -1
|
||||||
|
assert node is not None
|
||||||
if node.id == -1:
|
if node.id == -1:
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, entry.runtime_data.settings.uid)},
|
identifiers={(DOMAIN, entry.runtime_data.settings.uid)},
|
||||||
@@ -41,6 +42,8 @@ class HomeeEntity(Entity):
|
|||||||
model=get_name_for_enum(NodeProfile, node.profile),
|
model=get_name_for_enum(NodeProfile, node.profile),
|
||||||
via_device=(DOMAIN, entry.runtime_data.settings.uid),
|
via_device=(DOMAIN, entry.runtime_data.settings.uid),
|
||||||
)
|
)
|
||||||
|
if attribute.name:
|
||||||
|
self._attr_name = attribute.name
|
||||||
|
|
||||||
self._host_connected = entry.runtime_data.connected
|
self._host_connected = entry.runtime_data.connected
|
||||||
|
|
||||||
@@ -79,7 +82,7 @@ class HomeeEntity(Entity):
|
|||||||
def _on_node_updated(self, attribute: HomeeAttribute) -> None:
|
def _on_node_updated(self, attribute: HomeeAttribute) -> None:
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _on_connection_changed(self, connected: bool) -> None:
|
async def _on_connection_changed(self, connected: bool) -> None:
|
||||||
self._host_connected = connected
|
self._host_connected = connected
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
@@ -166,6 +169,6 @@ class HomeeNodeEntity(Entity):
|
|||||||
def _on_node_updated(self, node: HomeeNode) -> None:
|
def _on_node_updated(self, node: HomeeNode) -> None:
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _on_connection_changed(self, connected: bool) -> None:
|
async def _on_connection_changed(self, connected: bool) -> None:
|
||||||
self._host_connected = connected
|
self._host_connected = connected
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
@@ -58,9 +58,13 @@ class HomeeLock(HomeeEntity, LockEntity):
|
|||||||
AttributeChangedBy, self._attribute.changed_by
|
AttributeChangedBy, self._attribute.changed_by
|
||||||
)
|
)
|
||||||
if self._attribute.changed_by == AttributeChangedBy.USER:
|
if self._attribute.changed_by == AttributeChangedBy.USER:
|
||||||
changed_id = self._entry.runtime_data.get_user_by_id(
|
user = self._entry.runtime_data.get_user_by_id(
|
||||||
self._attribute.changed_by_id
|
self._attribute.changed_by_id
|
||||||
).username
|
)
|
||||||
|
if user is not None:
|
||||||
|
changed_id = user.username
|
||||||
|
else:
|
||||||
|
changed_id = "Unknown"
|
||||||
|
|
||||||
return f"{changed_by_name}-{changed_id}"
|
return f"{changed_by_name}-{changed_id}"
|
||||||
|
|
||||||
|
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["homee"],
|
"loggers": ["homee"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pyHomee==1.2.8"]
|
"requirements": ["pyHomee==1.2.10"]
|
||||||
}
|
}
|
||||||
|
@@ -28,6 +28,7 @@ def get_device_class(
|
|||||||
) -> SwitchDeviceClass:
|
) -> SwitchDeviceClass:
|
||||||
"""Check device class of Switch according to node profile."""
|
"""Check device class of Switch according to node profile."""
|
||||||
node = config_entry.runtime_data.get_node_by_id(attribute.node_id)
|
node = config_entry.runtime_data.get_node_by_id(attribute.node_id)
|
||||||
|
assert node is not None
|
||||||
if node.profile in [
|
if node.profile in [
|
||||||
NodeProfile.ON_OFF_PLUG,
|
NodeProfile.ON_OFF_PLUG,
|
||||||
NodeProfile.METERING_PLUG,
|
NodeProfile.METERING_PLUG,
|
||||||
|
@@ -355,11 +355,10 @@ class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_abort(reason="ignored_model")
|
return self.async_abort(reason="ignored_model")
|
||||||
|
|
||||||
# Late imports in case BLE is not available
|
# Late imports in case BLE is not available
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from aiohomekit.controller.ble.discovery import BleDiscovery # noqa: PLC0415
|
||||||
from aiohomekit.controller.ble.discovery import BleDiscovery
|
from aiohomekit.controller.ble.manufacturer_data import ( # noqa: PLC0415
|
||||||
|
HomeKitAdvertisement,
|
||||||
# pylint: disable-next=import-outside-toplevel
|
)
|
||||||
from aiohomekit.controller.ble.manufacturer_data import HomeKitAdvertisement
|
|
||||||
|
|
||||||
mfr_data = discovery_info.manufacturer_data
|
mfr_data = discovery_info.manufacturer_data
|
||||||
|
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["homematicip"],
|
"loggers": ["homematicip"],
|
||||||
"requirements": ["homematicip==2.0.5"]
|
"requirements": ["homematicip==2.0.6"]
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,13 @@ import logging
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
DOMAIN = "homewizard"
|
DOMAIN = "homewizard"
|
||||||
PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
|
PLATFORMS = [
|
||||||
|
Platform.BUTTON,
|
||||||
|
Platform.NUMBER,
|
||||||
|
Platform.SELECT,
|
||||||
|
Platform.SENSOR,
|
||||||
|
Platform.SWITCH,
|
||||||
|
]
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable, Coroutine
|
from collections.abc import Callable, Coroutine
|
||||||
from typing import Any, Concatenate
|
from typing import Any, Concatenate
|
||||||
|
|
||||||
from homewizard_energy.errors import DisabledError, RequestError
|
from homewizard_energy.errors import DisabledError, RequestError, UnauthorizedError
|
||||||
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
@@ -41,5 +41,10 @@ def homewizard_exception_handler[_HomeWizardEntityT: HomeWizardEntity, **_P](
|
|||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="api_disabled",
|
translation_key="api_disabled",
|
||||||
) from ex
|
) from ex
|
||||||
|
except UnauthorizedError as ex:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="api_unauthorized",
|
||||||
|
) from ex
|
||||||
|
|
||||||
return handler
|
return handler
|
||||||
|
89
homeassistant/components/homewizard/select.py
Normal file
89
homeassistant/components/homewizard/select.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"""Support for HomeWizard select platform."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homewizard_energy import HomeWizardEnergy
|
||||||
|
from homewizard_energy.models import Batteries, CombinedModels as DeviceResponseEntry
|
||||||
|
|
||||||
|
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
|
||||||
|
from .entity import HomeWizardEntity
|
||||||
|
from .helpers import homewizard_exception_handler
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class HomeWizardSelectEntityDescription(SelectEntityDescription):
|
||||||
|
"""Class describing HomeWizard select entities."""
|
||||||
|
|
||||||
|
available_fn: Callable[[DeviceResponseEntry], bool]
|
||||||
|
create_fn: Callable[[DeviceResponseEntry], bool]
|
||||||
|
current_fn: Callable[[DeviceResponseEntry], str | None]
|
||||||
|
set_fn: Callable[[HomeWizardEnergy, str], Awaitable[Any]]
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTIONS = [
|
||||||
|
HomeWizardSelectEntityDescription(
|
||||||
|
key="battery_group_mode",
|
||||||
|
translation_key="battery_group_mode",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
options=[Batteries.Mode.ZERO, Batteries.Mode.STANDBY, Batteries.Mode.TO_FULL],
|
||||||
|
available_fn=lambda x: x.batteries is not None,
|
||||||
|
create_fn=lambda x: x.batteries is not None,
|
||||||
|
current_fn=lambda x: x.batteries.mode if x.batteries else None,
|
||||||
|
set_fn=lambda api, mode: api.batteries(mode=Batteries.Mode(mode)),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: HomeWizardConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up HomeWizard select based on a config entry."""
|
||||||
|
async_add_entities(
|
||||||
|
HomeWizardSelectEntity(
|
||||||
|
coordinator=entry.runtime_data,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
for description in DESCRIPTIONS
|
||||||
|
if description.create_fn(entry.runtime_data.data)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HomeWizardSelectEntity(HomeWizardEntity, SelectEntity):
|
||||||
|
"""Defines a HomeWizard select entity."""
|
||||||
|
|
||||||
|
entity_description: HomeWizardSelectEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: HWEnergyDeviceUpdateCoordinator,
|
||||||
|
description: HomeWizardSelectEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the switch."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
"""Return the selected entity option to represent the entity state."""
|
||||||
|
return self.entity_description.current_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
@homewizard_exception_handler
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the selected option."""
|
||||||
|
await self.entity_description.set_fn(self.coordinator.api, option)
|
||||||
|
await self.coordinator.async_request_refresh()
|
@@ -152,14 +152,27 @@
|
|||||||
"cloud_connection": {
|
"cloud_connection": {
|
||||||
"name": "Cloud connection"
|
"name": "Cloud connection"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"select": {
|
||||||
|
"battery_group_mode": {
|
||||||
|
"name": "Battery group mode",
|
||||||
|
"state": {
|
||||||
|
"zero": "Zero mode",
|
||||||
|
"to_full": "Manual charge mode",
|
||||||
|
"standby": "Standby"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
"api_disabled": {
|
"api_disabled": {
|
||||||
"message": "The local API is disabled."
|
"message": "The local API is disabled."
|
||||||
},
|
},
|
||||||
|
"api_unauthorized": {
|
||||||
|
"message": "The local API is unauthorized. Restore API access by following the instructions in the repair issue."
|
||||||
|
},
|
||||||
"communication_error": {
|
"communication_error": {
|
||||||
"message": "An error occurred while communicating with HomeWizard device"
|
"message": "An error occurred while communicating with your HomeWizard Energy device"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"issues": {
|
"issues": {
|
||||||
|
@@ -278,8 +278,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
ssl_certificate is not None
|
ssl_certificate is not None
|
||||||
and (hass.config.external_url or hass.config.internal_url) is None
|
and (hass.config.external_url or hass.config.internal_url) is None
|
||||||
):
|
):
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from homeassistant.components.cloud import ( # noqa: PLC0415
|
||||||
from homeassistant.components.cloud import (
|
|
||||||
CloudNotAvailable,
|
CloudNotAvailable,
|
||||||
async_remote_ui_url,
|
async_remote_ui_url,
|
||||||
)
|
)
|
||||||
@@ -511,12 +510,14 @@ class HomeAssistantHTTP:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Register a folder or file to serve as a static path."""
|
"""Register a folder or file to serve as a static path."""
|
||||||
frame.report_usage(
|
frame.report_usage(
|
||||||
"calls hass.http.register_static_path which is deprecated because "
|
"calls hass.http.register_static_path which "
|
||||||
"it does blocking I/O in the event loop, instead "
|
"does blocking I/O in the event loop, instead "
|
||||||
"call `await hass.http.async_register_static_paths("
|
"call `await hass.http.async_register_static_paths("
|
||||||
f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`',
|
f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`',
|
||||||
exclude_integrations={"http"},
|
exclude_integrations={"http"},
|
||||||
core_behavior=frame.ReportBehavior.LOG,
|
core_behavior=frame.ReportBehavior.ERROR,
|
||||||
|
core_integration_behavior=frame.ReportBehavior.ERROR,
|
||||||
|
custom_integration_behavior=frame.ReportBehavior.ERROR,
|
||||||
breaks_in_ha_version="2025.7",
|
breaks_in_ha_version="2025.7",
|
||||||
)
|
)
|
||||||
configs = [StaticPathConfig(url_path, path, cache_headers)]
|
configs = [StaticPathConfig(url_path, path, cache_headers)]
|
||||||
|
@@ -136,8 +136,7 @@ async def process_wrong_login(request: Request) -> None:
|
|||||||
_LOGGER.warning(log_msg)
|
_LOGGER.warning(log_msg)
|
||||||
|
|
||||||
# Circular import with websocket_api
|
# Circular import with websocket_api
|
||||||
# pylint: disable=import-outside-toplevel
|
from homeassistant.components import persistent_notification # noqa: PLC0415
|
||||||
from homeassistant.components import persistent_notification
|
|
||||||
|
|
||||||
persistent_notification.async_create(
|
persistent_notification.async_create(
|
||||||
hass, notification_msg, "Login attempt failed", NOTIFICATION_ID_LOGIN
|
hass, notification_msg, "Login attempt failed", NOTIFICATION_ID_LOGIN
|
||||||
|
@@ -444,8 +444,9 @@ class TimerManager:
|
|||||||
timer.finish()
|
timer.finish()
|
||||||
|
|
||||||
if timer.conversation_command:
|
if timer.conversation_command:
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from homeassistant.components.conversation import ( # noqa: PLC0415
|
||||||
from homeassistant.components.conversation import async_converse
|
async_converse,
|
||||||
|
)
|
||||||
|
|
||||||
self.hass.async_create_background_task(
|
self.hass.async_create_background_task(
|
||||||
async_converse(
|
async_converse(
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
"""The JuiceNet integration."""
|
"""The JuiceNet integration."""
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -14,9 +13,9 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
|||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||||
|
from .coordinator import JuiceNetCoordinator
|
||||||
from .device import JuiceNetApi
|
from .device import JuiceNetApi
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -74,20 +73,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return False
|
return False
|
||||||
_LOGGER.debug("%d JuiceNet device(s) found", len(juicenet.devices))
|
_LOGGER.debug("%d JuiceNet device(s) found", len(juicenet.devices))
|
||||||
|
|
||||||
async def async_update_data():
|
coordinator = JuiceNetCoordinator(hass, entry, juicenet)
|
||||||
"""Update all device states from the JuiceNet API."""
|
|
||||||
for device in juicenet.devices:
|
|
||||||
await device.update_state(True)
|
|
||||||
return True
|
|
||||||
|
|
||||||
coordinator = DataUpdateCoordinator(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
config_entry=entry,
|
|
||||||
name="JuiceNet",
|
|
||||||
update_method=async_update_data,
|
|
||||||
update_interval=timedelta(seconds=30),
|
|
||||||
)
|
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
33
homeassistant/components/juicenet/coordinator.py
Normal file
33
homeassistant/components/juicenet/coordinator.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""The JuiceNet integration."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
|
from .device import JuiceNetApi
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class JuiceNetCoordinator(DataUpdateCoordinator[None]):
|
||||||
|
"""Coordinator for JuiceNet."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, entry: ConfigEntry, juicenet_api: JuiceNetApi
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the JuiceNet coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
|
name="JuiceNet",
|
||||||
|
update_interval=timedelta(seconds=30),
|
||||||
|
)
|
||||||
|
self.juicenet_api = juicenet_api
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> None:
|
||||||
|
for device in self.juicenet_api.devices:
|
||||||
|
await device.update_state(True)
|
@@ -1,19 +1,21 @@
|
|||||||
"""Adapter to wrap the pyjuicenet api for home assistant."""
|
"""Adapter to wrap the pyjuicenet api for home assistant."""
|
||||||
|
|
||||||
|
from pyjuicenet import Api, Charger
|
||||||
|
|
||||||
|
|
||||||
class JuiceNetApi:
|
class JuiceNetApi:
|
||||||
"""Represent a connection to JuiceNet."""
|
"""Represent a connection to JuiceNet."""
|
||||||
|
|
||||||
def __init__(self, api):
|
def __init__(self, api: Api) -> None:
|
||||||
"""Create an object from the provided API instance."""
|
"""Create an object from the provided API instance."""
|
||||||
self.api = api
|
self.api = api
|
||||||
self._devices = []
|
self._devices: list[Charger] = []
|
||||||
|
|
||||||
async def setup(self):
|
async def setup(self) -> None:
|
||||||
"""JuiceNet device setup."""
|
"""JuiceNet device setup."""
|
||||||
self._devices = await self.api.get_devices()
|
self._devices = await self.api.get_devices()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def devices(self) -> list:
|
def devices(self) -> list[Charger]:
|
||||||
"""Get a list of devices managed by this account."""
|
"""Get a list of devices managed by this account."""
|
||||||
return self._devices
|
return self._devices
|
||||||
|
@@ -3,21 +3,19 @@
|
|||||||
from pyjuicenet import Charger
|
from pyjuicenet import Charger
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
CoordinatorEntity,
|
|
||||||
DataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .coordinator import JuiceNetCoordinator
|
||||||
|
|
||||||
|
|
||||||
class JuiceNetDevice(CoordinatorEntity):
|
class JuiceNetEntity(CoordinatorEntity[JuiceNetCoordinator]):
|
||||||
"""Represent a base JuiceNet device."""
|
"""Represent a base JuiceNet device."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, device: Charger, key: str, coordinator: DataUpdateCoordinator
|
self, device: Charger, key: str, coordinator: JuiceNetCoordinator
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialise the sensor."""
|
"""Initialise the sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from pyjuicenet import Api, Charger
|
from pyjuicenet import Charger
|
||||||
|
|
||||||
from homeassistant.components.number import (
|
from homeassistant.components.number import (
|
||||||
DEFAULT_MAX_VALUE,
|
DEFAULT_MAX_VALUE,
|
||||||
@@ -14,10 +14,11 @@ from homeassistant.components.number import (
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||||
from .entity import JuiceNetDevice
|
from .coordinator import JuiceNetCoordinator
|
||||||
|
from .device import JuiceNetApi
|
||||||
|
from .entity import JuiceNetEntity
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
@@ -47,8 +48,8 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the JuiceNet Numbers."""
|
"""Set up the JuiceNet Numbers."""
|
||||||
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
|
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
api: Api = juicenet_data[JUICENET_API]
|
api: JuiceNetApi = juicenet_data[JUICENET_API]
|
||||||
coordinator = juicenet_data[JUICENET_COORDINATOR]
|
coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR]
|
||||||
|
|
||||||
entities = [
|
entities = [
|
||||||
JuiceNetNumber(device, description, coordinator)
|
JuiceNetNumber(device, description, coordinator)
|
||||||
@@ -58,7 +59,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class JuiceNetNumber(JuiceNetDevice, NumberEntity):
|
class JuiceNetNumber(JuiceNetEntity, NumberEntity):
|
||||||
"""Implementation of a JuiceNet number."""
|
"""Implementation of a JuiceNet number."""
|
||||||
|
|
||||||
entity_description: JuiceNetNumberEntityDescription
|
entity_description: JuiceNetNumberEntityDescription
|
||||||
@@ -67,7 +68,7 @@ class JuiceNetNumber(JuiceNetDevice, NumberEntity):
|
|||||||
self,
|
self,
|
||||||
device: Charger,
|
device: Charger,
|
||||||
description: JuiceNetNumberEntityDescription,
|
description: JuiceNetNumberEntityDescription,
|
||||||
coordinator: DataUpdateCoordinator,
|
coordinator: JuiceNetCoordinator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialise the number."""
|
"""Initialise the number."""
|
||||||
super().__init__(device, description.key, coordinator)
|
super().__init__(device, description.key, coordinator)
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pyjuicenet import Charger
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
@@ -21,7 +23,9 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||||
from .entity import JuiceNetDevice
|
from .coordinator import JuiceNetCoordinator
|
||||||
|
from .device import JuiceNetApi
|
||||||
|
from .entity import JuiceNetEntity
|
||||||
|
|
||||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
@@ -74,8 +78,8 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the JuiceNet Sensors."""
|
"""Set up the JuiceNet Sensors."""
|
||||||
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
|
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
api = juicenet_data[JUICENET_API]
|
api: JuiceNetApi = juicenet_data[JUICENET_API]
|
||||||
coordinator = juicenet_data[JUICENET_COORDINATOR]
|
coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR]
|
||||||
|
|
||||||
entities = [
|
entities = [
|
||||||
JuiceNetSensorDevice(device, coordinator, description)
|
JuiceNetSensorDevice(device, coordinator, description)
|
||||||
@@ -85,11 +89,14 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity):
|
class JuiceNetSensorDevice(JuiceNetEntity, SensorEntity):
|
||||||
"""Implementation of a JuiceNet sensor."""
|
"""Implementation of a JuiceNet sensor."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, device, coordinator, description: SensorEntityDescription
|
self,
|
||||||
|
device: Charger,
|
||||||
|
coordinator: JuiceNetCoordinator,
|
||||||
|
description: SensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialise the sensor."""
|
"""Initialise the sensor."""
|
||||||
super().__init__(device, description.key, coordinator)
|
super().__init__(device, description.key, coordinator)
|
||||||
|
@@ -2,13 +2,17 @@
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from pyjuicenet import Charger
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||||
from .entity import JuiceNetDevice
|
from .coordinator import JuiceNetCoordinator
|
||||||
|
from .device import JuiceNetApi
|
||||||
|
from .entity import JuiceNetEntity
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -18,20 +22,20 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the JuiceNet switches."""
|
"""Set up the JuiceNet switches."""
|
||||||
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
|
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
api = juicenet_data[JUICENET_API]
|
api: JuiceNetApi = juicenet_data[JUICENET_API]
|
||||||
coordinator = juicenet_data[JUICENET_COORDINATOR]
|
coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR]
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
JuiceNetChargeNowSwitch(device, coordinator) for device in api.devices
|
JuiceNetChargeNowSwitch(device, coordinator) for device in api.devices
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity):
|
class JuiceNetChargeNowSwitch(JuiceNetEntity, SwitchEntity):
|
||||||
"""Implementation of a JuiceNet switch."""
|
"""Implementation of a JuiceNet switch."""
|
||||||
|
|
||||||
_attr_translation_key = "charge_now"
|
_attr_translation_key = "charge_now"
|
||||||
|
|
||||||
def __init__(self, device, coordinator):
|
def __init__(self, device: Charger, coordinator: JuiceNetCoordinator) -> None:
|
||||||
"""Initialise the switch."""
|
"""Initialise the switch."""
|
||||||
super().__init__(device, "charge_now", coordinator)
|
super().__init__(device, "charge_now", coordinator)
|
||||||
|
|
||||||
|
@@ -2,15 +2,14 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
|
||||||
from .const import DOMAIN, PLATFORMS
|
from .const import PLATFORMS
|
||||||
from .coordinator import JustNimbusCoordinator
|
from .coordinator import JustNimbusConfigEntry, JustNimbusCoordinator
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: JustNimbusConfigEntry) -> bool:
|
||||||
"""Set up JustNimbus from a config entry."""
|
"""Set up JustNimbus from a config entry."""
|
||||||
if "zip_code" in entry.data:
|
if "zip_code" in entry.data:
|
||||||
coordinator = JustNimbusCoordinator(hass, entry)
|
coordinator = JustNimbusCoordinator(hass, entry)
|
||||||
@@ -18,13 +17,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
raise ConfigEntryAuthFailed
|
raise ConfigEntryAuthFailed
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
entry.runtime_data = coordinator
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: JustNimbusConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
return unload_ok
|
|
||||||
|
@@ -16,13 +16,17 @@ from .const import CONF_ZIP_CODE, DOMAIN
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type JustNimbusConfigEntry = ConfigEntry[JustNimbusCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class JustNimbusCoordinator(DataUpdateCoordinator[justnimbus.JustNimbusModel]):
|
class JustNimbusCoordinator(DataUpdateCoordinator[justnimbus.JustNimbusModel]):
|
||||||
"""Data update coordinator."""
|
"""Data update coordinator."""
|
||||||
|
|
||||||
config_entry: ConfigEntry
|
config_entry: JustNimbusConfigEntry
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, config_entry: JustNimbusConfigEntry
|
||||||
|
) -> None:
|
||||||
"""Initialize the coordinator."""
|
"""Initialize the coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
|
@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_CLIENT_ID,
|
CONF_CLIENT_ID,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
@@ -24,8 +23,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from . import JustNimbusCoordinator
|
from .coordinator import JustNimbusConfigEntry, JustNimbusCoordinator
|
||||||
from .const import DOMAIN
|
|
||||||
from .entity import JustNimbusEntity
|
from .entity import JustNimbusEntity
|
||||||
|
|
||||||
|
|
||||||
@@ -102,16 +100,15 @@ SENSOR_TYPES = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: JustNimbusConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the JustNimbus sensor."""
|
"""Set up the JustNimbus sensor."""
|
||||||
coordinator: JustNimbusCoordinator = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
JustNimbusSensor(
|
JustNimbusSensor(
|
||||||
device_id=entry.data[CONF_CLIENT_ID],
|
device_id=entry.data[CONF_CLIENT_ID],
|
||||||
description=description,
|
description=description,
|
||||||
coordinator=coordinator,
|
coordinator=entry.runtime_data,
|
||||||
)
|
)
|
||||||
for description in SENSOR_TYPES
|
for description in SENSOR_TYPES
|
||||||
)
|
)
|
||||||
|
@@ -3,26 +3,22 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from kaleidescape import Device as KaleidescapeDevice, KaleidescapeError
|
from kaleidescape import Device as KaleidescapeDevice, KaleidescapeError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform
|
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform
|
||||||
|
from homeassistant.core import Event, HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import Event, HomeAssistant
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE, Platform.SENSOR]
|
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE, Platform.SENSOR]
|
||||||
|
|
||||||
|
type KaleidescapeConfigEntry = ConfigEntry[KaleidescapeDevice]
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: KaleidescapeConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Set up Kaleidescape from a config entry."""
|
"""Set up Kaleidescape from a config entry."""
|
||||||
device = KaleidescapeDevice(
|
device = KaleidescapeDevice(
|
||||||
entry.data[CONF_HOST], timeout=5, reconnect=True, reconnect_delay=5
|
entry.data[CONF_HOST], timeout=5, reconnect=True, reconnect_delay=5
|
||||||
@@ -36,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
f"Unable to connect to {entry.data[CONF_HOST]}: {err}"
|
f"Unable to connect to {entry.data[CONF_HOST]}: {err}"
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device
|
entry.runtime_data = device
|
||||||
|
|
||||||
async def disconnect(event: Event) -> None:
|
async def disconnect(event: Event) -> None:
|
||||||
await device.disconnect()
|
await device.disconnect()
|
||||||
@@ -44,18 +40,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect)
|
||||||
)
|
)
|
||||||
|
entry.async_on_unload(device.disconnect)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: KaleidescapeConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload config entry."""
|
"""Unload config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
await hass.data[DOMAIN][entry.entry_id].disconnect()
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
return unload_ok
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from kaleidescape import const as kaleidescape_const
|
from kaleidescape import const as kaleidescape_const
|
||||||
|
|
||||||
@@ -12,19 +12,13 @@ from homeassistant.components.media_player import (
|
|||||||
MediaPlayerEntityFeature,
|
MediaPlayerEntityFeature,
|
||||||
MediaPlayerState,
|
MediaPlayerState,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import KaleidescapeConfigEntry
|
||||||
from .entity import KaleidescapeEntity
|
from .entity import KaleidescapeEntity
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
|
|
||||||
KALEIDESCAPE_PLAYING_STATES = [
|
KALEIDESCAPE_PLAYING_STATES = [
|
||||||
kaleidescape_const.PLAY_STATUS_PLAYING,
|
kaleidescape_const.PLAY_STATUS_PLAYING,
|
||||||
kaleidescape_const.PLAY_STATUS_FORWARD,
|
kaleidescape_const.PLAY_STATUS_FORWARD,
|
||||||
@@ -39,11 +33,11 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: KaleidescapeConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the platform from a config entry."""
|
"""Set up the platform from a config entry."""
|
||||||
entities = [KaleidescapeMediaPlayer(hass.data[DOMAIN][entry.entry_id])]
|
entities = [KaleidescapeMediaPlayer(entry.runtime_data)]
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -2,32 +2,27 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from collections.abc import Iterable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from kaleidescape import const as kaleidescape_const
|
from kaleidescape import const as kaleidescape_const
|
||||||
|
|
||||||
from homeassistant.components.remote import RemoteEntity
|
from homeassistant.components.remote import RemoteEntity
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import KaleidescapeConfigEntry
|
||||||
from .entity import KaleidescapeEntity
|
from .entity import KaleidescapeEntity
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from collections.abc import Iterable
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: KaleidescapeConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the platform from a config entry."""
|
"""Set up the platform from a config entry."""
|
||||||
entities = [KaleidescapeRemote(hass.data[DOMAIN][entry.entry_id])]
|
entities = [KaleidescapeRemote(entry.runtime_data)]
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -2,25 +2,20 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
from kaleidescape import Device as KaleidescapeDevice
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import KaleidescapeConfigEntry
|
||||||
from .entity import KaleidescapeEntity
|
from .entity import KaleidescapeEntity
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from collections.abc import Callable
|
|
||||||
|
|
||||||
from kaleidescape import Device as KaleidescapeDevice
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
from homeassistant.helpers.typing import StateType
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class KaleidescapeSensorEntityDescription(SensorEntityDescription):
|
class KaleidescapeSensorEntityDescription(SensorEntityDescription):
|
||||||
@@ -132,11 +127,11 @@ SENSOR_TYPES: tuple[KaleidescapeSensorEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: KaleidescapeConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the platform from a config entry."""
|
"""Set up the platform from a config entry."""
|
||||||
device: KaleidescapeDevice = hass.data[DOMAIN][entry.entry_id]
|
device = entry.runtime_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
KaleidescapeSensor(device, description) for description in SENSOR_TYPES
|
KaleidescapeSensor(device, description) for description in SENSOR_TYPES
|
||||||
)
|
)
|
||||||
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL, Platform
|
from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
@@ -19,16 +18,14 @@ from .const import (
|
|||||||
DEFAULT_INTERFACE,
|
DEFAULT_INTERFACE,
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ROUTER,
|
|
||||||
UNDO_UPDATE_LISTENER,
|
|
||||||
)
|
)
|
||||||
from .router import KeeneticRouter
|
from .router import KeeneticConfigEntry, KeeneticRouter
|
||||||
|
|
||||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER]
|
PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER]
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: KeeneticConfigEntry) -> bool:
|
||||||
"""Set up the component."""
|
"""Set up the component."""
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
async_add_defaults(hass, entry)
|
async_add_defaults(hass, entry)
|
||||||
@@ -36,32 +33,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
router = KeeneticRouter(hass, entry)
|
router = KeeneticRouter(hass, entry)
|
||||||
await router.async_setup()
|
await router.async_setup()
|
||||||
|
|
||||||
undo_listener = entry.add_update_listener(update_listener)
|
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||||
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = {
|
entry.runtime_data = router
|
||||||
ROUTER: router,
|
|
||||||
UNDO_UPDATE_LISTENER: undo_listener,
|
|
||||||
}
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, config_entry: KeeneticConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
|
|
||||||
|
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||||
config_entry, PLATFORMS
|
config_entry, PLATFORMS
|
||||||
)
|
)
|
||||||
|
|
||||||
router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER]
|
router = config_entry.runtime_data
|
||||||
|
|
||||||
await router.async_teardown()
|
await router.async_teardown()
|
||||||
|
|
||||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
|
||||||
|
|
||||||
new_tracked_interfaces: set[str] = set(config_entry.options[CONF_INTERFACES])
|
new_tracked_interfaces: set[str] = set(config_entry.options[CONF_INTERFACES])
|
||||||
|
|
||||||
if router.tracked_interfaces - new_tracked_interfaces:
|
if router.tracked_interfaces - new_tracked_interfaces:
|
||||||
@@ -96,12 +87,12 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
async def update_listener(hass: HomeAssistant, entry: KeeneticConfigEntry) -> None:
|
||||||
"""Handle options update."""
|
"""Handle options update."""
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
def async_add_defaults(hass: HomeAssistant, entry: ConfigEntry):
|
def async_add_defaults(hass: HomeAssistant, entry: KeeneticConfigEntry):
|
||||||
"""Populate default options."""
|
"""Populate default options."""
|
||||||
host: str = entry.data[CONF_HOST]
|
host: str = entry.data[CONF_HOST]
|
||||||
imported_options: dict = hass.data[DOMAIN].get(f"imported_options_{host}", {})
|
imported_options: dict = hass.data[DOMAIN].get(f"imported_options_{host}", {})
|
||||||
|
@@ -4,24 +4,20 @@ from homeassistant.components.binary_sensor import (
|
|||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import KeeneticRouter
|
from .router import KeeneticConfigEntry, KeeneticRouter
|
||||||
from .const import DOMAIN, ROUTER
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: KeeneticConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up device tracker for Keenetic NDMS2 component."""
|
"""Set up device tracker for Keenetic NDMS2 component."""
|
||||||
router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER]
|
async_add_entities([RouterOnlineBinarySensor(config_entry.runtime_data)])
|
||||||
|
|
||||||
async_add_entities([RouterOnlineBinarySensor(router)])
|
|
||||||
|
|
||||||
|
|
||||||
class RouterOnlineBinarySensor(BinarySensorEntity):
|
class RouterOnlineBinarySensor(BinarySensorEntity):
|
||||||
|
@@ -8,12 +8,7 @@ from urllib.parse import urlparse
|
|||||||
from ndms2_client import Client, ConnectionException, InterfaceInfo, TelnetConnection
|
from ndms2_client import Client, ConnectionException, InterfaceInfo, TelnetConnection
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||||
ConfigEntry,
|
|
||||||
ConfigFlow,
|
|
||||||
ConfigFlowResult,
|
|
||||||
OptionsFlow,
|
|
||||||
)
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
@@ -41,9 +36,8 @@ from .const import (
|
|||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DEFAULT_TELNET_PORT,
|
DEFAULT_TELNET_PORT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ROUTER,
|
|
||||||
)
|
)
|
||||||
from .router import KeeneticRouter
|
from .router import KeeneticConfigEntry
|
||||||
|
|
||||||
|
|
||||||
class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN):
|
class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
@@ -56,7 +50,7 @@ class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
config_entry: ConfigEntry,
|
config_entry: KeeneticConfigEntry,
|
||||||
) -> KeeneticOptionsFlowHandler:
|
) -> KeeneticOptionsFlowHandler:
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return KeeneticOptionsFlowHandler()
|
return KeeneticOptionsFlowHandler()
|
||||||
@@ -142,6 +136,8 @@ class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
class KeeneticOptionsFlowHandler(OptionsFlow):
|
class KeeneticOptionsFlowHandler(OptionsFlow):
|
||||||
"""Handle options."""
|
"""Handle options."""
|
||||||
|
|
||||||
|
config_entry: KeeneticConfigEntry
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize options flow."""
|
"""Initialize options flow."""
|
||||||
self._interface_options: dict[str, str] = {}
|
self._interface_options: dict[str, str] = {}
|
||||||
@@ -150,9 +146,7 @@ class KeeneticOptionsFlowHandler(OptionsFlow):
|
|||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Manage the options."""
|
"""Manage the options."""
|
||||||
router: KeeneticRouter = self.hass.data[DOMAIN][self.config_entry.entry_id][
|
router = self.config_entry.runtime_data
|
||||||
ROUTER
|
|
||||||
]
|
|
||||||
|
|
||||||
interfaces: list[InterfaceInfo] = await self.hass.async_add_executor_job(
|
interfaces: list[InterfaceInfo] = await self.hass.async_add_executor_job(
|
||||||
router.client.get_interfaces
|
router.client.get_interfaces
|
||||||
|
@@ -5,8 +5,6 @@ from homeassistant.components.device_tracker import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
DOMAIN = "keenetic_ndms2"
|
DOMAIN = "keenetic_ndms2"
|
||||||
ROUTER = "router"
|
|
||||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
|
||||||
DEFAULT_TELNET_PORT = 23
|
DEFAULT_TELNET_PORT = 23
|
||||||
DEFAULT_SCAN_INTERVAL = 120
|
DEFAULT_SCAN_INTERVAL = 120
|
||||||
DEFAULT_CONSIDER_HOME = _DEFAULT_CONSIDER_HOME.total_seconds()
|
DEFAULT_CONSIDER_HOME = _DEFAULT_CONSIDER_HOME.total_seconds()
|
||||||
|
@@ -10,26 +10,24 @@ from homeassistant.components.device_tracker import (
|
|||||||
DOMAIN as DEVICE_TRACKER_DOMAIN,
|
DOMAIN as DEVICE_TRACKER_DOMAIN,
|
||||||
ScannerEntity,
|
ScannerEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import DOMAIN, ROUTER
|
from .router import KeeneticConfigEntry, KeeneticRouter
|
||||||
from .router import KeeneticRouter
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: KeeneticConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up device tracker for Keenetic NDMS2 component."""
|
"""Set up device tracker for Keenetic NDMS2 component."""
|
||||||
router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER]
|
router = config_entry.runtime_data
|
||||||
|
|
||||||
tracked: set[str] = set()
|
tracked: set[str] = set()
|
||||||
|
|
||||||
|
@@ -35,11 +35,13 @@ from .const import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type KeeneticConfigEntry = ConfigEntry[KeeneticRouter]
|
||||||
|
|
||||||
|
|
||||||
class KeeneticRouter:
|
class KeeneticRouter:
|
||||||
"""Keenetic client Object."""
|
"""Keenetic client Object."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
def __init__(self, hass: HomeAssistant, config_entry: KeeneticConfigEntry) -> None:
|
||||||
"""Initialize the Client."""
|
"""Initialize the Client."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.config_entry = config_entry
|
self.config_entry = config_entry
|
||||||
|
@@ -14,27 +14,26 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type KegtronConfigEntry = ConfigEntry[PassiveBluetoothProcessorCoordinator]
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: KegtronConfigEntry) -> bool:
|
||||||
"""Set up Kegtron BLE device from a config entry."""
|
"""Set up Kegtron BLE device from a config entry."""
|
||||||
address = entry.unique_id
|
address = entry.unique_id
|
||||||
assert address is not None
|
assert address is not None
|
||||||
data = KegtronBluetoothDeviceData()
|
data = KegtronBluetoothDeviceData()
|
||||||
coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
|
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||||
PassiveBluetoothProcessorCoordinator(
|
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
address=address,
|
address=address,
|
||||||
mode=BluetoothScanningMode.PASSIVE,
|
mode=BluetoothScanningMode.PASSIVE,
|
||||||
update_method=data.update,
|
update_method=data.update,
|
||||||
)
|
)
|
||||||
)
|
entry.runtime_data = coordinator
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
coordinator.async_start()
|
coordinator.async_start()
|
||||||
@@ -42,9 +41,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: KegtronConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
@@ -8,11 +8,9 @@ from kegtron_ble import (
|
|||||||
Units,
|
Units,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant import config_entries
|
|
||||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||||
PassiveBluetoothDataProcessor,
|
PassiveBluetoothDataProcessor,
|
||||||
PassiveBluetoothDataUpdate,
|
PassiveBluetoothDataUpdate,
|
||||||
PassiveBluetoothProcessorCoordinator,
|
|
||||||
PassiveBluetoothProcessorEntity,
|
PassiveBluetoothProcessorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
@@ -30,7 +28,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
|
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import KegtronConfigEntry
|
||||||
from .device import device_key_to_bluetooth_entity_key
|
from .device import device_key_to_bluetooth_entity_key
|
||||||
|
|
||||||
SENSOR_DESCRIPTIONS = {
|
SENSOR_DESCRIPTIONS = {
|
||||||
@@ -109,13 +107,11 @@ def sensor_update_to_bluetooth_data_update(
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: config_entries.ConfigEntry,
|
entry: KegtronConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Kegtron BLE sensors."""
|
"""Set up the Kegtron BLE sensors."""
|
||||||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
coordinator = entry.runtime_data
|
||||||
entry.entry_id
|
|
||||||
]
|
|
||||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
processor.async_add_entities_listener(
|
processor.async_add_entities_listener(
|
||||||
|
@@ -2,26 +2,20 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from microbot import MicroBotApiClient
|
from microbot import MicroBotApiClient
|
||||||
|
|
||||||
from homeassistant.components import bluetooth
|
from homeassistant.components import bluetooth
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, Platform
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .coordinator import MicroBotConfigEntry, MicroBotDataUpdateCoordinator
|
||||||
from .coordinator import MicroBotDataUpdateCoordinator
|
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
|
||||||
PLATFORMS: list[str] = [Platform.SWITCH]
|
PLATFORMS: list[str] = [Platform.SWITCH]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: MicroBotConfigEntry) -> bool:
|
||||||
"""Set up this integration using UI."""
|
"""Set up this integration using UI."""
|
||||||
hass.data.setdefault(DOMAIN, {})
|
|
||||||
token: str = entry.data[CONF_ACCESS_TOKEN]
|
token: str = entry.data[CONF_ACCESS_TOKEN]
|
||||||
bdaddr: str = entry.data[CONF_ADDRESS]
|
bdaddr: str = entry.data[CONF_ADDRESS]
|
||||||
ble_device = bluetooth.async_ble_device_from_address(hass, bdaddr)
|
ble_device = bluetooth.async_ble_device_from_address(hass, bdaddr)
|
||||||
@@ -35,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
hass, client=client, ble_device=ble_device
|
hass, client=client, ble_device=ble_device
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
entry.async_on_unload(coordinator.async_start())
|
entry.async_on_unload(coordinator.async_start())
|
||||||
@@ -43,9 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: MicroBotConfigEntry) -> bool:
|
||||||
"""Handle removal of an entry."""
|
"""Handle removal of an entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
@@ -11,14 +11,15 @@ from homeassistant.components import bluetooth
|
|||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
PassiveBluetoothDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from homeassistant.const import Platform
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bleak.backends.device import BLEDevice
|
from bleak.backends.device import BLEDevice
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||||
PLATFORMS: list[str] = [Platform.SWITCH]
|
|
||||||
|
type MicroBotConfigEntry = ConfigEntry[MicroBotDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class MicroBotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
class MicroBotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||||
@@ -31,7 +32,7 @@ class MicroBotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
|||||||
ble_device: BLEDevice,
|
ble_device: BLEDevice,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self.api: MicroBotApiClient = client
|
self.api = client
|
||||||
self.data: dict[str, Any] = {}
|
self.data: dict[str, Any] = {}
|
||||||
self.ble_device = ble_device
|
self.ble_device = ble_device
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
@@ -19,7 +19,7 @@ class MicroBotEntity(PassiveBluetoothCoordinatorEntity[MicroBotDataUpdateCoordin
|
|||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(self, coordinator, config_entry):
|
def __init__(self, coordinator: MicroBotDataUpdateCoordinator) -> None:
|
||||||
"""Initialise the entity."""
|
"""Initialise the entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._address = self.coordinator.ble_device.address
|
self._address = self.coordinator.ble_device.address
|
||||||
|
@@ -7,7 +7,6 @@ from typing import Any
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import (
|
from homeassistant.helpers.entity_platform import (
|
||||||
@@ -16,8 +15,7 @@ from homeassistant.helpers.entity_platform import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.typing import VolDictType
|
from homeassistant.helpers.typing import VolDictType
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .coordinator import MicroBotConfigEntry
|
||||||
from .coordinator import MicroBotDataUpdateCoordinator
|
|
||||||
from .entity import MicroBotEntity
|
from .entity import MicroBotEntity
|
||||||
|
|
||||||
CALIBRATE = "calibrate"
|
CALIBRATE = "calibrate"
|
||||||
@@ -30,12 +28,11 @@ CALIBRATE_SCHEMA: VolDictType = {
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: MicroBotConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up MicroBot based on a config entry."""
|
"""Set up MicroBot based on a config entry."""
|
||||||
coordinator: MicroBotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
async_add_entities([MicroBotBinarySwitch(entry.runtime_data)])
|
||||||
async_add_entities([MicroBotBinarySwitch(coordinator, entry)])
|
|
||||||
platform = async_get_current_platform()
|
platform = async_get_current_platform()
|
||||||
platform.async_register_entity_service(
|
platform.async_register_entity_service(
|
||||||
CALIBRATE,
|
CALIBRATE,
|
||||||
|
@@ -1,27 +1,18 @@
|
|||||||
"""The kmtronic integration."""
|
"""The kmtronic integration."""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
from pykmtronic.auth import Auth
|
from pykmtronic.auth import Auth
|
||||||
from pykmtronic.hub import KMTronicHubAPI
|
from pykmtronic.hub import KMTronicHubAPI
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
||||||
|
|
||||||
from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER, UPDATE_LISTENER
|
from .coordinator import KMTronicConfigEntry, KMtronicCoordinator
|
||||||
|
|
||||||
PLATFORMS = [Platform.SWITCH]
|
PLATFORMS = [Platform.SWITCH]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: KMTronicConfigEntry) -> bool:
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
"""Set up kmtronic from a config entry."""
|
"""Set up kmtronic from a config entry."""
|
||||||
session = aiohttp_client.async_get_clientsession(hass)
|
session = aiohttp_client.async_get_clientsession(hass)
|
||||||
auth = Auth(
|
auth = Auth(
|
||||||
@@ -31,51 +22,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
entry.data[CONF_PASSWORD],
|
entry.data[CONF_PASSWORD],
|
||||||
)
|
)
|
||||||
hub = KMTronicHubAPI(auth)
|
hub = KMTronicHubAPI(auth)
|
||||||
|
coordinator = KMtronicCoordinator(hass, entry, hub)
|
||||||
async def async_update_data():
|
|
||||||
try:
|
|
||||||
async with asyncio.timeout(10):
|
|
||||||
await hub.async_update_relays()
|
|
||||||
except aiohttp.client_exceptions.ClientResponseError as err:
|
|
||||||
raise UpdateFailed(f"Wrong credentials: {err}") from err
|
|
||||||
except aiohttp.client_exceptions.ClientConnectorError as err:
|
|
||||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
|
||||||
|
|
||||||
coordinator = DataUpdateCoordinator(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
config_entry=entry,
|
|
||||||
name=f"{MANUFACTURER} {hub.name}",
|
|
||||||
update_method=async_update_data,
|
|
||||||
update_interval=timedelta(seconds=30),
|
|
||||||
)
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
entry.runtime_data = coordinator
|
||||||
hass.data[DOMAIN][entry.entry_id] = {
|
|
||||||
DATA_HUB: hub,
|
|
||||||
DATA_COORDINATOR: coordinator,
|
|
||||||
}
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
update_listener = entry.add_update_listener(async_update_options)
|
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||||
hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
async def async_update_options(
|
||||||
|
hass: HomeAssistant, config_entry: KMTronicConfigEntry
|
||||||
|
) -> None:
|
||||||
"""Update options."""
|
"""Update options."""
|
||||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: KMTronicConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
if unload_ok:
|
|
||||||
update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER]
|
|
||||||
update_listener()
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user