Compare commits

..

12 Commits

Author SHA1 Message Date
G Johansson
7f1846b34a comments 2026-04-14 20:03:09 +00:00
G Johansson
44b071762d Mods 2026-04-14 20:01:42 +00:00
G Johansson
f006283ee8 Mods 2026-04-14 19:58:59 +00:00
G Johansson
d25f3c6d2e Delete 2026-04-14 19:46:44 +00:00
G Johansson
f821952918 Test 2026-04-14 19:46:03 +00:00
G Johansson
49ce8ed944 Mods 2026-04-14 19:42:59 +00:00
G Johansson
d4fca3737d Mods 2026-04-14 19:34:11 +00:00
G Johansson
26d8dfb695 Mods 2026-04-14 16:53:14 +00:00
G Johansson
6e92ba2fc0 Mods 2026-04-14 16:35:17 +00:00
G Johansson
7288d19abf Mods 2026-04-10 14:53:04 +00:00
G Johansson
5bbfe69bbb import 2026-04-10 14:12:23 +00:00
G Johansson
564280cc65 Deprecate min_max and migrate to group sensor 2026-04-08 20:21:06 +00:00
109 changed files with 1348 additions and 4078 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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,
)

View File

@@ -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

View File

@@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["anthropic==0.92.0"]
"requirements": ["anthropic==0.83.0"]
}

View File

@@ -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*",

View File

@@ -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]

View File

@@ -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

View File

@@ -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"

View File

@@ -15,14 +15,7 @@
"name": "Button"
},
"doorbell": {
"name": "Doorbell",
"state_attributes": {
"event_type": {
"state": {
"ring": "Ring"
}
}
}
"name": "Doorbell"
},
"motion": {
"name": "Motion"

View File

@@ -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),
}
)

View File

@@ -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"],

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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):

View File

@@ -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]

View File

@@ -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()

View File

@@ -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()

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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")

View File

@@ -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)]

View File

@@ -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": {

View File

@@ -8,5 +8,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["pybotvac"],
"requirements": ["pybotvac==0.0.29"]
"requirements": ["pybotvac==0.0.28"]
}

View File

@@ -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

View File

@@ -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:

View File

@@ -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(

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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": {

View File

@@ -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

View File

@@ -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]

View File

@@ -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()

View File

@@ -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

View File

@@ -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:

View File

@@ -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]

View File

@@ -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"]:

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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:

View File

@@ -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()

View File

@@ -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()

View File

@@ -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):

View File

@@ -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()

View File

@@ -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"

View File

@@ -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]],

View File

@@ -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()

View File

@@ -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]

View File

@@ -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()

View File

@@ -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():

View File

@@ -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]

View File

@@ -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

View File

@@ -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()

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_push",
"loggers": ["aiotractive"],
"requirements": ["aiotractive==1.0.2"]
"requirements": ["aiotractive==1.0.1"]
}

View File

@@ -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)
]

View File

@@ -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."""

View File

@@ -14,5 +14,5 @@
"iot_class": "local_polling",
"loggers": ["pyvlx"],
"quality_scale": "silver",
"requirements": ["pyvlx==0.2.33"]
"requirements": ["pyvlx==0.2.32"]
}

View File

@@ -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:

View File

@@ -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)

View File

@@ -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]:

View File

@@ -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",

View File

@@ -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)

View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -62,7 +62,7 @@
'data': dict({
'host': 'fake_host',
'password': '**REDACTED**',
'port': 1234,
'port': '1234',
'ssl': False,
'username': '**REDACTED**',
}),

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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',
})
# ---

View File

@@ -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
)

View File

@@ -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",

View File

@@ -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,
),
]

View File

@@ -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

View File

@@ -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({

View File

@@ -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',
})
# ---

View File

@@ -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."""

View File

@@ -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

View 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,
})
# ---

View File

@@ -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"

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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, {})

View File

@@ -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,

View File

@@ -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"

View File

@@ -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)
)

View File

@@ -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,
)

View File

@@ -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