Compare commits

..

2 Commits

Author SHA1 Message Date
J. Nick Koston a662847e7b Update zeroconf interfaces when Supervisor reports a network change 2026-06-22 16:15:37 -05:00
J. Nick Koston c04448e186 Bump zeroconf to 0.150.0 2026-06-22 13:51:56 -05:00
1754 changed files with 2934 additions and 18438 deletions
Generated
+2 -2
View File
@@ -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
+1 -3
View File
@@ -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]],
-1
View File
@@ -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(
+1 -8
View File
@@ -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:
+1 -8
View File
@@ -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 -6
View File
@@ -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
+1 -5
View File
@@ -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:
+1 -2
View File
@@ -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])
+1 -1
View File
@@ -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."""
+2 -10
View File
@@ -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,
)
+75 -89
View File
@@ -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):
+3 -3
View File
@@ -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."
+1 -4
View File
@@ -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(
+3 -3
View File
@@ -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)
+23 -1
View File
@@ -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
+1
View File
@@ -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",
+2 -2
View File
@@ -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}"
+1 -1
View File
@@ -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"
+1 -2
View File
@@ -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)
+6 -6
View File
@@ -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
+45 -91
View File
@@ -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,
)
+2 -3
View File
@@ -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"
}
},
+1 -1
View File
@@ -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"]
}
+7 -32
View File
@@ -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 -10
View File
@@ -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": {
+4 -37
View File
@@ -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)
+17 -81
View File
@@ -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"
}
}
}
}
+1 -48
View File
@@ -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}"
}
}
}
+2 -16
View File
@@ -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
+1 -3
View File
@@ -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
+8 -8
View File
@@ -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
+20 -1
View File
@@ -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.",
+7 -3
View File
@@ -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
+2 -1
View File
@@ -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"
]
+1 -1
View File
@@ -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"]
}
+148 -87
View File
@@ -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):
+44 -61
View File
@@ -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
)
+9 -32
View File
@@ -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()
),
)
+5 -28
View File
@@ -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 -54
View File
@@ -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):
+41 -49
View File
@@ -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):
+22 -39
View File
@@ -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
)
+5 -28
View File
@@ -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,
)
)
+10 -25
View File
@@ -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)
+1 -2
View File
@@ -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 -2
View File
@@ -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