mirror of
https://github.com/home-assistant/core.git
synced 2026-04-19 16:09:06 +02:00
Compare commits
12 Commits
fritz/remo
...
gj-2026040
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f1846b34a | ||
|
|
44b071762d | ||
|
|
f006283ee8 | ||
|
|
d25f3c6d2e | ||
|
|
f821952918 | ||
|
|
49ce8ed944 | ||
|
|
d4fca3737d | ||
|
|
26d8dfb695 | ||
|
|
6e92ba2fc0 | ||
|
|
7288d19abf | ||
|
|
5bbfe69bbb | ||
|
|
564280cc65 |
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -499,7 +499,7 @@ jobs:
|
||||
python -m build
|
||||
|
||||
- name: Upload package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
|
||||
coordinator = AnthropicCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
LOGGER.debug("Available models: %s", coordinator.data)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import anthropic
|
||||
@@ -70,7 +71,6 @@ from .const import (
|
||||
WEB_SEARCH_UNSUPPORTED_MODELS,
|
||||
PromptCaching,
|
||||
)
|
||||
from .coordinator import model_alias
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AnthropicConfigEntry
|
||||
@@ -112,13 +112,25 @@ async def get_model_list(client: anthropic.AsyncAnthropic) -> list[SelectOptionD
|
||||
except anthropic.AnthropicError:
|
||||
models = []
|
||||
_LOGGER.debug("Available models: %s", models)
|
||||
return [
|
||||
SelectOptionDict(
|
||||
label=model_info.display_name,
|
||||
value=model_alias(model_info.id),
|
||||
model_options: list[SelectOptionDict] = []
|
||||
short_form = re.compile(r"[^\d]-\d$")
|
||||
for model_info in models:
|
||||
# Resolve alias from versioned model name:
|
||||
model_alias = (
|
||||
model_info.id[:-9]
|
||||
if model_info.id != "claude-3-haiku-20240307"
|
||||
and model_info.id[-2:-1] != "-"
|
||||
else model_info.id
|
||||
)
|
||||
for model_info in models
|
||||
]
|
||||
if short_form.search(model_alias):
|
||||
model_alias += "-0"
|
||||
model_options.append(
|
||||
SelectOptionDict(
|
||||
label=model_info.display_name,
|
||||
value=model_alias,
|
||||
)
|
||||
)
|
||||
return model_options
|
||||
|
||||
|
||||
class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import re
|
||||
from datetime import timedelta
|
||||
|
||||
import anthropic
|
||||
|
||||
@@ -16,28 +15,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
UPDATE_INTERVAL_CONNECTED = datetime.timedelta(hours=12)
|
||||
UPDATE_INTERVAL_DISCONNECTED = datetime.timedelta(minutes=1)
|
||||
UPDATE_INTERVAL_CONNECTED = timedelta(hours=12)
|
||||
UPDATE_INTERVAL_DISCONNECTED = timedelta(minutes=1)
|
||||
|
||||
type AnthropicConfigEntry = ConfigEntry[AnthropicCoordinator]
|
||||
|
||||
|
||||
_model_short_form = re.compile(r"[^\d]-\d$")
|
||||
|
||||
|
||||
@callback
|
||||
def model_alias(model_id: str) -> str:
|
||||
"""Resolve alias from versioned model name."""
|
||||
if model_id == "claude-3-haiku-20240307" or model_id.endswith("-preview"):
|
||||
return model_id
|
||||
if model_id[-2:-1] != "-":
|
||||
model_id = model_id[:-9]
|
||||
if _model_short_form.search(model_id):
|
||||
return model_id + "-0"
|
||||
return model_id
|
||||
|
||||
|
||||
class AnthropicCoordinator(DataUpdateCoordinator[list[anthropic.types.ModelInfo]]):
|
||||
class AnthropicCoordinator(DataUpdateCoordinator[None]):
|
||||
"""DataUpdateCoordinator which uses different intervals after successful and unsuccessful updates."""
|
||||
|
||||
client: anthropic.AsyncAnthropic
|
||||
@@ -58,16 +42,16 @@ class AnthropicCoordinator(DataUpdateCoordinator[list[anthropic.types.ModelInfo]
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_set_updated_data(self, data: list[anthropic.types.ModelInfo]) -> None:
|
||||
def async_set_updated_data(self, data: None) -> None:
|
||||
"""Manually update data, notify listeners and update refresh interval."""
|
||||
self.update_interval = UPDATE_INTERVAL_CONNECTED
|
||||
super().async_set_updated_data(data)
|
||||
|
||||
async def async_update_data(self) -> list[anthropic.types.ModelInfo]:
|
||||
async def async_update_data(self) -> None:
|
||||
"""Fetch data from the API."""
|
||||
try:
|
||||
self.update_interval = UPDATE_INTERVAL_DISCONNECTED
|
||||
result = await self.client.models.list(timeout=10.0)
|
||||
await self.client.models.list(timeout=10.0)
|
||||
self.update_interval = UPDATE_INTERVAL_CONNECTED
|
||||
except anthropic.APITimeoutError as err:
|
||||
raise TimeoutError(err.message or str(err)) from err
|
||||
@@ -83,7 +67,6 @@ class AnthropicCoordinator(DataUpdateCoordinator[list[anthropic.types.ModelInfo]
|
||||
translation_key="api_error",
|
||||
translation_placeholders={"message": err.message},
|
||||
) from err
|
||||
return result.data
|
||||
|
||||
def mark_connection_error(self) -> None:
|
||||
"""Mark the connection as having an error and reschedule background check."""
|
||||
@@ -93,23 +76,3 @@ class AnthropicCoordinator(DataUpdateCoordinator[list[anthropic.types.ModelInfo]
|
||||
self.async_update_listeners()
|
||||
if self._listeners and not self.hass.is_stopping:
|
||||
self._schedule_refresh()
|
||||
|
||||
@callback
|
||||
def get_model_info(self, model_id: str) -> anthropic.types.ModelInfo:
|
||||
"""Get model info for a given model ID."""
|
||||
# First try: exact name match
|
||||
for model in self.data or []:
|
||||
if model.id == model_id:
|
||||
return model
|
||||
# Second try: match by alias
|
||||
alias = model_alias(model_id)
|
||||
for model in self.data or []:
|
||||
if model_alias(model.id) == alias:
|
||||
return model
|
||||
# Model not found, return safe defaults
|
||||
return anthropic.types.ModelInfo(
|
||||
type="model",
|
||||
id=model_id,
|
||||
created_at=datetime.datetime(1970, 1, 1, tzinfo=datetime.UTC),
|
||||
display_name=model_id,
|
||||
)
|
||||
|
||||
@@ -689,17 +689,12 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
super().__init__(entry.runtime_data)
|
||||
self.entry = entry
|
||||
self.subentry = subentry
|
||||
coordinator = entry.runtime_data
|
||||
self.model_info = coordinator.get_model_info(
|
||||
subentry.data.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
|
||||
)
|
||||
self._attr_unique_id = subentry.subentry_id
|
||||
self._attr_device_info = dr.DeviceInfo(
|
||||
identifiers={(DOMAIN, subentry.subentry_id)},
|
||||
name=subentry.title,
|
||||
manufacturer="Anthropic",
|
||||
model=self.model_info.display_name,
|
||||
model_id=self.model_info.id,
|
||||
model=subentry.data.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL]),
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@@ -974,7 +969,7 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
) from err
|
||||
except anthropic.AnthropicError as err:
|
||||
# Non-connection error, mark connection as healthy
|
||||
coordinator.async_set_updated_data(coordinator.data)
|
||||
coordinator.async_set_updated_data(None)
|
||||
LOGGER.error("Error while talking to Anthropic: %s", err)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
@@ -987,7 +982,7 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
) from err
|
||||
|
||||
if not chat_log.unresponded_tool_results:
|
||||
coordinator.async_set_updated_data(coordinator.data)
|
||||
coordinator.async_set_updated_data(None)
|
||||
break
|
||||
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["anthropic==0.92.0"]
|
||||
"requirements": ["anthropic==0.83.0"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["brother==6.1.0"],
|
||||
"requirements": ["brother==6.0.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "brother*",
|
||||
|
||||
@@ -112,7 +112,7 @@ class ComelitAlarmEntity(
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if alarm is available."""
|
||||
if self._area.human_status == AlarmAreaState.UNKNOWN:
|
||||
if self._area.human_status in [AlarmAreaState.ANOMALY, AlarmAreaState.UNKNOWN]:
|
||||
return False
|
||||
return super().available
|
||||
|
||||
@@ -151,7 +151,7 @@ class ComelitAlarmEntity(
|
||||
if code != str(self.coordinator.api.device_pin):
|
||||
return
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[DISABLE], self._area.anomaly
|
||||
self._area.index, ALARM_ACTIONS[DISABLE]
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.DISARMED, ALARM_AREA_ARMED_STATUS[DISABLE]
|
||||
@@ -160,7 +160,7 @@ class ComelitAlarmEntity(
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[AWAY], self._area.anomaly
|
||||
self._area.index, ALARM_ACTIONS[AWAY]
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[AWAY]
|
||||
@@ -169,7 +169,7 @@ class ComelitAlarmEntity(
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[HOME], self._area.anomaly
|
||||
self._area.index, ALARM_ACTIONS[HOME]
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[HOME_P1]
|
||||
@@ -178,7 +178,7 @@ class ComelitAlarmEntity(
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[NIGHT], self._area.anomaly
|
||||
self._area.index, ALARM_ACTIONS[NIGHT]
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT]
|
||||
|
||||
@@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN, DoorbellEventType
|
||||
from .const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_COMPONENT: HassKey[EntityComponent[EventEntity]] = HassKey(DOMAIN)
|
||||
@@ -44,7 +44,6 @@ __all__ = [
|
||||
"DOMAIN",
|
||||
"PLATFORM_SCHEMA",
|
||||
"PLATFORM_SCHEMA_BASE",
|
||||
"DoorbellEventType",
|
||||
"EventDeviceClass",
|
||||
"EventEntity",
|
||||
"EventEntityDescription",
|
||||
@@ -190,21 +189,6 @@ class EventEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_)
|
||||
async def async_internal_added_to_hass(self) -> None:
|
||||
"""Call when the event entity is added to hass."""
|
||||
await super().async_internal_added_to_hass()
|
||||
|
||||
if (
|
||||
self.device_class == EventDeviceClass.DOORBELL
|
||||
and DoorbellEventType.RING not in self.event_types
|
||||
):
|
||||
report_issue = self._suggest_report_issue()
|
||||
_LOGGER.warning(
|
||||
"Entity %s is a doorbell event entity but does not support "
|
||||
"the '%s' event type. This will stop working in "
|
||||
"Home Assistant 2027.4, please %s",
|
||||
self.entity_id,
|
||||
DoorbellEventType.RING,
|
||||
report_issue,
|
||||
)
|
||||
|
||||
if (
|
||||
(state := await self.async_get_last_state())
|
||||
and state.state is not None
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
"""Provides the constants needed for the component."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
DOMAIN = "event"
|
||||
ATTR_EVENT_TYPE = "event_type"
|
||||
ATTR_EVENT_TYPES = "event_types"
|
||||
|
||||
|
||||
class DoorbellEventType(StrEnum):
|
||||
"""Standard event types for doorbell device class."""
|
||||
|
||||
RING = "ring"
|
||||
|
||||
@@ -15,14 +15,7 @@
|
||||
"name": "Button"
|
||||
},
|
||||
"doorbell": {
|
||||
"name": "Doorbell",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"ring": "Ring"
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "Doorbell"
|
||||
},
|
||||
"motion": {
|
||||
"name": "Motion"
|
||||
|
||||
@@ -36,6 +36,7 @@ from homeassistant.helpers.service_info.ssdp import (
|
||||
ATTR_UPNP_UDN,
|
||||
SsdpServiceInfo,
|
||||
)
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import (
|
||||
CONF_FEATURE_DEVICE_TRACKING,
|
||||
@@ -226,12 +227,19 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self, errors: dict[str, str] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show the setup form to the user."""
|
||||
|
||||
advanced_data_schema: VolDictType = {}
|
||||
if self.show_advanced_options:
|
||||
advanced_data_schema = {
|
||||
vol.Optional(CONF_PORT): vol.Coerce(int),
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): str,
|
||||
vol.Optional(CONF_PORT): vol.Coerce(int),
|
||||
**advanced_data_schema,
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
|
||||
@@ -351,14 +359,18 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any], errors: dict[str, str] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show the reconfigure form to the user."""
|
||||
advanced_data_schema: VolDictType = {}
|
||||
if self.show_advanced_options:
|
||||
advanced_data_schema = {
|
||||
vol.Optional(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce(int),
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST, default=user_input[CONF_HOST]): str,
|
||||
vol.Optional(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce(
|
||||
int
|
||||
),
|
||||
**advanced_data_schema,
|
||||
vol.Required(CONF_SSL, default=user_input[CONF_SSL]): bool,
|
||||
}
|
||||
),
|
||||
@@ -372,21 +384,11 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle reconfigure flow."""
|
||||
if user_input is None:
|
||||
reconfigure_entry_data = self._get_reconfigure_entry().data
|
||||
port = reconfigure_entry_data[CONF_PORT]
|
||||
ssl = reconfigure_entry_data.get(CONF_SSL, DEFAULT_SSL)
|
||||
|
||||
if (port == DEFAULT_HTTP_PORT and not ssl) or (
|
||||
port == DEFAULT_HTTPS_PORT and ssl
|
||||
):
|
||||
# don't show default ports in reconfigure flow, as they are determined by ssl value
|
||||
# this allows the user to toggle ssl without having to change the port
|
||||
port = vol.UNDEFINED
|
||||
|
||||
return self._show_setup_form_reconfigure(
|
||||
{
|
||||
CONF_HOST: reconfigure_entry_data[CONF_HOST],
|
||||
CONF_PORT: port,
|
||||
CONF_SSL: ssl,
|
||||
CONF_PORT: reconfigure_entry_data[CONF_PORT],
|
||||
CONF_SSL: reconfigure_entry_data.get(CONF_SSL, DEFAULT_SSL),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -453,13 +453,10 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
if not attributes.get("MACAddress"):
|
||||
continue
|
||||
|
||||
wan_access_result = None
|
||||
if (wan_access := attributes.get("X_AVM-DE_WANAccess")) is not None:
|
||||
# wan_access can be "granted", "denied", "unknown" or "error"
|
||||
if "granted" in wan_access:
|
||||
wan_access_result = True
|
||||
elif "denied" in wan_access:
|
||||
wan_access_result = False
|
||||
wan_access_result = "granted" in wan_access
|
||||
else:
|
||||
wan_access_result = None
|
||||
|
||||
hosts[attributes["MACAddress"]] = Device(
|
||||
name=attributes["HostName"],
|
||||
|
||||
@@ -47,7 +47,9 @@ from .const import ( # noqa: F401
|
||||
ATTR_OBJECT_ID,
|
||||
ATTR_ORDER,
|
||||
ATTR_REMOVE_ENTITIES,
|
||||
CONF_GROUP_TYPE,
|
||||
CONF_HIDE_MEMBERS,
|
||||
CONF_IGNORE_NON_NUMERIC,
|
||||
DATA_COMPONENT,
|
||||
DOMAIN,
|
||||
GROUP_ORDER,
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
||||
|
||||
from .binary_sensor import CONF_ALL, async_create_preview_binary_sensor
|
||||
from .button import async_create_preview_button
|
||||
from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC, DOMAIN
|
||||
from .const import CONF_GROUP_TYPE, CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC, DOMAIN
|
||||
from .cover import async_create_preview_cover
|
||||
from .entity import GroupEntity
|
||||
from .event import async_create_preview_event
|
||||
@@ -180,7 +180,7 @@ GROUP_TYPES = [
|
||||
|
||||
async def choose_options_step(options: dict[str, Any]) -> str:
|
||||
"""Return next step_id for options flow according to group_type."""
|
||||
return cast(str, options["group_type"])
|
||||
return cast(str, options[CONF_GROUP_TYPE])
|
||||
|
||||
|
||||
def set_group_type(
|
||||
@@ -194,7 +194,7 @@ def set_group_type(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Add group type to user input."""
|
||||
return {"group_type": group_type, **user_input}
|
||||
return {CONF_GROUP_TYPE: group_type, **user_input}
|
||||
|
||||
return _set_group_type
|
||||
|
||||
@@ -430,7 +430,7 @@ def ws_start_preview(
|
||||
config_entry = hass.config_entries.async_get_entry(config_entry_id)
|
||||
if not config_entry:
|
||||
raise HomeAssistantError
|
||||
group_type = config_entry.options["group_type"]
|
||||
group_type = config_entry.options[CONF_GROUP_TYPE]
|
||||
name = config_entry.options["name"]
|
||||
validated = PREVIEW_OPTIONS_SCHEMA[group_type](msg["user_input"])
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
|
||||
|
||||
CONF_HIDE_MEMBERS = "hide_members"
|
||||
CONF_IGNORE_NON_NUMERIC = "ignore_non_numeric"
|
||||
CONF_GROUP_TYPE = "group_type"
|
||||
|
||||
DOMAIN = "group"
|
||||
DATA_COMPONENT: HassKey[EntityComponent[Group]] = HassKey(DOMAIN)
|
||||
|
||||
@@ -98,9 +98,7 @@ class HabiticaCalendarEntity(HabiticaBase, CalendarEntity):
|
||||
start_date, end_date - timedelta(days=1), inc=True
|
||||
)
|
||||
# if no end_date is given, return only the next recurrence
|
||||
if (next_date := recurrences.after(start_date, inc=True)) is None:
|
||||
return []
|
||||
return [next_date]
|
||||
return [recurrences.after(start_date, inc=True)]
|
||||
|
||||
|
||||
class HabiticaTodosCalendarEntity(HabiticaCalendarEntity):
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.components.homeassistant_hardware.util import guess_firmware_
|
||||
from homeassistant.components.usb import (
|
||||
USBDevice,
|
||||
async_register_port_event_callback,
|
||||
async_scan_serial_ports,
|
||||
scan_serial_ports,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -163,7 +163,7 @@ async def async_migrate_entry(
|
||||
key not in config_entry.data
|
||||
for key in (VID, PID, MANUFACTURER, PRODUCT, SERIAL_NUMBER)
|
||||
):
|
||||
serial_ports = await async_scan_serial_ports(hass)
|
||||
serial_ports = await hass.async_add_executor_job(scan_serial_ports)
|
||||
serial_ports_info = {port.device: port for port in serial_ports}
|
||||
device = config_entry.data[DEVICE]
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LunatoneConfigEntry) ->
|
||||
"""Set up Lunatone from a config entry."""
|
||||
auth_api = Auth(async_get_clientsession(hass), entry.data[CONF_URL])
|
||||
info_api = Info(auth_api)
|
||||
devices_api = Devices(info_api)
|
||||
devices_api = Devices(auth_api)
|
||||
|
||||
coordinator_info = LunatoneInfoDataUpdateCoordinator(hass, entry, info_api)
|
||||
await coordinator_info.async_config_entry_first_refresh()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from lunatone_rest_api_client import DALIBroadcast
|
||||
@@ -9,9 +10,6 @@ from lunatone_rest_api_client.models import LineStatus
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_RGBW_COLOR,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
brightness_supported,
|
||||
@@ -30,6 +28,7 @@ from .coordinator import (
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
STATUS_UPDATE_DELAY = 0.04
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -75,8 +74,6 @@ class LunatoneLight(
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_should_poll = False
|
||||
_attr_min_color_temp_kelvin = 1000
|
||||
_attr_max_color_temp_kelvin = 10000
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -126,13 +123,7 @@ class LunatoneLight(
|
||||
@property
|
||||
def color_mode(self) -> ColorMode:
|
||||
"""Return the color mode of the light."""
|
||||
if self._device.rgbw_color is not None:
|
||||
return ColorMode.RGBW
|
||||
if self._device.rgb_color is not None:
|
||||
return ColorMode.RGB
|
||||
if self._device.color_temperature is not None:
|
||||
return ColorMode.COLOR_TEMP
|
||||
if self._device.brightness is not None:
|
||||
if self._device is not None and self._device.brightness is not None:
|
||||
return ColorMode.BRIGHTNESS
|
||||
return ColorMode.ONOFF
|
||||
|
||||
@@ -141,32 +132,6 @@ class LunatoneLight(
|
||||
"""Return the supported color modes."""
|
||||
return {self.color_mode}
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self) -> int | None:
|
||||
"""Return the color temp of this light in kelvin."""
|
||||
return self._device.color_temperature
|
||||
|
||||
@property
|
||||
def rgb_color(self) -> tuple[int, int, int] | None:
|
||||
"""Return the RGB color of this light."""
|
||||
rgb_color = self._device.rgb_color
|
||||
return rgb_color and (
|
||||
round(rgb_color[0] * 255),
|
||||
round(rgb_color[1] * 255),
|
||||
round(rgb_color[2] * 255),
|
||||
)
|
||||
|
||||
@property
|
||||
def rgbw_color(self) -> tuple[int, int, int, int] | None:
|
||||
"""Return the RGBW color of this light."""
|
||||
rgbw_color = self._device.rgbw_color
|
||||
return rgbw_color and (
|
||||
round(rgbw_color[0] * 255),
|
||||
round(rgbw_color[1] * 255),
|
||||
round(rgbw_color[2] * 255),
|
||||
round(rgbw_color[3] * 255),
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
@@ -176,26 +141,16 @@ class LunatoneLight(
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn on."""
|
||||
if brightness_supported(self.supported_color_modes):
|
||||
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||
await self._device.fade_to_color_temperature(
|
||||
kwargs[ATTR_COLOR_TEMP_KELVIN]
|
||||
)
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
await self._device.fade_to_rgbw_color(
|
||||
tuple(color / 255 for color in kwargs[ATTR_RGB_COLOR])
|
||||
)
|
||||
if ATTR_RGBW_COLOR in kwargs:
|
||||
rgbw_color = tuple(color / 255 for color in kwargs[ATTR_RGBW_COLOR])
|
||||
await self._device.fade_to_rgbw_color(rgbw_color[:-1], rgbw_color[-1])
|
||||
if ATTR_BRIGHTNESS in kwargs or not self.is_on:
|
||||
await self._device.fade_to_brightness(
|
||||
brightness_to_value(
|
||||
self.BRIGHTNESS_SCALE,
|
||||
kwargs.get(ATTR_BRIGHTNESS, self._last_brightness),
|
||||
)
|
||||
await self._device.fade_to_brightness(
|
||||
brightness_to_value(
|
||||
self.BRIGHTNESS_SCALE,
|
||||
kwargs.get(ATTR_BRIGHTNESS, self._last_brightness),
|
||||
)
|
||||
)
|
||||
else:
|
||||
await self._device.switch_on()
|
||||
|
||||
await asyncio.sleep(STATUS_UPDATE_DELAY)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
@@ -206,6 +161,8 @@ class LunatoneLight(
|
||||
await self._device.fade_to_brightness(0)
|
||||
else:
|
||||
await self._device.switch_off()
|
||||
|
||||
await asyncio.sleep(STATUS_UPDATE_DELAY)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
|
||||
@@ -264,9 +221,13 @@ class LunatoneLineBroadcastLight(
|
||||
await self._broadcast.fade_to_brightness(
|
||||
brightness_to_value(self.BRIGHTNESS_SCALE, kwargs.get(ATTR_BRIGHTNESS, 255))
|
||||
)
|
||||
|
||||
await asyncio.sleep(STATUS_UPDATE_DELAY)
|
||||
await self._coordinator_devices.async_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the line to turn off."""
|
||||
await self._broadcast.fade_to_brightness(0)
|
||||
|
||||
await asyncio.sleep(STATUS_UPDATE_DELAY)
|
||||
await self._coordinator_devices.async_refresh()
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["lunatone-rest-api-client==0.9.0"]
|
||||
"requirements": ["lunatone-rest-api-client==0.7.0"]
|
||||
}
|
||||
|
||||
@@ -1,19 +1,79 @@
|
||||
"""The min_max component."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
|
||||
from homeassistant.components.group import (
|
||||
CONF_ENTITIES,
|
||||
CONF_GROUP_TYPE,
|
||||
CONF_HIDE_MEMBERS,
|
||||
CONF_IGNORE_NON_NUMERIC,
|
||||
DOMAIN as GROUP_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Min/Max from a config entry."""
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# Create group config from entry options
|
||||
config = dict(entry.options)
|
||||
config[CONF_ENTITIES] = config.pop(CONF_ENTITY_IDS)
|
||||
config.pop(CONF_ROUND_DIGITS)
|
||||
# Set group sensor defaults
|
||||
config[CONF_HIDE_MEMBERS] = False
|
||||
config[CONF_IGNORE_NON_NUMERIC] = False
|
||||
config[CONF_GROUP_TYPE] = SENSOR_DOMAIN
|
||||
|
||||
new_config_entry = ConfigEntry(
|
||||
data={},
|
||||
discovery_keys=MappingProxyType({}),
|
||||
domain=GROUP_DOMAIN,
|
||||
minor_version=1,
|
||||
options=config,
|
||||
source=SOURCE_USER,
|
||||
subentries_data=[],
|
||||
title=entry.title,
|
||||
unique_id=None,
|
||||
version=1,
|
||||
)
|
||||
|
||||
entity_reg = er.async_get(hass)
|
||||
if old_entity := entity_reg.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, entry.entry_id
|
||||
):
|
||||
entity_reg.async_update_entity_platform(
|
||||
old_entity, GROUP_DOMAIN, new_config_entry_id=new_config_entry.entry_id
|
||||
)
|
||||
# If entity is not existing, it has already been migrated
|
||||
# and we should not create it again
|
||||
await hass.config_entries.async_add(new_config_entry)
|
||||
|
||||
# Wait for config entry setup to finish before removing the old config entry
|
||||
async def remove_old_entry(now: datetime) -> None:
|
||||
"""Remove the old config entry after migration."""
|
||||
if entry.state == ConfigEntryState.LOADED:
|
||||
await hass.config_entries.async_remove(entry.entry_id)
|
||||
else:
|
||||
async_call_later(hass, 5, remove_old_entry)
|
||||
|
||||
async_call_later(hass, 5, remove_old_entry)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
@@ -2,77 +2,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
|
||||
from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import CONF_TYPE
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
)
|
||||
|
||||
from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN
|
||||
|
||||
_STATISTIC_MEASURES = [
|
||||
"min",
|
||||
"max",
|
||||
"mean",
|
||||
"median",
|
||||
"last",
|
||||
"range",
|
||||
"sum",
|
||||
]
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_IDS): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[SENSOR_DOMAIN, NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN],
|
||||
multiple=True,
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_TYPE): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=_STATISTIC_MEASURES, translation_key=CONF_TYPE
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0, max=6, mode=selector.NumberSelectorMode.BOX
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
class MinMaxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for min_max integration."""
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("name"): selector.TextSelector(),
|
||||
}
|
||||
).extend(OPTIONS_SCHEMA.schema)
|
||||
VERSION = 1
|
||||
|
||||
CONFIG_FLOW = {
|
||||
"user": SchemaFlowFormStep(CONFIG_SCHEMA),
|
||||
}
|
||||
|
||||
OPTIONS_FLOW = {
|
||||
"init": SchemaFlowFormStep(OPTIONS_SCHEMA),
|
||||
}
|
||||
|
||||
|
||||
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow for Min/Max."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
return cast(str, options["name"]) if "name" in options else ""
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the user step."""
|
||||
return self.async_abort(reason="migrated_to_groups")
|
||||
|
||||
@@ -9,17 +9,18 @@ from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.group import CONF_ENTITIES
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
CONF_TYPE,
|
||||
CONF_UNIQUE_ID,
|
||||
STATE_UNAVAILABLE,
|
||||
@@ -27,15 +28,14 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity import get_device_class
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.reload import async_setup_reload_service
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
from homeassistant.util import ulid as ulid_util, yaml as yaml_util
|
||||
|
||||
from . import PLATFORMS
|
||||
from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN
|
||||
@@ -53,6 +53,7 @@ ATTR_LAST_ENTITY_ID = "last_entity_id"
|
||||
ATTR_RANGE = "range"
|
||||
ATTR_SUM = "sum"
|
||||
|
||||
|
||||
ICON = "mdi:calculator"
|
||||
|
||||
SENSOR_TYPES = {
|
||||
@@ -79,29 +80,27 @@ PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize min/max/mean config entry."""
|
||||
registry = er.async_get(hass)
|
||||
entity_ids = er.async_validate_entity_ids(
|
||||
registry, config_entry.options[CONF_ENTITY_IDS]
|
||||
)
|
||||
sensor_type = config_entry.options[CONF_TYPE]
|
||||
round_digits = int(config_entry.options[CONF_ROUND_DIGITS])
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
MinMaxSensor(
|
||||
entity_ids,
|
||||
config_entry.title,
|
||||
sensor_type,
|
||||
round_digits,
|
||||
config_entry.entry_id,
|
||||
)
|
||||
]
|
||||
async def yaml_deprecation_notice(hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Raise repair issue for YAML configuration deprecation."""
|
||||
platform_config = config.copy()
|
||||
platform_config[CONF_ENTITIES] = platform_config.pop(CONF_ENTITY_IDS)
|
||||
platform_config.pop(CONF_ROUND_DIGITS)
|
||||
platform_config.pop(CONF_PLATFORM)
|
||||
if CONF_NAME not in platform_config:
|
||||
platform_config[CONF_NAME] = f"{platform_config[CONF_TYPE]} sensor".capitalize()
|
||||
yaml_config = yaml_util.dump(platform_config)
|
||||
yaml_config = yaml_config.replace("\n", "\n ")
|
||||
yaml_config = "```yaml\nsensor:\n - platform: group\n " + yaml_config + "\n```"
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
ulid_util.ulid(),
|
||||
breaks_in_ha_version="2026.12.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
learn_more_url="https://www.home-assistant.io/integrations/group/",
|
||||
translation_key="yaml_deprecated",
|
||||
translation_placeholders={"yaml_config": yaml_config},
|
||||
)
|
||||
|
||||
|
||||
@@ -119,6 +118,7 @@ async def async_setup_platform(
|
||||
unique_id = config.get(CONF_UNIQUE_ID)
|
||||
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
await yaml_deprecation_notice(hass, config)
|
||||
|
||||
async_add_entities(
|
||||
[MinMaxSensor(entity_ids, name, sensor_type, round_digits, unique_id)]
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"migrated_to_groups": "The min/max integration has been migrated to use group sensors. Please use the group integration instead."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
@@ -16,6 +19,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"yaml_deprecated": {
|
||||
"description": "The min/max integration has been migrated to use group sensor.\n\nReplace your Min/MaxYAML configuration with this converted configuration:\n{yaml_config}\n\nTo use the new configuration and replace your sensor with a group sensor, restart your Home Assistant.\n\nGroup sensors have more configuration possibilities, refer to the documentation by clicking on learn more.",
|
||||
"title": "Min/Max YAML configuration is deprecated"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pybotvac"],
|
||||
"requirements": ["pybotvac==0.0.29"]
|
||||
"requirements": ["pybotvac==0.0.28"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ from sharkiq import (
|
||||
)
|
||||
|
||||
from homeassistant import exceptions
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -27,7 +28,7 @@ from .const import (
|
||||
SHARKIQ_REGION_DEFAULT,
|
||||
SHARKIQ_REGION_EUROPE,
|
||||
)
|
||||
from .coordinator import SharkIqConfigEntry, SharkIqUpdateCoordinator
|
||||
from .coordinator import SharkIqUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
@@ -59,9 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: SharkIqConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Initialize the sharkiq platform via config entry."""
|
||||
if CONF_REGION not in config_entry.data:
|
||||
hass.config_entries.async_update_entry(
|
||||
@@ -94,7 +93,8 @@ async def async_setup_entry(
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
config_entry.runtime_data = coordinator
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][config_entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
@@ -116,15 +116,15 @@ async def async_update_options(hass: HomeAssistant, config_entry):
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: SharkIqConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
if unload_ok:
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
with suppress(SharkIqAuthError):
|
||||
await async_disconnect_or_timeout(coordinator=config_entry.runtime_data)
|
||||
await async_disconnect_or_timeout(coordinator=domain_data)
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -20,18 +20,16 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
from .const import API_TIMEOUT, DOMAIN, LOGGER, UPDATE_INTERVAL
|
||||
|
||||
type SharkIqConfigEntry = ConfigEntry[SharkIqUpdateCoordinator]
|
||||
|
||||
|
||||
class SharkIqUpdateCoordinator(DataUpdateCoordinator[bool]):
|
||||
"""Define a wrapper class to update Shark IQ data."""
|
||||
|
||||
config_entry: SharkIqConfigEntry
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: SharkIqConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
ayla_api: AylaApi,
|
||||
shark_vacs: list[SharkIqVacuum],
|
||||
) -> None:
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.vacuum import (
|
||||
VacuumActivity,
|
||||
VacuumEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -19,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTR_ROOMS, DOMAIN, LOGGER, SHARK
|
||||
from .coordinator import SharkIqConfigEntry, SharkIqUpdateCoordinator
|
||||
from .coordinator import SharkIqUpdateCoordinator
|
||||
|
||||
OPERATING_STATE_MAP = {
|
||||
OperatingModes.PAUSE: VacuumActivity.PAUSED,
|
||||
@@ -45,11 +46,11 @@ ATTR_RECHARGE_RESUME = "recharge_and_resume"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: SharkIqConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Shark IQ vacuum cleaner."""
|
||||
coordinator = config_entry.runtime_data
|
||||
coordinator: SharkIqUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
devices: Iterable[SharkIqVacuum] = coordinator.shark_vacs.values()
|
||||
device_names = [d.name for d in devices]
|
||||
LOGGER.debug(
|
||||
|
||||
@@ -5,24 +5,20 @@ import logging
|
||||
from smart_meter_texas import Account
|
||||
from smart_meter_texas.exceptions import SmartMeterTexasAuthError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .coordinator import (
|
||||
SmartMeterTexasConfigEntry,
|
||||
SmartMeterTexasCoordinator,
|
||||
SmartMeterTexasData,
|
||||
)
|
||||
from .const import DATA_COORDINATOR, DATA_SMART_METER, DOMAIN
|
||||
from .coordinator import SmartMeterTexasCoordinator, SmartMeterTexasData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: SmartMeterTexasConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Smart Meter Texas from a config entry."""
|
||||
|
||||
username = entry.data[CONF_USERNAME]
|
||||
@@ -47,7 +43,11 @@ async def async_setup_entry(
|
||||
# too long to update.
|
||||
coordinator = SmartMeterTexasCoordinator(hass, entry, smart_meter_texas_data)
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_COORDINATOR: coordinator,
|
||||
DATA_SMART_METER: smart_meter_texas_data,
|
||||
}
|
||||
|
||||
entry.async_create_background_task(
|
||||
hass, coordinator.async_refresh(), "smart_meter_texas-coordinator-refresh"
|
||||
@@ -58,8 +58,10 @@ async def async_setup_entry(
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: SmartMeterTexasConfigEntry
|
||||
) -> 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)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -5,6 +5,9 @@ from datetime import timedelta
|
||||
SCAN_INTERVAL = timedelta(hours=1)
|
||||
DEBOUNCE_COOLDOWN = 1800 # Seconds
|
||||
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
DATA_SMART_METER = "smart_meter_data"
|
||||
|
||||
DOMAIN = "smart_meter_texas"
|
||||
|
||||
METER_NUMBER = "meter_number"
|
||||
|
||||
@@ -52,18 +52,15 @@ class SmartMeterTexasData:
|
||||
return self.meters
|
||||
|
||||
|
||||
type SmartMeterTexasConfigEntry = ConfigEntry[SmartMeterTexasCoordinator]
|
||||
|
||||
|
||||
class SmartMeterTexasCoordinator(DataUpdateCoordinator[None]):
|
||||
class SmartMeterTexasCoordinator(DataUpdateCoordinator[SmartMeterTexasData]):
|
||||
"""Class to manage fetching Smart Meter Texas data."""
|
||||
|
||||
config_entry: SmartMeterTexasConfigEntry
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: SmartMeterTexasConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
smart_meter_texas_data: SmartMeterTexasData,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
@@ -77,9 +74,10 @@ class SmartMeterTexasCoordinator(DataUpdateCoordinator[None]):
|
||||
hass, _LOGGER, cooldown=DEBOUNCE_COOLDOWN, immediate=True
|
||||
),
|
||||
)
|
||||
self.smart_meter_texas_data = smart_meter_texas_data
|
||||
self._smart_meter_texas_data = smart_meter_texas_data
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
async def _async_update_data(self) -> SmartMeterTexasData:
|
||||
"""Fetch latest data."""
|
||||
_LOGGER.debug("Fetching latest data")
|
||||
await self.smart_meter_texas_data.read_meters()
|
||||
await self._smart_meter_texas_data.read_meters()
|
||||
return self._smart_meter_texas_data
|
||||
|
||||
@@ -9,24 +9,32 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ELECTRIC_METER, ESIID, METER_NUMBER
|
||||
from .coordinator import SmartMeterTexasConfigEntry, SmartMeterTexasCoordinator
|
||||
from .const import (
|
||||
DATA_COORDINATOR,
|
||||
DATA_SMART_METER,
|
||||
DOMAIN,
|
||||
ELECTRIC_METER,
|
||||
ESIID,
|
||||
METER_NUMBER,
|
||||
)
|
||||
from .coordinator import SmartMeterTexasCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: SmartMeterTexasConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Smart Meter Texas sensors."""
|
||||
coordinator = config_entry.runtime_data
|
||||
meters = coordinator.smart_meter_texas_data.meters
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||
meters = hass.data[DOMAIN][config_entry.entry_id][DATA_SMART_METER].meters
|
||||
|
||||
async_add_entities(
|
||||
[SmartMeterTexasSensor(meter, coordinator) for meter in meters], False
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
},
|
||||
"infrared": {
|
||||
"infrared_emitter": {
|
||||
"name": "IR emitter"
|
||||
"name": "IR Emitter"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
@@ -164,9 +164,6 @@
|
||||
},
|
||||
"firmware_update_failed": {
|
||||
"message": "Firmware update failed for {device_name}."
|
||||
},
|
||||
"send_ir_code_failed": {
|
||||
"message": "Failed to send IR code: {error}."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Snapcast Integration."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
@@ -7,7 +8,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import SnapcastConfigEntry, SnapcastUpdateCoordinator
|
||||
from .coordinator import SnapcastUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
@@ -19,7 +20,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SnapcastConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Snapcast from a config entry."""
|
||||
coordinator = SnapcastUpdateCoordinator(hass, entry)
|
||||
|
||||
@@ -31,15 +32,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: SnapcastConfigEntry) ->
|
||||
f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}"
|
||||
) from ex
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SnapcastConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
snapcast_data = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
# disconnect from server
|
||||
await entry.runtime_data.disconnect()
|
||||
await snapcast_data.disconnect()
|
||||
return unload_ok
|
||||
|
||||
@@ -13,15 +13,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type SnapcastConfigEntry = ConfigEntry[SnapcastUpdateCoordinator]
|
||||
|
||||
|
||||
class SnapcastUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Data update coordinator for pushed data from Snapcast server."""
|
||||
|
||||
config_entry: SnapcastConfigEntry
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: SnapcastConfigEntry) -> None:
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize coordinator."""
|
||||
host = config_entry.data[CONF_HOST]
|
||||
port = config_entry.data[CONF_PORT]
|
||||
|
||||
@@ -17,13 +17,14 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerState,
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CLIENT_PREFIX, CLIENT_SUFFIX, DOMAIN
|
||||
from .coordinator import SnapcastConfigEntry, SnapcastUpdateCoordinator
|
||||
from .coordinator import SnapcastUpdateCoordinator
|
||||
from .entity import SnapcastCoordinatorEntity
|
||||
|
||||
STREAM_STATUS = {
|
||||
@@ -37,12 +38,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: SnapcastConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the snapcast config entry."""
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
# Fetch coordinator from global data
|
||||
coordinator: SnapcastUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
_known_client_ids: set[str] = set()
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"""Component for the Somfy MyLink device supporting the Synergy API."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from somfy_mylink_synergy import SomfyMyLinkSynergy
|
||||
|
||||
@@ -11,23 +9,15 @@ from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import CONF_SYSTEM_ID, PLATFORMS
|
||||
from .const import CONF_SYSTEM_ID, DATA_SOMFY_MYLINK, DOMAIN, MYLINK_STATUS, PLATFORMS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type SomfyMyLinkConfigEntry = ConfigEntry[SomfyMyLinkRuntimeData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SomfyMyLinkRuntimeData:
|
||||
"""Runtime data for Somfy MyLink."""
|
||||
|
||||
somfy_mylink: SomfyMyLinkSynergy
|
||||
mylink_status: dict[str, Any]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SomfyMyLinkConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Somfy MyLink from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
config = entry.data
|
||||
somfy_mylink = SomfyMyLinkSynergy(
|
||||
config[CONF_SYSTEM_ID], config[CONF_HOST], config[CONF_PORT]
|
||||
@@ -52,18 +42,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: SomfyMyLinkConfigEntry)
|
||||
if "result" not in mylink_status:
|
||||
raise ConfigEntryNotReady("The Somfy MyLink device returned an empty result")
|
||||
|
||||
entry.runtime_data = SomfyMyLinkRuntimeData(
|
||||
somfy_mylink=somfy_mylink,
|
||||
mylink_status=mylink_status,
|
||||
)
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_SOMFY_MYLINK: somfy_mylink,
|
||||
MYLINK_STATUS: mylink_status,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: SomfyMyLinkConfigEntry
|
||||
) -> 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)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
@@ -10,6 +10,7 @@ from somfy_mylink_synergy import SomfyMyLinkSynergy
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigEntryState,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
@@ -21,7 +22,6 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
|
||||
from . import SomfyMyLinkConfigEntry
|
||||
from .const import (
|
||||
CONF_REVERSE,
|
||||
CONF_REVERSED_TARGET_IDS,
|
||||
@@ -30,6 +30,7 @@ from .const import (
|
||||
CONF_TARGET_NAME,
|
||||
DEFAULT_PORT,
|
||||
DOMAIN,
|
||||
MYLINK_STATUS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -118,7 +119,7 @@ class SomfyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: SomfyMyLinkConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
@@ -127,9 +128,7 @@ class SomfyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
"""Handle a option flow for somfy_mylink."""
|
||||
|
||||
config_entry: SomfyMyLinkConfigEntry
|
||||
|
||||
def __init__(self, config_entry: SomfyMyLinkConfigEntry) -> None:
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.options = deepcopy(dict(config_entry.options))
|
||||
self._target_id: str | None = None
|
||||
@@ -137,7 +136,9 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
@callback
|
||||
def _async_callback_targets(self):
|
||||
"""Return the list of targets."""
|
||||
return self.config_entry.runtime_data.mylink_status["result"]
|
||||
return self.hass.data[DOMAIN][self.config_entry.entry_id][MYLINK_STATUS][
|
||||
"result"
|
||||
]
|
||||
|
||||
@callback
|
||||
def _async_get_target_name(self, target_id) -> str:
|
||||
|
||||
@@ -10,6 +10,8 @@ CONF_TARGET_ID = "target_id"
|
||||
|
||||
DEFAULT_PORT = 44100
|
||||
|
||||
DATA_SOMFY_MYLINK = "somfy_mylink_data"
|
||||
MYLINK_STATUS = "mylink_status"
|
||||
DOMAIN = "somfy_mylink"
|
||||
|
||||
PLATFORMS = [Platform.COVER]
|
||||
|
||||
@@ -4,13 +4,19 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import SomfyMyLinkConfigEntry
|
||||
from .const import CONF_REVERSED_TARGET_IDS, DOMAIN, MANUFACTURER
|
||||
from .const import (
|
||||
CONF_REVERSED_TARGET_IDS,
|
||||
DATA_SOMFY_MYLINK,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
MYLINK_STATUS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,14 +28,15 @@ MYLINK_COVER_TYPE_TO_DEVICE_CLASS = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: SomfyMyLinkConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Discover and configure Somfy covers."""
|
||||
reversed_target_ids = config_entry.options.get(CONF_REVERSED_TARGET_IDS, {})
|
||||
|
||||
mylink_status = config_entry.runtime_data.mylink_status
|
||||
somfy_mylink = config_entry.runtime_data.somfy_mylink
|
||||
data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
mylink_status = data[MYLINK_STATUS]
|
||||
somfy_mylink = data[DATA_SOMFY_MYLINK]
|
||||
cover_list = []
|
||||
|
||||
for cover in mylink_status["result"]:
|
||||
|
||||
@@ -22,10 +22,8 @@ from .const import (
|
||||
SERVICE_UPDATE_STATE,
|
||||
)
|
||||
|
||||
type StarlineConfigEntry = ConfigEntry[StarlineAccount]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: StarlineConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up the StarLine device from a config entry."""
|
||||
account = StarlineAccount(hass, entry)
|
||||
await account.update()
|
||||
@@ -33,7 +31,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: StarlineConfigEntry) ->
|
||||
if not account.api.available:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
entry.runtime_data = account
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
hass.data[DOMAIN][entry.entry_id] = account
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in account.api.devices.values():
|
||||
@@ -92,23 +92,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: StarlineConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: StarlineConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
|
||||
config_entry.runtime_data.unload()
|
||||
account: StarlineAccount = hass.data[DOMAIN][config_entry.entry_id]
|
||||
account.unload()
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_options_updated(
|
||||
hass: HomeAssistant, config_entry: StarlineConfigEntry
|
||||
) -> None:
|
||||
async def async_options_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Triggered by config entry options updates."""
|
||||
account = config_entry.runtime_data
|
||||
account: StarlineAccount = hass.data[DOMAIN][config_entry.entry_id]
|
||||
scan_interval = config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
scan_obd_interval = config_entry.options.get(
|
||||
CONF_SCAN_OBD_INTERVAL, DEFAULT_SCAN_OBD_INTERVAL
|
||||
|
||||
@@ -7,12 +7,13 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import StarlineConfigEntry
|
||||
from .account import StarlineAccount, StarlineDevice
|
||||
from .const import DOMAIN
|
||||
from .entity import StarlineEntity
|
||||
|
||||
BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
|
||||
@@ -70,11 +71,11 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: StarlineConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the StarLine sensors."""
|
||||
account = entry.runtime_data
|
||||
account: StarlineAccount = hass.data[DOMAIN][entry.entry_id]
|
||||
entities = [
|
||||
sensor
|
||||
for device in account.api.devices.values()
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import StarlineConfigEntry
|
||||
from .account import StarlineAccount, StarlineDevice
|
||||
from .const import DOMAIN
|
||||
from .entity import StarlineEntity
|
||||
|
||||
BUTTON_TYPES: tuple[ButtonEntityDescription, ...] = (
|
||||
@@ -34,11 +35,11 @@ BUTTON_TYPES: tuple[ButtonEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: StarlineConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the StarLine button."""
|
||||
account = entry.runtime_data
|
||||
account: StarlineAccount = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
StarlineButton(account, device, description)
|
||||
for device in account.api.devices.values()
|
||||
|
||||
@@ -3,22 +3,23 @@
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.device_tracker import TrackerEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import StarlineConfigEntry
|
||||
from .account import StarlineAccount, StarlineDevice
|
||||
from .const import DOMAIN
|
||||
from .entity import StarlineEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: StarlineConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up StarLine entry."""
|
||||
account = entry.runtime_data
|
||||
account: StarlineAccount = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
StarlineDeviceTracker(account, device)
|
||||
for device in account.api.devices.values()
|
||||
|
||||
@@ -5,21 +5,22 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.lock import LockEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import StarlineConfigEntry
|
||||
from .account import StarlineAccount, StarlineDevice
|
||||
from .const import DOMAIN
|
||||
from .entity import StarlineEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: StarlineConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the StarLine lock."""
|
||||
account = entry.runtime_data
|
||||
account: StarlineAccount = hass.data[DOMAIN][entry.entry_id]
|
||||
entities = []
|
||||
for device in account.api.devices.values():
|
||||
if device.support_state:
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
@@ -22,8 +23,8 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level
|
||||
|
||||
from . import StarlineConfigEntry
|
||||
from .account import StarlineAccount, StarlineDevice
|
||||
from .const import DOMAIN
|
||||
from .entity import StarlineEntity
|
||||
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
@@ -90,11 +91,11 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: StarlineConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the StarLine sensors."""
|
||||
account = entry.runtime_data
|
||||
account: StarlineAccount = hass.data[DOMAIN][entry.entry_id]
|
||||
entities = [
|
||||
sensor
|
||||
for device in account.api.devices.values()
|
||||
|
||||
@@ -5,11 +5,12 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import StarlineConfigEntry
|
||||
from .account import StarlineAccount, StarlineDevice
|
||||
from .const import DOMAIN
|
||||
from .entity import StarlineEntity
|
||||
|
||||
SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
|
||||
@@ -34,11 +35,11 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: StarlineConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the StarLine switch."""
|
||||
account = entry.runtime_data
|
||||
account: StarlineAccount = hass.data[DOMAIN][entry.entry_id]
|
||||
entities = [
|
||||
switch
|
||||
for device in account.api.devices.values()
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
|
||||
from subarulink import Controller as SubaruAPI, InvalidCredentials, SubaruException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_COUNTRY,
|
||||
CONF_DEVICE_ID,
|
||||
@@ -18,6 +19,9 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
ENTRY_CONTROLLER,
|
||||
ENTRY_COORDINATOR,
|
||||
ENTRY_VEHICLES,
|
||||
FETCH_INTERVAL,
|
||||
MANUFACTURER,
|
||||
PLATFORMS,
|
||||
@@ -33,16 +37,12 @@ from .const import (
|
||||
VEHICLE_NAME,
|
||||
VEHICLE_VIN,
|
||||
)
|
||||
from .coordinator import (
|
||||
SubaruConfigEntry,
|
||||
SubaruDataUpdateCoordinator,
|
||||
SubaruRuntimeData,
|
||||
)
|
||||
from .coordinator import SubaruDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SubaruConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Subaru from a config entry."""
|
||||
config = entry.data
|
||||
websession = aiohttp_client.async_create_clientsession(hass)
|
||||
@@ -77,20 +77,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: SubaruConfigEntry) -> bo
|
||||
|
||||
await coordinator.async_refresh()
|
||||
|
||||
entry.runtime_data = SubaruRuntimeData(
|
||||
controller=controller,
|
||||
coordinator=coordinator,
|
||||
vehicles=vehicle_info,
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
ENTRY_CONTROLLER: controller,
|
||||
ENTRY_COORDINATOR: coordinator,
|
||||
ENTRY_VEHICLES: vehicle_info,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SubaruConfigEntry) -> 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)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
|
||||
def get_vehicle_info(controller, vin):
|
||||
|
||||
@@ -15,7 +15,12 @@ from subarulink import (
|
||||
from subarulink.const import COUNTRY_CAN, COUNTRY_USA
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_COUNTRY,
|
||||
CONF_DEVICE_ID,
|
||||
@@ -27,7 +32,6 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
|
||||
from .const import CONF_UPDATE_ENABLED, DOMAIN
|
||||
from .coordinator import SubaruConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_CONTACT_METHOD = "contact_method"
|
||||
@@ -99,7 +103,7 @@ class SubaruConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: SubaruConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler()
|
||||
|
||||
@@ -9,6 +9,11 @@ FETCH_INTERVAL = 300
|
||||
UPDATE_INTERVAL = 7200
|
||||
CONF_UPDATE_ENABLED = "update_enabled"
|
||||
|
||||
# entry fields
|
||||
ENTRY_CONTROLLER = "controller"
|
||||
ENTRY_COORDINATOR = "coordinator"
|
||||
ENTRY_VEHICLES = "vehicles"
|
||||
|
||||
# update coordinator name
|
||||
COORDINATOR_NAME = "subaru_data"
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import time
|
||||
@@ -24,27 +23,16 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type SubaruConfigEntry = ConfigEntry[SubaruRuntimeData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubaruRuntimeData:
|
||||
"""Runtime data for Subaru."""
|
||||
|
||||
controller: SubaruAPI
|
||||
coordinator: SubaruDataUpdateCoordinator
|
||||
vehicles: dict[str, dict[str, Any]]
|
||||
|
||||
|
||||
class SubaruDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Class to manage fetching Subaru data."""
|
||||
|
||||
config_entry: SubaruConfigEntry
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: SubaruConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
*,
|
||||
controller: SubaruAPI,
|
||||
vehicle_info: dict[str, dict[str, Any]],
|
||||
|
||||
@@ -7,23 +7,32 @@ from typing import Any
|
||||
from subarulink.const import LATITUDE, LONGITUDE, TIMESTAMP
|
||||
|
||||
from homeassistant.components.device_tracker import TrackerEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import get_device_info
|
||||
from .const import VEHICLE_HAS_REMOTE_SERVICE, VEHICLE_STATUS, VEHICLE_VIN
|
||||
from .coordinator import SubaruConfigEntry, SubaruDataUpdateCoordinator
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
ENTRY_COORDINATOR,
|
||||
ENTRY_VEHICLES,
|
||||
VEHICLE_HAS_REMOTE_SERVICE,
|
||||
VEHICLE_STATUS,
|
||||
VEHICLE_VIN,
|
||||
)
|
||||
from .coordinator import SubaruDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: SubaruConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Subaru device tracker by config_entry."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
vehicle_info = config_entry.runtime_data.vehicles
|
||||
entry: dict = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator: SubaruDataUpdateCoordinator = entry[ENTRY_COORDINATOR]
|
||||
vehicle_info: dict = entry[ENTRY_VEHICLES]
|
||||
async_add_entities(
|
||||
SubaruDeviceTracker(vehicle, coordinator)
|
||||
for vehicle in vehicle_info.values()
|
||||
|
||||
@@ -13,23 +13,23 @@ from subarulink.const import (
|
||||
)
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .const import VEHICLE_VIN
|
||||
from .coordinator import SubaruConfigEntry
|
||||
from .const import DOMAIN, ENTRY_CONTROLLER, ENTRY_COORDINATOR, VEHICLE_VIN
|
||||
|
||||
CONFIG_FIELDS_TO_REDACT = [CONF_USERNAME, CONF_PASSWORD, CONF_PIN, CONF_DEVICE_ID]
|
||||
DATA_FIELDS_TO_REDACT = [VEHICLE_VIN, VEHICLE_NAME, LATITUDE, LONGITUDE, ODOMETER]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: SubaruConfigEntry
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][ENTRY_COORDINATOR]
|
||||
|
||||
return {
|
||||
"config_entry": async_redact_data(config_entry.data, CONFIG_FIELDS_TO_REDACT),
|
||||
@@ -42,11 +42,12 @@ async def async_get_config_entry_diagnostics(
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, config_entry: SubaruConfigEntry, device: DeviceEntry
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
controller = config_entry.runtime_data.controller
|
||||
entry = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = entry[ENTRY_COORDINATOR]
|
||||
controller = entry[ENTRY_CONTROLLER]
|
||||
|
||||
vin = next(iter(device.identifiers))[1]
|
||||
|
||||
|
||||
@@ -6,14 +6,17 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.lock import LockEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import SERVICE_LOCK, SERVICE_UNLOCK
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import get_device_info
|
||||
from . import DOMAIN, get_device_info
|
||||
from .const import (
|
||||
ATTR_DOOR,
|
||||
ENTRY_CONTROLLER,
|
||||
ENTRY_VEHICLES,
|
||||
SERVICE_UNLOCK_SPECIFIC_DOOR,
|
||||
UNLOCK_DOOR_ALL,
|
||||
UNLOCK_VALID_DOORS,
|
||||
@@ -21,7 +24,6 @@ from .const import (
|
||||
VEHICLE_NAME,
|
||||
VEHICLE_VIN,
|
||||
)
|
||||
from .coordinator import SubaruConfigEntry
|
||||
from .remote_service import async_call_remote_service
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -29,12 +31,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: SubaruConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Subaru locks by config_entry."""
|
||||
controller = config_entry.runtime_data.controller
|
||||
vehicle_info = config_entry.runtime_data.vehicles
|
||||
entry = hass.data[DOMAIN][config_entry.entry_id]
|
||||
controller = entry[ENTRY_CONTROLLER]
|
||||
vehicle_info = entry[ENTRY_VEHICLES]
|
||||
async_add_entities(
|
||||
SubaruLock(vehicle, controller)
|
||||
for vehicle in vehicle_info.values()
|
||||
|
||||
@@ -26,12 +26,15 @@ from . import get_device_info
|
||||
from .const import (
|
||||
API_GEN_2,
|
||||
API_GEN_3,
|
||||
DOMAIN,
|
||||
ENTRY_COORDINATOR,
|
||||
ENTRY_VEHICLES,
|
||||
VEHICLE_API_GEN,
|
||||
VEHICLE_HAS_EV,
|
||||
VEHICLE_STATUS,
|
||||
VEHICLE_VIN,
|
||||
)
|
||||
from .coordinator import SubaruConfigEntry, SubaruDataUpdateCoordinator
|
||||
from .coordinator import SubaruDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -135,12 +138,13 @@ EV_SENSORS = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: SubaruConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Subaru sensors by config_entry."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
vehicle_info = config_entry.runtime_data.vehicles
|
||||
entry = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = entry[ENTRY_COORDINATOR]
|
||||
vehicle_info = entry[ENTRY_VEHICLES]
|
||||
entities = []
|
||||
await _async_migrate_entries(hass, config_entry)
|
||||
for info in vehicle_info.values():
|
||||
|
||||
@@ -96,7 +96,6 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._discovered_advs: dict[str, SwitchBotAdvertisement] = {}
|
||||
self._cloud_username: str | None = None
|
||||
self._cloud_password: str | None = None
|
||||
self._encryption_method_selected = False
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
@@ -198,13 +197,6 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
assert self._discovered_adv is not None
|
||||
description_placeholders: dict[str, str] = {}
|
||||
|
||||
if user_input is None:
|
||||
if not self._encryption_method_selected and not (
|
||||
self._cloud_username and self._cloud_password
|
||||
):
|
||||
return await self.async_step_encrypted_choose_method()
|
||||
self._encryption_method_selected = False
|
||||
|
||||
# If we have saved credentials from cloud login, try them first
|
||||
if user_input is None and self._cloud_username and self._cloud_password:
|
||||
user_input = {
|
||||
@@ -266,7 +258,6 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the SwitchBot API chose method step."""
|
||||
assert self._discovered_adv is not None
|
||||
|
||||
self._encryption_method_selected = True
|
||||
return self.async_show_menu(
|
||||
step_id="encrypted_choose_method",
|
||||
menu_options=["encrypted_auth", "encrypted_key"],
|
||||
@@ -281,12 +272,6 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the encryption key step."""
|
||||
errors: dict[str, str] = {}
|
||||
assert self._discovered_adv is not None
|
||||
|
||||
if user_input is None:
|
||||
if not self._encryption_method_selected:
|
||||
return await self.async_step_encrypted_choose_method()
|
||||
self._encryption_method_selected = False
|
||||
|
||||
if user_input is not None:
|
||||
model: SwitchbotModel = self._discovered_adv.data["modelName"]
|
||||
cls = ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS[model]
|
||||
|
||||
@@ -18,19 +18,26 @@ from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import EVENTS, RECONNECT_INTERVAL, SERVER_AVAILABLE, SERVER_UNAVAILABLE
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
EVENTS,
|
||||
RECONNECT_INTERVAL,
|
||||
SERVER_AVAILABLE,
|
||||
SERVER_UNAVAILABLE,
|
||||
)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type SyncthingConfigEntry = ConfigEntry[SyncthingClient]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SyncthingConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up syncthing from a config entry."""
|
||||
data = entry.data
|
||||
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
client = aiosyncthing.Syncthing(
|
||||
data[CONF_TOKEN],
|
||||
url=data[CONF_URL],
|
||||
@@ -47,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SyncthingConfigEntry) ->
|
||||
|
||||
syncthing = SyncthingClient(hass, client, server_id)
|
||||
syncthing.subscribe()
|
||||
entry.runtime_data = syncthing
|
||||
hass.data[DOMAIN][entry.entry_id] = syncthing
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -62,11 +69,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: SyncthingConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SyncthingConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
await entry.runtime_data.unsubscribe()
|
||||
syncthing = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
await syncthing.unsubscribe()
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any
|
||||
import aiosyncthing
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
@@ -13,7 +14,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from . import SyncthingClient, SyncthingConfigEntry
|
||||
from . import SyncthingClient
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FOLDER_PAUSED_RECEIVED,
|
||||
@@ -27,11 +28,11 @@ from .const import (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: SyncthingConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Syncthing sensors."""
|
||||
syncthing = config_entry.runtime_data
|
||||
syncthing = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
try:
|
||||
config = await syncthing.system.config()
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiotractive"],
|
||||
"requirements": ["aiotractive==1.0.2"]
|
||||
"requirements": ["aiotractive==1.0.1"]
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@ from .models import (
|
||||
USBDevice,
|
||||
)
|
||||
from .utils import (
|
||||
async_scan_serial_ports,
|
||||
scan_serial_ports, # noqa: F401
|
||||
scan_serial_ports,
|
||||
usb_device_from_path, # noqa: F401
|
||||
usb_device_from_port, # noqa: F401
|
||||
usb_device_matches_matcher,
|
||||
@@ -434,7 +433,7 @@ class USBDiscovery:
|
||||
# Only consider USB-serial ports for discovery
|
||||
usb_ports = [
|
||||
p
|
||||
for p in await async_scan_serial_ports(self.hass)
|
||||
for p in await self.hass.async_add_executor_job(scan_serial_ports)
|
||||
if isinstance(p, USBDevice)
|
||||
]
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import os
|
||||
from serial.tools.list_ports import comports
|
||||
from serial.tools.list_ports_common import ListPortInfo
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.service_info.usb import UsbServiceInfo
|
||||
from homeassistant.loader import USBMatcher
|
||||
|
||||
@@ -77,13 +76,6 @@ def scan_serial_ports() -> Sequence[USBDevice | SerialDevice]:
|
||||
return serial_ports
|
||||
|
||||
|
||||
async def async_scan_serial_ports(
|
||||
hass: HomeAssistant,
|
||||
) -> Sequence[USBDevice | SerialDevice]:
|
||||
"""Scan serial ports and return USB and other serial devices, async."""
|
||||
return await hass.async_add_executor_job(scan_serial_ports)
|
||||
|
||||
|
||||
def usb_device_from_path(device_path: str) -> USBDevice | None:
|
||||
"""Get USB device info from a device path."""
|
||||
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyvlx"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyvlx==0.2.33"]
|
||||
"requirements": ["pyvlx==0.2.32"]
|
||||
}
|
||||
|
||||
@@ -26,11 +26,7 @@ from homeassistant.components.homeassistant_hardware.firmware_config_flow import
|
||||
ZigbeeFlowStrategy,
|
||||
)
|
||||
from homeassistant.components.homeassistant_yellow import hardware as yellow_hardware
|
||||
from homeassistant.components.usb import (
|
||||
SerialDevice,
|
||||
USBDevice,
|
||||
async_scan_serial_ports,
|
||||
)
|
||||
from homeassistant.components.usb import SerialDevice, USBDevice, scan_serial_ports
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_ZEROCONF,
|
||||
@@ -159,7 +155,7 @@ def _format_serial_port_choice(
|
||||
async def list_serial_ports(hass: HomeAssistant) -> list[USBDevice | SerialDevice]:
|
||||
"""List all serial ports, including the Yellow radio and the multi-PAN addon."""
|
||||
ports: list[USBDevice | SerialDevice] = []
|
||||
ports.extend(await async_scan_serial_ports(hass))
|
||||
ports.extend(await hass.async_add_executor_job(scan_serial_ports))
|
||||
|
||||
# Add useful info to the Yellow's serial port selection screen
|
||||
try:
|
||||
|
||||
@@ -2,69 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonManager
|
||||
from homeassistant.components.hassio import AddonManager
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.redact import async_redact_data
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
|
||||
from .const import (
|
||||
ADDON_SLUG,
|
||||
CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY,
|
||||
CONF_ADDON_LR_S2_AUTHENTICATED_KEY,
|
||||
CONF_ADDON_NETWORK_KEY,
|
||||
CONF_ADDON_S0_LEGACY_KEY,
|
||||
CONF_ADDON_S2_ACCESS_CONTROL_KEY,
|
||||
CONF_ADDON_S2_AUTHENTICATED_KEY,
|
||||
CONF_ADDON_S2_UNAUTHENTICATED_KEY,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
from .const import ADDON_SLUG, DOMAIN, LOGGER
|
||||
|
||||
DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager"
|
||||
REDACT_ADDON_OPTION_KEYS = {
|
||||
CONF_ADDON_S0_LEGACY_KEY,
|
||||
CONF_ADDON_S2_ACCESS_CONTROL_KEY,
|
||||
CONF_ADDON_S2_AUTHENTICATED_KEY,
|
||||
CONF_ADDON_S2_UNAUTHENTICATED_KEY,
|
||||
CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY,
|
||||
CONF_ADDON_LR_S2_AUTHENTICATED_KEY,
|
||||
CONF_ADDON_NETWORK_KEY,
|
||||
}
|
||||
|
||||
|
||||
def _redact_sensitive_option_values(message: str, config: dict[str, Any]) -> str:
|
||||
"""Redact sensitive add-on option values in an error string."""
|
||||
redacted_config = async_redact_data(config, REDACT_ADDON_OPTION_KEYS)
|
||||
|
||||
for key in REDACT_ADDON_OPTION_KEYS:
|
||||
option_value = config.get(key)
|
||||
if not isinstance(option_value, str) or not option_value:
|
||||
continue
|
||||
redacted_value = redacted_config.get(key)
|
||||
if not isinstance(redacted_value, str):
|
||||
continue
|
||||
message = message.replace(option_value, redacted_value)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
class ZwaveAddonManager(AddonManager):
|
||||
"""Addon manager for Z-Wave JS with redacted option errors."""
|
||||
|
||||
async def async_set_addon_options(self, config: dict[str, Any]) -> None:
|
||||
"""Set add-on options."""
|
||||
try:
|
||||
await super().async_set_addon_options(config)
|
||||
except AddonError as err:
|
||||
raise AddonError(
|
||||
_redact_sensitive_option_values(str(err), config)
|
||||
) from None
|
||||
|
||||
|
||||
@singleton(DATA_ADDON_MANAGER)
|
||||
@callback
|
||||
def get_addon_manager(hass: HomeAssistant) -> AddonManager:
|
||||
"""Get the add-on manager."""
|
||||
return ZwaveAddonManager(hass, LOGGER, "Z-Wave JS", ADDON_SLUG)
|
||||
return AddonManager(hass, LOGGER, "Z-Wave JS", ADDON_SLUG)
|
||||
|
||||
@@ -1156,6 +1156,72 @@ def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]:
|
||||
return list(found.values())
|
||||
|
||||
|
||||
def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]:
|
||||
"""Get entity ids for entities tied to an integration/domain.
|
||||
|
||||
Provide entry_name as domain to get all entity id's for a integration/domain
|
||||
or provide a config entry title for filtering between instances of the same
|
||||
integration.
|
||||
"""
|
||||
|
||||
# Don't allow searching for config entries without title
|
||||
if not entry_name:
|
||||
return []
|
||||
|
||||
# first try if there are any config entries with a matching title
|
||||
entities: list[str] = []
|
||||
ent_reg = er.async_get(hass)
|
||||
for entry in hass.config_entries.async_entries():
|
||||
if entry.title != entry_name:
|
||||
continue
|
||||
entries = er.async_entries_for_config_entry(ent_reg, entry.entry_id)
|
||||
entities.extend(entry.entity_id for entry in entries)
|
||||
if entities:
|
||||
return entities
|
||||
|
||||
# fallback to just returning all entities for a domain
|
||||
from homeassistant.helpers.entity import entity_sources # noqa: PLC0415
|
||||
|
||||
return [
|
||||
entity_id
|
||||
for entity_id, info in entity_sources(hass).items()
|
||||
if info["domain"] == entry_name
|
||||
]
|
||||
|
||||
|
||||
def config_entry_id(hass: HomeAssistant, entity_id: str) -> str | None:
|
||||
"""Get an config entry ID from an entity ID."""
|
||||
entity_reg = er.async_get(hass)
|
||||
if entity := entity_reg.async_get(entity_id):
|
||||
return entity.config_entry_id
|
||||
return None
|
||||
|
||||
|
||||
def config_entry_attr(
|
||||
hass: HomeAssistant, config_entry_id_: str, attr_name: str
|
||||
) -> Any:
|
||||
"""Get config entry specific attribute."""
|
||||
if not isinstance(config_entry_id_, str):
|
||||
raise TemplateError("Must provide a config entry ID")
|
||||
|
||||
if attr_name not in (
|
||||
"domain",
|
||||
"title",
|
||||
"state",
|
||||
"source",
|
||||
"disabled_by",
|
||||
"pref_disable_polling",
|
||||
):
|
||||
raise TemplateError("Invalid config entry attribute")
|
||||
|
||||
config_entry = hass.config_entries.async_get_entry(config_entry_id_)
|
||||
|
||||
if config_entry is None:
|
||||
return None
|
||||
|
||||
return getattr(config_entry, attr_name)
|
||||
|
||||
|
||||
def closest(hass: HomeAssistant, *args: Any) -> State | None:
|
||||
"""Find closest entity.
|
||||
|
||||
@@ -1474,9 +1540,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
self.add_extension(
|
||||
"homeassistant.helpers.template.extensions.CollectionExtension"
|
||||
)
|
||||
self.add_extension(
|
||||
"homeassistant.helpers.template.extensions.ConfigEntryExtension"
|
||||
)
|
||||
self.add_extension("homeassistant.helpers.template.extensions.CryptoExtension")
|
||||
self.add_extension(
|
||||
"homeassistant.helpers.template.extensions.DateTimeExtension"
|
||||
@@ -1524,6 +1587,19 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
|
||||
return jinja_context(wrapper)
|
||||
|
||||
# Integration extensions
|
||||
|
||||
self.globals["integration_entities"] = hassfunction(integration_entities)
|
||||
self.filters["integration_entities"] = self.globals["integration_entities"]
|
||||
|
||||
# Config entry extensions
|
||||
|
||||
self.globals["config_entry_attr"] = hassfunction(config_entry_attr)
|
||||
self.filters["config_entry_attr"] = self.globals["config_entry_attr"]
|
||||
|
||||
self.globals["config_entry_id"] = hassfunction(config_entry_id)
|
||||
self.filters["config_entry_id"] = self.globals["config_entry_id"]
|
||||
|
||||
if limited:
|
||||
|
||||
def unsupported(name: str) -> Callable[[], NoReturn]:
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from .areas import AreaExtension
|
||||
from .base64 import Base64Extension
|
||||
from .collection import CollectionExtension
|
||||
from .config_entries import ConfigEntryExtension
|
||||
from .crypto import CryptoExtension
|
||||
from .datetime import DateTimeExtension
|
||||
from .devices import DeviceExtension
|
||||
@@ -22,7 +21,6 @@ __all__ = [
|
||||
"AreaExtension",
|
||||
"Base64Extension",
|
||||
"CollectionExtension",
|
||||
"ConfigEntryExtension",
|
||||
"CryptoExtension",
|
||||
"DateTimeExtension",
|
||||
"DeviceExtension",
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
"""Config entry functions for Home Assistant templates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .base import BaseTemplateExtension, TemplateFunction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.helpers.template import TemplateEnvironment
|
||||
|
||||
|
||||
class ConfigEntryExtension(BaseTemplateExtension):
|
||||
"""Jinja2 extension for config entry functions."""
|
||||
|
||||
def __init__(self, environment: TemplateEnvironment) -> None:
|
||||
"""Initialize the config entry extension."""
|
||||
super().__init__(
|
||||
environment,
|
||||
functions=[
|
||||
TemplateFunction(
|
||||
"integration_entities",
|
||||
self.integration_entities,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
requires_hass=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"config_entry_id",
|
||||
self.config_entry_id,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
requires_hass=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"config_entry_attr",
|
||||
self.config_entry_attr,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
requires_hass=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def integration_entities(self, entry_name: str) -> Iterable[str]:
|
||||
"""Get entity IDs for entities tied to an integration/domain.
|
||||
|
||||
Provide entry_name as domain to get all entity IDs for an integration/domain
|
||||
or provide a config entry title for filtering between instances of the same
|
||||
integration.
|
||||
"""
|
||||
# Don't allow searching for config entries without title
|
||||
if not entry_name:
|
||||
return []
|
||||
|
||||
hass = self.hass
|
||||
|
||||
# first try if there are any config entries with a matching title
|
||||
entities: list[str] = []
|
||||
ent_reg = er.async_get(hass)
|
||||
for entry in hass.config_entries.async_entries():
|
||||
if entry.title != entry_name:
|
||||
continue
|
||||
entries = er.async_entries_for_config_entry(ent_reg, entry.entry_id)
|
||||
entities.extend(entry.entity_id for entry in entries)
|
||||
if entities:
|
||||
return entities
|
||||
|
||||
# fallback to just returning all entities for a domain
|
||||
from homeassistant.helpers.entity import entity_sources # noqa: PLC0415
|
||||
|
||||
return [
|
||||
entity_id
|
||||
for entity_id, info in entity_sources(hass).items()
|
||||
if info["domain"] == entry_name
|
||||
]
|
||||
|
||||
def config_entry_id(self, entity_id: str) -> str | None:
|
||||
"""Get a config entry ID from an entity ID."""
|
||||
entity_reg = er.async_get(self.hass)
|
||||
if entity := entity_reg.async_get(entity_id):
|
||||
return entity.config_entry_id
|
||||
return None
|
||||
|
||||
def config_entry_attr(self, config_entry_id: str, attr_name: str) -> Any:
|
||||
"""Get config entry specific attribute."""
|
||||
if not isinstance(config_entry_id, str):
|
||||
raise TemplateError("Must provide a config entry ID")
|
||||
|
||||
if attr_name not in (
|
||||
"domain",
|
||||
"title",
|
||||
"state",
|
||||
"source",
|
||||
"disabled_by",
|
||||
"pref_disable_polling",
|
||||
):
|
||||
raise TemplateError("Invalid config entry attribute")
|
||||
|
||||
config_entry = self.hass.config_entries.async_get_entry(config_entry_id)
|
||||
|
||||
if config_entry is None:
|
||||
return None
|
||||
|
||||
return getattr(config_entry, attr_name)
|
||||
@@ -473,7 +473,7 @@ filterwarnings = [
|
||||
"ignore:.*a temporary mapping .* from `updated_parsed` to `published_parsed` if `updated_parsed` doesn't exist:DeprecationWarning:feedparser.util",
|
||||
|
||||
# -- design choice 3rd party
|
||||
# https://github.com/gwww/elkm1/blob/2.2.13/elkm1_lib/util.py#L8-L19
|
||||
# https://github.com/gwww/elkm1/blob/2.2.11/elkm1_lib/util.py#L8-L19
|
||||
"ignore:ssl.TLSVersion.TLSv1 is deprecated:DeprecationWarning:elkm1_lib.util",
|
||||
# https://github.com/bachya/regenmaschine/blob/2024.03.0/regenmaschine/client.py#L52
|
||||
"ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client",
|
||||
@@ -489,13 +489,8 @@ filterwarnings = [
|
||||
# -- fixed, waiting for release / update
|
||||
# https://github.com/httplib2/httplib2/pull/226 - >=0.21.0
|
||||
"ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:httplib2",
|
||||
# https://github.com/httplib2/httplib2/pull/253 - >=0.31.1
|
||||
"ignore:'(addParseAction|delimitedList|leaveWhitespace|setName|setParseAction)' deprecated:DeprecationWarning:httplib2.auth",
|
||||
# https://github.com/lawtancool/pyControl4/pull/47 - >=1.6.0
|
||||
"ignore:with timeout\\(\\) is deprecated, use async with timeout\\(\\) instead:DeprecationWarning:pyControl4.account",
|
||||
# https://pypi.org/project/pyqwikswitch/ - >=1.0
|
||||
"ignore:client.loop property is deprecated:DeprecationWarning:pyqwikswitch.async_",
|
||||
"ignore:with timeout\\(\\) is deprecated:DeprecationWarning:pyqwikswitch.async_",
|
||||
# https://github.com/BerriAI/litellm/pull/17657 - >1.80.9
|
||||
"ignore:Support for class-based `config` is deprecated, use ConfigDict instead:DeprecationWarning:litellm.types.llms.anthropic",
|
||||
# https://github.com/allenporter/python-google-nest-sdm/pull/1229 - >9.1.2
|
||||
"ignore:datetime.*utcnow\\(\\) is deprecated:DeprecationWarning:google_nest_sdm.device",
|
||||
# https://github.com/rytilahti/python-miio/pull/1809 - >=0.6.0.dev0
|
||||
@@ -503,9 +498,7 @@ filterwarnings = [
|
||||
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.miioprotocol",
|
||||
# https://github.com/rytilahti/python-miio/pull/1993 - >0.6.0.dev0
|
||||
"ignore:functools.partial will be a method descriptor in future Python versions; wrap it in enum.member\\(\\) if you want to preserve the old behavior:FutureWarning:miio.miot_device",
|
||||
# https://github.com/pyusb/pyusb/pull/545 - >1.3.1
|
||||
"ignore:Due to '_pack_', the '.*' Structure will use memory layout compatible with MSVC:DeprecationWarning:usb.backend.libusb0",
|
||||
# https://github.com/xchwarze/samsung-tv-ws-api/pull/151 - >=3.0.0 - 2024-12-06 # wrong stacklevel in aiohttp
|
||||
# https://github.com/xchwarze/samsung-tv-ws-api/pull/151 - >2.7.2 - 2024-12-06 # wrong stacklevel in aiohttp
|
||||
"ignore:verify_ssl is deprecated, use ssl=False instead:DeprecationWarning:aiohttp.client",
|
||||
|
||||
# -- other
|
||||
@@ -529,8 +522,8 @@ filterwarnings = [
|
||||
# https://pypi.org/project/motionblindsble/ - v0.1.3 - 2024-11-12
|
||||
# https://github.com/LennP/motionblindsble/blob/0.1.3/motionblindsble/device.py#L390
|
||||
"ignore:Passing additional arguments for BLEDevice is deprecated and has no effect:DeprecationWarning:motionblindsble.device",
|
||||
# https://pypi.org/project/pyeconet/ - v0.2.2 - 2026-03-05
|
||||
# https://github.com/w1ll1am23/pyeconet/blob/v0.2.2/src/pyeconet/api.py#L39
|
||||
# https://pypi.org/project/pyeconet/ - v0.2.0 - 2025-10-05
|
||||
# https://github.com/w1ll1am23/pyeconet/blob/v0.2.0/src/pyeconet/api.py#L39
|
||||
"ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:pyeconet.api",
|
||||
# https://github.com/thecynic/pylutron - v0.2.18 - 2025-04-15
|
||||
"ignore:setDaemon\\(\\) is deprecated, set the daemon attribute instead:DeprecationWarning:pylutron",
|
||||
@@ -541,17 +534,15 @@ filterwarnings = [
|
||||
# https://github.com/lextudio/pysnmp/blob/v7.1.21/pysnmp/smi/compiler.py#L23-L31 - v7.1.21 - 2025-06-19
|
||||
"ignore:smiV1Relaxed is deprecated. Please use smi_v1_relaxed instead:DeprecationWarning:pysnmp.smi.compiler",
|
||||
"ignore:getReadersFromUrls is deprecated. Please use get_readers_from_urls instead:DeprecationWarning:pysnmp.smi.compiler",
|
||||
# https://github.com/frenck/python-radios/blob/v0.3.2/src/radios/radio_browser.py#L76 - v0.3.2 - 2024-10-26
|
||||
"ignore:query\\(\\) is deprecated, use query_dns\\(\\) instead:DeprecationWarning:radios.radio_browser",
|
||||
# https://github.com/python-telegram-bot/python-telegram-bot/blob/v22.7/src/telegram/error.py#L243 - 22.7 - 2026-03-16
|
||||
"ignore:Deprecated since version v22.2.*attribute `retry_after` will be of type `datetime.timedelta`:DeprecationWarning:telegram.error",
|
||||
# https://github.com/Python-roborock/python-roborock/issues/305 - 2.19.0 - 2025-05-13
|
||||
"ignore:Callback API version 1 is deprecated, update to latest version:DeprecationWarning:roborock.cloud_api",
|
||||
# https://github.com/briis/pyweatherflowudp/blob/v1.4.5/pyweatherflowudp/const.py#L20 - v1.4.5 - 2023-10-10
|
||||
"ignore:This function will be removed in future versions of pint:DeprecationWarning:pyweatherflowudp.const",
|
||||
# - SyntaxWarnings - invalid escape sequence
|
||||
# - SyntaxWarnings
|
||||
# https://pypi.org/project/aprslib/ - v0.7.2 - 2022-07-10
|
||||
"ignore:.*invalid escape sequence:SyntaxWarning:.*aprslib.parsing.common",
|
||||
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aprslib.parsing.common",
|
||||
# https://pypi.org/project/panasonic-viera/ - v0.4.4 - 2025-11-25
|
||||
# https://pypi.org/project/panasonic-viera/ - v0.4.2 - 2024-04-24
|
||||
# https://github.com/florianholzapfel/panasonic-viera/blob/0.4.4/panasonic_viera/remote_control.py#L665
|
||||
"ignore:.*invalid escape sequence:SyntaxWarning:.*panasonic_viera",
|
||||
# https://pypi.org/project/pyblackbird/ - v0.6 - 2023-03-15
|
||||
@@ -564,6 +555,11 @@ filterwarnings = [
|
||||
"ignore:.*invalid escape sequence:SyntaxWarning:.*sanix",
|
||||
# https://pypi.org/project/sleekxmppfs/ - v1.4.1 - 2022-08-18
|
||||
"ignore:.*invalid escape sequence:SyntaxWarning:.*sleekxmppfs.thirdparty.mini_dateutil", # codespell:ignore thirdparty
|
||||
# - pkg_resources
|
||||
# https://pypi.org/project/aiomusiccast/ - v0.14.8 - 2023-03-20
|
||||
"ignore:pkg_resources is deprecated as an API:UserWarning:aiomusiccast",
|
||||
# https://pypi.org/project/pybotvac/ - v0.0.28 - 2025-06-11
|
||||
"ignore:pkg_resources is deprecated as an API:UserWarning:pybotvac.version",
|
||||
# - SyntaxWarning - is with literal
|
||||
# https://github.com/majuss/lupupy/pull/15 - >0.3.2
|
||||
"ignore:\"is.*\" with '.*' literal:SyntaxWarning:.*lupupy.devices.alarm",
|
||||
@@ -576,6 +572,8 @@ filterwarnings = [
|
||||
"ignore:'return' in a 'finally' block:SyntaxWarning:.*nextcord.(gateway|player)",
|
||||
# https://pypi.org/project/sleekxmppfs/ - v1.4.1 - 2022-08-18
|
||||
"ignore:'return' in a 'finally' block:SyntaxWarning:.*sleekxmppfs.(roster.single|xmlstream.xmlstream)",
|
||||
# https://github.com/cereal2nd/velbus-aio/pull/153 - >2025.11.0
|
||||
"ignore:'return' in a 'finally' block:SyntaxWarning:.*velbusaio.vlp_reader",
|
||||
|
||||
# -- New in Python 3.13
|
||||
# https://github.com/youknowone/python-deadlib - Backports for aifc, telnetlib
|
||||
@@ -591,6 +589,8 @@ filterwarnings = [
|
||||
"ignore:Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater:UserWarning:elevenlabs.core.pydantic_utilities",
|
||||
# https://github.com/Lektrico/lektricowifi - v0.1 - 2025-05-19
|
||||
"ignore:Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater:UserWarning:lektricowifi.models",
|
||||
# https://github.com/bang-olufsen/mozart-open-api - v4.1.1.116.4 - 2025-01-22
|
||||
"ignore:Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater:UserWarning:mozart_api.api.beolink_api",
|
||||
# https://github.com/sstallion/sensorpush-api - v2.1.3 - 2025-06-10
|
||||
"ignore:Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater:UserWarning:sensorpush_api.api.api_api",
|
||||
|
||||
@@ -599,23 +599,28 @@ filterwarnings = [
|
||||
"ignore:'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16:DeprecationWarning:(backoff._decorator|backoff._async)",
|
||||
# https://github.com/albertogeniola/elmax-api - v0.0.6.3 - 2024-11-30
|
||||
"ignore:'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16:DeprecationWarning:elmax_api.http",
|
||||
# https://github.com/BerriAI/litellm - v1.80.9 - 2025-12-08
|
||||
"ignore:'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16:DeprecationWarning:litellm.litellm_core_utils.logging_utils",
|
||||
# https://github.com/nextcord/nextcord/pull/1269 - >3.1.1 - 2025-08-16
|
||||
"ignore:'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16:DeprecationWarning:nextcord.member",
|
||||
# https://github.com/SteveEasley/pykaleidescape/pull/7 - v2022.2.6 - 2022-03-07
|
||||
"ignore:'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16:DeprecationWarning:kaleidescape.dispatcher",
|
||||
# https://github.com/svinota/pyroute2
|
||||
"ignore:Due to '_pack_', the '.*' Structure will use memory layout compatible with MSVC:DeprecationWarning:pyroute2.ethtool.ioctl",
|
||||
# https://github.com/googleapis/python-genai
|
||||
"ignore:Inheritance class AiohttpClientSession from ClientSession is discouraged:DeprecationWarning:google.genai._api_client",
|
||||
"ignore:'_UnionGenericAlias' is deprecated and slated for removal in Python 3.17:DeprecationWarning:google.genai.types",
|
||||
# https://github.com/pyusb/pyusb/issues/535
|
||||
"ignore:Due to '_pack_', the '.*' Structure will use memory layout compatible with MSVC:DeprecationWarning:usb.backend.libusb0",
|
||||
|
||||
# -- Websockets 14.1
|
||||
# https://websockets.readthedocs.io/en/stable/howto/upgrade.html
|
||||
"ignore:websockets.legacy is deprecated:DeprecationWarning:websockets.legacy",
|
||||
# https://github.com/graphql-python/gql/pull/543 - >=4.0.0b0
|
||||
"ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning:gql.transport.websockets_base",
|
||||
|
||||
# -- unmaintained projects, last release about 2+ years
|
||||
# https://pypi.org/project/aiomodernforms/ - v0.1.8 - 2021-06-27
|
||||
"ignore:with timeout\\(\\) is deprecated:DeprecationWarning:aiomodernforms.modernforms",
|
||||
# https://pypi.org/project/colorthief/ - v0.2.1 - 2017-02-09
|
||||
"ignore:Image.Image.getdata is deprecated and will be removed in Pillow 14.* Use get_flattened_data instead:DeprecationWarning:colorthief",
|
||||
# https://pypi.org/project/directv/ - v0.4.0 - 2020-09-12
|
||||
"ignore:with timeout\\(\\) is deprecated:DeprecationWarning:directv.directv",
|
||||
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:directv.models",
|
||||
@@ -641,6 +646,9 @@ filterwarnings = [
|
||||
"ignore:.*invalid escape sequence:SyntaxWarning:.*pydub.utils",
|
||||
# https://pypi.org/project/PyPasser/ - v0.0.5 - 2021-10-21
|
||||
"ignore:.*invalid escape sequence:SyntaxWarning:.*pypasser.utils",
|
||||
# https://pypi.org/project/pyqwikswitch/ - v0.94 - 2019-08-19
|
||||
"ignore:client.loop property is deprecated:DeprecationWarning:pyqwikswitch.async_",
|
||||
"ignore:with timeout\\(\\) is deprecated:DeprecationWarning:pyqwikswitch.async_",
|
||||
# https://pypi.org/project/rxv/ - v0.7.0 - 2021-10-10
|
||||
"ignore:defusedxml.cElementTree is deprecated, import from defusedxml.ElementTree instead:DeprecationWarning:rxv.ssdp",
|
||||
]
|
||||
|
||||
12
requirements_all.txt
generated
12
requirements_all.txt
generated
@@ -428,7 +428,7 @@ aiotankerkoenig==0.5.1
|
||||
aiotedee==0.3.0
|
||||
|
||||
# homeassistant.components.tractive
|
||||
aiotractive==1.0.2
|
||||
aiotractive==1.0.1
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==90
|
||||
@@ -512,7 +512,7 @@ anova-wifi==0.17.0
|
||||
anthemav==1.4.1
|
||||
|
||||
# homeassistant.components.anthropic
|
||||
anthropic==0.92.0
|
||||
anthropic==0.83.0
|
||||
|
||||
# homeassistant.components.mcp_server
|
||||
anyio==4.10.0
|
||||
@@ -702,7 +702,7 @@ bring-api==1.1.1
|
||||
broadlink==0.19.0
|
||||
|
||||
# homeassistant.components.brother
|
||||
brother==6.1.0
|
||||
brother==6.0.0
|
||||
|
||||
# homeassistant.components.brottsplatskartan
|
||||
brottsplatskartan==1.0.5
|
||||
@@ -1467,7 +1467,7 @@ loqedAPI==2.1.11
|
||||
luftdaten==0.7.4
|
||||
|
||||
# homeassistant.components.lunatone
|
||||
lunatone-rest-api-client==0.9.0
|
||||
lunatone-rest-api-client==0.7.0
|
||||
|
||||
# homeassistant.components.lupusec
|
||||
lupupy==0.3.2
|
||||
@@ -1995,7 +1995,7 @@ pyblackbird==0.6
|
||||
pyblu==2.0.6
|
||||
|
||||
# homeassistant.components.neato
|
||||
pybotvac==0.0.29
|
||||
pybotvac==0.0.28
|
||||
|
||||
# homeassistant.components.braviatv
|
||||
pybravia==0.4.1
|
||||
@@ -2742,7 +2742,7 @@ pyvesync==3.4.1
|
||||
pyvizio==0.1.61
|
||||
|
||||
# homeassistant.components.velux
|
||||
pyvlx==0.2.33
|
||||
pyvlx==0.2.32
|
||||
|
||||
# homeassistant.components.volumio
|
||||
pyvolumio==0.1.5
|
||||
|
||||
@@ -38,17 +38,17 @@ syrupy==5.0.0
|
||||
tqdm==4.67.1
|
||||
types-aiofiles==24.1.0.20250822
|
||||
types-atomicwrites==1.4.5.1
|
||||
types-croniter==6.2.2.20260408
|
||||
types-croniter==6.0.0.20250809
|
||||
types-caldav==1.3.0.20250516
|
||||
types-chardet==0.1.5
|
||||
types-decorator==5.2.0.20260408
|
||||
types-pexpect==4.9.0.20260408
|
||||
types-protobuf==6.32.1.20260221
|
||||
types-psutil==7.2.2.20260408
|
||||
types-pyserial==3.5.0.20260408
|
||||
types-python-dateutil==2.9.0.20260408
|
||||
types-decorator==5.2.0.20251101
|
||||
types-pexpect==4.9.0.20250916
|
||||
types-protobuf==6.30.2.20250914
|
||||
types-psutil==7.2.2.20260402
|
||||
types-pyserial==3.5.0.20251001
|
||||
types-python-dateutil==2.9.0.20260124
|
||||
types-python-slugify==8.0.2.20240310
|
||||
types-pytz==2026.1.1.20260408
|
||||
types-PyYAML==6.0.12.20260408
|
||||
types-requests==2.33.0.20260408
|
||||
types-xmltodict==1.0.1.20260408
|
||||
types-pytz==2025.2.0.20251108
|
||||
types-PyYAML==6.0.12.20250915
|
||||
types-requests==2.32.4.20260107
|
||||
types-xmltodict==1.0.1.20260113
|
||||
|
||||
12
requirements_test_all.txt
generated
12
requirements_test_all.txt
generated
@@ -413,7 +413,7 @@ aiotankerkoenig==0.5.1
|
||||
aiotedee==0.3.0
|
||||
|
||||
# homeassistant.components.tractive
|
||||
aiotractive==1.0.2
|
||||
aiotractive==1.0.1
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==90
|
||||
@@ -488,7 +488,7 @@ anova-wifi==0.17.0
|
||||
anthemav==1.4.1
|
||||
|
||||
# homeassistant.components.anthropic
|
||||
anthropic==0.92.0
|
||||
anthropic==0.83.0
|
||||
|
||||
# homeassistant.components.mcp_server
|
||||
anyio==4.10.0
|
||||
@@ -632,7 +632,7 @@ bring-api==1.1.1
|
||||
broadlink==0.19.0
|
||||
|
||||
# homeassistant.components.brother
|
||||
brother==6.1.0
|
||||
brother==6.0.0
|
||||
|
||||
# homeassistant.components.brottsplatskartan
|
||||
brottsplatskartan==1.0.5
|
||||
@@ -1289,7 +1289,7 @@ loqedAPI==2.1.11
|
||||
luftdaten==0.7.4
|
||||
|
||||
# homeassistant.components.lunatone
|
||||
lunatone-rest-api-client==0.9.0
|
||||
lunatone-rest-api-client==0.7.0
|
||||
|
||||
# homeassistant.components.lupusec
|
||||
lupupy==0.3.2
|
||||
@@ -1729,7 +1729,7 @@ pyblackbird==0.6
|
||||
pyblu==2.0.6
|
||||
|
||||
# homeassistant.components.neato
|
||||
pybotvac==0.0.29
|
||||
pybotvac==0.0.28
|
||||
|
||||
# homeassistant.components.braviatv
|
||||
pybravia==0.4.1
|
||||
@@ -2332,7 +2332,7 @@ pyvesync==3.4.1
|
||||
pyvizio==0.1.61
|
||||
|
||||
# homeassistant.components.velux
|
||||
pyvlx==0.2.33
|
||||
pyvlx==0.2.32
|
||||
|
||||
# homeassistant.components.volumio
|
||||
pyvolumio==0.1.5
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Tests for the Anthropic integration."""
|
||||
|
||||
import datetime
|
||||
from typing import Any
|
||||
|
||||
from anthropic.types import (
|
||||
@@ -9,16 +8,11 @@ from anthropic.types import (
|
||||
BashCodeExecutionToolResultBlock,
|
||||
BashCodeExecutionToolResultError,
|
||||
BashCodeExecutionToolResultErrorCode,
|
||||
CapabilitySupport,
|
||||
CitationsDelta,
|
||||
CodeExecutionToolResultBlock,
|
||||
CodeExecutionToolResultBlockContent,
|
||||
ContextManagementCapability,
|
||||
DirectCaller,
|
||||
EffortCapability,
|
||||
InputJSONDelta,
|
||||
ModelCapabilities,
|
||||
ModelInfo,
|
||||
RawContentBlockDeltaEvent,
|
||||
RawContentBlockStartEvent,
|
||||
RawContentBlockStopEvent,
|
||||
@@ -31,9 +25,7 @@ from anthropic.types import (
|
||||
TextDelta,
|
||||
TextEditorCodeExecutionToolResultBlock,
|
||||
ThinkingBlock,
|
||||
ThinkingCapability,
|
||||
ThinkingDelta,
|
||||
ThinkingTypes,
|
||||
ToolSearchToolResultBlock,
|
||||
ToolUseBlock,
|
||||
WebSearchResultBlock,
|
||||
@@ -48,333 +40,6 @@ from anthropic.types.tool_search_tool_result_block import (
|
||||
Content as ToolSearchToolResultBlockContent,
|
||||
)
|
||||
|
||||
model_list = [
|
||||
ModelInfo(
|
||||
id="claude-sonnet-4-6",
|
||||
capabilities=ModelCapabilities(
|
||||
batch=CapabilitySupport(supported=True),
|
||||
citations=CapabilitySupport(supported=True),
|
||||
code_execution=CapabilitySupport(supported=True),
|
||||
context_management=ContextManagementCapability(
|
||||
clear_thinking_20251015=CapabilitySupport(supported=True),
|
||||
clear_tool_uses_20250919=CapabilitySupport(supported=True),
|
||||
compact_20260112=CapabilitySupport(supported=True),
|
||||
supported=True,
|
||||
),
|
||||
effort=EffortCapability(
|
||||
high=CapabilitySupport(supported=True),
|
||||
low=CapabilitySupport(supported=True),
|
||||
max=CapabilitySupport(supported=True),
|
||||
medium=CapabilitySupport(supported=True),
|
||||
supported=True,
|
||||
),
|
||||
image_input=CapabilitySupport(supported=True),
|
||||
pdf_input=CapabilitySupport(supported=True),
|
||||
structured_outputs=CapabilitySupport(supported=True),
|
||||
thinking=ThinkingCapability(
|
||||
supported=True,
|
||||
types=ThinkingTypes(
|
||||
adaptive=CapabilitySupport(supported=True),
|
||||
enabled=CapabilitySupport(supported=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
created_at=datetime.datetime(2026, 2, 17, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Sonnet 4.6",
|
||||
max_input_tokens=1000000,
|
||||
max_tokens=128000,
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-opus-4-6",
|
||||
capabilities=ModelCapabilities(
|
||||
batch=CapabilitySupport(supported=True),
|
||||
citations=CapabilitySupport(supported=True),
|
||||
code_execution=CapabilitySupport(supported=True),
|
||||
context_management=ContextManagementCapability(
|
||||
clear_thinking_20251015=CapabilitySupport(supported=True),
|
||||
clear_tool_uses_20250919=CapabilitySupport(supported=True),
|
||||
compact_20260112=CapabilitySupport(supported=True),
|
||||
supported=True,
|
||||
),
|
||||
effort=EffortCapability(
|
||||
high=CapabilitySupport(supported=True),
|
||||
low=CapabilitySupport(supported=True),
|
||||
max=CapabilitySupport(supported=True),
|
||||
medium=CapabilitySupport(supported=True),
|
||||
supported=True,
|
||||
),
|
||||
image_input=CapabilitySupport(supported=True),
|
||||
pdf_input=CapabilitySupport(supported=True),
|
||||
structured_outputs=CapabilitySupport(supported=True),
|
||||
thinking=ThinkingCapability(
|
||||
supported=True,
|
||||
types=ThinkingTypes(
|
||||
adaptive=CapabilitySupport(supported=True),
|
||||
enabled=CapabilitySupport(supported=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
created_at=datetime.datetime(2026, 2, 4, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Opus 4.6",
|
||||
max_input_tokens=1000000,
|
||||
max_tokens=128000,
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-opus-4-5-20251101",
|
||||
capabilities=ModelCapabilities(
|
||||
batch=CapabilitySupport(supported=True),
|
||||
citations=CapabilitySupport(supported=True),
|
||||
code_execution=CapabilitySupport(supported=True),
|
||||
context_management=ContextManagementCapability(
|
||||
clear_thinking_20251015=CapabilitySupport(supported=True),
|
||||
clear_tool_uses_20250919=CapabilitySupport(supported=True),
|
||||
compact_20260112=CapabilitySupport(supported=False),
|
||||
supported=True,
|
||||
),
|
||||
effort=EffortCapability(
|
||||
high=CapabilitySupport(supported=True),
|
||||
low=CapabilitySupport(supported=True),
|
||||
max=CapabilitySupport(supported=False),
|
||||
medium=CapabilitySupport(supported=True),
|
||||
supported=True,
|
||||
),
|
||||
image_input=CapabilitySupport(supported=True),
|
||||
pdf_input=CapabilitySupport(supported=True),
|
||||
structured_outputs=CapabilitySupport(supported=True),
|
||||
thinking=ThinkingCapability(
|
||||
supported=True,
|
||||
types=ThinkingTypes(
|
||||
adaptive=CapabilitySupport(supported=False),
|
||||
enabled=CapabilitySupport(supported=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
created_at=datetime.datetime(2025, 11, 24, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Opus 4.5",
|
||||
max_input_tokens=200000,
|
||||
max_tokens=64000,
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-haiku-4-5-20251001",
|
||||
capabilities=ModelCapabilities(
|
||||
batch=CapabilitySupport(supported=True),
|
||||
citations=CapabilitySupport(supported=True),
|
||||
code_execution=CapabilitySupport(supported=False),
|
||||
context_management=ContextManagementCapability(
|
||||
clear_thinking_20251015=CapabilitySupport(supported=True),
|
||||
clear_tool_uses_20250919=CapabilitySupport(supported=True),
|
||||
compact_20260112=CapabilitySupport(supported=False),
|
||||
supported=True,
|
||||
),
|
||||
effort=EffortCapability(
|
||||
high=CapabilitySupport(supported=False),
|
||||
low=CapabilitySupport(supported=False),
|
||||
max=CapabilitySupport(supported=False),
|
||||
medium=CapabilitySupport(supported=False),
|
||||
supported=False,
|
||||
),
|
||||
image_input=CapabilitySupport(supported=True),
|
||||
pdf_input=CapabilitySupport(supported=True),
|
||||
structured_outputs=CapabilitySupport(supported=True),
|
||||
thinking=ThinkingCapability(
|
||||
supported=True,
|
||||
types=ThinkingTypes(
|
||||
adaptive=CapabilitySupport(supported=False),
|
||||
enabled=CapabilitySupport(supported=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
created_at=datetime.datetime(2025, 10, 15, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Haiku 4.5",
|
||||
max_input_tokens=200000,
|
||||
max_tokens=64000,
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-sonnet-4-5-20250929",
|
||||
capabilities=ModelCapabilities(
|
||||
batch=CapabilitySupport(supported=True),
|
||||
citations=CapabilitySupport(supported=True),
|
||||
code_execution=CapabilitySupport(supported=True),
|
||||
context_management=ContextManagementCapability(
|
||||
clear_thinking_20251015=CapabilitySupport(supported=True),
|
||||
clear_tool_uses_20250919=CapabilitySupport(supported=True),
|
||||
compact_20260112=CapabilitySupport(supported=False),
|
||||
supported=True,
|
||||
),
|
||||
effort=EffortCapability(
|
||||
high=CapabilitySupport(supported=False),
|
||||
low=CapabilitySupport(supported=False),
|
||||
max=CapabilitySupport(supported=False),
|
||||
medium=CapabilitySupport(supported=False),
|
||||
supported=False,
|
||||
),
|
||||
image_input=CapabilitySupport(supported=True),
|
||||
pdf_input=CapabilitySupport(supported=True),
|
||||
structured_outputs=CapabilitySupport(supported=True),
|
||||
thinking=ThinkingCapability(
|
||||
supported=True,
|
||||
types=ThinkingTypes(
|
||||
adaptive=CapabilitySupport(supported=False),
|
||||
enabled=CapabilitySupport(supported=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
created_at=datetime.datetime(2025, 9, 29, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Sonnet 4.5",
|
||||
max_input_tokens=1000000,
|
||||
max_tokens=64000,
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-opus-4-1-20250805",
|
||||
capabilities=ModelCapabilities(
|
||||
batch=CapabilitySupport(supported=True),
|
||||
citations=CapabilitySupport(supported=True),
|
||||
code_execution=CapabilitySupport(supported=False),
|
||||
context_management=ContextManagementCapability(
|
||||
clear_thinking_20251015=CapabilitySupport(supported=True),
|
||||
clear_tool_uses_20250919=CapabilitySupport(supported=True),
|
||||
compact_20260112=CapabilitySupport(supported=False),
|
||||
supported=True,
|
||||
),
|
||||
effort=EffortCapability(
|
||||
high=CapabilitySupport(supported=False),
|
||||
low=CapabilitySupport(supported=False),
|
||||
max=CapabilitySupport(supported=False),
|
||||
medium=CapabilitySupport(supported=False),
|
||||
supported=False,
|
||||
),
|
||||
image_input=CapabilitySupport(supported=True),
|
||||
pdf_input=CapabilitySupport(supported=True),
|
||||
structured_outputs=CapabilitySupport(supported=True),
|
||||
thinking=ThinkingCapability(
|
||||
supported=True,
|
||||
types=ThinkingTypes(
|
||||
adaptive=CapabilitySupport(supported=False),
|
||||
enabled=CapabilitySupport(supported=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
created_at=datetime.datetime(2025, 8, 5, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Opus 4.1",
|
||||
max_input_tokens=200000,
|
||||
max_tokens=32000,
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-opus-4-20250514",
|
||||
capabilities=ModelCapabilities(
|
||||
batch=CapabilitySupport(supported=True),
|
||||
citations=CapabilitySupport(supported=True),
|
||||
code_execution=CapabilitySupport(supported=False),
|
||||
context_management=ContextManagementCapability(
|
||||
clear_thinking_20251015=CapabilitySupport(supported=True),
|
||||
clear_tool_uses_20250919=CapabilitySupport(supported=True),
|
||||
compact_20260112=CapabilitySupport(supported=False),
|
||||
supported=True,
|
||||
),
|
||||
effort=EffortCapability(
|
||||
high=CapabilitySupport(supported=False),
|
||||
low=CapabilitySupport(supported=False),
|
||||
max=CapabilitySupport(supported=False),
|
||||
medium=CapabilitySupport(supported=False),
|
||||
supported=False,
|
||||
),
|
||||
image_input=CapabilitySupport(supported=True),
|
||||
pdf_input=CapabilitySupport(supported=True),
|
||||
structured_outputs=CapabilitySupport(supported=False),
|
||||
thinking=ThinkingCapability(
|
||||
supported=True,
|
||||
types=ThinkingTypes(
|
||||
adaptive=CapabilitySupport(supported=False),
|
||||
enabled=CapabilitySupport(supported=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
created_at=datetime.datetime(2025, 5, 22, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Opus 4",
|
||||
max_input_tokens=200000,
|
||||
max_tokens=32000,
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-sonnet-4-20250514",
|
||||
capabilities=ModelCapabilities(
|
||||
batch=CapabilitySupport(supported=True),
|
||||
citations=CapabilitySupport(supported=True),
|
||||
code_execution=CapabilitySupport(supported=False),
|
||||
context_management=ContextManagementCapability(
|
||||
clear_thinking_20251015=CapabilitySupport(supported=True),
|
||||
clear_tool_uses_20250919=CapabilitySupport(supported=True),
|
||||
compact_20260112=CapabilitySupport(supported=False),
|
||||
supported=True,
|
||||
),
|
||||
effort=EffortCapability(
|
||||
high=CapabilitySupport(supported=False),
|
||||
low=CapabilitySupport(supported=False),
|
||||
max=CapabilitySupport(supported=False),
|
||||
medium=CapabilitySupport(supported=False),
|
||||
supported=False,
|
||||
),
|
||||
image_input=CapabilitySupport(supported=True),
|
||||
pdf_input=CapabilitySupport(supported=True),
|
||||
structured_outputs=CapabilitySupport(supported=False),
|
||||
thinking=ThinkingCapability(
|
||||
supported=True,
|
||||
types=ThinkingTypes(
|
||||
adaptive=CapabilitySupport(supported=False),
|
||||
enabled=CapabilitySupport(supported=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
created_at=datetime.datetime(2025, 5, 22, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Sonnet 4",
|
||||
max_input_tokens=1000000,
|
||||
max_tokens=64000,
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-3-haiku-20240307",
|
||||
capabilities=ModelCapabilities(
|
||||
batch=CapabilitySupport(supported=True),
|
||||
citations=CapabilitySupport(supported=False),
|
||||
code_execution=CapabilitySupport(supported=False),
|
||||
context_management=ContextManagementCapability(
|
||||
clear_thinking_20251015=CapabilitySupport(supported=False),
|
||||
clear_tool_uses_20250919=CapabilitySupport(supported=False),
|
||||
compact_20260112=CapabilitySupport(supported=False),
|
||||
supported=False,
|
||||
),
|
||||
effort=EffortCapability(
|
||||
high=CapabilitySupport(supported=False),
|
||||
low=CapabilitySupport(supported=False),
|
||||
max=CapabilitySupport(supported=False),
|
||||
medium=CapabilitySupport(supported=False),
|
||||
supported=False,
|
||||
),
|
||||
image_input=CapabilitySupport(supported=True),
|
||||
pdf_input=CapabilitySupport(supported=False),
|
||||
structured_outputs=CapabilitySupport(supported=False),
|
||||
thinking=ThinkingCapability(
|
||||
supported=False,
|
||||
types=ThinkingTypes(
|
||||
adaptive=CapabilitySupport(supported=False),
|
||||
enabled=CapabilitySupport(supported=False),
|
||||
),
|
||||
),
|
||||
),
|
||||
created_at=datetime.datetime(2024, 3, 7, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Haiku 3",
|
||||
max_input_tokens=200000,
|
||||
max_tokens=4096,
|
||||
type="model",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def create_content_block(
|
||||
index: int, text_parts: list[str], citations: list[TextCitation] | None = None
|
||||
|
||||
@@ -9,6 +9,7 @@ from anthropic.types import (
|
||||
Container,
|
||||
Message,
|
||||
MessageDeltaUsage,
|
||||
ModelInfo,
|
||||
RawContentBlockStartEvent,
|
||||
RawMessageDeltaEvent,
|
||||
RawMessageStartEvent,
|
||||
@@ -30,8 +31,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import llm
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import model_list
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@@ -82,10 +81,68 @@ async def mock_init_component(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> AsyncGenerator[None]:
|
||||
"""Initialize integration."""
|
||||
model_list = AsyncPage(
|
||||
data=[
|
||||
ModelInfo(
|
||||
id="claude-sonnet-4-6",
|
||||
created_at=datetime.datetime(2026, 2, 17, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Sonnet 4.6",
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-opus-4-6",
|
||||
created_at=datetime.datetime(2026, 2, 4, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Opus 4.6",
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-opus-4-5-20251101",
|
||||
created_at=datetime.datetime(2025, 11, 1, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Opus 4.5",
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-haiku-4-5-20251001",
|
||||
created_at=datetime.datetime(2025, 10, 15, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Haiku 4.5",
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-sonnet-4-5-20250929",
|
||||
created_at=datetime.datetime(2025, 9, 29, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Sonnet 4.5",
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-opus-4-1-20250805",
|
||||
created_at=datetime.datetime(2025, 8, 5, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Opus 4.1",
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-opus-4-20250514",
|
||||
created_at=datetime.datetime(2025, 5, 22, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Opus 4",
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-sonnet-4-20250514",
|
||||
created_at=datetime.datetime(2025, 5, 22, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Sonnet 4",
|
||||
type="model",
|
||||
),
|
||||
ModelInfo(
|
||||
id="claude-3-haiku-20240307",
|
||||
created_at=datetime.datetime(2024, 3, 7, 0, 0, tzinfo=datetime.UTC),
|
||||
display_name="Claude Haiku 3",
|
||||
type="model",
|
||||
),
|
||||
]
|
||||
)
|
||||
with patch(
|
||||
"anthropic.resources.models.AsyncModels.list",
|
||||
new_callable=AsyncMock,
|
||||
return_value=AsyncPage(data=model_list),
|
||||
return_value=model_list,
|
||||
):
|
||||
assert await async_setup_component(hass, "anthropic", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -46,19 +46,12 @@ from homeassistant.components.anthropic.const import (
|
||||
CONF_WEB_SEARCH_REGION,
|
||||
CONF_WEB_SEARCH_TIMEZONE,
|
||||
CONF_WEB_SEARCH_USER_LOCATION,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.anthropic.entity import CitationDetails, ContentDetails
|
||||
from homeassistant.const import CONF_LLM_HASS_API
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
chat_session,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
intent,
|
||||
llm,
|
||||
)
|
||||
from homeassistant.helpers import chat_session, entity_registry as er, intent, llm
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import ulid as ulid_util
|
||||
|
||||
@@ -106,24 +99,6 @@ async def test_entity(
|
||||
)
|
||||
|
||||
|
||||
async def test_device(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component,
|
||||
) -> None:
|
||||
"""Test device parameters."""
|
||||
subentry = next(iter(mock_config_entry.subentries.values()))
|
||||
device = device_registry.async_get_device({(DOMAIN, subentry.subentry_id)})
|
||||
|
||||
assert device is not None
|
||||
assert device.name == "Claude conversation"
|
||||
assert device.manufacturer == "Anthropic"
|
||||
assert device.model == "Claude Haiku 4.5"
|
||||
assert device.model_id == "claude-haiku-4-5-20251001"
|
||||
assert device.entry_type == dr.DeviceEntryType.SERVICE
|
||||
|
||||
|
||||
async def test_translation_key(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
@@ -10,7 +10,6 @@ from homeassistant.components.event import (
|
||||
ATTR_EVENT_TYPE,
|
||||
ATTR_EVENT_TYPES,
|
||||
DOMAIN,
|
||||
DoorbellEventType,
|
||||
EventDeviceClass,
|
||||
EventEntity,
|
||||
EventEntityDescription,
|
||||
@@ -35,7 +34,6 @@ from tests.common import (
|
||||
mock_platform,
|
||||
mock_restore_cache,
|
||||
mock_restore_cache_with_extra_data,
|
||||
setup_test_component_platform,
|
||||
)
|
||||
|
||||
|
||||
@@ -346,68 +344,3 @@ async def test_name(hass: HomeAssistant) -> None:
|
||||
"device_class": "doorbell",
|
||||
"friendly_name": "Doorbell",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("config_flow_fixture")
|
||||
async def test_doorbell_missing_ring_event_type(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test warning when a doorbell entity does not include the standard ring event type."""
|
||||
|
||||
async def async_setup_entry_init(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> bool:
|
||||
"""Set up test config entry."""
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
config_entry, [Platform.EVENT]
|
||||
)
|
||||
return True
|
||||
|
||||
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule(
|
||||
TEST_DOMAIN,
|
||||
async_setup_entry=async_setup_entry_init,
|
||||
),
|
||||
)
|
||||
|
||||
# Doorbell entity WITHOUT the standard "ring" event type
|
||||
entity_without_ring = EventEntity()
|
||||
entity_without_ring._attr_event_types = ["ding"]
|
||||
entity_without_ring._attr_device_class = EventDeviceClass.DOORBELL
|
||||
entity_without_ring._attr_has_entity_name = True
|
||||
entity_without_ring.entity_id = "event.doorbell_without_ring"
|
||||
|
||||
# Doorbell entity WITH the standard "ring" event type
|
||||
entity_with_ring = EventEntity()
|
||||
entity_with_ring._attr_event_types = [DoorbellEventType.RING, "ding"]
|
||||
entity_with_ring._attr_device_class = EventDeviceClass.DOORBELL
|
||||
entity_with_ring._attr_has_entity_name = True
|
||||
entity_with_ring.entity_id = "event.doorbell_with_ring"
|
||||
|
||||
# Non-doorbell entity should not warn
|
||||
entity_button = EventEntity()
|
||||
entity_button._attr_event_types = ["press"]
|
||||
entity_button._attr_device_class = EventDeviceClass.BUTTON
|
||||
entity_button._attr_has_entity_name = True
|
||||
entity_button.entity_id = "event.button"
|
||||
|
||||
setup_test_component_platform(
|
||||
hass,
|
||||
DOMAIN,
|
||||
[entity_without_ring, entity_with_ring, entity_button],
|
||||
from_config_entry=True,
|
||||
)
|
||||
config_entry = MockConfigEntry(domain=TEST_DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
"Entity event.doorbell_without_ring is a doorbell event entity "
|
||||
"but does not support the 'ring' event type"
|
||||
) in caplog.text
|
||||
assert "event.doorbell_with_ring" not in caplog.text
|
||||
assert "event.button" not in caplog.text
|
||||
|
||||
@@ -22,6 +22,20 @@ from homeassistant.helpers.service_info.ssdp import (
|
||||
ATTR_HOST = "host"
|
||||
ATTR_NEW_SERIAL_NUMBER = "NewSerialNumber"
|
||||
|
||||
MOCK_CONFIG = {
|
||||
DOMAIN: {
|
||||
CONF_DEVICES: [
|
||||
{
|
||||
CONF_HOST: "fake_host",
|
||||
CONF_PORT: "1234",
|
||||
CONF_PASSWORD: "fake_pass",
|
||||
CONF_USERNAME: "fake_user",
|
||||
CONF_SSL: False,
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
MOCK_HOST = "fake_host"
|
||||
MOCK_IPS = {
|
||||
"fritz.box": "192.168.178.1",
|
||||
@@ -990,26 +1004,14 @@ MOCK_STATUS_AVM_DEVICE_LOG_DATA = MOCK_FB_SERVICES["DeviceInfo1"]["GetInfo"][
|
||||
"NewDeviceLog"
|
||||
]
|
||||
|
||||
MOCK_USER_DATA = {
|
||||
CONF_HOST: "fake_host",
|
||||
CONF_PORT: 1234,
|
||||
CONF_PASSWORD: "fake_pass",
|
||||
CONF_USERNAME: "fake_user",
|
||||
CONF_SSL: False,
|
||||
}
|
||||
|
||||
MOCK_CONFIG = {DOMAIN: {CONF_DEVICES: [MOCK_USER_DATA]}}
|
||||
|
||||
MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0]
|
||||
MOCK_USER_INPUT_ADVANCED = MOCK_USER_DATA
|
||||
"""User input data with optional port."""
|
||||
|
||||
MOCK_USER_INPUT_SIMPLE = {
|
||||
CONF_HOST: "fake_host",
|
||||
CONF_PASSWORD: "fake_pass",
|
||||
CONF_USERNAME: "fake_user",
|
||||
CONF_SSL: False,
|
||||
}
|
||||
"""User input data without optional port."""
|
||||
|
||||
MOCK_DEVICE_INFO = {
|
||||
ATTR_HOST: MOCK_HOST,
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
'data': dict({
|
||||
'host': 'fake_host',
|
||||
'password': '**REDACTED**',
|
||||
'port': 1234,
|
||||
'port': '1234',
|
||||
'ssl': False,
|
||||
'username': '**REDACTED**',
|
||||
}),
|
||||
|
||||
@@ -57,9 +57,10 @@ from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_input", "expected_config", "expected_options"),
|
||||
("show_advanced_options", "user_input", "expected_config", "expected_options"),
|
||||
[
|
||||
(
|
||||
True,
|
||||
MOCK_USER_INPUT_ADVANCED,
|
||||
{
|
||||
CONF_HOST: "fake_host",
|
||||
@@ -75,6 +76,7 @@ from tests.common import MockConfigEntry
|
||||
},
|
||||
),
|
||||
(
|
||||
False,
|
||||
MOCK_USER_INPUT_SIMPLE,
|
||||
{
|
||||
CONF_HOST: "fake_host",
|
||||
@@ -90,6 +92,7 @@ from tests.common import MockConfigEntry
|
||||
},
|
||||
),
|
||||
(
|
||||
False,
|
||||
{
|
||||
**MOCK_USER_INPUT_SIMPLE,
|
||||
CONF_SSL: True,
|
||||
@@ -113,6 +116,7 @@ from tests.common import MockConfigEntry
|
||||
async def test_user(
|
||||
hass: HomeAssistant,
|
||||
fc_class_mock,
|
||||
show_advanced_options: bool,
|
||||
user_input: dict,
|
||||
expected_config: dict,
|
||||
expected_options: dict,
|
||||
@@ -146,7 +150,10 @@ async def test_user(
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
context={
|
||||
"source": SOURCE_USER,
|
||||
"show_advanced_options": show_advanced_options,
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
@@ -163,12 +170,13 @@ async def test_user(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_input"),
|
||||
[(MOCK_USER_INPUT_ADVANCED), (MOCK_USER_INPUT_SIMPLE)],
|
||||
("show_advanced_options", "user_input"),
|
||||
[(True, MOCK_USER_INPUT_ADVANCED), (False, MOCK_USER_INPUT_SIMPLE)],
|
||||
)
|
||||
async def test_user_already_configured(
|
||||
hass: HomeAssistant,
|
||||
fc_class_mock,
|
||||
show_advanced_options: bool,
|
||||
user_input,
|
||||
) -> None:
|
||||
"""Test starting a flow by user with an already configured device."""
|
||||
@@ -203,7 +211,10 @@ async def test_user_already_configured(
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
context={
|
||||
"source": SOURCE_USER,
|
||||
"show_advanced_options": show_advanced_options,
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
@@ -221,19 +232,20 @@ async def test_user_already_configured(
|
||||
FRITZ_AUTH_EXCEPTIONS,
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("user_input"),
|
||||
[(MOCK_USER_INPUT_ADVANCED), (MOCK_USER_INPUT_SIMPLE)],
|
||||
("show_advanced_options", "user_input"),
|
||||
[(True, MOCK_USER_INPUT_ADVANCED), (False, MOCK_USER_INPUT_SIMPLE)],
|
||||
)
|
||||
async def test_exception_security(
|
||||
hass: HomeAssistant,
|
||||
error,
|
||||
show_advanced_options: bool,
|
||||
user_input,
|
||||
) -> None:
|
||||
"""Test starting a flow by user with invalid credentials."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
context={"source": SOURCE_USER, "show_advanced_options": show_advanced_options},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
@@ -252,18 +264,19 @@ async def test_exception_security(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_input"),
|
||||
[(MOCK_USER_INPUT_ADVANCED), (MOCK_USER_INPUT_SIMPLE)],
|
||||
("show_advanced_options", "user_input"),
|
||||
[(True, MOCK_USER_INPUT_ADVANCED), (False, MOCK_USER_INPUT_SIMPLE)],
|
||||
)
|
||||
async def test_exception_connection(
|
||||
hass: HomeAssistant,
|
||||
show_advanced_options: bool,
|
||||
user_input,
|
||||
) -> None:
|
||||
"""Test starting a flow by user with a connection error."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
context={"source": SOURCE_USER, "show_advanced_options": show_advanced_options},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
@@ -282,15 +295,17 @@ async def test_exception_connection(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_input"),
|
||||
[(MOCK_USER_INPUT_ADVANCED), (MOCK_USER_INPUT_SIMPLE)],
|
||||
("show_advanced_options", "user_input"),
|
||||
[(True, MOCK_USER_INPUT_ADVANCED), (False, MOCK_USER_INPUT_SIMPLE)],
|
||||
)
|
||||
async def test_exception_unknown(hass: HomeAssistant, user_input) -> None:
|
||||
async def test_exception_unknown(
|
||||
hass: HomeAssistant, show_advanced_options: bool, user_input
|
||||
) -> None:
|
||||
"""Test starting a flow by user with an unknown exception."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
context={"source": SOURCE_USER, "show_advanced_options": show_advanced_options},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
@@ -398,52 +413,30 @@ async def test_reauth_not_successful(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("initial_config", "user_input", "expected_config"),
|
||||
("show_advanced_options", "user_input", "expected_config"),
|
||||
[
|
||||
(
|
||||
MOCK_USER_DATA,
|
||||
True,
|
||||
{CONF_HOST: "host_a", CONF_PORT: 49000, CONF_SSL: False},
|
||||
{CONF_HOST: "host_a", CONF_PORT: 49000, CONF_SSL: False},
|
||||
),
|
||||
(
|
||||
MOCK_USER_DATA,
|
||||
True,
|
||||
{CONF_HOST: "host_a", CONF_PORT: 49443, CONF_SSL: True},
|
||||
{CONF_HOST: "host_a", CONF_PORT: 49443, CONF_SSL: True},
|
||||
),
|
||||
(
|
||||
MOCK_USER_DATA,
|
||||
True,
|
||||
{CONF_HOST: "host_a", CONF_PORT: 12345, CONF_SSL: True},
|
||||
{CONF_HOST: "host_a", CONF_PORT: 12345, CONF_SSL: True},
|
||||
),
|
||||
(
|
||||
MOCK_USER_DATA,
|
||||
{CONF_HOST: "host_b", CONF_SSL: False},
|
||||
{CONF_HOST: "host_b", CONF_PORT: 1234, CONF_SSL: False},
|
||||
),
|
||||
(
|
||||
MOCK_USER_DATA,
|
||||
{CONF_HOST: "host_b", CONF_SSL: True},
|
||||
{CONF_HOST: "host_b", CONF_PORT: 1234, CONF_SSL: True},
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_HOST: "fake_host",
|
||||
CONF_PORT: 49000,
|
||||
CONF_PASSWORD: "fake_pass",
|
||||
CONF_USERNAME: "fake_user",
|
||||
CONF_SSL: False,
|
||||
},
|
||||
False,
|
||||
{CONF_HOST: "host_b", CONF_SSL: False},
|
||||
{CONF_HOST: "host_b", CONF_PORT: 49000, CONF_SSL: False},
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_HOST: "fake_host",
|
||||
CONF_PORT: 49000,
|
||||
CONF_PASSWORD: "fake_pass",
|
||||
CONF_USERNAME: "fake_user",
|
||||
CONF_SSL: False,
|
||||
},
|
||||
False,
|
||||
{CONF_HOST: "host_b", CONF_SSL: True},
|
||||
{CONF_HOST: "host_b", CONF_PORT: 49443, CONF_SSL: True},
|
||||
),
|
||||
@@ -452,13 +445,13 @@ async def test_reauth_not_successful(
|
||||
async def test_reconfigure_successful(
|
||||
hass: HomeAssistant,
|
||||
fc_class_mock,
|
||||
initial_config: dict,
|
||||
show_advanced_options: bool,
|
||||
user_input: dict,
|
||||
expected_config: dict,
|
||||
) -> None:
|
||||
"""Test starting a reconfigure flow."""
|
||||
|
||||
mock_config = MockConfigEntry(domain=DOMAIN, data=initial_config)
|
||||
mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA)
|
||||
mock_config.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
@@ -485,7 +478,10 @@ async def test_reconfigure_successful(
|
||||
mock_request_post.return_value.status_code = 200
|
||||
mock_request_post.return_value.text = MOCK_REQUEST
|
||||
|
||||
result = await mock_config.start_reconfigure_flow(hass)
|
||||
result = await mock_config.start_reconfigure_flow(
|
||||
hass,
|
||||
show_advanced_options=show_advanced_options,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
@@ -570,7 +566,7 @@ async def test_reconfigure_not_successful(
|
||||
CONF_HOST: "fake_host",
|
||||
CONF_PASSWORD: "fake_pass",
|
||||
CONF_USERNAME: "fake_user",
|
||||
CONF_PORT: 1234,
|
||||
CONF_PORT: 49000,
|
||||
CONF_SSL: False,
|
||||
}
|
||||
|
||||
|
||||
@@ -333,42 +333,30 @@ async def test_switch_no_mesh_wifi_uplink(
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("wan_access_data", "expected_state"),
|
||||
[
|
||||
(None, STATE_UNAVAILABLE),
|
||||
("unknown", STATE_UNAVAILABLE),
|
||||
("error", STATE_UNAVAILABLE),
|
||||
("granted", STATE_ON),
|
||||
("denied", STATE_OFF),
|
||||
],
|
||||
)
|
||||
async def test_switch_device_wan_access(
|
||||
async def test_switch_device_no_wan_access(
|
||||
hass: HomeAssistant,
|
||||
fc_class_mock,
|
||||
fh_class_mock,
|
||||
fs_class_mock,
|
||||
wan_access_data: str | None,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test Fritz!Tools switches have proper WAN access state."""
|
||||
"""Test Fritz!Tools switches when device has no WAN access."""
|
||||
|
||||
entity_id = "switch.printer_internet_access"
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
attributes = deepcopy(MOCK_HOST_ATTRIBUTES_DATA)
|
||||
for host in attributes:
|
||||
host["X_AVM-DE_WANAccess"] = wan_access_data
|
||||
|
||||
attributes = [
|
||||
{k: v for k, v in host.items() if k != "X_AVM-DE_WANAccess"}
|
||||
for host in MOCK_HOST_ATTRIBUTES_DATA
|
||||
]
|
||||
fh_class_mock.get_hosts_attributes = MagicMock(return_value=attributes)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state == expected_state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_switch_device_no_ip_address(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,81 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_cloud_all_water_heaters[water_heater.hot_water-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max_temp': 80.0,
|
||||
'min_temp': 30.0,
|
||||
'operation_list': list([
|
||||
'off',
|
||||
'auto',
|
||||
'manual',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'water_heater',
|
||||
'entity_category': None,
|
||||
'entity_id': 'water_heater.hot_water',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Hot Water',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Hot Water',
|
||||
'platform': 'geniushub',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <WaterHeaterEntityFeature: 3>,
|
||||
'translation_key': None,
|
||||
'unique_id': '01J71MQF0EC62D620DGYNG2R8H_zone_10',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_cloud_all_water_heaters[water_heater.hot_water-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 45.5,
|
||||
'friendly_name': 'Hot Water',
|
||||
'max_temp': 80.0,
|
||||
'min_temp': 30.0,
|
||||
'operation_list': list([
|
||||
'off',
|
||||
'auto',
|
||||
'manual',
|
||||
]),
|
||||
'operation_mode': 'manual',
|
||||
'status': dict({
|
||||
'mode': 'override',
|
||||
'override': dict({
|
||||
'duration': 3600,
|
||||
'setpoint': 80,
|
||||
}),
|
||||
'temperature': 45.5,
|
||||
'type': 'hot water temperature',
|
||||
}),
|
||||
'supported_features': <WaterHeaterEntityFeature: 3>,
|
||||
'target_temp_high': None,
|
||||
'target_temp_low': None,
|
||||
'temperature': 60,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'water_heater.hot_water',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'manual',
|
||||
})
|
||||
# ---
|
||||
@@ -1,30 +0,0 @@
|
||||
"""Tests for the Geniushub water heater platform."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_geniushub_cloud")
|
||||
async def test_cloud_all_water_heaters(
|
||||
hass: HomeAssistant,
|
||||
mock_cloud_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the creation of the Genius Hub water heater entities."""
|
||||
with patch("homeassistant.components.geniushub.PLATFORMS", [Platform.WATER_HEATER]):
|
||||
await setup_integration(hass, mock_cloud_config_entry)
|
||||
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, snapshot, mock_cloud_config_entry.entry_id
|
||||
)
|
||||
@@ -265,7 +265,7 @@ async def test_bad_config_entry_fixing(hass: HomeAssistant) -> None:
|
||||
fixable_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.async_scan_serial_ports",
|
||||
"homeassistant.components.homeassistant_sky_connect.scan_serial_ports",
|
||||
return_value=[
|
||||
USBDevice(
|
||||
device="/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_4f5f3b26d59f8714a78b599690741999-if00-port0",
|
||||
|
||||
@@ -11,7 +11,7 @@ from lunatone_rest_api_client.models import (
|
||||
InfoData,
|
||||
LineStatus,
|
||||
)
|
||||
from lunatone_rest_api_client.models.common import ColorRGBData, ColorWAFData, Status
|
||||
from lunatone_rest_api_client.models.common import Status
|
||||
from lunatone_rest_api_client.models.devices import DeviceStatus
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -126,46 +126,6 @@ def build_device_data_list() -> list[DeviceData]:
|
||||
address=1,
|
||||
line=0,
|
||||
),
|
||||
DeviceData(
|
||||
id=3,
|
||||
name="Device 3",
|
||||
available=True,
|
||||
status=DeviceStatus(),
|
||||
features=FeaturesStatus(
|
||||
switchable=Status[bool](status=False),
|
||||
dimmable=Status[float](status=0.0),
|
||||
colorKelvin=Status[int](status=1000),
|
||||
),
|
||||
address=2,
|
||||
line=0,
|
||||
),
|
||||
DeviceData(
|
||||
id=4,
|
||||
name="Device 4",
|
||||
available=True,
|
||||
status=DeviceStatus(),
|
||||
features=FeaturesStatus(
|
||||
switchable=Status[bool](status=False),
|
||||
dimmable=Status[float](status=0.0),
|
||||
colorRGB=Status[ColorRGBData](status=ColorRGBData(r=0, g=0, b=0)),
|
||||
),
|
||||
address=3,
|
||||
line=0,
|
||||
),
|
||||
DeviceData(
|
||||
id=5,
|
||||
name="Device 5",
|
||||
available=True,
|
||||
status=DeviceStatus(),
|
||||
features=FeaturesStatus(
|
||||
switchable=Status[bool](status=False),
|
||||
dimmable=Status[float](status=0.0),
|
||||
colorRGB=Status[ColorRGBData](status=ColorRGBData(r=0, g=0, b=0)),
|
||||
colorWAF=Status[ColorWAFData](status=ColorWAFData(w=0, a=0, f=0)),
|
||||
),
|
||||
address=4,
|
||||
line=0,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -45,30 +45,6 @@ def mock_lunatone_devices() -> Generator[AsyncMock]:
|
||||
if device.data.features.dimmable
|
||||
else None
|
||||
)
|
||||
device.color_temperature = (
|
||||
device.data.features.color_kelvin.status
|
||||
if device.data.features.color_kelvin
|
||||
else None
|
||||
)
|
||||
device.rgb_color = (
|
||||
(
|
||||
device.data.features.color_rgb.status.red,
|
||||
device.data.features.color_rgb.status.green,
|
||||
device.data.features.color_rgb.status.blue,
|
||||
)
|
||||
if device.data.features.color_rgb
|
||||
else None
|
||||
)
|
||||
device.rgbw_color = (
|
||||
(
|
||||
device.data.features.color_rgb.status.red,
|
||||
device.data.features.color_rgb.status.green,
|
||||
device.data.features.color_rgb.status.blue,
|
||||
device.data.features.color_waf.status.white,
|
||||
)
|
||||
if device.data.features.color_rgb and device.data.features.color_waf
|
||||
else None
|
||||
)
|
||||
device_list.append(device)
|
||||
return device_list
|
||||
|
||||
|
||||
@@ -114,197 +114,6 @@
|
||||
'time_signature': None,
|
||||
'type': 'default',
|
||||
}),
|
||||
dict({
|
||||
'address': 2,
|
||||
'available': True,
|
||||
'dali_types': list([
|
||||
]),
|
||||
'features': dict({
|
||||
'color_kelvin': dict({
|
||||
'status': 1000.0,
|
||||
}),
|
||||
'color_kelvin_with_fade': None,
|
||||
'color_rgb': None,
|
||||
'color_rgb_with_fade': None,
|
||||
'color_waf': None,
|
||||
'color_waf_with_fade': None,
|
||||
'color_xy': None,
|
||||
'color_xy_with_fade': None,
|
||||
'dali_cmd16': None,
|
||||
'dim_down': None,
|
||||
'dim_up': None,
|
||||
'dimmable': dict({
|
||||
'status': 0.0,
|
||||
}),
|
||||
'dimmable_kelvin': None,
|
||||
'dimmable_rgb': None,
|
||||
'dimmable_waf': None,
|
||||
'dimmable_with_fade': None,
|
||||
'dimmable_xy': None,
|
||||
'fade_rate': None,
|
||||
'fade_time': None,
|
||||
'goto_last_active': None,
|
||||
'goto_last_active_with_fade': None,
|
||||
'save_to_scene': None,
|
||||
'scene': None,
|
||||
'scene_with_fade': None,
|
||||
'switchable': dict({
|
||||
'status': False,
|
||||
}),
|
||||
}),
|
||||
'groups': list([
|
||||
]),
|
||||
'id': 3,
|
||||
'line': 0,
|
||||
'name': 'Device 3',
|
||||
'scenes': list([
|
||||
]),
|
||||
'status': dict({
|
||||
'control_gear_failure': False,
|
||||
'fade_running': False,
|
||||
'is_unaddressed': False,
|
||||
'lamp_failure': False,
|
||||
'lamp_on': False,
|
||||
'limit_error': False,
|
||||
'power_cycle_see': False,
|
||||
'raw': 0,
|
||||
'reset_state': False,
|
||||
}),
|
||||
'time_signature': None,
|
||||
'type': 'default',
|
||||
}),
|
||||
dict({
|
||||
'address': 3,
|
||||
'available': True,
|
||||
'dali_types': list([
|
||||
]),
|
||||
'features': dict({
|
||||
'color_kelvin': None,
|
||||
'color_kelvin_with_fade': None,
|
||||
'color_rgb': dict({
|
||||
'status': dict({
|
||||
'blue': 0.0,
|
||||
'green': 0.0,
|
||||
'red': 0.0,
|
||||
}),
|
||||
}),
|
||||
'color_rgb_with_fade': None,
|
||||
'color_waf': None,
|
||||
'color_waf_with_fade': None,
|
||||
'color_xy': None,
|
||||
'color_xy_with_fade': None,
|
||||
'dali_cmd16': None,
|
||||
'dim_down': None,
|
||||
'dim_up': None,
|
||||
'dimmable': dict({
|
||||
'status': 0.0,
|
||||
}),
|
||||
'dimmable_kelvin': None,
|
||||
'dimmable_rgb': None,
|
||||
'dimmable_waf': None,
|
||||
'dimmable_with_fade': None,
|
||||
'dimmable_xy': None,
|
||||
'fade_rate': None,
|
||||
'fade_time': None,
|
||||
'goto_last_active': None,
|
||||
'goto_last_active_with_fade': None,
|
||||
'save_to_scene': None,
|
||||
'scene': None,
|
||||
'scene_with_fade': None,
|
||||
'switchable': dict({
|
||||
'status': False,
|
||||
}),
|
||||
}),
|
||||
'groups': list([
|
||||
]),
|
||||
'id': 4,
|
||||
'line': 0,
|
||||
'name': 'Device 4',
|
||||
'scenes': list([
|
||||
]),
|
||||
'status': dict({
|
||||
'control_gear_failure': False,
|
||||
'fade_running': False,
|
||||
'is_unaddressed': False,
|
||||
'lamp_failure': False,
|
||||
'lamp_on': False,
|
||||
'limit_error': False,
|
||||
'power_cycle_see': False,
|
||||
'raw': 0,
|
||||
'reset_state': False,
|
||||
}),
|
||||
'time_signature': None,
|
||||
'type': 'default',
|
||||
}),
|
||||
dict({
|
||||
'address': 4,
|
||||
'available': True,
|
||||
'dali_types': list([
|
||||
]),
|
||||
'features': dict({
|
||||
'color_kelvin': None,
|
||||
'color_kelvin_with_fade': None,
|
||||
'color_rgb': dict({
|
||||
'status': dict({
|
||||
'blue': 0.0,
|
||||
'green': 0.0,
|
||||
'red': 0.0,
|
||||
}),
|
||||
}),
|
||||
'color_rgb_with_fade': None,
|
||||
'color_waf': dict({
|
||||
'status': dict({
|
||||
'amber': 0.0,
|
||||
'free_color': 0.0,
|
||||
'white': 0.0,
|
||||
}),
|
||||
}),
|
||||
'color_waf_with_fade': None,
|
||||
'color_xy': None,
|
||||
'color_xy_with_fade': None,
|
||||
'dali_cmd16': None,
|
||||
'dim_down': None,
|
||||
'dim_up': None,
|
||||
'dimmable': dict({
|
||||
'status': 0.0,
|
||||
}),
|
||||
'dimmable_kelvin': None,
|
||||
'dimmable_rgb': None,
|
||||
'dimmable_waf': None,
|
||||
'dimmable_with_fade': None,
|
||||
'dimmable_xy': None,
|
||||
'fade_rate': None,
|
||||
'fade_time': None,
|
||||
'goto_last_active': None,
|
||||
'goto_last_active_with_fade': None,
|
||||
'save_to_scene': None,
|
||||
'scene': None,
|
||||
'scene_with_fade': None,
|
||||
'switchable': dict({
|
||||
'status': False,
|
||||
}),
|
||||
}),
|
||||
'groups': list([
|
||||
]),
|
||||
'id': 5,
|
||||
'line': 0,
|
||||
'name': 'Device 5',
|
||||
'scenes': list([
|
||||
]),
|
||||
'status': dict({
|
||||
'control_gear_failure': False,
|
||||
'fade_running': False,
|
||||
'is_unaddressed': False,
|
||||
'lamp_failure': False,
|
||||
'lamp_on': False,
|
||||
'limit_error': False,
|
||||
'power_cycle_see': False,
|
||||
'raw': 0,
|
||||
'reset_state': False,
|
||||
}),
|
||||
'time_signature': None,
|
||||
'type': 'default',
|
||||
}),
|
||||
]),
|
||||
'info': dict({
|
||||
'descriptor': dict({
|
||||
|
||||
@@ -240,198 +240,3 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.device_3-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max_color_temp_kelvin': 10000,
|
||||
'min_color_temp_kelvin': 1000,
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.COLOR_TEMP: 'color_temp'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.device_3',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'lunatone',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'be37ca9c47c24498a38bc62c7c711840-device3',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.device_3-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'brightness': None,
|
||||
'color_mode': None,
|
||||
'color_temp_kelvin': None,
|
||||
'friendly_name': 'Device 3',
|
||||
'hs_color': None,
|
||||
'max_color_temp_kelvin': 10000,
|
||||
'min_color_temp_kelvin': 1000,
|
||||
'rgb_color': None,
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.COLOR_TEMP: 'color_temp'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
'xy_color': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.device_3',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.device_4-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.RGB: 'rgb'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.device_4',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'lunatone',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'be37ca9c47c24498a38bc62c7c711840-device4',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.device_4-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'brightness': None,
|
||||
'color_mode': None,
|
||||
'friendly_name': 'Device 4',
|
||||
'hs_color': None,
|
||||
'rgb_color': None,
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.RGB: 'rgb'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
'xy_color': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.device_4',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.device_5-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.RGBW: 'rgbw'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.device_5',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'lunatone',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'be37ca9c47c24498a38bc62c7c711840-device5',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.device_5-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'brightness': None,
|
||||
'color_mode': None,
|
||||
'friendly_name': 'Device 5',
|
||||
'hs_color': None,
|
||||
'rgb_color': None,
|
||||
'rgbw_color': None,
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.RGBW: 'rgbw'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
'xy_color': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.device_5',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -95,7 +95,6 @@ async def test_config_entry_not_ready_devices_api_fail(
|
||||
async def test_config_entry_not_ready_no_info_data(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_lunatone_devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the Lunatone configuration entry not ready due to missing info data."""
|
||||
@@ -126,7 +125,6 @@ async def test_config_entry_not_ready_no_devices_data(
|
||||
async def test_config_entry_not_ready_no_serial_number(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_lunatone_devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the Lunatone configuration entry not ready due to a missing serial number."""
|
||||
|
||||
@@ -4,16 +4,9 @@ import copy
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from lunatone_rest_api_client.models import LineStatus
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_RGBW_COLOR,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
@@ -237,125 +230,3 @@ async def test_line_broadcast_line_present(
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert not hass.states.async_entity_ids("light")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"color_temp_kelvin",
|
||||
[10000, 5000, 1000],
|
||||
)
|
||||
async def test_turn_on_with_color_temperature(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_lunatone_devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
color_temp_kelvin: int,
|
||||
) -> None:
|
||||
"""Test the color temperature of the light can be set."""
|
||||
device_id = 3
|
||||
entity_id = f"light.device_{device_id}"
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
async def fake_update():
|
||||
device = mock_lunatone_devices.data.devices[device_id - 1]
|
||||
device.features.switchable.status = True
|
||||
device.features.color_kelvin.status = float(color_temp_kelvin)
|
||||
|
||||
mock_lunatone_devices.async_update.side_effect = fake_update
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_COLOR_TEMP_KELVIN: color_temp_kelvin,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == color_temp_kelvin
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rgb_color",
|
||||
[(255, 128, 0), (0, 255, 128), (128, 0, 255)],
|
||||
)
|
||||
async def test_turn_on_with_rgb_color(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_lunatone_devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
rgb_color: tuple[int, int, int],
|
||||
) -> None:
|
||||
"""Test the RGB color of the light can be set."""
|
||||
device_id = 4
|
||||
entity_id = f"light.device_{device_id}"
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
async def fake_update():
|
||||
device = mock_lunatone_devices.data.devices[device_id - 1]
|
||||
device.features.switchable.status = True
|
||||
device.features.color_rgb.status.red = rgb_color[0] / 255
|
||||
device.features.color_rgb.status.green = rgb_color[1] / 255
|
||||
device.features.color_rgb.status.blue = rgb_color[2] / 255
|
||||
|
||||
mock_lunatone_devices.async_update.side_effect = fake_update
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_RGB_COLOR: rgb_color,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_RGB_COLOR] == rgb_color
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rgbw_color",
|
||||
[(255, 128, 0, 255), (0, 255, 128, 128), (128, 0, 255, 0)],
|
||||
)
|
||||
async def test_turn_on_with_rgbw_color(
|
||||
hass: HomeAssistant,
|
||||
mock_lunatone_info: AsyncMock,
|
||||
mock_lunatone_devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
rgbw_color: tuple[int, int, int, int],
|
||||
) -> None:
|
||||
"""Test the RGBW color of the light can be set."""
|
||||
device_id = 5
|
||||
entity_id = f"light.device_{device_id}"
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
async def fake_update():
|
||||
device = mock_lunatone_devices.data.devices[device_id - 1]
|
||||
device.features.switchable.status = True
|
||||
device.features.color_rgb.status.red = rgbw_color[0] / 255
|
||||
device.features.color_rgb.status.green = rgbw_color[1] / 255
|
||||
device.features.color_rgb.status.blue = rgbw_color[2] / 255
|
||||
device.features.color_waf.status.white = rgbw_color[3] / 255
|
||||
|
||||
mock_lunatone_devices.async_update.side_effect = fake_update
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_RGBW_COLOR: rgbw_color,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_RGBW_COLOR] == rgbw_color
|
||||
|
||||
31
tests/components/min_max/snapshots/test_init.ambr
Normal file
31
tests/components/min_max/snapshots/test_init.ambr
Normal file
@@ -0,0 +1,31 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup_migrates_to_groups
|
||||
dict({
|
||||
'data': dict({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'discovery_keys': dict({
|
||||
}),
|
||||
'domain': 'group',
|
||||
'minor_version': 1,
|
||||
'options': dict({
|
||||
'entities': list([
|
||||
'sensor.input_one',
|
||||
'sensor.input_two',
|
||||
]),
|
||||
'group_type': 'sensor',
|
||||
'hide_members': False,
|
||||
'ignore_non_numeric': False,
|
||||
'name': 'My min_max',
|
||||
'type': 'max',
|
||||
}),
|
||||
'pref_disable_new_entities': False,
|
||||
'pref_disable_polling': False,
|
||||
'source': 'user',
|
||||
'subentries': list([
|
||||
]),
|
||||
'title': 'My min_max',
|
||||
'unique_id': None,
|
||||
'version': 1,
|
||||
})
|
||||
# ---
|
||||
@@ -1,124 +1,16 @@
|
||||
"""Test the Min/Max config flow."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.min_max.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry, get_schema_suggested_value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", ["sensor"])
|
||||
async def test_config_flow(hass: HomeAssistant, platform: str) -> None:
|
||||
"""Test the config flow."""
|
||||
input_sensors = ["sensor.input_one", "sensor.input_two"]
|
||||
async def test_config_flow_aborts(hass: HomeAssistant) -> None:
|
||||
"""Test the config flow aborts."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.min_max.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"name": "My min_max", "entity_ids": input_sensors, "type": "max"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "My min_max"
|
||||
assert result["data"] == {}
|
||||
assert result["options"] == {
|
||||
"entity_ids": input_sensors,
|
||||
"name": "My min_max",
|
||||
"round_digits": 2.0,
|
||||
"type": "max",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
assert config_entry.data == {}
|
||||
assert config_entry.options == {
|
||||
"entity_ids": input_sensors,
|
||||
"name": "My min_max",
|
||||
"round_digits": 2.0,
|
||||
"type": "max",
|
||||
}
|
||||
assert config_entry.title == "My min_max"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", ["sensor"])
|
||||
async def test_options(hass: HomeAssistant, platform: str) -> None:
|
||||
"""Test reconfiguring."""
|
||||
hass.states.async_set("sensor.input_one", "10")
|
||||
hass.states.async_set("sensor.input_two", "20")
|
||||
hass.states.async_set("sensor.input_three", "33.33")
|
||||
|
||||
input_sensors1 = ["sensor.input_one", "sensor.input_two"]
|
||||
input_sensors2 = ["sensor.input_one", "sensor.input_two", "sensor.input_three"]
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
"entity_ids": input_sensors1,
|
||||
"name": "My min_max",
|
||||
"round_digits": 0,
|
||||
"type": "min",
|
||||
},
|
||||
title="My min_max",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
schema = result["data_schema"].schema
|
||||
assert get_schema_suggested_value(schema, "entity_ids") == input_sensors1
|
||||
assert get_schema_suggested_value(schema, "round_digits") == 0
|
||||
assert get_schema_suggested_value(schema, "type") == "min"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"entity_ids": input_sensors2,
|
||||
"round_digits": 1,
|
||||
"type": "mean",
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"entity_ids": input_sensors2,
|
||||
"name": "My min_max",
|
||||
"round_digits": 1,
|
||||
"type": "mean",
|
||||
}
|
||||
assert config_entry.data == {}
|
||||
assert config_entry.options == {
|
||||
"entity_ids": input_sensors2,
|
||||
"name": "My min_max",
|
||||
"round_digits": 1,
|
||||
"type": "mean",
|
||||
}
|
||||
assert config_entry.title == "My min_max"
|
||||
|
||||
# Check config entry is reloaded with new options
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check the entity was updated, no new entity was created
|
||||
assert len(hass.states.async_all()) == 4
|
||||
|
||||
# Check the state of the entity has changed as expected
|
||||
state = hass.states.get(f"{platform}.my_min_max")
|
||||
assert state.state == "21.1"
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "migrated_to_groups"
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
"""Test the Min/Max integration."""
|
||||
|
||||
import pytest
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from syrupy.filters import props
|
||||
|
||||
from homeassistant.components.group import DOMAIN as GROUP_DOMAIN
|
||||
from homeassistant.components.min_max.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", ["sensor"])
|
||||
async def test_setup_and_remove_config_entry(
|
||||
async def test_setup_migrates_to_groups(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
platform: str,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test setting up and removing a config entry."""
|
||||
hass.states.async_set("sensor.input_one", "10")
|
||||
@@ -21,7 +24,7 @@ async def test_setup_and_remove_config_entry(
|
||||
|
||||
input_sensors = ["sensor.input_one", "sensor.input_two"]
|
||||
|
||||
min_max_entity_id = f"{platform}.my_min_max"
|
||||
min_max_entity_id = "sensor.my_min_max"
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
@@ -37,19 +40,29 @@ async def test_setup_and_remove_config_entry(
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Check the entity is registered in the entity registry
|
||||
assert entity_registry.async_get(min_max_entity_id) is not None
|
||||
entity = entity_registry.async_get(min_max_entity_id)
|
||||
assert entity is not None
|
||||
assert entity.config_entry_id is not None
|
||||
assert entity.config_entry_id != config_entry.entry_id
|
||||
assert entity.platform == GROUP_DOMAIN
|
||||
|
||||
# Check the platform is setup correctly
|
||||
assert len(hass.states.async_all()) == 3
|
||||
state = hass.states.get(min_max_entity_id)
|
||||
assert state.state == "20.0"
|
||||
|
||||
# Remove the config entry
|
||||
assert await hass.config_entries.async_remove(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
config_entry = hass.config_entries.async_entries("group")[0]
|
||||
assert config_entry.as_dict() == snapshot(
|
||||
exclude=props("created_at", "entry_id", "modified_at")
|
||||
)
|
||||
config_entry_min_max = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
assert config_entry_min_max
|
||||
|
||||
# Check the state and entity registry entry are removed
|
||||
assert hass.states.get(min_max_entity_id) is None
|
||||
assert entity_registry.async_get(min_max_entity_id) is None
|
||||
freezer.tick(60 * 5)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
config_entry_min_max = hass.config_entries.async_entries(DOMAIN)
|
||||
assert not config_entry_min_max
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import get_fixture_path
|
||||
@@ -42,6 +42,28 @@ RANGE_4_DIGITS = round(max(VALUES) - min(VALUES), 4)
|
||||
SUM_VALUE = sum(VALUES)
|
||||
|
||||
|
||||
async def test_deprecation_warning(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test the min sensor with a default name."""
|
||||
config = {
|
||||
"sensor": {
|
||||
"platform": "min_max",
|
||||
"type": "min",
|
||||
"entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
|
||||
}
|
||||
}
|
||||
|
||||
with patch("homeassistant.util.ulid.ulid", return_value="1234"):
|
||||
assert await async_setup_component(hass, "sensor", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(DOMAIN, "1234")
|
||||
assert issue is not None
|
||||
assert issue.severity == ir.IssueSeverity.WARNING
|
||||
assert issue.translation_key == "yaml_deprecated"
|
||||
|
||||
|
||||
async def test_default_name_sensor(hass: HomeAssistant) -> None:
|
||||
"""Test the min sensor with a default name."""
|
||||
config = {
|
||||
|
||||
@@ -290,9 +290,7 @@ async def test_coordinator_updates(
|
||||
hass: HomeAssistant, side_effect: Exception | None, success: bool
|
||||
) -> None:
|
||||
"""Test the update coordinator update functions."""
|
||||
entry = hass.config_entries.async_get_entry(ENTRY_ID)
|
||||
assert entry is not None
|
||||
coordinator = entry.runtime_data
|
||||
coordinator = hass.data[DOMAIN][ENTRY_ID]
|
||||
|
||||
await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {})
|
||||
|
||||
|
||||
@@ -124,19 +124,6 @@ def mock_smlight_client(request: pytest.FixtureRequest) -> Generator[MagicMock]:
|
||||
yield api
|
||||
|
||||
|
||||
MOCK_ULTIMA = Info(
|
||||
MAC="AA:BB:CC:DD:EE:FF",
|
||||
model="SLZB-Ultima3",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ultima_client(mock_smlight_client: MagicMock) -> MagicMock:
|
||||
"""Configure api client to return an Ultima device."""
|
||||
mock_smlight_client.get_info.side_effect = lambda *arg, **kwargs: MOCK_ULTIMA
|
||||
return mock_smlight_client
|
||||
|
||||
|
||||
async def setup_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from infrared_protocols import Command, Timing
|
||||
from pysmlight import Info
|
||||
from pysmlight.exceptions import SmlightError
|
||||
from pysmlight.models import IRPayload
|
||||
import pytest
|
||||
@@ -35,12 +36,20 @@ def platforms() -> list[Platform]:
|
||||
return [Platform.INFRARED]
|
||||
|
||||
|
||||
MOCK_ULTIMA = Info(
|
||||
MAC="AA:BB:CC:DD:EE:FF",
|
||||
model="SLZB-Ultima3",
|
||||
)
|
||||
|
||||
|
||||
async def test_infrared_setup_ultima(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test infrared entity is created for Ultima devices."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("infrared.mock_title_ir_emitter")
|
||||
@@ -53,6 +62,11 @@ async def test_infrared_not_created_non_ultima(
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test infrared entity is not created for non-Ultima devices."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = Info(
|
||||
MAC="AA:BB:CC:DD:EE:FF",
|
||||
model="SLZB-MR1",
|
||||
)
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("infrared.mock_title_ir_emitter")
|
||||
@@ -62,9 +76,11 @@ async def test_infrared_not_created_non_ultima(
|
||||
async def test_infrared_send_command(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test sending IR command."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "infrared.mock_title_ir_emitter"
|
||||
@@ -77,7 +93,7 @@ async def test_infrared_send_command(
|
||||
MockCommand(),
|
||||
)
|
||||
|
||||
mock_ultima_client.actions.send_ir_code.assert_called_once_with(
|
||||
mock_smlight_client.actions.send_ir_code.assert_called_once_with(
|
||||
IRPayload.from_raw_timings([9000, 4500, 560, 1690], freq=38000)
|
||||
)
|
||||
|
||||
@@ -85,16 +101,18 @@ async def test_infrared_send_command(
|
||||
async def test_infrared_send_command_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test connection error handling."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "infrared.mock_title_ir_emitter"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
|
||||
mock_ultima_client.actions.send_ir_code.side_effect = SmlightError("Failed")
|
||||
mock_smlight_client.actions.send_ir_code.side_effect = SmlightError("Failed")
|
||||
|
||||
with pytest.raises(HomeAssistantError) as exc_info:
|
||||
await async_send_command(
|
||||
@@ -108,16 +126,18 @@ async def test_infrared_send_command_error(
|
||||
async def test_infrared_send_empty_command_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test ValueError from pysmlight is surfaced as HomeAssistantError."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "infrared.mock_title_ir_emitter"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
|
||||
mock_ultima_client.actions.send_ir_code.side_effect = ValueError("empty payload")
|
||||
mock_smlight_client.actions.send_ir_code.side_effect = ValueError("empty payload")
|
||||
|
||||
with pytest.raises(HomeAssistantError) as exc_info:
|
||||
await async_send_command(
|
||||
@@ -132,9 +152,11 @@ async def test_infrared_send_empty_command_error(
|
||||
async def test_infrared_state_updated_after_send(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that entity state is updated with a timestamp after a successful send."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "infrared.mock_title_ir_emitter"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pysmlight import Info
|
||||
from pysmlight.const import AmbiEffect
|
||||
from pysmlight.exceptions import SmlightConnectionError
|
||||
from pysmlight.models import AmbilightPayload
|
||||
@@ -39,11 +40,17 @@ def platforms() -> Platform | list[Platform]:
|
||||
return [Platform.LIGHT]
|
||||
|
||||
|
||||
MOCK_ULTIMA = Info(
|
||||
MAC="AA:BB:CC:DD:EE:FF",
|
||||
model="SLZB-Ultima3",
|
||||
)
|
||||
|
||||
|
||||
def _build_fire_sse_ambilight(
|
||||
hass: HomeAssistant, mock_ultima_client: MagicMock
|
||||
hass: HomeAssistant, mock_smlight_client: MagicMock
|
||||
) -> Callable[[dict[str, object]], Awaitable[None]]:
|
||||
"""Build helper to push ambilight SSE events and wait for state updates."""
|
||||
page_callback = mock_ultima_client.sse.register_page_cb.call_args[0][1]
|
||||
page_callback = mock_smlight_client.sse.register_page_cb.call_args[0][1]
|
||||
|
||||
async def fire_ambi(changes: dict[str, object]) -> None:
|
||||
page_callback(changes)
|
||||
@@ -56,10 +63,12 @@ async def test_light_setup_ultima(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test light entity is created for Ultima devices."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
entry = await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
|
||||
@@ -73,6 +82,11 @@ async def test_light_not_created_non_ultima(
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test light entity is not created for non-Ultima devices."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = Info(
|
||||
MAC="AA:BB:CC:DD:EE:FF",
|
||||
model="SLZB-MR1",
|
||||
)
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("light.mock_title_ambilight")
|
||||
@@ -82,18 +96,20 @@ async def test_light_not_created_non_ultima(
|
||||
async def test_light_turn_on_off(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test turning light on and off."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "light.mock_title_ambilight"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_ultima_client)
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_smlight_client)
|
||||
|
||||
mock_ultima_client.actions.ambilight.reset_mock()
|
||||
mock_smlight_client.actions.ambilight.reset_mock()
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
@@ -101,7 +117,7 @@ async def test_light_turn_on_off(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_ultima_client.actions.ambilight.assert_called_once_with(
|
||||
mock_smlight_client.actions.ambilight.assert_called_once_with(
|
||||
AmbilightPayload(ultLedMode=AmbiEffect.WSULT_SOLID)
|
||||
)
|
||||
await fire_ambi({"ultLedMode": 0, "ultLedBri": 158, "ultLedColor": 0x7FACFF})
|
||||
@@ -109,7 +125,7 @@ async def test_light_turn_on_off(
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
mock_ultima_client.actions.ambilight.reset_mock()
|
||||
mock_smlight_client.actions.ambilight.reset_mock()
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
@@ -117,7 +133,7 @@ async def test_light_turn_on_off(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_ultima_client.actions.ambilight.assert_called_once_with(
|
||||
mock_smlight_client.actions.ambilight.assert_called_once_with(
|
||||
AmbilightPayload(ultLedMode=AmbiEffect.WSULT_OFF)
|
||||
)
|
||||
await fire_ambi({"ultLedMode": 1})
|
||||
@@ -129,18 +145,20 @@ async def test_light_turn_on_off(
|
||||
async def test_light_brightness(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test setting brightness."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "light.mock_title_ambilight"
|
||||
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_ultima_client)
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_smlight_client)
|
||||
|
||||
# Seed current state as on so brightness-only update does not force solid mode.
|
||||
await fire_ambi({"ultLedMode": 0, "ultLedBri": 158, "ultLedColor": 0x7FACFF})
|
||||
mock_ultima_client.actions.ambilight.reset_mock()
|
||||
mock_smlight_client.actions.ambilight.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
@@ -149,7 +167,7 @@ async def test_light_brightness(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_ultima_client.actions.ambilight.assert_called_once_with(
|
||||
mock_smlight_client.actions.ambilight.assert_called_once_with(
|
||||
AmbilightPayload(ultLedBri=200)
|
||||
)
|
||||
await fire_ambi({"ultLedMode": 0, "ultLedBri": 200, "ultLedColor": 0x7FACFF})
|
||||
@@ -162,16 +180,18 @@ async def test_light_brightness(
|
||||
async def test_light_rgb_color(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test setting RGB color."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "light.mock_title_ambilight"
|
||||
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_ultima_client)
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_smlight_client)
|
||||
|
||||
mock_ultima_client.actions.ambilight.reset_mock()
|
||||
mock_smlight_client.actions.ambilight.reset_mock()
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
@@ -179,7 +199,7 @@ async def test_light_rgb_color(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_ultima_client.actions.ambilight.assert_called_once_with(
|
||||
mock_smlight_client.actions.ambilight.assert_called_once_with(
|
||||
AmbilightPayload(ultLedMode=AmbiEffect.WSULT_SOLID, ultLedColor="#ff8040")
|
||||
)
|
||||
await fire_ambi({"ultLedMode": 0, "ultLedColor": 0xFF8040})
|
||||
@@ -192,17 +212,19 @@ async def test_light_rgb_color(
|
||||
async def test_light_effect(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test setting effect."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "light.mock_title_ambilight"
|
||||
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_ultima_client)
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_smlight_client)
|
||||
|
||||
# Test Rainbow effect
|
||||
mock_ultima_client.actions.ambilight.reset_mock()
|
||||
mock_smlight_client.actions.ambilight.reset_mock()
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
@@ -210,13 +232,13 @@ async def test_light_effect(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_ultima_client.actions.ambilight.assert_called_once_with(
|
||||
mock_smlight_client.actions.ambilight.assert_called_once_with(
|
||||
AmbilightPayload(ultLedMode=AmbiEffect.WSULT_RAINBOW)
|
||||
)
|
||||
await fire_ambi({"ultLedMode": 3})
|
||||
|
||||
# Test Blur effect
|
||||
mock_ultima_client.actions.ambilight.reset_mock()
|
||||
mock_smlight_client.actions.ambilight.reset_mock()
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
@@ -224,7 +246,7 @@ async def test_light_effect(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_ultima_client.actions.ambilight.assert_called_once_with(
|
||||
mock_smlight_client.actions.ambilight.assert_called_once_with(
|
||||
AmbilightPayload(ultLedMode=AmbiEffect.WSULT_BLUR)
|
||||
)
|
||||
await fire_ambi({"ultLedMode": 2})
|
||||
@@ -237,14 +259,16 @@ async def test_light_effect(
|
||||
async def test_light_invalid_effect(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test handling of invalid effect name is ignored."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "light.mock_title_ambilight"
|
||||
|
||||
mock_ultima_client.actions.ambilight.reset_mock()
|
||||
mock_smlight_client.actions.ambilight.reset_mock()
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
@@ -252,23 +276,25 @@ async def test_light_invalid_effect(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_ultima_client.actions.ambilight.assert_not_called()
|
||||
mock_smlight_client.actions.ambilight.assert_not_called()
|
||||
|
||||
|
||||
async def test_light_turn_on_when_on_is_noop(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test calling turn_on with no attributes does nothing when already on."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "light.mock_title_ambilight"
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_ultima_client)
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_smlight_client)
|
||||
|
||||
await fire_ambi({"ultLedMode": 0})
|
||||
|
||||
mock_ultima_client.actions.ambilight.reset_mock()
|
||||
mock_smlight_client.actions.ambilight.reset_mock()
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
@@ -276,19 +302,21 @@ async def test_light_turn_on_when_on_is_noop(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_ultima_client.actions.ambilight.assert_not_called()
|
||||
mock_smlight_client.actions.ambilight.assert_not_called()
|
||||
|
||||
|
||||
async def test_light_state_handles_invalid_attributes_from_sse(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test state update gracefully handles invalid mode and invalid hex color."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "light.mock_title_ambilight"
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_ultima_client)
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_smlight_client)
|
||||
|
||||
await fire_ambi({"ultLedMode": None, "ultLedColor": "#GG0000"})
|
||||
|
||||
@@ -307,16 +335,18 @@ async def test_light_state_handles_invalid_attributes_from_sse(
|
||||
async def test_ambilight_connection_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ultima_client: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test connection error handling."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = MOCK_ULTIMA
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = "light.mock_title_ambilight"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
mock_ultima_client.actions.ambilight.side_effect = SmlightConnectionError
|
||||
mock_smlight_client.actions.ambilight.side_effect = SmlightConnectionError
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
@@ -326,10 +356,10 @@ async def test_ambilight_connection_error(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_ultima_client.actions.ambilight.side_effect = None
|
||||
mock_ultima_client.actions.ambilight.reset_mock()
|
||||
mock_smlight_client.actions.ambilight.side_effect = None
|
||||
mock_smlight_client.actions.ambilight.reset_mock()
|
||||
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_ultima_client)
|
||||
fire_ambi = _build_fire_sse_ambilight(hass, mock_smlight_client)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
@@ -338,7 +368,7 @@ async def test_ambilight_connection_error(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_ultima_client.actions.ambilight.assert_called_once_with(
|
||||
mock_smlight_client.actions.ambilight.assert_called_once_with(
|
||||
AmbilightPayload(ultLedMode=AmbiEffect.WSULT_SOLID)
|
||||
)
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.subaru.const import DOMAIN
|
||||
from homeassistant.components.subaru.sensor import (
|
||||
API_GEN_2_SENSORS,
|
||||
DOMAIN,
|
||||
EV_SENSORS,
|
||||
SAFETY_SENSORS,
|
||||
)
|
||||
|
||||
@@ -221,113 +221,6 @@ async def test_bluetooth_discovery_key(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_bluetooth_discovery_encrypted_key_back_navigation(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that resuming an abandoned encrypted_key flow resets to the method menu."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=WOLOCK_SERVICE_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
|
||||
# User selects encrypted_key
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "encrypted_key"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "encrypted_key"
|
||||
|
||||
# Simulate user closing dialog and re-opening: call the step with no input
|
||||
# (as HA does when resuming an in-progress flow)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=None
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
|
||||
# User can now pick a method again and complete the flow
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "encrypted_key"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "encrypted_key"
|
||||
|
||||
with (
|
||||
patch_async_setup_entry() as mock_setup_entry,
|
||||
patch(
|
||||
"switchbot.SwitchbotLock.verify_encryption_key",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_KEY_ID: "ff",
|
||||
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_bluetooth_discovery_encrypted_auth_back_navigation(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that resuming an abandoned encrypted_auth flow resets to the method menu."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=WOLOCK_SERVICE_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
|
||||
# User selects encrypted_auth
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "encrypted_auth"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "encrypted_auth"
|
||||
|
||||
# Simulate user closing dialog and re-opening: call the step with no input
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=None
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
|
||||
# User can switch to encrypted_key and complete the flow
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "encrypted_key"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "encrypted_key"
|
||||
|
||||
with (
|
||||
patch_async_setup_entry() as mock_setup_entry,
|
||||
patch(
|
||||
"switchbot.SwitchbotLock.verify_encryption_key",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_KEY_ID: "ff",
|
||||
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None:
|
||||
"""Test discovery via bluetooth with a valid device when already setup."""
|
||||
entry = MockConfigEntry(
|
||||
@@ -1315,22 +1208,12 @@ async def test_user_cloud_login_then_encrypted_device(hass: HomeAssistant) -> No
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "encrypted_auth"
|
||||
|
||||
# Simulate the user navigating away and re-opening the dialog.
|
||||
# The failed auto-auth cleared credentials, so calling with None now
|
||||
# redirects back to the method selection menu.
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
None,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
|
||||
# User selects encrypted_auth again and manually enters credentials
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "encrypted_auth"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "encrypted_auth"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user