mirror of
https://github.com/home-assistant/core.git
synced 2026-06-26 16:45:29 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a662847e7b | |||
| c04448e186 |
Generated
+2
-2
@@ -1712,8 +1712,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/sql/ @gjohansson-ST @dougiteixeira
|
||||
/homeassistant/components/squeezebox/ @rajlaud @pssc @peteS-UK
|
||||
/tests/components/squeezebox/ @rajlaud @pssc @peteS-UK
|
||||
/homeassistant/components/srp_energy/ @briglx @ammmze
|
||||
/tests/components/srp_energy/ @briglx @ammmze
|
||||
/homeassistant/components/srp_energy/ @briglx
|
||||
/tests/components/srp_energy/ @briglx
|
||||
/homeassistant/components/starline/ @anonym-tsk
|
||||
/tests/components/starline/ @anonym-tsk
|
||||
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Mapping
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import time
|
||||
from typing import Any, cast, override
|
||||
from typing import Any, cast
|
||||
|
||||
import jwt
|
||||
|
||||
@@ -109,7 +109,6 @@ class AuthManagerFlowManager(
|
||||
super().__init__(hass)
|
||||
self.auth_manager = auth_manager
|
||||
|
||||
@override
|
||||
async def async_create_flow(
|
||||
self,
|
||||
handler_key: tuple[str, str],
|
||||
@@ -123,7 +122,6 @@ class AuthManagerFlowManager(
|
||||
raise KeyError(f"Unknown auth provider {handler_key}")
|
||||
return await auth_provider.async_login_flow(context)
|
||||
|
||||
@override
|
||||
async def async_finish_flow(
|
||||
self,
|
||||
flow: FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]],
|
||||
|
||||
@@ -39,7 +39,6 @@ class _PyJWSWithLoadCache(PyJWS):
|
||||
# We only ever have a global instance of this class
|
||||
# so we do not have to worry about the LRU growing
|
||||
# each time we create a new instance.
|
||||
@override
|
||||
def _load(self, jwt: str | bytes) -> tuple[bytes, bytes, dict, bytes]:
|
||||
"""Load a JWS."""
|
||||
return super()._load(jwt)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Example auth module."""
|
||||
|
||||
from typing import Any, override
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -35,7 +35,6 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
||||
self._data = config["data"]
|
||||
|
||||
@property
|
||||
@override
|
||||
def input_schema(self) -> vol.Schema:
|
||||
"""Validate login flow input data."""
|
||||
return vol.Schema({vol.Required("pin"): str})
|
||||
@@ -45,7 +44,6 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
||||
"""Validate async_setup_user input data."""
|
||||
return vol.Schema({vol.Required("pin"): str})
|
||||
|
||||
@override
|
||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
||||
@@ -53,7 +51,6 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
||||
"""
|
||||
return SetupFlow(self, self.setup_schema, user_id)
|
||||
|
||||
@override
|
||||
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
|
||||
"""Set up user to use mfa module."""
|
||||
# data shall has been validate in caller
|
||||
@@ -67,7 +64,6 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
||||
|
||||
self._data.append({"user_id": user_id, "pin": pin})
|
||||
|
||||
@override
|
||||
async def async_depose_user(self, user_id: str) -> None:
|
||||
"""Remove user from mfa module."""
|
||||
found = None
|
||||
@@ -78,12 +74,10 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
||||
if found:
|
||||
self._data.remove(found)
|
||||
|
||||
@override
|
||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||
"""Return whether user is setup."""
|
||||
return any(data["user_id"] == user_id for data in self._data)
|
||||
|
||||
@override
|
||||
async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
return any(
|
||||
|
||||
@@ -5,7 +5,7 @@ Sending HOTP through notify service
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, cast, override
|
||||
from typing import Any, cast
|
||||
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
@@ -107,7 +107,6 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
self._init_lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
@override
|
||||
def input_schema(self) -> vol.Schema:
|
||||
"""Validate login flow input data."""
|
||||
return vol.Schema({vol.Required(INPUT_FIELD_CODE): str})
|
||||
@@ -160,7 +159,6 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
|
||||
return sorted(unordered_services)
|
||||
|
||||
@override
|
||||
async def async_setup_flow(self, user_id: str) -> NotifySetupFlow:
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
||||
@@ -170,7 +168,6 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
self, self.input_schema, user_id, self.aync_get_available_notify_services()
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
|
||||
"""Set up auth module for user."""
|
||||
if self._user_settings is None:
|
||||
@@ -184,7 +181,6 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
|
||||
await self._async_save()
|
||||
|
||||
@override
|
||||
async def async_depose_user(self, user_id: str) -> None:
|
||||
"""Depose auth module for user."""
|
||||
if self._user_settings is None:
|
||||
@@ -194,7 +190,6 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
if self._user_settings.pop(user_id, None):
|
||||
await self._async_save()
|
||||
|
||||
@override
|
||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||
"""Return whether user is setup."""
|
||||
if self._user_settings is None:
|
||||
@@ -203,7 +198,6 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
|
||||
return user_id in self._user_settings
|
||||
|
||||
@override
|
||||
async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
if self._user_settings is None:
|
||||
@@ -289,7 +283,6 @@ class NotifySetupFlow(SetupFlow[NotifyAuthModule]):
|
||||
self._notify_service: str | None = None
|
||||
self._target: str | None = None
|
||||
|
||||
@override
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> FlowResult:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
from io import BytesIO
|
||||
from typing import Any, cast, override
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -87,7 +87,6 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
self._init_lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
@override
|
||||
def input_schema(self) -> vol.Schema:
|
||||
"""Validate login flow input data."""
|
||||
return vol.Schema({vol.Required(INPUT_FIELD_CODE): str})
|
||||
@@ -116,7 +115,6 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
self._users[user_id] = ota_secret # type: ignore[index]
|
||||
return ota_secret
|
||||
|
||||
@override
|
||||
async def async_setup_flow(self, user_id: str) -> TotpSetupFlow:
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
||||
@@ -126,7 +124,6 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
assert user is not None
|
||||
return TotpSetupFlow(self, self.input_schema, user)
|
||||
|
||||
@override
|
||||
async def async_setup_user(self, user_id: str, setup_data: Any) -> str:
|
||||
"""Set up auth module for user."""
|
||||
if self._users is None:
|
||||
@@ -139,7 +136,6 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
await self._async_save()
|
||||
return result
|
||||
|
||||
@override
|
||||
async def async_depose_user(self, user_id: str) -> None:
|
||||
"""Depose auth module for user."""
|
||||
if self._users is None:
|
||||
@@ -148,7 +144,6 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
if self._users.pop(user_id, None): # type: ignore[union-attr]
|
||||
await self._async_save()
|
||||
|
||||
@override
|
||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||
"""Return whether user is setup."""
|
||||
if self._users is None:
|
||||
@@ -156,7 +151,6 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
|
||||
return user_id in self._users # type: ignore[operator]
|
||||
|
||||
@override
|
||||
async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
if self._users is None:
|
||||
@@ -195,7 +189,6 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]):
|
||||
super().__init__(auth_module, setup_schema, user.id)
|
||||
self._user = user
|
||||
|
||||
@override
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> FlowResult:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Permissions for Home Assistant."""
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import TYPE_CHECKING, override
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -68,17 +68,14 @@ class PolicyPermissions(AbstractPermissions):
|
||||
self._policy = policy
|
||||
self._perm_lookup = perm_lookup
|
||||
|
||||
@override
|
||||
def access_all_entities(self, key: str) -> bool:
|
||||
"""Check if we have a certain access to all entities."""
|
||||
return test_all(self._policy.get(CAT_ENTITIES), key)
|
||||
|
||||
@override
|
||||
def _entity_func(self) -> Callable[[str, str], bool]:
|
||||
"""Return a function that can test entity access."""
|
||||
return compile_entities(self._policy.get(CAT_ENTITIES), self._perm_lookup)
|
||||
|
||||
@override
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""Equals check."""
|
||||
return isinstance(other, PolicyPermissions) and other._policy == self._policy
|
||||
@@ -87,12 +84,10 @@ class PolicyPermissions(AbstractPermissions):
|
||||
class _OwnerPermissions(AbstractPermissions):
|
||||
"""Owner permissions."""
|
||||
|
||||
@override
|
||||
def access_all_entities(self, key: str) -> bool:
|
||||
"""Check if we have a certain access to all entities."""
|
||||
return True
|
||||
|
||||
@override
|
||||
def _entity_func(self) -> Callable[[str, str], bool]:
|
||||
"""Return a function that can test entity access."""
|
||||
return lambda entity_id, key: True
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, override
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -57,7 +57,6 @@ class CommandLineAuthProvider(AuthProvider):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._user_meta: dict[str, dict[str, Any]] = {}
|
||||
|
||||
@override
|
||||
async def async_login_flow(
|
||||
self, context: AuthFlowContext | None
|
||||
) -> CommandLineLoginFlow:
|
||||
@@ -106,7 +105,6 @@ class CommandLineAuthProvider(AuthProvider):
|
||||
meta[key] = value
|
||||
self._user_meta[username] = meta
|
||||
|
||||
@override
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Mapping[str, str]
|
||||
) -> Credentials:
|
||||
@@ -119,7 +117,6 @@ class CommandLineAuthProvider(AuthProvider):
|
||||
# Create new credentials.
|
||||
return self.async_create_credentials({"username": username})
|
||||
|
||||
@override
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials
|
||||
) -> UserMeta:
|
||||
@@ -139,7 +136,6 @@ class CommandLineAuthProvider(AuthProvider):
|
||||
class CommandLineLoginFlow(LoginFlow[CommandLineAuthProvider]):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
@override
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> AuthFlowResult:
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
import base64
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any, cast, override
|
||||
from typing import Any, cast
|
||||
|
||||
import bcrypt
|
||||
import voluptuous as vol
|
||||
@@ -302,7 +302,6 @@ class HassAuthProvider(AuthProvider):
|
||||
self.data: Data | None = None
|
||||
self._init_lock = asyncio.Lock()
|
||||
|
||||
@override
|
||||
async def async_initialize(self) -> None:
|
||||
"""Initialize the auth provider."""
|
||||
async with self._init_lock:
|
||||
@@ -313,7 +312,6 @@ class HassAuthProvider(AuthProvider):
|
||||
await data.async_load()
|
||||
self.data = data
|
||||
|
||||
@override
|
||||
async def async_login_flow(self, context: AuthFlowContext | None) -> HassLoginFlow:
|
||||
"""Return a flow to login."""
|
||||
return HassLoginFlow(self)
|
||||
@@ -371,7 +369,6 @@ class HassAuthProvider(AuthProvider):
|
||||
)
|
||||
await self.data.async_save()
|
||||
|
||||
@override
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Mapping[str, str]
|
||||
) -> Credentials:
|
||||
@@ -390,7 +387,6 @@ class HassAuthProvider(AuthProvider):
|
||||
# Create new credentials.
|
||||
return self.async_create_credentials({"username": username})
|
||||
|
||||
@override
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials
|
||||
) -> UserMeta:
|
||||
@@ -414,7 +410,6 @@ class HassAuthProvider(AuthProvider):
|
||||
class HassLoginFlow(LoginFlow[HassAuthProvider]):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
@override
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> AuthFlowResult:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from collections.abc import Mapping
|
||||
import hmac
|
||||
from typing import override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -34,7 +33,6 @@ class InvalidAuthError(HomeAssistantError):
|
||||
class ExampleAuthProvider(AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
||||
@override
|
||||
async def async_login_flow(
|
||||
self, context: AuthFlowContext | None
|
||||
) -> ExampleLoginFlow:
|
||||
@@ -63,7 +61,6 @@ class ExampleAuthProvider(AuthProvider):
|
||||
):
|
||||
raise InvalidAuthError
|
||||
|
||||
@override
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Mapping[str, str]
|
||||
) -> Credentials:
|
||||
@@ -77,7 +74,6 @@ class ExampleAuthProvider(AuthProvider):
|
||||
# Create new credentials.
|
||||
return self.async_create_credentials({"username": username})
|
||||
|
||||
@override
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials
|
||||
) -> UserMeta:
|
||||
@@ -99,7 +95,6 @@ class ExampleAuthProvider(AuthProvider):
|
||||
class ExampleLoginFlow(LoginFlow[ExampleAuthProvider]):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
@override
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> AuthFlowResult:
|
||||
|
||||
@@ -13,7 +13,7 @@ from ipaddress import (
|
||||
ip_address,
|
||||
ip_network,
|
||||
)
|
||||
from typing import Any, cast, override
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -98,12 +98,10 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
]
|
||||
|
||||
@property
|
||||
@override
|
||||
def support_mfa(self) -> bool:
|
||||
"""Trusted Networks auth provider does not support MFA."""
|
||||
return False
|
||||
|
||||
@override
|
||||
async def async_login_flow(
|
||||
self, context: AuthFlowContext | None
|
||||
) -> TrustedNetworksLoginFlow:
|
||||
@@ -146,7 +144,6 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
self.config[CONF_ALLOW_BYPASS_LOGIN],
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Mapping[str, str]
|
||||
) -> Credentials:
|
||||
@@ -175,7 +172,6 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
# We only allow login as exist user
|
||||
raise InvalidUserError
|
||||
|
||||
@override
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials
|
||||
) -> UserMeta:
|
||||
@@ -207,7 +203,6 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
raise InvalidAuthError("Can't allow access from Home Assistant Cloud")
|
||||
|
||||
@callback
|
||||
@override
|
||||
def async_validate_refresh_token(
|
||||
self, refresh_token: RefreshToken, remote_ip: str | None = None
|
||||
) -> None:
|
||||
@@ -235,7 +230,6 @@ class TrustedNetworksLoginFlow(LoginFlow[TrustedNetworksAuthProvider]):
|
||||
self._ip_address = ip_addr
|
||||
self._allow_bypass_login = allow_bypass_login
|
||||
|
||||
@override
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> AuthFlowResult:
|
||||
|
||||
@@ -14,7 +14,7 @@ import platform
|
||||
import sys
|
||||
import threading
|
||||
from time import monotonic
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
# Import cryptography early since import openssl is not thread-safe
|
||||
# _frozen_importlib._DeadlockError: deadlock detected by
|
||||
@@ -697,7 +697,6 @@ def _create_log_file(
|
||||
class _RotatingFileHandlerWithoutShouldRollOver(RotatingFileHandler):
|
||||
"""RotatingFileHandler that does not check if it should roll over on every log."""
|
||||
|
||||
@override
|
||||
def shouldRollover(self, record: logging.LogRecord) -> bool:
|
||||
"""Never roll over.
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ class AmazonNotifyEntityDescription(NotifyEntityDescription):
|
||||
"""Alexa Devices notify entity description."""
|
||||
|
||||
is_supported: Callable[[AmazonDevice], bool] = lambda _device: True
|
||||
is_available_fn: Callable[[AmazonDevice], bool] = lambda _device: True
|
||||
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
|
||||
subkey: str
|
||||
|
||||
@@ -40,9 +39,6 @@ NOTIFY: Final = (
|
||||
key="announce",
|
||||
translation_key="announce",
|
||||
subkey="AUDIO_PLAYER",
|
||||
is_available_fn=lambda device: (
|
||||
device.communication_settings.get("communications") != "OFF"
|
||||
),
|
||||
method=lambda api, device, message: api.call_alexa_announcement(
|
||||
device, message
|
||||
),
|
||||
@@ -83,13 +79,6 @@ class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
|
||||
|
||||
entity_description: AmazonNotifyEntityDescription
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return (
|
||||
self.entity_description.is_available_fn(self.device) and super().available
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_send_message(
|
||||
self, message: str, title: str | None = None, **kwargs: Any
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Platform for Alexa To-do integration."""
|
||||
|
||||
from typing import TYPE_CHECKING, override
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aioamazondevices.structures import (
|
||||
AmazonListInfo,
|
||||
@@ -88,7 +88,6 @@ class AlexaToDoList(AmazonServiceEntity, TodoListEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def todo_items(self) -> list[TodoItem]:
|
||||
"""Return all to-do items in the list."""
|
||||
|
||||
@@ -105,7 +104,6 @@ class AlexaToDoList(AmazonServiceEntity, TodoListEntity):
|
||||
for item in todo_items
|
||||
]
|
||||
|
||||
@override
|
||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||
"""Add an item to the To-do list."""
|
||||
_LOGGER.debug(
|
||||
@@ -126,7 +124,6 @@ class AlexaToDoList(AmazonServiceEntity, TodoListEntity):
|
||||
self._list.name,
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_delete_todo_items(self, uids: list[str]) -> None:
|
||||
"""Delete items from the to-do list."""
|
||||
_LOGGER.debug("Called async_delete_todo_items for %s item(s)", len(uids))
|
||||
@@ -153,7 +150,6 @@ class AlexaToDoList(AmazonServiceEntity, TodoListEntity):
|
||||
existing_item.version,
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_update_todo_item(self, item: TodoItem) -> None:
|
||||
"""Update an item in the To-do list."""
|
||||
list_items_lookup = self.coordinator.todo_list_items[self._list.id]
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "gold",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["anthropic==0.108.0"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ rules:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration has no actions.
|
||||
appropriate-polling: done
|
||||
appropriate-polling:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration does not poll.
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
|
||||
@@ -210,7 +210,6 @@ class AqvifyAggrDataCoordinator(
|
||||
end_time = base_time.replace(minute=59).strftime(date_time_fmt)
|
||||
return beg_time, end_time
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> dict[str, AqvifyHourAggregatedValues]:
|
||||
"""Fetch device state."""
|
||||
devices = self.config_entry.runtime_data.coordinator.data.devices
|
||||
|
||||
@@ -171,7 +171,6 @@ class AqvifyAggrSensor(AqvifyAggrEntity, SensorEntity):
|
||||
entity_description: AqvifySensorAggrEntityDescription
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_value(self) -> StateType | datetime | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data[self.device_key])
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyatome"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyAtome==0.1.2"]
|
||||
"requirements": ["pyAtome==0.1.1"]
|
||||
}
|
||||
|
||||
@@ -174,12 +174,6 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
"""Set the fan mode."""
|
||||
await self.coordinator.async_set_fan_mode(self._ac_index, self.data, fan_mode)
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
"""Set the swing mode."""
|
||||
await self.coordinator.async_set_swing_mode(
|
||||
self._ac_index, self.data, swing_mode
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off."""
|
||||
|
||||
@@ -4,10 +4,10 @@ import datetime
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from ccm15 import CCM15Device, CCM15DeviceState, CCM15SlaveDevice, TriState
|
||||
from ccm15 import CCM15Device, CCM15DeviceState, CCM15SlaveDevice
|
||||
import httpx
|
||||
|
||||
from homeassistant.components.climate import SWING_ON, HVACMode
|
||||
from homeassistant.components.climate import HVACMode
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
@@ -86,14 +86,6 @@ class CCM15Coordinator(DataUpdateCoordinator[CCM15DeviceState]):
|
||||
data.fan_mode = CONST_FAN_CMD_MAP[fan_mode]
|
||||
await self.async_set_state(ac_index, data)
|
||||
|
||||
async def async_set_swing_mode(
|
||||
self, ac_index: int, data: CCM15SlaveDevice, swing_mode: str
|
||||
) -> None:
|
||||
"""Set the swing mode."""
|
||||
_LOGGER.debug("Set Swing[%s]='%s'", ac_index, swing_mode)
|
||||
data.desired_swing = TriState.ON if swing_mode == SWING_ON else TriState.OFF
|
||||
await self.async_set_state(ac_index, data)
|
||||
|
||||
async def async_set_temperature(
|
||||
self,
|
||||
ac_index: int,
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
"""Update the IP addresses of your Cloudflare DNS records."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, SERVICE_UPDATE_RECORDS
|
||||
from .coordinator import CloudflareConfigEntry, CloudflareCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Cloudflare."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -> bool:
|
||||
@@ -25,6 +14,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -
|
||||
# Since we are not using coordinator for data reads, we need to add dummy listener
|
||||
entry.async_on_unload(entry.runtime_data.async_add_listener(lambda: None))
|
||||
|
||||
async def update_records_service(_: ServiceCall) -> None:
|
||||
"""Set up service for manual trigger."""
|
||||
await entry.runtime_data.async_request_refresh()
|
||||
|
||||
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
|
||||
hass.services.async_register(DOMAIN, SERVICE_UPDATE_RECORDS, update_records_service)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Services for cloudflare."""
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import service
|
||||
|
||||
from .const import DOMAIN, SERVICE_UPDATE_RECORDS
|
||||
from .coordinator import CloudflareConfigEntry
|
||||
|
||||
|
||||
async def update_records_service(call: ServiceCall) -> None:
|
||||
"""Set up service for manual trigger."""
|
||||
entry: CloudflareConfigEntry = service.async_get_config_entry(
|
||||
call.hass, DOMAIN, None
|
||||
)
|
||||
await entry.runtime_data.async_request_refresh()
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services."""
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_UPDATE_RECORDS,
|
||||
update_records_service,
|
||||
)
|
||||
@@ -1,15 +1,16 @@
|
||||
"""Module contains the CompitClimate class for controlling climate entities."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
from compit_inext_api import Parameter
|
||||
from compit_inext_api.consts import (
|
||||
CompitFanMode,
|
||||
CompitHVACMode,
|
||||
CompitParameter,
|
||||
CompitPresetMode,
|
||||
)
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
FAN_AUTO,
|
||||
@@ -22,7 +23,6 @@ from homeassistant.components.climate import (
|
||||
PRESET_HOME,
|
||||
PRESET_NONE,
|
||||
ClimateEntity,
|
||||
ClimateEntityDescription,
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
@@ -38,19 +38,10 @@ from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
# Device class for climate devices in Compit system
|
||||
CLIMATE_DEVICE_CLASS = 10
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class CompitDeviceDescription(ClimateEntityDescription):
|
||||
"""Class to describe a Compit climate entity."""
|
||||
|
||||
supported_features: ClimateEntityFeature
|
||||
available_presets: list[str]
|
||||
available_fan_modes: list[str]
|
||||
available_hvac_modes: list[HVACMode]
|
||||
|
||||
|
||||
COMPIT_MODE_MAP = {
|
||||
CompitHVACMode.COOL: HVACMode.COOL,
|
||||
CompitHVACMode.HEAT: HVACMode.HEAT,
|
||||
@@ -77,55 +68,6 @@ HVAC_MODE_TO_COMPIT_MODE = {v: k for k, v in COMPIT_MODE_MAP.items()}
|
||||
FAN_MODE_TO_COMPIT_FAN_MODE = {v: k for k, v in COMPIT_FANSPEED_MAP.items()}
|
||||
PRESET_MODE_TO_COMPIT_PRESET_MODE = {v: k for k, v in COMPIT_PRESET_MAP.items()}
|
||||
|
||||
DEVICE_DEFINITIONS: dict[int, CompitDeviceDescription] = {
|
||||
224: CompitDeviceDescription(
|
||||
key="R 900",
|
||||
supported_features=ClimateEntityFeature.PRESET_MODE,
|
||||
available_presets=[PRESET_HOME, PRESET_AWAY],
|
||||
available_fan_modes=[],
|
||||
available_hvac_modes=[HVACMode.HEAT, HVACMode.OFF],
|
||||
),
|
||||
223: CompitDeviceDescription(
|
||||
key="Nano Color 2",
|
||||
supported_features=ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE,
|
||||
available_presets=[PRESET_HOME, PRESET_ECO, PRESET_NONE, PRESET_AWAY],
|
||||
available_fan_modes=[FAN_OFF, FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH],
|
||||
available_hvac_modes=[
|
||||
HVACMode.COOL,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.OFF,
|
||||
],
|
||||
),
|
||||
12: CompitDeviceDescription(
|
||||
key="Nano Color",
|
||||
supported_features=ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE,
|
||||
available_presets=[PRESET_HOME, PRESET_ECO, PRESET_NONE, PRESET_AWAY],
|
||||
available_fan_modes=[FAN_OFF, FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH],
|
||||
available_hvac_modes=[
|
||||
HVACMode.COOL,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.OFF,
|
||||
],
|
||||
),
|
||||
7: CompitDeviceDescription(
|
||||
key="Nano One",
|
||||
supported_features=ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE,
|
||||
available_presets=[PRESET_HOME, PRESET_ECO, PRESET_NONE, PRESET_AWAY],
|
||||
available_fan_modes=[FAN_OFF, FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH],
|
||||
available_hvac_modes=[
|
||||
HVACMode.COOL,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.OFF,
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -135,49 +77,64 @@ async def async_setup_entry(
|
||||
"""Set up the CompitClimate platform from a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
async_add_devices(
|
||||
CompitClimate(
|
||||
coordinator,
|
||||
device_id,
|
||||
device_definition,
|
||||
)
|
||||
for device_id, device in coordinator.connector.all_devices.items()
|
||||
if (device_definition := DEVICE_DEFINITIONS.get(device.definition.code))
|
||||
)
|
||||
climate_entities = []
|
||||
for device_id in coordinator.connector.all_devices:
|
||||
device = coordinator.connector.all_devices[device_id]
|
||||
|
||||
if device.definition.device_class == CLIMATE_DEVICE_CLASS:
|
||||
climate_entities.append(
|
||||
CompitClimate(
|
||||
coordinator,
|
||||
device_id,
|
||||
{
|
||||
parameter.parameter_code: parameter
|
||||
for parameter in device.definition.parameters
|
||||
},
|
||||
device.definition.name,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_devices(climate_entities)
|
||||
|
||||
|
||||
class CompitClimate(CoordinatorEntity[CompitDataUpdateCoordinator], ClimateEntity):
|
||||
"""Representation of a Compit climate device."""
|
||||
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_hvac_modes = [*COMPIT_MODE_MAP.values()]
|
||||
_attr_name = None
|
||||
_attr_has_entity_name = True
|
||||
entity_description: CompitDeviceDescription
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: CompitDataUpdateCoordinator,
|
||||
device_id: int,
|
||||
entity_description: CompitDeviceDescription,
|
||||
parameters: dict[str, Parameter],
|
||||
device_name: str,
|
||||
) -> None:
|
||||
"""Initialize the climate device."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{entity_description.key}_{device_id}"
|
||||
self._attr_unique_id = f"{device_name}_{device_id}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, str(device_id))},
|
||||
name=entity_description.key,
|
||||
name=device_name,
|
||||
manufacturer=MANUFACTURER_NAME,
|
||||
model=entity_description.key,
|
||||
model=device_name,
|
||||
)
|
||||
|
||||
self.parameters = parameters
|
||||
self.device_id = device_id
|
||||
self.entity_description = entity_description
|
||||
self._attr_supported_features = entity_description.supported_features
|
||||
self._attr_preset_modes = entity_description.available_presets
|
||||
self._attr_fan_modes = entity_description.available_fan_modes
|
||||
self._attr_hvac_modes = [
|
||||
HVACMode(mode) for mode in entity_description.available_hvac_modes
|
||||
]
|
||||
self.available_presets: Parameter | None = self.parameters.get(
|
||||
CompitParameter.PRESET_MODE.value
|
||||
)
|
||||
self.available_fan_modes: Parameter | None = self.parameters.get(
|
||||
CompitParameter.FAN_MODE.value
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
@@ -206,6 +163,38 @@ class CompitClimate(CoordinatorEntity[CompitDataUpdateCoordinator], ClimateEntit
|
||||
return None
|
||||
return float(value)
|
||||
|
||||
@cached_property
|
||||
@override
|
||||
def preset_modes(self) -> list[str] | None:
|
||||
"""Return the available preset modes."""
|
||||
if self.available_presets is None or self.available_presets.details is None:
|
||||
return []
|
||||
|
||||
preset_modes = []
|
||||
for item in self.available_presets.details:
|
||||
if item is not None:
|
||||
ha_preset = COMPIT_PRESET_MAP.get(CompitPresetMode(item.state))
|
||||
if ha_preset and ha_preset not in preset_modes:
|
||||
preset_modes.append(ha_preset)
|
||||
|
||||
return preset_modes
|
||||
|
||||
@cached_property
|
||||
@override
|
||||
def fan_modes(self) -> list[str] | None:
|
||||
"""Return the available fan modes."""
|
||||
if self.available_fan_modes is None or self.available_fan_modes.details is None:
|
||||
return []
|
||||
|
||||
fan_modes = []
|
||||
for item in self.available_fan_modes.details:
|
||||
if item is not None:
|
||||
ha_fan_mode = COMPIT_FANSPEED_MAP.get(CompitFanMode(item.state))
|
||||
if ha_fan_mode and ha_fan_mode not in fan_modes:
|
||||
fan_modes.append(ha_fan_mode)
|
||||
|
||||
return fan_modes
|
||||
|
||||
@property
|
||||
@override
|
||||
def preset_mode(self) -> str | None:
|
||||
@@ -213,8 +202,7 @@ class CompitClimate(CoordinatorEntity[CompitDataUpdateCoordinator], ClimateEntit
|
||||
preset_mode = self.get_parameter_value(CompitParameter.PRESET_MODE)
|
||||
|
||||
if preset_mode is not None:
|
||||
compit_preset_mode = CompitPresetMode(preset_mode)
|
||||
return COMPIT_PRESET_MAP.get(compit_preset_mode)
|
||||
return COMPIT_PRESET_MAP.get(CompitPresetMode(preset_mode))
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -223,8 +211,7 @@ class CompitClimate(CoordinatorEntity[CompitDataUpdateCoordinator], ClimateEntit
|
||||
"""Return the current fan mode."""
|
||||
fan_mode = self.get_parameter_value(CompitParameter.FAN_MODE)
|
||||
if fan_mode is not None:
|
||||
compit_fan_mode = CompitFanMode(fan_mode)
|
||||
return COMPIT_FANSPEED_MAP.get(compit_fan_mode)
|
||||
return COMPIT_FANSPEED_MAP.get(CompitFanMode(fan_mode))
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -233,8 +220,7 @@ class CompitClimate(CoordinatorEntity[CompitDataUpdateCoordinator], ClimateEntit
|
||||
"""Return the current HVAC mode."""
|
||||
hvac_mode = self.get_parameter_value(CompitParameter.HVAC_MODE)
|
||||
if hvac_mode is not None:
|
||||
compit_hvac_mode = CompitHVACMode(hvac_mode)
|
||||
return COMPIT_MODE_MAP.get(compit_hvac_mode)
|
||||
return COMPIT_MODE_MAP.get(CompitHVACMode(hvac_mode))
|
||||
return None
|
||||
|
||||
@override
|
||||
|
||||
@@ -575,9 +575,6 @@ DEVICE_DEFINITIONS: dict[int, CompitDeviceDescription] = {
|
||||
CompitParameter.VENTILATION_ALARM: DESCRIPTIONS[
|
||||
CompitParameter.VENTILATION_ALARM
|
||||
],
|
||||
CompitParameter.VENTILATION_GEAR: DESCRIPTIONS[
|
||||
CompitParameter.VENTILATION_GEAR
|
||||
],
|
||||
},
|
||||
),
|
||||
14: CompitDeviceDescription(
|
||||
|
||||
@@ -733,7 +733,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
|
||||
# If already playing, or don't want to autoplay, no need to call Play
|
||||
autoplay = extra.get("autoplay", True)
|
||||
if self._device.transport_state == TransportState.PLAYING or not autoplay:
|
||||
if self._device.transport_state is TransportState.PLAYING or not autoplay:
|
||||
return
|
||||
|
||||
# Play it
|
||||
@@ -766,7 +766,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
if not (play_mode := self._device.play_mode):
|
||||
return None
|
||||
|
||||
if play_mode == PlayMode.VENDOR_DEFINED:
|
||||
if play_mode is PlayMode.VENDOR_DEFINED:
|
||||
return None
|
||||
|
||||
return play_mode in (PlayMode.SHUFFLE, PlayMode.RANDOM)
|
||||
@@ -802,10 +802,10 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
if not (play_mode := self._device.play_mode):
|
||||
return None
|
||||
|
||||
if play_mode == PlayMode.VENDOR_DEFINED:
|
||||
if play_mode is PlayMode.VENDOR_DEFINED:
|
||||
return None
|
||||
|
||||
if play_mode == PlayMode.REPEAT_ONE:
|
||||
if play_mode is PlayMode.REPEAT_ONE:
|
||||
return RepeatMode.ONE
|
||||
|
||||
if play_mode in (PlayMode.REPEAT_ALL, PlayMode.RANDOM):
|
||||
|
||||
@@ -157,9 +157,9 @@ class ElevenLabsSTTEntity(SpeechToTextEntity):
|
||||
|
||||
raw_pcm_compatible = (
|
||||
metadata.codec == AudioCodecs.PCM
|
||||
and metadata.sample_rate == AudioSampleRates.SAMPLERATE_16000
|
||||
and metadata.channel == AudioChannels.CHANNEL_MONO
|
||||
and metadata.bit_rate == AudioBitRates.BITRATE_16
|
||||
and metadata.sample_rate is AudioSampleRates.SAMPLERATE_16000
|
||||
and metadata.channel is AudioChannels.CHANNEL_MONO
|
||||
and metadata.bit_rate is AudioBitRates.BITRATE_16
|
||||
)
|
||||
if raw_pcm_compatible:
|
||||
file_format = "pcm_s16le_16"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==3.0.0"],
|
||||
"requirements": ["pyenphase==2.4.9"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -109,10 +109,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
||||
state_float = state.state
|
||||
if not math.isfinite(state_float):
|
||||
return None
|
||||
if self.device_class in (
|
||||
SensorDeviceClass.TIMESTAMP,
|
||||
SensorDeviceClass.UPTIME,
|
||||
):
|
||||
if self.device_class is SensorDeviceClass.TIMESTAMP:
|
||||
return dt_util.utc_from_timestamp(state_float)
|
||||
return state_float
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ class FrontierSiliconConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self._async_abort_entries_match({CONF_WEBFSAPI_URL: self._webfsapi_url})
|
||||
return await self._async_step_device_config_if_needed()
|
||||
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
|
||||
@@ -143,7 +143,7 @@ def _get_entity_descriptions(
|
||||
local_sync = True
|
||||
if (
|
||||
search := data.get(CONF_SEARCH)
|
||||
) or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
|
||||
) or calendar_item.access_role is AccessRole.FREE_BUSY_READER:
|
||||
read_only = True
|
||||
local_sync = False
|
||||
entity_description = GoogleCalendarEntityDescription(
|
||||
@@ -388,14 +388,14 @@ class GoogleCalendarEntity(
|
||||
"""Return True if the event is visible and not declined."""
|
||||
|
||||
if any(
|
||||
attendee.is_self and attendee.response_status == ResponseStatus.DECLINED
|
||||
attendee.is_self and attendee.response_status is ResponseStatus.DECLINED
|
||||
for attendee in event.attendees
|
||||
):
|
||||
return False
|
||||
# Calendar enttiy may be limited to a specific event type
|
||||
if (
|
||||
self.entity_description.event_type is not None
|
||||
and self.entity_description.event_type != event.event_type
|
||||
and self.entity_description.event_type is not event.event_type
|
||||
):
|
||||
return False
|
||||
# Default calendar entity omits the special types but includes all the others
|
||||
|
||||
@@ -754,7 +754,7 @@ async def async_prepare_files_for_prompt(
|
||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||
)
|
||||
|
||||
if uploaded_file.state == FileState.FAILED:
|
||||
if uploaded_file.state is FileState.FAILED:
|
||||
raise HomeAssistantError(
|
||||
f"File `{uploaded_file.name}` processing"
|
||||
" failed, reason:"
|
||||
@@ -766,7 +766,7 @@ async def async_prepare_files_for_prompt(
|
||||
tasks = [
|
||||
asyncio.create_task(wait_for_file_processing(part))
|
||||
for part in prompt_parts
|
||||
if part.state != FileState.ACTIVE
|
||||
if part.state is not FileState.ACTIVE
|
||||
]
|
||||
async with asyncio.timeout(TIMEOUT_MILLIS / 1000):
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
@@ -5,6 +5,7 @@ from functools import partial
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
from aiohasupervisor import SupervisorBadRequestError, SupervisorError
|
||||
from aiohasupervisor.models import (
|
||||
@@ -16,7 +17,7 @@ from aiohasupervisor.models import (
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.auth.models import RefreshToken, User
|
||||
from homeassistant.components import frontend
|
||||
from homeassistant.components import frontend, network
|
||||
from homeassistant.components.homeassistant import async_set_stop_handler
|
||||
from homeassistant.components.onboarding import async_is_onboarded
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
@@ -30,6 +31,7 @@ from homeassistant.helpers import (
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
@@ -54,14 +56,19 @@ from .auth import async_setup_auth_view
|
||||
from .config import HassioConfig
|
||||
from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
ATTR_UPDATE_KEY,
|
||||
ATTR_WS_EVENT,
|
||||
DATA_COMPONENT,
|
||||
DATA_CONFIG_STORE,
|
||||
DATA_HASSIO_HOST,
|
||||
DATA_HASSIO_SUPERVISOR_USER,
|
||||
DATA_KEY_SUPERVISOR_ISSUES,
|
||||
DOMAIN,
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
EVENT_SUPERVISOR_UPDATE,
|
||||
MAIN_COORDINATOR,
|
||||
STATS_COORDINATOR,
|
||||
UPDATE_KEY_NETWORK,
|
||||
)
|
||||
from .coordinator import (
|
||||
HassioAddOnDataUpdateCoordinator,
|
||||
@@ -385,6 +392,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
entry.async_on_unload(hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config))
|
||||
|
||||
@callback
|
||||
def _async_supervisor_event(event: dict[str, Any]) -> None:
|
||||
"""Reload network adapters when Supervisor reports a network change."""
|
||||
if (
|
||||
event.get(ATTR_WS_EVENT) == EVENT_SUPERVISOR_UPDATE
|
||||
and event.get(ATTR_UPDATE_KEY) == UPDATE_KEY_NETWORK
|
||||
):
|
||||
entry.async_create_background_task(
|
||||
hass, network.async_reload_adapters(hass), "hassio_reload_adapters"
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, EVENT_SUPERVISOR_EVENT, _async_supervisor_event)
|
||||
)
|
||||
|
||||
async def update_hass_api(refresh_token: RefreshToken) -> None:
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
# hass.config.api is always set here: hassio depends on http, and the
|
||||
|
||||
@@ -91,6 +91,7 @@ EVENT_ISSUE_CHANGED = "issue_changed"
|
||||
EVENT_ISSUE_REMOVED = "issue_removed"
|
||||
EVENT_JOB = "job"
|
||||
|
||||
UPDATE_KEY_NETWORK = "network"
|
||||
UPDATE_KEY_SUPERVISOR = "supervisor"
|
||||
STARTUP_COMPLETE = "complete"
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"domain": "hassio",
|
||||
"name": "Home Assistant Supervisor",
|
||||
"after_dependencies": ["network"],
|
||||
"codeowners": ["@home-assistant/supervisor"],
|
||||
"dependencies": ["http", "repairs"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/hassio",
|
||||
|
||||
@@ -261,7 +261,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
|
||||
def release_url(self) -> str | None:
|
||||
"""URL to the full release notes of the latest version available."""
|
||||
version = AwesomeVersion(self.latest_version)
|
||||
if version.dev or version.strategy == AwesomeVersionStrategy.UNKNOWN:
|
||||
if version.dev or version.strategy is AwesomeVersionStrategy.UNKNOWN:
|
||||
return "https://github.com/home-assistant/operating-system/commits/dev"
|
||||
return (
|
||||
f"https://github.com/home-assistant/operating-system/releases/tag/{version}"
|
||||
@@ -324,7 +324,7 @@ class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):
|
||||
def release_url(self) -> str | None:
|
||||
"""URL to the full release notes of the latest version available."""
|
||||
version = AwesomeVersion(self.latest_version)
|
||||
if version.dev or version.strategy == AwesomeVersionStrategy.UNKNOWN:
|
||||
if version.dev or version.strategy is AwesomeVersionStrategy.UNKNOWN:
|
||||
return "https://github.com/home-assistant/supervisor/commits/main"
|
||||
return f"https://github.com/home-assistant/supervisor/releases/tag/{version}"
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyhelty"],
|
||||
"quality_scale": "silver",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyhelty==0.2.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.99", "babel==2.18.0"]
|
||||
"requirements": ["holidays==0.98", "babel==2.18.0"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ from homeassistant.const import Platform
|
||||
DOMAIN = "hr_energy_qube"
|
||||
PLATFORMS = (
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.WATER_HEATER,
|
||||
|
||||
@@ -25,7 +25,6 @@ class QubeData:
|
||||
|
||||
state: QubeState
|
||||
switches: dict[str, bool | None]
|
||||
sg_ready_mode: str | None
|
||||
|
||||
|
||||
class QubeCoordinator(DataUpdateCoordinator[QubeData]):
|
||||
@@ -50,7 +49,6 @@ class QubeCoordinator(DataUpdateCoordinator[QubeData]):
|
||||
try:
|
||||
state = await self.client.get_all_data()
|
||||
switches = await self.client.read_all_switches()
|
||||
sg_ready_mode = await self.client.get_sg_ready_mode()
|
||||
except (ConnectionError, TimeoutError, OSError) as exc:
|
||||
raise UpdateFailed(
|
||||
f"Error communicating with Qube heat pump: {exc}"
|
||||
@@ -59,4 +57,4 @@ class QubeCoordinator(DataUpdateCoordinator[QubeData]):
|
||||
if state is None:
|
||||
raise UpdateFailed("No data received from Qube heat pump")
|
||||
|
||||
return QubeData(state=state, switches=switches, sg_ready_mode=sg_ready_mode)
|
||||
return QubeData(state=state, switches=switches)
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
"""Select platform for Qube Heat Pump."""
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import QubeConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import QubeCoordinator
|
||||
from .entity import QubeEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
SG_READY_OPTIONS = ["off", "block", "plus", "max"]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: QubeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Qube select entities."""
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
async_add_entities([QubeSGReadySelect(coordinator, entry)])
|
||||
|
||||
|
||||
class QubeSGReadySelect(QubeEntity, SelectEntity):
|
||||
"""Qube SG Ready mode select entity."""
|
||||
|
||||
_attr_options = SG_READY_OPTIONS
|
||||
_attr_translation_key = "sg_ready_mode"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: QubeCoordinator,
|
||||
entry: QubeConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the select entity."""
|
||||
super().__init__(coordinator, entry)
|
||||
self._attr_unique_id = f"{entry.entry_id}-sg_ready_mode"
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.coordinator.data.sg_ready_mode is not None
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the current SG Ready mode."""
|
||||
return self.coordinator.data.sg_ready_mode
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set the SG Ready mode."""
|
||||
try:
|
||||
success = await self.coordinator.client.set_sg_ready_mode(option)
|
||||
except (ConnectionError, TimeoutError, OSError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="switch_command_failed",
|
||||
) from err
|
||||
if not success:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="switch_command_failed",
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -130,17 +130,6 @@
|
||||
"name": "User pump"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"sg_ready_mode": {
|
||||
"name": "Smart grid ready mode",
|
||||
"state": {
|
||||
"block": "Block",
|
||||
"max": "Max",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"plus": "Plus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"compressor_speed": {
|
||||
"name": "Compressor speed"
|
||||
|
||||
@@ -57,11 +57,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bo
|
||||
gps_accuracy_threshold,
|
||||
entry,
|
||||
)
|
||||
await hass.async_add_executor_job(account.setup)
|
||||
|
||||
entry.runtime_data = account
|
||||
|
||||
await hass.async_add_executor_job(account.setup)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
await async_setup_photo_cache(hass, account)
|
||||
|
||||
|
||||
@@ -110,12 +110,16 @@ class IcloudAccount:
|
||||
with_family=self._with_family,
|
||||
)
|
||||
|
||||
if self.api.requires_2fa:
|
||||
# Trigger a new log in to ensure the user enters the 2FA code again.
|
||||
raise PyiCloudFailedLoginException("2FA Required") # noqa: TRY301
|
||||
|
||||
except PyiCloudFailedLoginException:
|
||||
self.api = None
|
||||
# Login failed which means credentials/2fa need to be updated.
|
||||
# Login failed which means credentials need to be updated.
|
||||
_LOGGER.error(
|
||||
(
|
||||
"Your iCloud account for '%s' is no longer working; Go to the "
|
||||
"Your password for '%s' is no longer working; Go to the "
|
||||
"Integrations menu and click on Configure on the discovered Apple "
|
||||
"iCloud card to login again"
|
||||
),
|
||||
@@ -125,10 +129,6 @@ class IcloudAccount:
|
||||
self._require_reauth()
|
||||
return
|
||||
|
||||
if self.api.requires_2fa:
|
||||
self._require_reauth()
|
||||
return
|
||||
|
||||
try:
|
||||
# Gets device owners infos
|
||||
user_info = self.api.devices.user_info
|
||||
|
||||
@@ -32,13 +32,12 @@ from .const import (
|
||||
|
||||
CONF_TRUSTED_DEVICE = "trusted_device"
|
||||
CONF_VERIFICATION_CODE = "verification_code"
|
||||
CONF_REQUEST_NEW_CODE = "request_new_code"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IcloudFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle an iCloud config flow."""
|
||||
"""Handle a iCloud config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@@ -90,74 +89,44 @@ class IcloudFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
description_placeholders=self._description_placeholders,
|
||||
)
|
||||
|
||||
async def _request_2fa_code(self, errors: dict[str, str]) -> dict[str, str]:
|
||||
"""Request an Apple 2FA code."""
|
||||
if TYPE_CHECKING:
|
||||
assert self.api is not None
|
||||
|
||||
try:
|
||||
result = await self.hass.async_add_executor_job(self.api.request_2fa_code)
|
||||
except PyiCloudException as error:
|
||||
_LOGGER.error("Failed to request iCloud 2FA verification code: %s", error)
|
||||
errors["base"] = "send_verification_code"
|
||||
return errors
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Unexpected error requesting iCloud 2FA verification code"
|
||||
)
|
||||
errors["base"] = "send_verification_code"
|
||||
return errors
|
||||
|
||||
if result is False:
|
||||
_LOGGER.error("PyiCloud request_2fa_code returned False")
|
||||
errors["base"] = "send_verification_code"
|
||||
return errors
|
||||
|
||||
_LOGGER.debug("Requested iCloud 2FA verification code")
|
||||
return errors
|
||||
|
||||
async def _validate_and_create_entry(self, user_input, step_id):
|
||||
"""Check if config is valid and create entry if so."""
|
||||
self._password = user_input[CONF_PASSWORD]
|
||||
|
||||
extra_inputs = user_input
|
||||
|
||||
# If an existing entry was found, meaning this is a password update attempt,
|
||||
# use those to get config values that aren't changing.
|
||||
# use those to get config values that aren't changing
|
||||
if self._existing_entry_data:
|
||||
extra_inputs = self._existing_entry_data
|
||||
|
||||
if user_input is not None and CONF_PASSWORD in user_input:
|
||||
extra_inputs[CONF_PASSWORD] = user_input[CONF_PASSWORD]
|
||||
|
||||
self._username = extra_inputs[CONF_USERNAME]
|
||||
self._password = extra_inputs.get(CONF_PASSWORD, "")
|
||||
self._with_family = extra_inputs.get(CONF_WITH_FAMILY, DEFAULT_WITH_FAMILY)
|
||||
self._max_interval = extra_inputs.get(CONF_MAX_INTERVAL, DEFAULT_MAX_INTERVAL)
|
||||
self._gps_accuracy_threshold = extra_inputs.get(
|
||||
CONF_GPS_ACCURACY_THRESHOLD, DEFAULT_GPS_ACCURACY_THRESHOLD
|
||||
)
|
||||
|
||||
# Check if already configured.
|
||||
# Check if already configured
|
||||
if self.unique_id is None:
|
||||
await self.async_set_unique_id(self._username)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
if self.api is None:
|
||||
try:
|
||||
self.api = await self.hass.async_add_executor_job(
|
||||
PyiCloudService,
|
||||
self._username,
|
||||
self._password,
|
||||
Store(self.hass, STORAGE_VERSION, STORAGE_KEY).path,
|
||||
True,
|
||||
None,
|
||||
self._with_family,
|
||||
)
|
||||
except PyiCloudFailedLoginException as error:
|
||||
_LOGGER.error("Error logging into iCloud service: %s", error)
|
||||
self.api = None
|
||||
errors = {CONF_PASSWORD: "invalid_auth"}
|
||||
return self._show_setup_form(user_input, errors, step_id)
|
||||
try:
|
||||
self.api = await self.hass.async_add_executor_job(
|
||||
PyiCloudService,
|
||||
self._username,
|
||||
self._password,
|
||||
Store(self.hass, STORAGE_VERSION, STORAGE_KEY).path,
|
||||
True,
|
||||
None,
|
||||
self._with_family,
|
||||
)
|
||||
except PyiCloudFailedLoginException as error:
|
||||
_LOGGER.error("Error logging into iCloud service: %s", error)
|
||||
self.api = None
|
||||
errors = {CONF_PASSWORD: "invalid_auth"}
|
||||
return self._show_setup_form(user_input, errors, step_id)
|
||||
|
||||
if self.api.requires_2fa:
|
||||
return await self.async_step_verification_code()
|
||||
@@ -184,7 +153,7 @@ class IcloudFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_GPS_ACCURACY_THRESHOLD: self._gps_accuracy_threshold,
|
||||
}
|
||||
|
||||
# If this is a password update attempt, don't try creating a new one.
|
||||
# If this is a password update attempt, don't try and creating one
|
||||
if self.source == SOURCE_USER:
|
||||
return self.async_create_entry(title=self._username, data=data)
|
||||
|
||||
@@ -215,31 +184,19 @@ class IcloudFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Initialise re-authentication."""
|
||||
# Store existing entry data so it can be used later and set unique ID
|
||||
# so existing config entry can be updated.
|
||||
# so existing config entry can be updated
|
||||
await self.async_set_unique_id(self.context["unique_id"])
|
||||
self._existing_entry_data = {**entry_data}
|
||||
self._description_placeholders = {"username": entry_data[CONF_USERNAME]}
|
||||
|
||||
# Get the API from the existing entry runtime data
|
||||
self.api = self._get_reauth_entry().runtime_data.api
|
||||
|
||||
# If the API is None, it means the existing entry was never successfully authenticated,
|
||||
# so we need to show the setup form again to get the password.
|
||||
if self.api is None:
|
||||
return self._show_setup_form(step_id="reauth_confirm")
|
||||
|
||||
# If the API is not None, it means the existing entry was successfully authenticated before,
|
||||
# so we can proceed to the reauth_confirm step to trigger 2FA challenge.
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Initialise re-authentication confirmation.
|
||||
"""Update password for a config entry that can't authenticate."""
|
||||
if user_input is None:
|
||||
return self._show_setup_form(step_id="reauth_confirm")
|
||||
|
||||
Update password for a config entry that can't authenticate (if changed)
|
||||
and trigger 2FA challenge if needed.
|
||||
"""
|
||||
return await self._validate_and_create_entry(user_input, "reauth_confirm")
|
||||
|
||||
async def async_step_trusted_device(
|
||||
@@ -311,16 +268,6 @@ class IcloudFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is None:
|
||||
return await self._show_verification_code_form(errors)
|
||||
|
||||
if user_input[CONF_REQUEST_NEW_CODE]:
|
||||
# If the user requested a new code, request it
|
||||
errors = await self._request_2fa_code(errors)
|
||||
return await self._show_verification_code_form(errors)
|
||||
|
||||
if user_input[CONF_VERIFICATION_CODE] == "":
|
||||
# If the user didn't provide a code, show the form again with an error
|
||||
errors["base"] = "validate_verification_code"
|
||||
return await self.async_step_verification_code(errors=errors)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self.api is not None
|
||||
|
||||
@@ -339,19 +286,31 @@ class IcloudFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
):
|
||||
raise PyiCloudException("The code you entered is not valid.") # noqa: TRY301
|
||||
except PyiCloudException as error:
|
||||
# Redisplay the verification form after a failed verification attempt.
|
||||
# For 2FA, do not request a new Apple verification code on every bad
|
||||
# user entry. The original code may still be valid, and repeatedly
|
||||
# requesting new codes can invalidate prior codes or trigger rate limits.
|
||||
# Reset to the initial 2FA state to allow the user to retry
|
||||
_LOGGER.error("Failed to verify verification code: %s", error)
|
||||
self._trusted_device = None
|
||||
self._verification_code = None
|
||||
errors["base"] = "validate_verification_code"
|
||||
|
||||
if self.api.requires_2fa:
|
||||
return await self.async_step_verification_code(errors=errors)
|
||||
|
||||
return await self.async_step_trusted_device(errors=errors)
|
||||
try:
|
||||
self.api = await self.hass.async_add_executor_job(
|
||||
PyiCloudService,
|
||||
self._username,
|
||||
self._password,
|
||||
Store(self.hass, STORAGE_VERSION, STORAGE_KEY).path,
|
||||
True,
|
||||
None,
|
||||
self._with_family,
|
||||
)
|
||||
return await self.async_step_verification_code(None, errors)
|
||||
except PyiCloudFailedLoginException as error_login:
|
||||
_LOGGER.error("Error logging into iCloud service: %s", error_login)
|
||||
self.api = None
|
||||
errors = {CONF_PASSWORD: "invalid_auth"}
|
||||
return self._show_setup_form(user_input, errors, "user")
|
||||
else:
|
||||
return await self.async_step_trusted_device(None, errors)
|
||||
|
||||
return await self.async_step_user(
|
||||
{
|
||||
@@ -364,17 +323,12 @@ class IcloudFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
async def _show_verification_code_form(
|
||||
self, errors: dict[str, str]
|
||||
self, errors: dict[str, str] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show the verification_code form to the user."""
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="verification_code",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_VERIFICATION_CODE): str,
|
||||
vol.Optional(CONF_REQUEST_NEW_CODE, default=False): bool,
|
||||
}
|
||||
),
|
||||
data_schema=vol.Schema({vol.Required(CONF_VERIFICATION_CODE): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
},
|
||||
"verification_code": {
|
||||
"data": {
|
||||
"request_new_code": "Request a new verification code",
|
||||
"verification_code": "Verification code"
|
||||
},
|
||||
"description": "Please enter the verification code you just received from Apple",
|
||||
@@ -119,7 +118,7 @@
|
||||
"name": "Message"
|
||||
},
|
||||
"number": {
|
||||
"description": "The phone number to call in lost mode. Must contain country code.",
|
||||
"description": "The phone number to call in lost mode (must contain country code).",
|
||||
"name": "Number"
|
||||
}
|
||||
},
|
||||
@@ -143,7 +142,7 @@
|
||||
"description": "Asks for a state update of all devices linked to an Apple Account.",
|
||||
"fields": {
|
||||
"account": {
|
||||
"description": "Your Apple Account username/email.",
|
||||
"description": "Your Apple Account username (email).",
|
||||
"name": "Account"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["kiwiki"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["kiwiki-client==0.1.2"]
|
||||
"requirements": ["kiwiki-client==0.1.1"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from datetime import datetime, time, timedelta
|
||||
import logging
|
||||
import random
|
||||
from typing import override
|
||||
@@ -612,19 +612,9 @@ class ThinQEnergySensorEntityDescription(SensorEntityDescription):
|
||||
start_date_fn: Callable[[datetime], datetime]
|
||||
end_date_fn: Callable[[datetime], datetime]
|
||||
update_interval: timedelta = timedelta(days=1)
|
||||
reset_at_midnight: bool = False
|
||||
|
||||
|
||||
ENERGY_USAGE_SENSORS: tuple[ThinQEnergySensorEntityDescription, ...] = (
|
||||
ThinQEnergySensorEntityDescription(
|
||||
key="today",
|
||||
translation_key="energy_usage_today",
|
||||
usage_period=USAGE_DAILY,
|
||||
start_date_fn=lambda today: today,
|
||||
end_date_fn=lambda today: today,
|
||||
update_interval=timedelta(hours=1),
|
||||
reset_at_midnight=True,
|
||||
),
|
||||
ThinQEnergySensorEntityDescription(
|
||||
key="yesterday",
|
||||
translation_key="energy_usage_yesterday",
|
||||
@@ -825,7 +815,6 @@ class ThinQEnergySensorEntity(ThinQEntity, SensorEntity):
|
||||
|
||||
entity_description: ThinQEnergySensorEntityDescription
|
||||
_stop_update: Callable[[], None] | None = None
|
||||
_last_fetch_date: date | None = None
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@@ -870,36 +859,22 @@ class ThinQEnergySensorEntity(ThinQEntity, SensorEntity):
|
||||
dt_util.get_time_zone(self.coordinator.hass.config.time_zone)
|
||||
)
|
||||
next_update = local_now + self.entity_description.update_interval
|
||||
if (
|
||||
self.coordinator.update_energy_at_time_of_day is not None
|
||||
and self.entity_description.update_interval >= timedelta(days=1)
|
||||
):
|
||||
# For daily sensors: align to a consistent time of day to avoid
|
||||
# clock drift and reduce the chance of multiple sensors fetching
|
||||
# simultaneously across restarts.
|
||||
if self.coordinator.update_energy_at_time_of_day is not None:
|
||||
# calculate next_update time by combining tomorrow
|
||||
# and update_energy_at_time_of_day
|
||||
next_update = datetime.combine(
|
||||
next_update.date(),
|
||||
(next_update).date(),
|
||||
self.coordinator.update_energy_at_time_of_day,
|
||||
next_update.tzinfo,
|
||||
)
|
||||
start_date = (self.entity_description.start_date_fn(local_now)).date()
|
||||
end_date = (self.entity_description.end_date_fn(local_now)).date()
|
||||
try:
|
||||
self._attr_native_value = await self.coordinator.api.async_get_energy_usage(
|
||||
energy_property=self.property_id,
|
||||
period=self.entity_description.usage_period,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
start_date=(self.entity_description.start_date_fn(local_now)).date(),
|
||||
end_date=(self.entity_description.end_date_fn(local_now)).date(),
|
||||
detail=False,
|
||||
)
|
||||
if (
|
||||
self.entity_description.reset_at_midnight
|
||||
and start_date != self._last_fetch_date
|
||||
):
|
||||
self._attr_last_reset = datetime.combine(
|
||||
start_date, time.min, local_now.tzinfo
|
||||
)
|
||||
self._last_fetch_date = start_date
|
||||
except ThinQAPIException as exc:
|
||||
_LOGGER.warning(
|
||||
"[%s:%s] Failed to fetch energy usage data. reason=%s",
|
||||
|
||||
@@ -769,9 +769,6 @@
|
||||
"energy_usage_this_month": {
|
||||
"name": "Energy this month"
|
||||
},
|
||||
"energy_usage_today": {
|
||||
"name": "Energy today"
|
||||
},
|
||||
"energy_usage_yesterday": {
|
||||
"name": "Energy yesterday"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Litter-Robot entities for common data and methods."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from typing import Any, Concatenate, Generic, NoReturn, TypeVar, override
|
||||
from typing import Any, Concatenate, Generic, TypeVar, override
|
||||
|
||||
from pylitterbot import Pet, Robot
|
||||
from pylitterbot.exceptions import LitterRobotException
|
||||
@@ -38,15 +38,6 @@ def whisker_command[_WhiskerEntityT2: LitterRobotEntity, **_P](
|
||||
return handler
|
||||
|
||||
|
||||
def raise_update_failed(entity_id: str) -> NoReturn:
|
||||
"""Raise when the robot rejected an update without an error response."""
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"entity_id": entity_id},
|
||||
)
|
||||
|
||||
|
||||
def get_device_info(whisker_entity: Robot | Pet) -> DeviceInfo:
|
||||
"""Get device info for a robot or pet."""
|
||||
if isinstance(whisker_entity, Robot):
|
||||
|
||||
@@ -105,71 +105,6 @@
|
||||
"state": {
|
||||
"on": "mdi:lock"
|
||||
}
|
||||
},
|
||||
"sleep_mode_friday": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"sleep_mode_monday": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"sleep_mode_saturday": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"sleep_mode_sunday": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"sleep_mode_thursday": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"sleep_mode_tuesday": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"sleep_mode_wednesday": {
|
||||
"default": "mdi:sleep"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"sleep_mode_end_time_friday": {
|
||||
"default": "mdi:alarm"
|
||||
},
|
||||
"sleep_mode_end_time_monday": {
|
||||
"default": "mdi:alarm"
|
||||
},
|
||||
"sleep_mode_end_time_saturday": {
|
||||
"default": "mdi:alarm"
|
||||
},
|
||||
"sleep_mode_end_time_sunday": {
|
||||
"default": "mdi:alarm"
|
||||
},
|
||||
"sleep_mode_end_time_thursday": {
|
||||
"default": "mdi:alarm"
|
||||
},
|
||||
"sleep_mode_end_time_tuesday": {
|
||||
"default": "mdi:alarm"
|
||||
},
|
||||
"sleep_mode_end_time_wednesday": {
|
||||
"default": "mdi:alarm"
|
||||
},
|
||||
"sleep_mode_start_time_friday": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"sleep_mode_start_time_monday": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"sleep_mode_start_time_saturday": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"sleep_mode_start_time_sunday": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"sleep_mode_start_time_thursday": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"sleep_mode_start_time_tuesday": {
|
||||
"default": "mdi:sleep"
|
||||
},
|
||||
"sleep_mode_start_time_wednesday": {
|
||||
"default": "mdi:sleep"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -214,74 +214,11 @@
|
||||
},
|
||||
"panel_lockout": {
|
||||
"name": "Panel lockout"
|
||||
},
|
||||
"sleep_mode_friday": {
|
||||
"name": "Friday sleep mode"
|
||||
},
|
||||
"sleep_mode_monday": {
|
||||
"name": "Monday sleep mode"
|
||||
},
|
||||
"sleep_mode_saturday": {
|
||||
"name": "Saturday sleep mode"
|
||||
},
|
||||
"sleep_mode_sunday": {
|
||||
"name": "Sunday sleep mode"
|
||||
},
|
||||
"sleep_mode_thursday": {
|
||||
"name": "Thursday sleep mode"
|
||||
},
|
||||
"sleep_mode_tuesday": {
|
||||
"name": "Tuesday sleep mode"
|
||||
},
|
||||
"sleep_mode_wednesday": {
|
||||
"name": "Wednesday sleep mode"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"sleep_mode_end_time_friday": {
|
||||
"name": "Friday sleep mode end time"
|
||||
},
|
||||
"sleep_mode_end_time_monday": {
|
||||
"name": "Monday sleep mode end time"
|
||||
},
|
||||
"sleep_mode_end_time_saturday": {
|
||||
"name": "Saturday sleep mode end time"
|
||||
},
|
||||
"sleep_mode_end_time_sunday": {
|
||||
"name": "Sunday sleep mode end time"
|
||||
},
|
||||
"sleep_mode_end_time_thursday": {
|
||||
"name": "Thursday sleep mode end time"
|
||||
},
|
||||
"sleep_mode_end_time_tuesday": {
|
||||
"name": "Tuesday sleep mode end time"
|
||||
},
|
||||
"sleep_mode_end_time_wednesday": {
|
||||
"name": "Wednesday sleep mode end time"
|
||||
},
|
||||
"sleep_mode_start_time": {
|
||||
"name": "[%key:component::litterrobot::entity::sensor::sleep_mode_start_time::name%]"
|
||||
},
|
||||
"sleep_mode_start_time_friday": {
|
||||
"name": "Friday sleep mode start time"
|
||||
},
|
||||
"sleep_mode_start_time_monday": {
|
||||
"name": "Monday sleep mode start time"
|
||||
},
|
||||
"sleep_mode_start_time_saturday": {
|
||||
"name": "Saturday sleep mode start time"
|
||||
},
|
||||
"sleep_mode_start_time_sunday": {
|
||||
"name": "Sunday sleep mode start time"
|
||||
},
|
||||
"sleep_mode_start_time_thursday": {
|
||||
"name": "Thursday sleep mode start time"
|
||||
},
|
||||
"sleep_mode_start_time_tuesday": {
|
||||
"name": "Tuesday sleep mode start time"
|
||||
},
|
||||
"sleep_mode_start_time_wednesday": {
|
||||
"name": "Wednesday sleep mode start time"
|
||||
}
|
||||
},
|
||||
"vacuum": {
|
||||
@@ -302,9 +239,6 @@
|
||||
},
|
||||
"invalid_credentials": {
|
||||
"message": "Invalid credentials. Please check your username and password, then try again"
|
||||
},
|
||||
"update_failed": {
|
||||
"message": "Unable to update {entity_id}"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Generic, override
|
||||
|
||||
from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, LitterRobot5, Robot
|
||||
from pylitterbot.sleep_schedule import DayOfWeek
|
||||
from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, Robot
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
@@ -14,30 +12,11 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import LitterRobotConfigEntry
|
||||
from .entity import (
|
||||
LitterRobotEntity,
|
||||
_WhiskerEntityT,
|
||||
raise_update_failed,
|
||||
whisker_command,
|
||||
)
|
||||
from .entity import LitterRobotEntity, _WhiskerEntityT, whisker_command
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
def _lr5_sleep_day_enabled(robot: LitterRobot5, *, day: DayOfWeek) -> bool:
|
||||
"""Return whether a day's sleep schedule is enabled."""
|
||||
if (schedule := robot.sleep_schedule) is None:
|
||||
return False
|
||||
return (entry := schedule.get_day(day)) is not None and entry.is_enabled
|
||||
|
||||
|
||||
async def _lr5_set_sleep_day_enabled(
|
||||
robot: LitterRobot5, value: bool, *, day: DayOfWeek
|
||||
) -> bool:
|
||||
"""Enable or disable a day's sleep schedule, preserving its times."""
|
||||
return await robot.set_sleep_mode(value, day_of_week=day)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class RobotSwitchEntityDescription(SwitchEntityDescription, Generic[_WhiskerEntityT]): # noqa: UP046
|
||||
"""A class that describes robot switch entities."""
|
||||
@@ -67,16 +46,6 @@ SWITCH_MAP: dict[type[Robot], tuple[RobotSwitchEntityDescription, ...]] = {
|
||||
NIGHT_LIGHT_MODE_ENTITY_DESCRIPTION,
|
||||
),
|
||||
LitterRobot3: (NIGHT_LIGHT_MODE_ENTITY_DESCRIPTION,),
|
||||
LitterRobot5: tuple(
|
||||
RobotSwitchEntityDescription[LitterRobot5](
|
||||
key=f"sleep_mode_{day.name.lower()}",
|
||||
translation_key=f"sleep_mode_{day.name.lower()}",
|
||||
entity_registry_enabled_default=False,
|
||||
set_fn=partial(_lr5_set_sleep_day_enabled, day=day),
|
||||
value_fn=partial(_lr5_sleep_day_enabled, day=day),
|
||||
)
|
||||
for day in DayOfWeek
|
||||
),
|
||||
Robot: ( # type: ignore[type-abstract] # only used for isinstance check
|
||||
RobotSwitchEntityDescription[LitterRobot | FeederRobot](
|
||||
key="panel_lock_enabled",
|
||||
@@ -133,12 +102,10 @@ class RobotSwitchEntity(LitterRobotEntity[_WhiskerEntityT], SwitchEntity):
|
||||
@override
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
if not await self.entity_description.set_fn(self.robot, True):
|
||||
raise_update_failed(self.entity_id)
|
||||
await self.entity_description.set_fn(self.robot, True)
|
||||
|
||||
@whisker_command
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
if not await self.entity_description.set_fn(self.robot, False):
|
||||
raise_update_failed(self.entity_id)
|
||||
await self.entity_description.set_fn(self.robot, False)
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, time
|
||||
from functools import partial
|
||||
from typing import Any, Generic, override
|
||||
|
||||
from pylitterbot import LitterRobot3, LitterRobot5, Robot
|
||||
from pylitterbot.sleep_schedule import DayOfWeek, SleepScheduleDay
|
||||
from pylitterbot import LitterRobot3
|
||||
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
@@ -16,12 +14,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .coordinator import LitterRobotConfigEntry
|
||||
from .entity import (
|
||||
LitterRobotEntity,
|
||||
_WhiskerEntityT,
|
||||
raise_update_failed,
|
||||
whisker_command,
|
||||
)
|
||||
from .entity import LitterRobotEntity, _WhiskerEntityT, whisker_command
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -39,74 +32,18 @@ def _as_local_time(start: datetime | None) -> time | None:
|
||||
return dt_util.as_local(start).time() if start else None
|
||||
|
||||
|
||||
def _lr5_schedule_day(robot: LitterRobot5, day: DayOfWeek) -> SleepScheduleDay | None:
|
||||
"""Return the robot's sleep schedule entry for a day."""
|
||||
if (schedule := robot.sleep_schedule) is None:
|
||||
return None
|
||||
return schedule.get_day(day)
|
||||
|
||||
|
||||
def _lr5_schedule_time(
|
||||
robot: LitterRobot5, *, day: DayOfWeek, wake: bool
|
||||
) -> time | None:
|
||||
"""Return a day's configured sleep start or wake time.
|
||||
|
||||
The schedule stores wall-clock times in the robot's own timezone, so the
|
||||
value is reported as-is (unlike Litter-Robot 3, which stores a UTC
|
||||
datetime that must be converted).
|
||||
"""
|
||||
if (entry := _lr5_schedule_day(robot, day)) is None:
|
||||
return None
|
||||
return entry.wake_time if wake else entry.sleep_time
|
||||
|
||||
|
||||
async def _lr5_set_schedule_time(
|
||||
robot: LitterRobot5, value: time, *, day: DayOfWeek, wake: bool
|
||||
) -> bool:
|
||||
"""Set a day's sleep start or wake time, preserving its enabled state.
|
||||
|
||||
The schedule is minute-granular; seconds are dropped.
|
||||
"""
|
||||
entry = _lr5_schedule_day(robot, day)
|
||||
enabled = entry.is_enabled if entry else False
|
||||
minutes = value.hour * 60 + value.minute
|
||||
if wake:
|
||||
return await robot.set_sleep_mode(enabled, wake_time=minutes, day_of_week=day)
|
||||
return await robot.set_sleep_mode(enabled, minutes, day_of_week=day)
|
||||
|
||||
|
||||
LITTER_ROBOT_5_SLEEP_TIMES: tuple[RobotTimeEntityDescription[LitterRobot5], ...] = (
|
||||
tuple(
|
||||
RobotTimeEntityDescription[LitterRobot5](
|
||||
key=f"sleep_mode_{kind}_time_{day.name.lower()}",
|
||||
translation_key=f"sleep_mode_{kind}_time_{day.name.lower()}",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=partial(_lr5_schedule_time, day=day, wake=wake),
|
||||
set_fn=partial(_lr5_set_schedule_time, day=day, wake=wake),
|
||||
LITTER_ROBOT_3_SLEEP_START = RobotTimeEntityDescription[LitterRobot3](
|
||||
key="sleep_mode_start_time",
|
||||
translation_key="sleep_mode_start_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda robot: _as_local_time(robot.sleep_mode_start_time),
|
||||
set_fn=(
|
||||
lambda robot, value: robot.set_sleep_mode(
|
||||
robot.sleep_mode_enabled,
|
||||
value.replace(tzinfo=dt_util.get_default_time_zone()),
|
||||
)
|
||||
for day in DayOfWeek
|
||||
for kind, wake in (("start", False), ("end", True))
|
||||
)
|
||||
)
|
||||
|
||||
ROBOT_TIME_MAP: dict[type[Robot], tuple[RobotTimeEntityDescription[Any], ...]] = {
|
||||
LitterRobot3: (
|
||||
RobotTimeEntityDescription[LitterRobot3](
|
||||
key="sleep_mode_start_time",
|
||||
translation_key="sleep_mode_start_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda robot: _as_local_time(robot.sleep_mode_start_time),
|
||||
set_fn=(
|
||||
lambda robot, value: robot.set_sleep_mode(
|
||||
robot.sleep_mode_enabled,
|
||||
value.replace(tzinfo=dt_util.get_default_time_zone()),
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
LitterRobot5: LITTER_ROBOT_5_SLEEP_TIMES,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -126,13 +63,13 @@ async def async_setup_entry(
|
||||
known_robots.update(new_robots)
|
||||
async_add_entities(
|
||||
LitterRobotTimeEntity(
|
||||
robot=robot, coordinator=coordinator, description=description
|
||||
robot=robot,
|
||||
coordinator=coordinator,
|
||||
description=LITTER_ROBOT_3_SLEEP_START,
|
||||
)
|
||||
for robot in all_robots
|
||||
if robot.serial in new_robots
|
||||
for robot_type, descriptions in ROBOT_TIME_MAP.items()
|
||||
if isinstance(robot, robot_type)
|
||||
for description in descriptions
|
||||
if isinstance(robot, LitterRobot3)
|
||||
)
|
||||
|
||||
_check_robots()
|
||||
@@ -154,5 +91,4 @@ class LitterRobotTimeEntity(LitterRobotEntity[_WhiskerEntityT], TimeEntity):
|
||||
@override
|
||||
async def async_set_value(self, value: time) -> None:
|
||||
"""Update the current value."""
|
||||
if not await self.entity_description.set_fn(self.robot, value):
|
||||
raise_update_failed(self.entity_id)
|
||||
await self.entity_description.set_fn(self.robot, value)
|
||||
|
||||
@@ -8,12 +8,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import MelCloudHomeConfigEntry, MelCloudHomeCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.CLIMATE,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
"off": "mdi:snowflake-off"
|
||||
}
|
||||
},
|
||||
"holiday_mode": {
|
||||
"default": "mdi:beach"
|
||||
},
|
||||
"overheat_protection": {
|
||||
"default": "mdi:fire",
|
||||
"state": {
|
||||
@@ -36,20 +33,6 @@
|
||||
"tank_water_temperature": {
|
||||
"default": "mdi:water-thermometer"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"frost_protection": {
|
||||
"default": "mdi:snowflake",
|
||||
"state": {
|
||||
"off": "mdi:snowflake-off"
|
||||
}
|
||||
},
|
||||
"overheat_protection": {
|
||||
"default": "mdi:fire",
|
||||
"state": {
|
||||
"off": "mdi:fire-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,14 +109,6 @@
|
||||
"tank_water_temperature": {
|
||||
"name": "Tank water temperature"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"frost_protection": {
|
||||
"name": "Frost protection"
|
||||
},
|
||||
"overheat_protection": {
|
||||
"name": "Overheat protection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
"""Switch platform for MELCloud Home."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from aiomelcloudhome import ATAUnit, ATWUnit, MELCloudHome
|
||||
from aiomelcloudhome.exceptions import (
|
||||
MelCloudHomeAuthenticationError,
|
||||
MelCloudHomeConnectionError,
|
||||
MelCloudHomeTimeoutError,
|
||||
)
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelCloudHomeConfigEntry, MelCloudHomeCoordinator
|
||||
from .entity import MelCloudHomeATAUnitEntity, MelCloudHomeATWUnitEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ATASwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Class to hold MELCloud Home ATA switch description."""
|
||||
|
||||
available_fn: Callable[[ATAUnit], bool]
|
||||
is_on_fn: Callable[[ATAUnit], bool | None]
|
||||
turn_on_fn: Callable[[MELCloudHome, ATAUnit], Coroutine[Any, Any, None]]
|
||||
turn_off_fn: Callable[[MELCloudHome, ATAUnit], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ATWSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Class to hold MELCloud Home ATW switch description."""
|
||||
|
||||
available_fn: Callable[[ATWUnit], bool]
|
||||
is_on_fn: Callable[[ATWUnit], bool | None]
|
||||
turn_on_fn: Callable[[MELCloudHome, ATWUnit], Coroutine[Any, Any, None]]
|
||||
turn_off_fn: Callable[[MELCloudHome, ATWUnit], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
ATA_SWITCHES: tuple[ATASwitchEntityDescription, ...] = (
|
||||
ATASwitchEntityDescription(
|
||||
key="frost_protection",
|
||||
translation_key="frost_protection",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
available_fn=lambda unit: unit.frost_protection is not None,
|
||||
is_on_fn=lambda unit: (
|
||||
unit.frost_protection.enabled if unit.frost_protection else None
|
||||
),
|
||||
turn_on_fn=lambda client, unit: client.set_frost_protection(
|
||||
enabled=True,
|
||||
min_temp=unit.frost_protection.min if unit.frost_protection else 0.0,
|
||||
max_temp=unit.frost_protection.max if unit.frost_protection else 0.0,
|
||||
ata_unit_ids=[unit.id],
|
||||
),
|
||||
turn_off_fn=lambda client, unit: client.set_frost_protection(
|
||||
enabled=False,
|
||||
min_temp=unit.frost_protection.min if unit.frost_protection else 0.0,
|
||||
max_temp=unit.frost_protection.max if unit.frost_protection else 0.0,
|
||||
ata_unit_ids=[unit.id],
|
||||
),
|
||||
),
|
||||
ATASwitchEntityDescription(
|
||||
key="overheat_protection",
|
||||
translation_key="overheat_protection",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
available_fn=lambda unit: unit.overheat_protection is not None,
|
||||
is_on_fn=lambda unit: (
|
||||
unit.overheat_protection.enabled if unit.overheat_protection else None
|
||||
),
|
||||
turn_on_fn=lambda client, unit: client.set_overheat_protection(
|
||||
enabled=True,
|
||||
min_temp=unit.overheat_protection.min if unit.overheat_protection else 0.0,
|
||||
max_temp=unit.overheat_protection.max if unit.overheat_protection else 0.0,
|
||||
ata_unit_ids=[unit.id],
|
||||
),
|
||||
turn_off_fn=lambda client, unit: client.set_overheat_protection(
|
||||
enabled=False,
|
||||
min_temp=unit.overheat_protection.min if unit.overheat_protection else 0.0,
|
||||
max_temp=unit.overheat_protection.max if unit.overheat_protection else 0.0,
|
||||
ata_unit_ids=[unit.id],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
ATW_SWITCHES: tuple[ATWSwitchEntityDescription, ...] = (
|
||||
ATWSwitchEntityDescription(
|
||||
key="frost_protection",
|
||||
translation_key="frost_protection",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
available_fn=lambda unit: unit.frost_protection is not None,
|
||||
is_on_fn=lambda unit: (
|
||||
unit.frost_protection.enabled if unit.frost_protection else None
|
||||
),
|
||||
turn_on_fn=lambda client, unit: client.set_frost_protection(
|
||||
enabled=True,
|
||||
min_temp=unit.frost_protection.min if unit.frost_protection else 0.0,
|
||||
max_temp=unit.frost_protection.max if unit.frost_protection else 0.0,
|
||||
atw_unit_ids=[unit.id],
|
||||
),
|
||||
turn_off_fn=lambda client, unit: client.set_frost_protection(
|
||||
enabled=False,
|
||||
min_temp=unit.frost_protection.min if unit.frost_protection else 0.0,
|
||||
max_temp=unit.frost_protection.max if unit.frost_protection else 0.0,
|
||||
atw_unit_ids=[unit.id],
|
||||
),
|
||||
),
|
||||
ATWSwitchEntityDescription(
|
||||
key="overheat_protection",
|
||||
translation_key="overheat_protection",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
available_fn=lambda unit: unit.overheat_protection is not None,
|
||||
is_on_fn=lambda unit: (
|
||||
unit.overheat_protection.enabled if unit.overheat_protection else None
|
||||
),
|
||||
turn_on_fn=lambda client, unit: client.set_overheat_protection(
|
||||
enabled=True,
|
||||
min_temp=unit.overheat_protection.min if unit.overheat_protection else 0.0,
|
||||
max_temp=unit.overheat_protection.max if unit.overheat_protection else 0.0,
|
||||
atw_unit_ids=[unit.id],
|
||||
),
|
||||
turn_off_fn=lambda client, unit: client.set_overheat_protection(
|
||||
enabled=False,
|
||||
min_temp=unit.overheat_protection.min if unit.overheat_protection else 0.0,
|
||||
max_temp=unit.overheat_protection.max if unit.overheat_protection else 0.0,
|
||||
atw_unit_ids=[unit.id],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def _perform_action(
|
||||
coordinator: MelCloudHomeCoordinator,
|
||||
coroutine: Coroutine[Any, Any, None],
|
||||
) -> None:
|
||||
"""Perform a MELCloud Home action with error handling and coordinator refresh."""
|
||||
try:
|
||||
await coroutine
|
||||
except MelCloudHomeAuthenticationError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
) from err
|
||||
except MelCloudHomeConnectionError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
except MelCloudHomeTimeoutError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout_connect",
|
||||
) from err
|
||||
else:
|
||||
await coordinator.async_request_refresh()
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MelCloudHomeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MELCloud Home switches."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
def _async_add_new_ata_units(units: list[ATAUnit]) -> None:
|
||||
async_add_entities(
|
||||
ATASwitch(coordinator, entity_description, unit)
|
||||
for entity_description in ATA_SWITCHES
|
||||
for unit in units
|
||||
)
|
||||
|
||||
def _async_add_new_atw_units(units: list[ATWUnit]) -> None:
|
||||
async_add_entities(
|
||||
ATWSwitch(coordinator, entity_description, unit)
|
||||
for entity_description in ATW_SWITCHES
|
||||
for unit in units
|
||||
)
|
||||
|
||||
coordinator.new_ata_callbacks.append(_async_add_new_ata_units)
|
||||
coordinator.new_atw_callbacks.append(_async_add_new_atw_units)
|
||||
|
||||
_async_add_new_ata_units(list(coordinator.ata_units.values()))
|
||||
_async_add_new_atw_units(list(coordinator.atw_units.values()))
|
||||
|
||||
|
||||
class ATASwitch(MelCloudHomeATAUnitEntity, SwitchEntity):
|
||||
"""Representation of a MELCloud Home ATA switch."""
|
||||
|
||||
entity_description: ATASwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MelCloudHomeCoordinator,
|
||||
entity_description: ATASwitchEntityDescription,
|
||||
unit: ATAUnit,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator, unit)
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{unit.id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the entity is available."""
|
||||
return super().available and self.entity_description.available_fn(self.unit)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the state of the switch."""
|
||||
return self.entity_description.is_on_fn(self.unit)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Enable the protection."""
|
||||
await _perform_action(
|
||||
self.coordinator,
|
||||
self.entity_description.turn_on_fn(self.coordinator.client, self.unit),
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Disable the protection."""
|
||||
await _perform_action(
|
||||
self.coordinator,
|
||||
self.entity_description.turn_off_fn(self.coordinator.client, self.unit),
|
||||
)
|
||||
|
||||
|
||||
class ATWSwitch(MelCloudHomeATWUnitEntity, SwitchEntity):
|
||||
"""Representation of a MELCloud Home ATW switch."""
|
||||
|
||||
entity_description: ATWSwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MelCloudHomeCoordinator,
|
||||
entity_description: ATWSwitchEntityDescription,
|
||||
unit: ATWUnit,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator, unit)
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{unit.id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the entity is available."""
|
||||
return super().available and self.entity_description.available_fn(self.unit)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the state of the switch."""
|
||||
return self.entity_description.is_on_fn(self.unit)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Enable the protection."""
|
||||
await _perform_action(
|
||||
self.coordinator,
|
||||
self.entity_description.turn_on_fn(self.coordinator.client, self.unit),
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Disable the protection."""
|
||||
await _perform_action(
|
||||
self.coordinator,
|
||||
self.entity_description.turn_off_fn(self.coordinator.client, self.unit),
|
||||
)
|
||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
||||
from homeassistant.util import package
|
||||
|
||||
@@ -17,6 +18,7 @@ from .const import (
|
||||
LOOPBACK_TARGET_IP,
|
||||
MDNS_TARGET_IP,
|
||||
PUBLIC_TARGET_IP,
|
||||
SIGNAL_NETWORK_ADAPTERS_CHANGED,
|
||||
)
|
||||
from .models import Adapter
|
||||
from .network import Network, async_get_loaded_network, async_get_network
|
||||
@@ -51,6 +53,12 @@ def async_get_loaded_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||
return async_get_loaded_network(hass).adapters
|
||||
|
||||
|
||||
async def async_reload_adapters(hass: HomeAssistant) -> None:
|
||||
"""Reload the network adapters and notify listeners if they changed."""
|
||||
if await async_get_loaded_network(hass).async_reload():
|
||||
async_dispatcher_send(hass, SIGNAL_NETWORK_ADAPTERS_CHANGED)
|
||||
|
||||
|
||||
async def async_get_source_ip(
|
||||
hass: HomeAssistant, target_ip: str | UndefinedType = UNDEFINED
|
||||
) -> str:
|
||||
|
||||
@@ -19,6 +19,8 @@ MDNS_TARGET_IP: Final = "224.0.0.251"
|
||||
PUBLIC_TARGET_IP: Final = "8.8.8.8"
|
||||
IPV4_BROADCAST_ADDR: Final = "255.255.255.255"
|
||||
|
||||
SIGNAL_NETWORK_ADAPTERS_CHANGED: Final = "network_adapters_changed"
|
||||
|
||||
NETWORK_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
|
||||
@@ -63,6 +63,15 @@ class Network:
|
||||
self.adapters = await async_load_adapters()
|
||||
await storage_load_task
|
||||
|
||||
async def async_reload(self) -> bool:
|
||||
"""Reload adapters from the system, returning True if they changed."""
|
||||
adapters = await async_load_adapters()
|
||||
if adapters == self.adapters:
|
||||
return False
|
||||
self.adapters = adapters
|
||||
self.async_configure()
|
||||
return True
|
||||
|
||||
@callback
|
||||
def async_configure(self) -> None:
|
||||
"""Configure from storage."""
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
"""The NFAndroidTV integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from notifications_android_tv.notifications import ConnectError, Notifications
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, Platform
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DATA_HASS_CONFIG, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = [Platform.NOTIFY]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
type NFAndroidTVConfigEntry = ConfigEntry[Notifications]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@@ -27,21 +20,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: NFAndroidTVConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up NFAndroidTV from a config entry."""
|
||||
|
||||
try:
|
||||
client = await hass.async_add_executor_job(Notifications, entry.data[CONF_HOST])
|
||||
except ConnectError as e:
|
||||
_LOGGER.debug("Full exception:", exc_info=True)
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_connection_error",
|
||||
translation_placeholders={CONF_NAME: entry.title},
|
||||
) from e
|
||||
|
||||
entry.runtime_data = client
|
||||
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hass,
|
||||
@@ -52,13 +33,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: NFAndroidTVConfigEntry)
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: NFAndroidTVConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"notify": {
|
||||
"notify": {
|
||||
"default": "mdi:television"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,18 +14,13 @@ from homeassistant.components.notify import (
|
||||
ATTR_TITLE,
|
||||
ATTR_TITLE_DEFAULT,
|
||||
BaseNotificationService,
|
||||
NotifyEntity,
|
||||
NotifyEntityFeature,
|
||||
)
|
||||
from homeassistant.const import ATTR_ICON, CONF_HOST, CONF_NAME
|
||||
from homeassistant.const import ATTR_ICON, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import NFAndroidTVConfigEntry
|
||||
from .const import (
|
||||
ATTR_COLOR,
|
||||
ATTR_DURATION,
|
||||
@@ -53,48 +48,6 @@ from .const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: NFAndroidTVConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the notify platform."""
|
||||
async_add_entities([NFAndroidTVNotifyEntity(config_entry)])
|
||||
|
||||
|
||||
class NFAndroidTVNotifyEntity(NotifyEntity):
|
||||
"""Representation of a notify entity."""
|
||||
|
||||
_attr_supported_features = NotifyEntityFeature.TITLE
|
||||
_attr_translation_key = "notify"
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, entry: NFAndroidTVConfigEntry) -> None:
|
||||
"""Initialize the entity."""
|
||||
self._attr_unique_id = entry.entry_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
name=entry.title,
|
||||
model="Notifications",
|
||||
manufacturer="dream apps",
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
)
|
||||
self.entry = entry
|
||||
self.client = entry.runtime_data
|
||||
|
||||
def send_message(self, message: str, title: str | None = None) -> None:
|
||||
"""Send a message via notify.send_message action."""
|
||||
try:
|
||||
self.client.send(message=message, title=title)
|
||||
except ConnectError as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="notify_connection_error",
|
||||
translation_placeholders={CONF_NAME: self.entry.title},
|
||||
) from e
|
||||
|
||||
|
||||
async def async_get_service(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
|
||||
@@ -39,12 +39,6 @@
|
||||
},
|
||||
"invalid_notification_image": {
|
||||
"message": "Invalid image data provided. Got {type}"
|
||||
},
|
||||
"notify_connection_error": {
|
||||
"message": "Failed to send notification to {name} due to a connection error"
|
||||
},
|
||||
"setup_connection_error": {
|
||||
"message": "Failed to connect to {name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ from .const import (
|
||||
DEFAULT_MAX_HISTORY,
|
||||
DEFAULT_NUM_CTX,
|
||||
DOMAIN,
|
||||
KEEP_ALIVE_FOREVER,
|
||||
)
|
||||
from .models import MessageHistory, MessageRole
|
||||
|
||||
@@ -244,21 +243,8 @@ class OllamaBaseLLMEntity(Entity):
|
||||
messages=list(message_history.messages),
|
||||
tools=tools,
|
||||
stream=True,
|
||||
# keep_alive: -1 is a special sentinel meaning "keep loaded
|
||||
# forever" and must be passed as the integer -1, not as a
|
||||
# duration string ("-1s" would be treated as an invalid
|
||||
# negative duration by the Ollama server). All other values
|
||||
# are expressed as a duration string with a seconds suffix.
|
||||
keep_alive=(
|
||||
keep_alive_seconds
|
||||
if (
|
||||
keep_alive_seconds := int(
|
||||
settings.get(CONF_KEEP_ALIVE, DEFAULT_KEEP_ALIVE)
|
||||
)
|
||||
)
|
||||
== KEEP_ALIVE_FOREVER
|
||||
else f"{keep_alive_seconds}s"
|
||||
),
|
||||
# keep_alive requires specifying unit. In this case, seconds
|
||||
keep_alive=f"{settings.get(CONF_KEEP_ALIVE, DEFAULT_KEEP_ALIVE)}s",
|
||||
options={CONF_NUM_CTX: settings.get(CONF_NUM_CTX, DEFAULT_NUM_CTX)},
|
||||
think=settings.get(CONF_THINK),
|
||||
format=output_format,
|
||||
|
||||
@@ -11,12 +11,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OpenEVSEConfigEntry, OpenEVSEDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.NUMBER,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.NUMBER, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: OpenEVSEConfigEntry) -> bool:
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
"""Support for OpenEVSE button entities."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from openevsehttp.__main__ import OpenEVSE
|
||||
|
||||
from homeassistant.components.button import (
|
||||
ButtonDeviceClass,
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.const import ATTR_CONNECTIONS, ATTR_SERIAL_NUMBER, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OpenEVSEConfigEntry, OpenEVSEDataUpdateCoordinator
|
||||
from .helpers import openevse_exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class OpenEVSEButtonDescription(ButtonEntityDescription):
|
||||
"""Describes an OpenEVSE button entity."""
|
||||
|
||||
press_fn: Callable[[OpenEVSE], Awaitable[Any]]
|
||||
|
||||
|
||||
BUTTON_TYPES: tuple[OpenEVSEButtonDescription, ...] = (
|
||||
OpenEVSEButtonDescription(
|
||||
key="restart_wifi",
|
||||
translation_key="restart_wifi",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda ev: ev.restart_wifi(),
|
||||
),
|
||||
OpenEVSEButtonDescription(
|
||||
key="restart_evse",
|
||||
translation_key="restart",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda ev: ev.restart_evse(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: OpenEVSEConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up OpenEVSE buttons based on config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
identifier = entry.unique_id or entry.entry_id
|
||||
async_add_entities(
|
||||
OpenEVSEButton(coordinator, description, identifier, entry.unique_id)
|
||||
for description in BUTTON_TYPES
|
||||
)
|
||||
|
||||
|
||||
class OpenEVSEButton(CoordinatorEntity[OpenEVSEDataUpdateCoordinator], ButtonEntity):
|
||||
"""Implementation of an OpenEVSE button."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: OpenEVSEButtonDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: OpenEVSEDataUpdateCoordinator,
|
||||
description: OpenEVSEButtonDescription,
|
||||
identifier: str,
|
||||
unique_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize the button."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{identifier}-{description.key}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, identifier)},
|
||||
manufacturer="OpenEVSE",
|
||||
)
|
||||
if unique_id:
|
||||
self._attr_device_info[ATTR_CONNECTIONS] = {
|
||||
(CONNECTION_NETWORK_MAC, unique_id)
|
||||
}
|
||||
self._attr_device_info[ATTR_SERIAL_NUMBER] = unique_id
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
with openevse_exception_handler(0.0):
|
||||
await self.entity_description.press_fn(self.coordinator.charger)
|
||||
@@ -62,14 +62,6 @@
|
||||
"name": "Vehicle connected"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"restart": {
|
||||
"name": "Restart"
|
||||
},
|
||||
"restart_wifi": {
|
||||
"name": "Restart Wi-Fi"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"charge_rate": {
|
||||
"name": "Charge rate"
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opensensemap_api"],
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["opensensemap-api==0.4.1"]
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: The integration does not register any actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: The integration does not register any actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Entities do not subscribe to events; they read from a
|
||||
DataUpdateCoordinator.
|
||||
entity-unique-id: done
|
||||
has-entity-name:
|
||||
status: exempt
|
||||
comment: |
|
||||
Sensor entities set has_entity_name. The legacy air_quality entity does
|
||||
not, but it is being deprecated and removed in a follow-up.
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: The integration does not register any actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: The integration has no options to configure.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: The openSenseMap API requires no authentication.
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: openSenseMap is a cloud service and cannot be discovered.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: openSenseMap stations cannot be discovered on the network.
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: Each config entry represents a single, fixed station.
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: The integration only provides primary entities that should be enabled.
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: Entities use their device class icons; no custom icons are needed.
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
The station ID is the unique identifier of the config entry; a different
|
||||
station is a separate entry, so there is nothing to reconfigure.
|
||||
repair-issues: done
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: Each config entry represents a single, fixed station.
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
||||
@@ -10,7 +10,7 @@ from pyoverkiz.auth.credentials import (
|
||||
RexelTokenCredentials,
|
||||
UsernamePasswordCredentials,
|
||||
)
|
||||
from pyoverkiz.client import OverkizClient, OverkizClientSettings
|
||||
from pyoverkiz.client import OverkizClient
|
||||
from pyoverkiz.const import REXEL_OAUTH_CLIENT_ID
|
||||
from pyoverkiz.enums import APIType, OverkizState, Server, UIClass, UIWidget
|
||||
from pyoverkiz.exceptions import (
|
||||
@@ -317,7 +317,6 @@ def create_local_client(
|
||||
credentials=LocalTokenCredentials(token),
|
||||
session=session,
|
||||
verify_ssl=verify_ssl,
|
||||
settings=OverkizClientSettings(default_rts_command_duration=0),
|
||||
)
|
||||
|
||||
|
||||
@@ -333,7 +332,6 @@ def create_cloud_client(
|
||||
server=server,
|
||||
credentials=UsernamePasswordCredentials(username, password),
|
||||
session=session,
|
||||
settings=OverkizClientSettings(default_rts_command_duration=0),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from pyoverkiz.client import GatewayCandidate, OverkizClient
|
||||
from pyoverkiz.const import SERVERS_WITH_LOCAL_API, SUPPORTED_SERVERS
|
||||
from pyoverkiz.enums import APIType, Server
|
||||
from pyoverkiz.exceptions import (
|
||||
ApplicationNotAllowedError,
|
||||
BadCredentialsError,
|
||||
CozyTouchBadCredentialsError,
|
||||
MaintenanceError,
|
||||
@@ -194,8 +193,6 @@ class OverkizConfigFlow(
|
||||
await self.async_validate_input(user_input)
|
||||
except TooManyRequestsError:
|
||||
errors["base"] = "too_many_requests"
|
||||
except ApplicationNotAllowedError:
|
||||
errors["base"] = "application_not_allowed"
|
||||
except (BadCredentialsError, NotAuthenticatedError) as exception:
|
||||
# If authentication with CozyTouch auth server is
|
||||
# valid, but token is invalid for Overkiz API
|
||||
|
||||
@@ -118,9 +118,9 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
|
||||
stop_command=OverkizCommand.STOP,
|
||||
# position (1-127), execution duration (0-15, optional)
|
||||
open_tilt_command=OverkizCommand.TILT_POSITIVE,
|
||||
open_tilt_command_args=(_TILT_STEP_SIZE,),
|
||||
open_tilt_command_args=(_TILT_STEP_SIZE, 0),
|
||||
close_tilt_command=OverkizCommand.TILT_NEGATIVE,
|
||||
close_tilt_command_args=(_TILT_STEP_SIZE,),
|
||||
close_tilt_command_args=(_TILT_STEP_SIZE, 0),
|
||||
stop_tilt_command=OverkizCommand.STOP,
|
||||
),
|
||||
# Needs override to support very specific tilt commands
|
||||
@@ -134,9 +134,9 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
|
||||
stop_command=OverkizCommand.STOP,
|
||||
# position (1-127), execution duration (0-15, optional)
|
||||
open_tilt_command=OverkizCommand.TILT_POSITIVE,
|
||||
open_tilt_command_args=(_TILT_STEP_SIZE,),
|
||||
open_tilt_command_args=(_TILT_STEP_SIZE, 0),
|
||||
close_tilt_command=OverkizCommand.TILT_NEGATIVE,
|
||||
close_tilt_command_args=(_TILT_STEP_SIZE,),
|
||||
close_tilt_command_args=(_TILT_STEP_SIZE, 0),
|
||||
stop_tilt_command=OverkizCommand.STOP,
|
||||
),
|
||||
# Needs override to support very specific tilt commands
|
||||
@@ -150,9 +150,9 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
|
||||
stop_command=OverkizCommand.STOP,
|
||||
# position (1-127), execution duration (0-15, optional)
|
||||
open_tilt_command=OverkizCommand.TILT_POSITIVE,
|
||||
open_tilt_command_args=(_TILT_STEP_SIZE,),
|
||||
open_tilt_command_args=(_TILT_STEP_SIZE, 0),
|
||||
close_tilt_command=OverkizCommand.TILT_NEGATIVE,
|
||||
close_tilt_command_args=(_TILT_STEP_SIZE,),
|
||||
close_tilt_command_args=(_TILT_STEP_SIZE, 0),
|
||||
stop_tilt_command=OverkizCommand.STOP,
|
||||
),
|
||||
# Needs override to support very specific tilt commands (rts:SheerBlindRTSComponent)
|
||||
@@ -165,9 +165,9 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
|
||||
stop_command=OverkizCommand.STOP,
|
||||
# position (1-127), execution duration (0-15, optional)
|
||||
open_tilt_command=OverkizCommand.TILT_POSITIVE,
|
||||
open_tilt_command_args=(_TILT_STEP_SIZE,),
|
||||
open_tilt_command_args=(_TILT_STEP_SIZE, 0),
|
||||
close_tilt_command=OverkizCommand.TILT_NEGATIVE,
|
||||
close_tilt_command_args=(_TILT_STEP_SIZE,),
|
||||
close_tilt_command_args=(_TILT_STEP_SIZE, 0),
|
||||
stop_tilt_command=OverkizCommand.STOP,
|
||||
),
|
||||
# Needs override since BioclimaticPergola uses core:SlatsOpenClosedState
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyoverkiz.enums import OverkizCommand
|
||||
from pyoverkiz.enums import OverkizCommand, Protocol
|
||||
from pyoverkiz.exceptions import BaseOverkizError
|
||||
from pyoverkiz.models import Action, Command, Device, StateDefinition
|
||||
|
||||
@@ -10,6 +10,18 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .coordinator import OverkizDataUpdateCoordinator
|
||||
|
||||
# Commands that don't support setting
|
||||
# the delay to another value
|
||||
COMMANDS_WITHOUT_DELAY = [
|
||||
OverkizCommand.IDENTIFY,
|
||||
OverkizCommand.OFF,
|
||||
OverkizCommand.ON,
|
||||
OverkizCommand.ON_WITH_TIMER,
|
||||
OverkizCommand.TEST,
|
||||
OverkizCommand.TILT_POSITIVE,
|
||||
OverkizCommand.TILT_NEGATIVE,
|
||||
]
|
||||
|
||||
|
||||
class OverkizExecutor:
|
||||
"""Representation of an Overkiz device with execution handler."""
|
||||
@@ -49,6 +61,13 @@ class OverkizExecutor:
|
||||
commands are executed, it will be refreshed only once.
|
||||
"""
|
||||
parameters = [arg for arg in args if arg is not None]
|
||||
# Set the execution duration to 0 seconds for RTS devices on supported commands
|
||||
# Default execution duration is 30 seconds and will block consecutive commands
|
||||
if (
|
||||
self.device.identifier.protocol == Protocol.RTS
|
||||
and command_name not in COMMANDS_WITHOUT_DELAY
|
||||
):
|
||||
parameters.append(0)
|
||||
|
||||
try:
|
||||
exec_id = await self.coordinator.client.execute_action_group(
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]"
|
||||
},
|
||||
"error": {
|
||||
"application_not_allowed": "Your setup cannot be accessed through this application.",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"certificate_verify_failed": "Cannot connect to host, certificate verify failed.",
|
||||
"developer_mode_disabled": "Developer Mode disabled. Activate the Developer Mode of your Somfy TaHoma box first.",
|
||||
|
||||
@@ -5,9 +5,10 @@ from typing import cast
|
||||
from python_picnic_api2 import PicnicAPI
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
ATTR_AMOUNT,
|
||||
@@ -49,9 +50,12 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
async def get_api_client(hass: HomeAssistant, config_entry_id: str) -> PicnicAPI:
|
||||
"""Get the right Picnic API client based on the config entry id."""
|
||||
entry: PicnicConfigEntry = service.async_get_config_entry(
|
||||
hass, DOMAIN, config_entry_id
|
||||
|
||||
entry: PicnicConfigEntry | None = hass.config_entries.async_get_entry(
|
||||
config_entry_id
|
||||
)
|
||||
if entry is None or entry.state is not ConfigEntryState.LOADED:
|
||||
raise ValueError(f"Config entry with id {config_entry_id} not found!")
|
||||
return entry.runtime_data.picnic_api_client
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.components.calendar import (
|
||||
CalendarEvent,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -120,7 +121,7 @@ class RachioCalendarEntity(
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get all events in a specific time frame."""
|
||||
if not self.coordinator.data:
|
||||
return []
|
||||
raise HomeAssistantError("No events scheduled")
|
||||
schedule = self.coordinator.data
|
||||
event_list: list[CalendarEvent] = []
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ class RainforestRavenConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
meter_info = await raven_device.get_meter_info(meter=meter)
|
||||
if meter_info and (
|
||||
meter_info.meter_type is None
|
||||
or meter_info.meter_type == MeterType.ELECTRIC
|
||||
or meter_info.meter_type is MeterType.ELECTRIC
|
||||
):
|
||||
self._meter_macs.add(meter.hex())
|
||||
self._dev_path = dev_path
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"SQLAlchemy==2.0.51",
|
||||
"SQLAlchemy==2.0.50",
|
||||
"fnv-hash-fast==2.0.3",
|
||||
"psutil-home-assistant==0.0.1"
|
||||
]
|
||||
|
||||
@@ -705,7 +705,7 @@ class ReolinkHost:
|
||||
self._api.host,
|
||||
sub_type,
|
||||
)
|
||||
if sub_type == SubType.push:
|
||||
if sub_type is SubType.push:
|
||||
await self.subscribe()
|
||||
return
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.21.2"]
|
||||
"requirements": ["reolink-aio==0.21.1"]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""The Roborock component."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Coroutine
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from roborock import (
|
||||
RoborockException,
|
||||
@@ -17,11 +19,10 @@ from roborock.map.map_parser import MapParserConfig
|
||||
from roborock.mqtt.session import MqttSessionUnauthorized
|
||||
|
||||
from homeassistant.const import CONF_USERNAME, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
@@ -74,18 +75,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
||||
base_url=entry.data[CONF_BASE_URL],
|
||||
)
|
||||
cache = CacheStore(hass, entry.entry_id)
|
||||
|
||||
entry.runtime_data = RoborockCoordinators()
|
||||
|
||||
@callback
|
||||
def handle_device_ready(device: RoborockDevice) -> None:
|
||||
"""Handle a device becoming ready."""
|
||||
entry.async_create_background_task(
|
||||
hass,
|
||||
async_setup_device(hass, entry, device),
|
||||
name=f"roborock_device_setup_{device.duid}",
|
||||
)
|
||||
|
||||
try:
|
||||
device_manager = await create_device_manager(
|
||||
user_params,
|
||||
@@ -102,7 +91,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
||||
show_walls=entry.options.get(CONF_SHOW_WALLS, True),
|
||||
map_scale=MAP_SCALE,
|
||||
),
|
||||
ready_callback=handle_device_ready,
|
||||
mqtt_session_unauthorized_hook=lambda: entry.async_start_reauth(hass),
|
||||
prefer_cache=False,
|
||||
)
|
||||
@@ -144,6 +132,76 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
||||
devices = await device_manager.get_devices()
|
||||
_LOGGER.debug("Device manager found %d devices", len(devices))
|
||||
|
||||
# Register all discovered devices in the device registry so we can
|
||||
# check the disabled state before creating coordinators.
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in devices:
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
**get_device_info(device),
|
||||
)
|
||||
|
||||
enabled_devices = []
|
||||
disabled_devices = []
|
||||
for device in devices:
|
||||
if _is_device_disabled(device_registry, device):
|
||||
disabled_devices.append(device)
|
||||
else:
|
||||
enabled_devices.append(device)
|
||||
_LOGGER.debug("%d of %d devices are enabled", len(enabled_devices), len(devices))
|
||||
|
||||
# Close connections for disabled devices to prevent their background
|
||||
# reconnect loops from triggering MQTT session restarts that would
|
||||
# disrupt coordinator setup for the enabled devices.
|
||||
if disabled_devices:
|
||||
close_results = await asyncio.gather(
|
||||
*[device.close() for device in disabled_devices],
|
||||
return_exceptions=True,
|
||||
)
|
||||
for device, close_result in zip(disabled_devices, close_results, strict=True):
|
||||
if isinstance(close_result, Exception):
|
||||
_LOGGER.debug(
|
||||
"Failed to close disabled Roborock device %s: %s",
|
||||
device.duid,
|
||||
close_result,
|
||||
)
|
||||
|
||||
coordinators = await asyncio.gather(
|
||||
*build_setup_functions(hass, entry, enabled_devices, user_data),
|
||||
return_exceptions=True,
|
||||
)
|
||||
v1_coords = [
|
||||
coord
|
||||
for coord in coordinators
|
||||
if isinstance(coord, RoborockDataUpdateCoordinator)
|
||||
]
|
||||
a01_coords = [
|
||||
coord
|
||||
for coord in coordinators
|
||||
if isinstance(coord, RoborockDataUpdateCoordinatorA01)
|
||||
]
|
||||
b01_q7_coords = [
|
||||
coord
|
||||
for coord in coordinators
|
||||
if isinstance(coord, RoborockB01Q7UpdateCoordinator)
|
||||
]
|
||||
b01_q10_coords = [
|
||||
coord
|
||||
for coord in coordinators
|
||||
if isinstance(coord, RoborockB01Q10UpdateCoordinator)
|
||||
]
|
||||
if (
|
||||
len(v1_coords) + len(a01_coords) + len(b01_q7_coords) + len(b01_q10_coords) == 0
|
||||
and enabled_devices
|
||||
):
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_coordinators",
|
||||
)
|
||||
entry.runtime_data = RoborockCoordinators(
|
||||
v1_coords, a01_coords, b01_q7_coords, b01_q10_coords
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
_remove_stale_devices(hass, entry, devices)
|
||||
@@ -165,7 +223,6 @@ def _remove_stale_devices(
|
||||
entry: RoborockConfigEntry,
|
||||
devices: list[RoborockDevice],
|
||||
) -> None:
|
||||
"""Remove devices from the registry that are no longer in the account."""
|
||||
device_map: dict[str, RoborockDevice] = {device.duid: device for device in devices}
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
@@ -180,7 +237,7 @@ def _remove_stale_devices(
|
||||
if any(device_duid in device_map for device_duid in device_duids):
|
||||
continue
|
||||
_LOGGER.info(
|
||||
"Removing device: %s because it no longer exists in your account",
|
||||
"Removing device: %s because it is no longer exists in your account",
|
||||
device.name,
|
||||
)
|
||||
device_registry.async_update_device(
|
||||
@@ -211,83 +268,87 @@ async def async_migrate_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_device(
|
||||
def build_setup_functions(
|
||||
hass: HomeAssistant,
|
||||
entry: RoborockConfigEntry,
|
||||
device: RoborockDevice,
|
||||
) -> None:
|
||||
"""Set up a single Roborock device and its coordinator."""
|
||||
_LOGGER.debug("Device %s is ready, setting it up", device.duid)
|
||||
if device.duid in entry.runtime_data:
|
||||
return
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
**get_device_info(device),
|
||||
)
|
||||
if _is_device_disabled(device_registry, device):
|
||||
_LOGGER.debug("Device %s is disabled, skipping setup", device.duid)
|
||||
try:
|
||||
await device.close()
|
||||
except RoborockException as err:
|
||||
_LOGGER.warning("Failed to close device %s: %s", device.duid, err)
|
||||
return
|
||||
|
||||
_LOGGER.debug("Creating device %s: %s", device.name, device)
|
||||
coordinator: (
|
||||
devices: list[RoborockDevice],
|
||||
user_data: UserData,
|
||||
) -> list[
|
||||
Coroutine[
|
||||
Any,
|
||||
Any,
|
||||
RoborockDataUpdateCoordinator
|
||||
| RoborockDataUpdateCoordinatorA01
|
||||
| RoborockDataUpdateCoordinatorB01
|
||||
| RoborockB01Q10UpdateCoordinator
|
||||
| None
|
||||
) = None
|
||||
if device.v1_properties is not None:
|
||||
coordinator = RoborockDataUpdateCoordinator(
|
||||
hass, entry, device, device.v1_properties
|
||||
)
|
||||
elif device.dyad is not None:
|
||||
coordinator = RoborockWetDryVacUpdateCoordinator(
|
||||
hass, entry, device, device.dyad
|
||||
)
|
||||
elif device.zeo is not None:
|
||||
coordinator = RoborockWashingMachineUpdateCoordinator(
|
||||
hass, entry, device, device.zeo
|
||||
)
|
||||
elif device.b01_q7_properties is not None:
|
||||
coordinator = RoborockB01Q7UpdateCoordinator(
|
||||
hass, entry, device, device.b01_q7_properties
|
||||
)
|
||||
elif device.b01_q10_properties is not None:
|
||||
coordinator = RoborockB01Q10UpdateCoordinator(
|
||||
hass, entry, device, device.b01_q10_properties
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Not adding entities for device %s (%s/%s) because its protocol"
|
||||
" version %s or category %s is not supported",
|
||||
device.duid,
|
||||
device.product.name,
|
||||
device.product.model,
|
||||
device.device_info.pv,
|
||||
device.product.category.name,
|
||||
)
|
||||
try:
|
||||
await device.close()
|
||||
except RoborockException as err:
|
||||
_LOGGER.warning("Failed to close device %s: %s", device.duid, err)
|
||||
return
|
||||
| None,
|
||||
]
|
||||
]:
|
||||
"""Create a list of setup functions that can later be called asynchronously."""
|
||||
coordinators: list[
|
||||
RoborockDataUpdateCoordinator
|
||||
| RoborockDataUpdateCoordinatorA01
|
||||
| RoborockDataUpdateCoordinatorB01
|
||||
| RoborockB01Q10UpdateCoordinator
|
||||
] = []
|
||||
for device in devices:
|
||||
_LOGGER.debug("Creating device %s: %s", device.name, device)
|
||||
if device.v1_properties is not None:
|
||||
coordinators.append(
|
||||
RoborockDataUpdateCoordinator(hass, entry, device, device.v1_properties)
|
||||
)
|
||||
elif device.dyad is not None:
|
||||
coordinators.append(
|
||||
RoborockWetDryVacUpdateCoordinator(hass, entry, device, device.dyad)
|
||||
)
|
||||
elif device.zeo is not None:
|
||||
coordinators.append(
|
||||
RoborockWashingMachineUpdateCoordinator(hass, entry, device, device.zeo)
|
||||
)
|
||||
elif device.b01_q7_properties is not None:
|
||||
coordinators.append(
|
||||
RoborockB01Q7UpdateCoordinator(
|
||||
hass, entry, device, device.b01_q7_properties
|
||||
)
|
||||
)
|
||||
elif device.b01_q10_properties is not None:
|
||||
coordinators.append(
|
||||
RoborockB01Q10UpdateCoordinator(
|
||||
hass, entry, device, device.b01_q10_properties
|
||||
)
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Not adding device %s because its protocol version"
|
||||
" %s or category %s is not supported",
|
||||
device.duid,
|
||||
device.device_info.pv,
|
||||
device.product.category.name,
|
||||
)
|
||||
|
||||
entry.runtime_data.add(coordinator)
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
f"roborock_coordinator_added_{entry.entry_id}",
|
||||
coordinator,
|
||||
)
|
||||
entry.async_create_background_task(
|
||||
hass,
|
||||
coordinator.async_refresh(),
|
||||
name=f"roborock_coordinator_refresh_{coordinator.duid}",
|
||||
)
|
||||
return [setup_coordinator(coordinator) for coordinator in coordinators]
|
||||
|
||||
|
||||
async def setup_coordinator(
|
||||
coordinator: RoborockDataUpdateCoordinator
|
||||
| RoborockDataUpdateCoordinatorA01
|
||||
| RoborockDataUpdateCoordinatorB01
|
||||
| RoborockB01Q10UpdateCoordinator,
|
||||
) -> (
|
||||
RoborockDataUpdateCoordinator
|
||||
| RoborockDataUpdateCoordinatorA01
|
||||
| RoborockDataUpdateCoordinatorB01
|
||||
| RoborockB01Q10UpdateCoordinator
|
||||
| None
|
||||
):
|
||||
"""Set up a single coordinator."""
|
||||
try:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
except ConfigEntryNotReady:
|
||||
await coordinator.async_shutdown()
|
||||
raise
|
||||
else:
|
||||
return coordinator
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> bool:
|
||||
|
||||
@@ -15,14 +15,12 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import ATTR_BATTERY_CHARGING, EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .coordinator import (
|
||||
RoborockConfigEntry,
|
||||
RoborockCoordinatorType,
|
||||
RoborockDataUpdateCoordinator,
|
||||
RoborockDataUpdateCoordinatorA01,
|
||||
RoborockWashingMachineUpdateCoordinator,
|
||||
@@ -170,38 +168,26 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Roborock vacuum binary sensors."""
|
||||
coordinators = config_entry.runtime_data
|
||||
|
||||
@callback
|
||||
def async_add_coordinator_entities(
|
||||
coordinator: RoborockCoordinatorType,
|
||||
) -> None:
|
||||
"""Add entities for a specific coordinator."""
|
||||
entities: list[BinarySensorEntity] = []
|
||||
if isinstance(coordinator, RoborockDataUpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockBinarySensorEntity(coordinator, description)
|
||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||
if description.support_fn(coordinator.properties_api)
|
||||
)
|
||||
elif isinstance(coordinator, RoborockWashingMachineUpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockBinarySensorEntityA01(coordinator, description)
|
||||
for description in ZEO_BINARY_SENSOR_DESCRIPTIONS
|
||||
if description.data_protocol in coordinator.request_protocols
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
async_add_coordinator_entities(coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"roborock_coordinator_added_{config_entry.entry_id}",
|
||||
async_add_coordinator_entities,
|
||||
entities: list[BinarySensorEntity] = [
|
||||
RoborockBinarySensorEntity(
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for coordinator in config_entry.runtime_data.v1
|
||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||
if description.support_fn(coordinator.properties_api)
|
||||
]
|
||||
entities.extend(
|
||||
RoborockBinarySensorEntityA01(
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for coordinator in config_entry.runtime_data.a01
|
||||
if isinstance(coordinator, RoborockWashingMachineUpdateCoordinator)
|
||||
for description in ZEO_BINARY_SENSOR_DESCRIPTIONS
|
||||
if description.data_protocol in coordinator.request_protocols
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class RoborockBinarySensorEntity(RoborockCoordinatedEntityV1, BinarySensorEntity):
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Support for Roborock button."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
@@ -11,16 +13,14 @@ from roborock.roborock_message import RoborockZeoProtocol
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
RoborockB01Q10UpdateCoordinator,
|
||||
RoborockConfigEntry,
|
||||
RoborockCoordinatorType,
|
||||
RoborockDataUpdateCoordinator,
|
||||
RoborockDataUpdateCoordinatorA01,
|
||||
RoborockWashingMachineUpdateCoordinator,
|
||||
@@ -140,68 +140,51 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Roborock button platform."""
|
||||
coordinators = config_entry.runtime_data
|
||||
|
||||
@callback
|
||||
def async_add_coordinator_entities(
|
||||
coordinator: RoborockCoordinatorType,
|
||||
) -> None:
|
||||
"""Add entities for a specific coordinator."""
|
||||
entities: list[ButtonEntity] = []
|
||||
if isinstance(coordinator, RoborockDataUpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockButtonEntity(coordinator, description)
|
||||
routines_lists = await asyncio.gather(
|
||||
*[coordinator.get_routines() for coordinator in config_entry.runtime_data.v1],
|
||||
)
|
||||
async_add_entities(
|
||||
itertools.chain(
|
||||
(
|
||||
RoborockButtonEntity(
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for coordinator in config_entry.runtime_data.v1
|
||||
if isinstance(coordinator, RoborockDataUpdateCoordinator)
|
||||
for description in CONSUMABLE_BUTTON_DESCRIPTIONS
|
||||
if description.is_supported(coordinator)
|
||||
)
|
||||
|
||||
async def async_add_routine_buttons() -> None:
|
||||
try:
|
||||
routines = await coordinator.get_routines()
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(
|
||||
"Failed to get routines for %s: %s",
|
||||
coordinator.device.name,
|
||||
err,
|
||||
)
|
||||
return
|
||||
routine_entities = [
|
||||
RoborockRoutineButtonEntity(
|
||||
coordinator,
|
||||
ButtonEntityDescription(
|
||||
key=str(routine.id),
|
||||
name=routine.name,
|
||||
),
|
||||
)
|
||||
for routine in routines
|
||||
]
|
||||
async_add_entities(routine_entities)
|
||||
|
||||
config_entry.async_create_background_task(
|
||||
hass,
|
||||
async_add_routine_buttons(),
|
||||
f"roborock_setup_routines_{coordinator.duid}",
|
||||
)
|
||||
elif isinstance(coordinator, RoborockWashingMachineUpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockButtonEntityA01(coordinator, description)
|
||||
),
|
||||
(
|
||||
RoborockRoutineButtonEntity(
|
||||
coordinator,
|
||||
ButtonEntityDescription(
|
||||
key=str(routine.id),
|
||||
name=routine.name,
|
||||
),
|
||||
)
|
||||
for coordinator, routines in zip(
|
||||
config_entry.runtime_data.v1, routines_lists, strict=True
|
||||
)
|
||||
for routine in routines
|
||||
),
|
||||
(
|
||||
RoborockButtonEntityA01(
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for coordinator in config_entry.runtime_data.a01
|
||||
if isinstance(coordinator, RoborockWashingMachineUpdateCoordinator)
|
||||
for description in ZEO_BUTTON_DESCRIPTIONS
|
||||
)
|
||||
elif isinstance(coordinator, RoborockB01Q10UpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockQ10EmptyDustbinButtonEntity(coordinator, description)
|
||||
),
|
||||
(
|
||||
RoborockQ10EmptyDustbinButtonEntity(
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for coordinator in config_entry.runtime_data.b01_q10
|
||||
for description in Q10_BUTTON_DESCRIPTIONS
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
async_add_coordinator_entities(coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"roborock_coordinator_added_{config_entry.entry_id}",
|
||||
async_add_coordinator_entities,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Roborock Coordinator."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any, override
|
||||
@@ -57,38 +58,14 @@ MIN_UNAVAILABLE_DURATION = timedelta(minutes=2)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoborockCoordinators:
|
||||
"""Roborock coordinators type."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self._coordinators: dict[
|
||||
str,
|
||||
RoborockDataUpdateCoordinator
|
||||
| RoborockDataUpdateCoordinatorA01
|
||||
| RoborockB01Q7UpdateCoordinator
|
||||
| RoborockB01Q10UpdateCoordinator,
|
||||
] = {}
|
||||
|
||||
def add(
|
||||
self,
|
||||
coordinator: RoborockDataUpdateCoordinator
|
||||
| RoborockDataUpdateCoordinatorA01
|
||||
| RoborockB01Q7UpdateCoordinator
|
||||
| RoborockB01Q10UpdateCoordinator,
|
||||
) -> None:
|
||||
"""Add a coordinator."""
|
||||
self._coordinators[coordinator.duid] = coordinator
|
||||
|
||||
def __contains__(self, duid: str) -> bool:
|
||||
"""Check if a coordinator exists by DUID."""
|
||||
return duid in self._coordinators
|
||||
|
||||
def keys(self) -> list[str]:
|
||||
"""Return DUIDs of all registered coordinators."""
|
||||
return list(self._coordinators.keys())
|
||||
v1: list[RoborockDataUpdateCoordinator]
|
||||
a01: list[RoborockDataUpdateCoordinatorA01]
|
||||
b01_q7: list[RoborockB01Q7UpdateCoordinator]
|
||||
b01_q10: list[RoborockB01Q10UpdateCoordinator]
|
||||
|
||||
def values(
|
||||
self,
|
||||
@@ -101,42 +78,6 @@ class RoborockCoordinators:
|
||||
"""Return all coordinators."""
|
||||
return self.v1 + self.a01 + self.b01_q7 + self.b01_q10
|
||||
|
||||
@property
|
||||
def v1(self) -> list[RoborockDataUpdateCoordinator]:
|
||||
"""Return V1 coordinators."""
|
||||
return [
|
||||
coord
|
||||
for coord in self._coordinators.values()
|
||||
if isinstance(coord, RoborockDataUpdateCoordinator)
|
||||
]
|
||||
|
||||
@property
|
||||
def a01(self) -> list[RoborockDataUpdateCoordinatorA01]:
|
||||
"""Return A01 coordinators."""
|
||||
return [
|
||||
coord
|
||||
for coord in self._coordinators.values()
|
||||
if isinstance(coord, RoborockDataUpdateCoordinatorA01)
|
||||
]
|
||||
|
||||
@property
|
||||
def b01_q7(self) -> list[RoborockB01Q7UpdateCoordinator]:
|
||||
"""Return Q7 coordinators."""
|
||||
return [
|
||||
coord
|
||||
for coord in self._coordinators.values()
|
||||
if isinstance(coord, RoborockB01Q7UpdateCoordinator)
|
||||
]
|
||||
|
||||
@property
|
||||
def b01_q10(self) -> list[RoborockB01Q10UpdateCoordinator]:
|
||||
"""Return Q10 coordinators."""
|
||||
return [
|
||||
coord
|
||||
for coord in self._coordinators.values()
|
||||
if isinstance(coord, RoborockB01Q10UpdateCoordinator)
|
||||
]
|
||||
|
||||
|
||||
type RoborockConfigEntry = ConfigEntry[RoborockCoordinators]
|
||||
|
||||
@@ -169,14 +110,13 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState | None]):
|
||||
self.device_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, mac)}
|
||||
self.last_update_state: str | None = None
|
||||
# Keep track of last attempt to refresh maps/rooms to know when to try again.
|
||||
self._last_home_update_attempt = dt_util.utcnow()
|
||||
self._last_home_update_attempt: datetime
|
||||
self.last_home_update: datetime | None = None
|
||||
# Tracks the last successful update to control when we report failure
|
||||
# to the base class. This is reset on successful data update.
|
||||
self._last_update_success_time: datetime | None = None
|
||||
self._has_connected_locally: bool = False
|
||||
self._unsubs: list[Callable[[], None]] = []
|
||||
self._setup_completed = False
|
||||
|
||||
@cached_property
|
||||
def dock_device_info(self) -> DeviceInfo:
|
||||
@@ -202,6 +142,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState | None]):
|
||||
try:
|
||||
await self.properties_api.status.refresh()
|
||||
except RoborockException as err:
|
||||
_LOGGER.debug("Failed to update data during setup: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_data_fail",
|
||||
@@ -298,11 +239,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState | None]):
|
||||
@override
|
||||
async def _async_update_data(self) -> DeviceState | None:
|
||||
"""Update data via library."""
|
||||
if not self._setup_completed:
|
||||
await self._async_setup()
|
||||
self._setup_completed = True
|
||||
else:
|
||||
await self._verify_api()
|
||||
await self._verify_api()
|
||||
try:
|
||||
# Update device props and standard api information
|
||||
await self._update_device_prop()
|
||||
@@ -731,11 +668,3 @@ class RoborockB01Q10UpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
def device(self) -> RoborockDevice:
|
||||
"""Get the RoborockDevice."""
|
||||
return self._device
|
||||
|
||||
|
||||
type RoborockCoordinatorType = (
|
||||
RoborockDataUpdateCoordinator
|
||||
| RoborockDataUpdateCoordinatorA01[Any]
|
||||
| RoborockB01Q7UpdateCoordinator
|
||||
| RoborockB01Q10UpdateCoordinator
|
||||
)
|
||||
|
||||
@@ -12,14 +12,9 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import (
|
||||
RoborockConfigEntry,
|
||||
RoborockCoordinatorType,
|
||||
RoborockDataUpdateCoordinator,
|
||||
)
|
||||
from .coordinator import RoborockConfigEntry, RoborockDataUpdateCoordinator
|
||||
from .entity import RoborockCoordinatedEntityV1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -33,38 +28,20 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Roborock image platform."""
|
||||
coordinators = config_entry.runtime_data
|
||||
|
||||
@callback
|
||||
def async_add_coordinator_entities(
|
||||
coordinator: RoborockCoordinatorType,
|
||||
) -> None:
|
||||
"""Add entities for a specific coordinator."""
|
||||
if not isinstance(coordinator, RoborockDataUpdateCoordinator):
|
||||
return
|
||||
entities = [
|
||||
async_add_entities(
|
||||
(
|
||||
RoborockMap(
|
||||
config_entry,
|
||||
coordinator,
|
||||
coordinator.properties_api.home,
|
||||
coord,
|
||||
coord.properties_api.home,
|
||||
map_info.map_flag,
|
||||
map_info.name,
|
||||
)
|
||||
for map_info in (
|
||||
coordinator.properties_api.home.home_map_info or {}
|
||||
).values()
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
async_add_coordinator_entities(coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"roborock_coordinator_added_{config_entry.entry_id}",
|
||||
async_add_coordinator_entities,
|
||||
)
|
||||
for coord in config_entry.runtime_data.v1
|
||||
if coord.properties_api.home is not None
|
||||
for map_info in (coord.properties_api.home.home_map_info or {}).values()
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,17 +10,12 @@ from roborock.exceptions import RoborockException
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
RoborockConfigEntry,
|
||||
RoborockCoordinatorType,
|
||||
RoborockDataUpdateCoordinator,
|
||||
)
|
||||
from .coordinator import RoborockConfigEntry, RoborockDataUpdateCoordinator
|
||||
from .entity import RoborockEntityV1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -63,36 +58,18 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Roborock number platform."""
|
||||
coordinators = config_entry.runtime_data
|
||||
|
||||
@callback
|
||||
def async_add_coordinator_entities(
|
||||
coordinator: RoborockCoordinatorType,
|
||||
) -> None:
|
||||
"""Add entities for a specific coordinator."""
|
||||
if not isinstance(coordinator, RoborockDataUpdateCoordinator):
|
||||
return
|
||||
entities = [
|
||||
async_add_entities(
|
||||
[
|
||||
RoborockNumberEntity(
|
||||
f"{description.key}_{coordinator.duid_slug}",
|
||||
coordinator=coordinator,
|
||||
entity_description=description,
|
||||
trait=trait,
|
||||
)
|
||||
for coordinator in config_entry.runtime_data.v1
|
||||
for description in NUMBER_DESCRIPTIONS
|
||||
if (trait := description.trait(coordinator.properties_api)) is not None
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
async_add_coordinator_entities(coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"roborock_coordinator_added_{config_entry.entry_id}",
|
||||
async_add_coordinator_entities,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -32,9 +32,8 @@ from roborock.roborock_typing import RoborockCommand
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, MAP_SLEEP
|
||||
@@ -42,10 +41,8 @@ from .coordinator import (
|
||||
RoborockB01Q7UpdateCoordinator,
|
||||
RoborockB01Q10UpdateCoordinator,
|
||||
RoborockConfigEntry,
|
||||
RoborockCoordinatorType,
|
||||
RoborockDataUpdateCoordinator,
|
||||
RoborockDataUpdateCoordinatorA01,
|
||||
RoborockWashingMachineUpdateCoordinator,
|
||||
)
|
||||
from .entity import (
|
||||
RoborockCoordinatedEntityA01,
|
||||
@@ -253,59 +250,40 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Roborock select platform."""
|
||||
coordinators = config_entry.runtime_data
|
||||
|
||||
@callback
|
||||
def async_add_coordinator_entities(
|
||||
coordinator: RoborockCoordinatorType,
|
||||
) -> None:
|
||||
"""Add entities for a specific coordinator."""
|
||||
entities: list[SelectEntity] = []
|
||||
if isinstance(coordinator, RoborockDataUpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockSelectEntity(coordinator, description, options)
|
||||
for description in SELECT_DESCRIPTIONS
|
||||
if (options := description.options_lambda(coordinator.properties_api))
|
||||
is not None
|
||||
)
|
||||
if (
|
||||
coordinator.properties_api.home is not None
|
||||
and coordinator.properties_api.maps is not None
|
||||
):
|
||||
entities.append(
|
||||
RoborockCurrentMapSelectEntity(
|
||||
f"selected_map_{coordinator.duid_slug}",
|
||||
coordinator,
|
||||
coordinator.properties_api.home,
|
||||
coordinator.properties_api.maps,
|
||||
)
|
||||
)
|
||||
elif isinstance(coordinator, RoborockB01Q7UpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockB01SelectEntity(coordinator, description, options)
|
||||
for description in B01_SELECT_DESCRIPTIONS
|
||||
if (options := description.options_lambda(coordinator.api)) is not None
|
||||
)
|
||||
elif isinstance(coordinator, RoborockWashingMachineUpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockSelectEntityA01(coordinator, description)
|
||||
for description in A01_SELECT_DESCRIPTIONS
|
||||
if description.data_protocol in coordinator.request_protocols
|
||||
)
|
||||
elif isinstance(coordinator, RoborockB01Q10UpdateCoordinator):
|
||||
entities.append(RoborockQ10CleanModeSelectEntity(coordinator))
|
||||
async_add_entities(entities)
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
async_add_coordinator_entities(coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"roborock_coordinator_added_{config_entry.entry_id}",
|
||||
async_add_coordinator_entities,
|
||||
async_add_entities(
|
||||
RoborockSelectEntity(coordinator, description, options)
|
||||
for coordinator in config_entry.runtime_data.v1
|
||||
for description in SELECT_DESCRIPTIONS
|
||||
if (
|
||||
(options := description.options_lambda(coordinator.properties_api))
|
||||
is not None
|
||||
)
|
||||
)
|
||||
async_add_entities(
|
||||
RoborockCurrentMapSelectEntity(
|
||||
f"selected_map_{coordinator.duid_slug}", coordinator, home_trait, map_trait
|
||||
)
|
||||
for coordinator in config_entry.runtime_data.v1
|
||||
if (home_trait := coordinator.properties_api.home) is not None
|
||||
if (map_trait := coordinator.properties_api.maps) is not None
|
||||
)
|
||||
async_add_entities(
|
||||
RoborockB01SelectEntity(coordinator, description, options)
|
||||
for coordinator in config_entry.runtime_data.b01_q7
|
||||
for description in B01_SELECT_DESCRIPTIONS
|
||||
if (options := description.options_lambda(coordinator.api)) is not None
|
||||
)
|
||||
async_add_entities(
|
||||
RoborockSelectEntityA01(coordinator, description)
|
||||
for coordinator in config_entry.runtime_data.a01
|
||||
for description in A01_SELECT_DESCRIPTIONS
|
||||
if description.data_protocol in coordinator.request_protocols
|
||||
)
|
||||
async_add_entities(
|
||||
RoborockQ10CleanModeSelectEntity(coordinator)
|
||||
for coordinator in config_entry.runtime_data.b01_q10
|
||||
)
|
||||
|
||||
|
||||
class RoborockB01SelectEntity(RoborockCoordinatedEntityB01Q7, SelectEntity):
|
||||
|
||||
@@ -30,8 +30,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfArea, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
@@ -39,7 +38,6 @@ from .coordinator import (
|
||||
RoborockB01Q7UpdateCoordinator,
|
||||
RoborockB01Q10UpdateCoordinator,
|
||||
RoborockConfigEntry,
|
||||
RoborockCoordinatorType,
|
||||
RoborockDataUpdateCoordinator,
|
||||
RoborockDataUpdateCoordinatorA01,
|
||||
RoborockWashingMachineUpdateCoordinator,
|
||||
@@ -541,54 +539,48 @@ async def async_setup_entry(
|
||||
"""Set up the Roborock vacuum sensors."""
|
||||
coordinators = config_entry.runtime_data
|
||||
|
||||
@callback
|
||||
def async_add_coordinator_entities(
|
||||
coordinator: RoborockCoordinatorType,
|
||||
) -> None:
|
||||
"""Add entities for a specific coordinator."""
|
||||
entities: list[RoborockEntity] = []
|
||||
if isinstance(coordinator, RoborockDataUpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockSensorEntity(coordinator, description)
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
if description.support_fn(coordinator.properties_api)
|
||||
)
|
||||
entities.append(RoborockCurrentRoom(coordinator))
|
||||
elif isinstance(coordinator, RoborockWetDryVacUpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockSensorEntityA01(coordinator, description)
|
||||
for description in DYAD_SENSOR_DESCRIPTIONS
|
||||
if description.data_protocol in coordinator.request_protocols
|
||||
)
|
||||
elif isinstance(coordinator, RoborockWashingMachineUpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockSensorEntityA01(coordinator, description)
|
||||
for description in ZEO_SENSOR_DESCRIPTIONS
|
||||
if description.data_protocol in coordinator.request_protocols
|
||||
)
|
||||
elif isinstance(coordinator, RoborockB01Q7UpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockSensorEntityB01Q7(coordinator, description)
|
||||
for description in Q7_B01_SENSOR_DESCRIPTIONS
|
||||
if description.value_fn(coordinator.data) is not None
|
||||
)
|
||||
elif isinstance(coordinator, RoborockB01Q10UpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockSensorEntityB01Q10(coordinator, description)
|
||||
for description in Q10_B01_SENSOR_DESCRIPTIONS
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
async_add_coordinator_entities(coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"roborock_coordinator_added_{config_entry.entry_id}",
|
||||
async_add_coordinator_entities,
|
||||
entities: list[RoborockEntity] = [
|
||||
RoborockSensorEntity(
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for coordinator in coordinators.v1
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
if description.support_fn(coordinator.properties_api)
|
||||
]
|
||||
entities.extend(RoborockCurrentRoom(coordinator) for coordinator in coordinators.v1)
|
||||
entities.extend(
|
||||
RoborockSensorEntityA01(
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for coordinator in coordinators.a01
|
||||
if isinstance(coordinator, RoborockWetDryVacUpdateCoordinator)
|
||||
for description in DYAD_SENSOR_DESCRIPTIONS
|
||||
if description.data_protocol in coordinator.request_protocols
|
||||
)
|
||||
entities.extend(
|
||||
RoborockSensorEntityA01(
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for coordinator in coordinators.a01
|
||||
if isinstance(coordinator, RoborockWashingMachineUpdateCoordinator)
|
||||
for description in ZEO_SENSOR_DESCRIPTIONS
|
||||
if description.data_protocol in coordinator.request_protocols
|
||||
)
|
||||
entities.extend(
|
||||
RoborockSensorEntityB01Q7(coordinator, description)
|
||||
for coordinator in coordinators.b01_q7
|
||||
for description in Q7_B01_SENSOR_DESCRIPTIONS
|
||||
if description.value_fn(coordinator.data) is not None
|
||||
)
|
||||
entities.extend(
|
||||
RoborockSensorEntityB01Q10(coordinator, description)
|
||||
for coordinator in coordinators.b01_q10
|
||||
for description in Q10_B01_SENSOR_DESCRIPTIONS
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class RoborockSensorEntity(RoborockCoordinatedEntityV1, SensorEntity):
|
||||
|
||||
@@ -12,15 +12,13 @@ from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProto
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
RoborockConfigEntry,
|
||||
RoborockCoordinatorType,
|
||||
RoborockDataUpdateCoordinator,
|
||||
RoborockDataUpdateCoordinatorA01,
|
||||
)
|
||||
@@ -95,45 +93,30 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Roborock switch platform."""
|
||||
coordinators = config_entry.runtime_data
|
||||
|
||||
@callback
|
||||
def async_add_coordinator_entities(
|
||||
coordinator: RoborockCoordinatorType,
|
||||
) -> None:
|
||||
"""Add entities for a specific coordinator."""
|
||||
entities: list[SwitchEntity] = []
|
||||
if isinstance(coordinator, RoborockDataUpdateCoordinator):
|
||||
entities.extend(
|
||||
RoborockSwitch(
|
||||
f"{description.key}_{coordinator.duid_slug}",
|
||||
coordinator,
|
||||
description,
|
||||
trait,
|
||||
)
|
||||
for description in SWITCH_DESCRIPTIONS
|
||||
if (trait := description.trait(coordinator.properties_api)) is not None
|
||||
# V1 switches - using trait pattern from HEAD
|
||||
async_add_entities(
|
||||
[
|
||||
RoborockSwitch(
|
||||
f"{description.key}_{coordinator.duid_slug}",
|
||||
coordinator,
|
||||
description,
|
||||
trait,
|
||||
)
|
||||
elif isinstance(coordinator, RoborockDataUpdateCoordinatorA01):
|
||||
entities.extend(
|
||||
RoborockSwitchA01(
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for description in A01_SWITCH_DESCRIPTIONS
|
||||
if description.data_protocol in coordinator.request_protocols
|
||||
)
|
||||
async_add_entities(entities)
|
||||
for coordinator in config_entry.runtime_data.v1
|
||||
for description in SWITCH_DESCRIPTIONS
|
||||
if (trait := description.trait(coordinator.properties_api)) is not None
|
||||
]
|
||||
)
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
async_add_coordinator_entities(coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"roborock_coordinator_added_{config_entry.entry_id}",
|
||||
async_add_coordinator_entities,
|
||||
# A01 switches
|
||||
async_add_entities(
|
||||
RoborockSwitchA01(
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for coordinator in config_entry.runtime_data.a01
|
||||
for description in A01_SWITCH_DESCRIPTIONS
|
||||
if description.data_protocol in coordinator.request_protocols
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -12,17 +12,12 @@ from roborock.exceptions import RoborockException
|
||||
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
RoborockConfigEntry,
|
||||
RoborockCoordinatorType,
|
||||
RoborockDataUpdateCoordinator,
|
||||
)
|
||||
from .coordinator import RoborockConfigEntry, RoborockDataUpdateCoordinator
|
||||
from .entity import RoborockEntityV1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -128,36 +123,18 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Roborock time platform."""
|
||||
coordinators = config_entry.runtime_data
|
||||
|
||||
@callback
|
||||
def async_add_coordinator_entities(
|
||||
coordinator: RoborockCoordinatorType,
|
||||
) -> None:
|
||||
"""Add entities for a specific coordinator."""
|
||||
if not isinstance(coordinator, RoborockDataUpdateCoordinator):
|
||||
return
|
||||
entities = [
|
||||
async_add_entities(
|
||||
[
|
||||
RoborockTimeEntity(
|
||||
f"{description.key}_{coordinator.duid_slug}",
|
||||
coordinator,
|
||||
description,
|
||||
trait,
|
||||
)
|
||||
for coordinator in config_entry.runtime_data.v1
|
||||
for description in TIME_DESCRIPTIONS
|
||||
if (trait := description.trait(coordinator.properties_api)) is not None
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
async_add_coordinator_entities(coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"roborock_coordinator_added_{config_entry.entry_id}",
|
||||
async_add_coordinator_entities,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ from homeassistant.exceptions import (
|
||||
ServiceNotSupported,
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -32,7 +31,6 @@ from .coordinator import (
|
||||
RoborockB01Q7UpdateCoordinator,
|
||||
RoborockB01Q10UpdateCoordinator,
|
||||
RoborockConfigEntry,
|
||||
RoborockCoordinatorType,
|
||||
RoborockDataUpdateCoordinator,
|
||||
)
|
||||
from .entity import (
|
||||
@@ -114,29 +112,16 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Roborock sensor."""
|
||||
coordinators = config_entry.runtime_data
|
||||
|
||||
@callback
|
||||
def async_add_coordinator_entities(
|
||||
coordinator: RoborockCoordinatorType,
|
||||
) -> None:
|
||||
"""Add entities for a specific coordinator."""
|
||||
if isinstance(coordinator, RoborockDataUpdateCoordinator):
|
||||
async_add_entities([RoborockVacuum(coordinator)])
|
||||
elif isinstance(coordinator, RoborockB01Q7UpdateCoordinator):
|
||||
async_add_entities([RoborockQ7Vacuum(coordinator)])
|
||||
elif isinstance(coordinator, RoborockB01Q10UpdateCoordinator):
|
||||
async_add_entities([RoborockQ10Vacuum(coordinator)])
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
async_add_coordinator_entities(coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"roborock_coordinator_added_{config_entry.entry_id}",
|
||||
async_add_coordinator_entities,
|
||||
)
|
||||
async_add_entities(
|
||||
RoborockVacuum(coordinator) for coordinator in config_entry.runtime_data.v1
|
||||
)
|
||||
async_add_entities(
|
||||
RoborockQ7Vacuum(coordinator)
|
||||
for coordinator in config_entry.runtime_data.b01_q7
|
||||
)
|
||||
async_add_entities(
|
||||
RoborockQ10Vacuum(coordinator)
|
||||
for coordinator in config_entry.runtime_data.b01_q10
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, override
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@@ -54,7 +54,6 @@ class SabnzbdBinarySensor(SabnzbdEntity, BinarySensorEntity):
|
||||
entity_description: SabnzbdBinarySensorEntityDescription
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool:
|
||||
"""Return latest sensor data."""
|
||||
return self.entity_description.is_on_fn(self.coordinator.data)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, override
|
||||
from typing import Any
|
||||
|
||||
from pysabnzbd import SabnzbdApiException
|
||||
|
||||
@@ -55,7 +55,6 @@ class SabnzbdButton(SabnzbdEntity, ButtonEntity):
|
||||
|
||||
entity_description: SabnzbdButtonEntityDescription
|
||||
|
||||
@override
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Adds config flow for SabNzbd."""
|
||||
|
||||
import logging
|
||||
from typing import Any, override
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
@@ -51,7 +51,6 @@ class SABnzbdConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle reconfiguration flow."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, override
|
||||
from typing import Any
|
||||
|
||||
from pysabnzbd import SabnzbdApi, SabnzbdApiException
|
||||
|
||||
@@ -37,7 +37,6 @@ class SabnzbdUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Get the latest data from the SABnzbd API."""
|
||||
try:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import override
|
||||
|
||||
from pysabnzbd import SabnzbdApiException
|
||||
|
||||
@@ -63,12 +62,10 @@ class SabnzbdNumber(SabnzbdEntity, NumberEntity):
|
||||
entity_description: SabnzbdNumberEntityDescription
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_value(self) -> float:
|
||||
"""Return latest value for number."""
|
||||
return self.coordinator.data[self.entity_description.key]
|
||||
|
||||
@override
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set the new number value."""
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for monitoring an SABnzbd NZB client."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import override
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -128,7 +127,6 @@ class SabnzbdSensor(SabnzbdEntity, SensorEntity):
|
||||
entity_description: SabnzbdSensorEntityDescription
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_value(self) -> StateType:
|
||||
"""Return latest sensor data."""
|
||||
return self.coordinator.data.get(self.entity_description.key)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Config flow for SAJ."""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pysaj
|
||||
import voluptuous as vol
|
||||
@@ -124,7 +124,6 @@ class SAJConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return serial_number
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user