mirror of
https://github.com/home-assistant/core.git
synced 2026-03-28 18:40:26 +01:00
Compare commits
29 Commits
knx-expose
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2285db5bb1 | ||
|
|
738b85c17d | ||
|
|
b7bb185d50 | ||
|
|
f4544cf952 | ||
|
|
beab473dcc | ||
|
|
96891228c9 | ||
|
|
a4a36b5cbd | ||
|
|
4a0a400e22 | ||
|
|
fbe4195ae0 | ||
|
|
116fa57903 | ||
|
|
2399da93db | ||
|
|
3850bb0e57 | ||
|
|
f45c84b2a8 | ||
|
|
a2e60f84da | ||
|
|
3757289c73 | ||
|
|
09067a18b7 | ||
|
|
6eb834946b | ||
|
|
0e1663f259 | ||
|
|
0ba3a94a3b | ||
|
|
3562a3800f | ||
|
|
de0efa1639 | ||
|
|
818cf41c22 | ||
|
|
25bfb16936 | ||
|
|
75782e6f17 | ||
|
|
3e5c291338 | ||
|
|
30163fa2e7 | ||
|
|
16231d8d36 | ||
|
|
0c0d6595d6 | ||
|
|
a443060faa |
@@ -45,9 +45,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
|
||||
try:
|
||||
await client.models.list(timeout=10.0)
|
||||
except anthropic.AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_authentication_error",
|
||||
translation_placeholders={"message": err.message},
|
||||
) from err
|
||||
except anthropic.AnthropicError as err:
|
||||
raise ConfigEntryNotReady(err) from err
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={
|
||||
"message": err.message
|
||||
if isinstance(err, anthropic.APIError)
|
||||
else str(err)
|
||||
},
|
||||
) from err
|
||||
|
||||
entry.runtime_data = client
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.json import json_loads
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import AnthropicBaseLLMEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -60,7 +61,7 @@ class AnthropicTaskEntity(
|
||||
|
||||
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
|
||||
raise HomeAssistantError(
|
||||
"Last content in chat log is not an AssistantContent"
|
||||
translation_domain=DOMAIN, translation_key="response_not_found"
|
||||
)
|
||||
|
||||
text = chat_log.content[-1].content or ""
|
||||
@@ -78,7 +79,9 @@ class AnthropicTaskEntity(
|
||||
err,
|
||||
text,
|
||||
)
|
||||
raise HomeAssistantError("Error with Claude structured response") from err
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="json_parse_error"
|
||||
) from err
|
||||
|
||||
return ai_task.GenDataTaskResult(
|
||||
conversation_id=chat_log.conversation_id,
|
||||
|
||||
@@ -401,7 +401,11 @@ def _convert_content(
|
||||
messages[-1]["content"] = messages[-1]["content"][0]["text"]
|
||||
else:
|
||||
# Note: We don't pass SystemContent here as it's passed to the API as the prompt
|
||||
raise HomeAssistantError("Unexpected content type in chat log")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unexpected_chat_log_content",
|
||||
translation_placeholders={"type": type(content).__name__},
|
||||
)
|
||||
|
||||
return messages, container_id
|
||||
|
||||
@@ -443,7 +447,9 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
||||
Each message could contain multiple blocks of the same type.
|
||||
"""
|
||||
if stream is None or not hasattr(stream, "__aiter__"):
|
||||
raise HomeAssistantError("Expected a stream of messages")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="unexpected_stream_object"
|
||||
)
|
||||
|
||||
current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None
|
||||
current_tool_args: str
|
||||
@@ -605,7 +611,9 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
||||
chat_log.async_trace(_create_token_stats(input_usage, usage))
|
||||
content_details.container = response.delta.container
|
||||
if response.delta.stop_reason == "refusal":
|
||||
raise HomeAssistantError("Potential policy violation detected")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="api_refusal"
|
||||
)
|
||||
elif isinstance(response, RawMessageStopEvent):
|
||||
if content_details:
|
||||
content_details.delete_empty()
|
||||
@@ -664,7 +672,9 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
|
||||
system = chat_log.content[0]
|
||||
if not isinstance(system, conversation.SystemContent):
|
||||
raise HomeAssistantError("First message must be a system message")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="system_message_not_found"
|
||||
)
|
||||
|
||||
# System prompt with caching enabled
|
||||
system_prompt: list[TextBlockParam] = [
|
||||
@@ -754,7 +764,7 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
last_message = messages[-1]
|
||||
if last_message["role"] != "user":
|
||||
raise HomeAssistantError(
|
||||
"Last message must be a user message to add attachments"
|
||||
translation_domain=DOMAIN, translation_key="user_message_not_found"
|
||||
)
|
||||
if isinstance(last_message["content"], str):
|
||||
last_message["content"] = [
|
||||
@@ -859,11 +869,19 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
except anthropic.AuthenticationError as err:
|
||||
self.entry.async_start_reauth(self.hass)
|
||||
raise HomeAssistantError(
|
||||
"Authentication error with Anthropic API, reauthentication required"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_authentication_error",
|
||||
translation_placeholders={"message": err.message},
|
||||
) from err
|
||||
except anthropic.AnthropicError as err:
|
||||
raise HomeAssistantError(
|
||||
f"Sorry, I had a problem talking to Anthropic: {err}"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={
|
||||
"message": err.message
|
||||
if isinstance(err, anthropic.APIError)
|
||||
else str(err)
|
||||
},
|
||||
) from err
|
||||
|
||||
if not chat_log.unresponded_tool_results:
|
||||
@@ -883,15 +901,23 @@ async def async_prepare_files_for_prompt(
|
||||
|
||||
for file_path, mime_type in files:
|
||||
if not file_path.exists():
|
||||
raise HomeAssistantError(f"`{file_path}` does not exist")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="wrong_file_path",
|
||||
translation_placeholders={"file_path": file_path.as_posix()},
|
||||
)
|
||||
|
||||
if mime_type is None:
|
||||
mime_type = guess_file_type(file_path)[0]
|
||||
|
||||
if not mime_type or not mime_type.startswith(("image/", "application/pdf")):
|
||||
raise HomeAssistantError(
|
||||
"Only images and PDF are supported by the Anthropic API,"
|
||||
f"`{file_path}` is not an image file or PDF"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="wrong_file_type",
|
||||
translation_placeholders={
|
||||
"file_path": file_path.as_posix(),
|
||||
"mime_type": mime_type or "unknown",
|
||||
},
|
||||
)
|
||||
if mime_type == "image/jpg":
|
||||
mime_type = "image/jpeg"
|
||||
|
||||
@@ -88,7 +88,7 @@ rules:
|
||||
comment: |
|
||||
No entities disabled by default.
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
repair-issues: done
|
||||
|
||||
@@ -161,7 +161,9 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
|
||||
is None
|
||||
or (subentry := entry.subentries.get(self._current_subentry_id)) is None
|
||||
):
|
||||
raise HomeAssistantError("Subentry not found")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="subentry_not_found"
|
||||
)
|
||||
|
||||
updated_data = {
|
||||
**subentry.data,
|
||||
@@ -190,4 +192,6 @@ async def async_create_fix_flow(
|
||||
"""Create flow."""
|
||||
if issue_id == "model_deprecated":
|
||||
return ModelDeprecatedRepairFlow()
|
||||
raise HomeAssistantError("Unknown issue ID")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="unknown_issue_id"
|
||||
)
|
||||
|
||||
@@ -149,6 +149,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"api_authentication_error": {
|
||||
"message": "Authentication error with Anthropic API: {message}. Reauthentication required."
|
||||
},
|
||||
"api_error": {
|
||||
"message": "Anthropic API error: {message}."
|
||||
},
|
||||
"api_refusal": {
|
||||
"message": "Potential policy violation detected."
|
||||
},
|
||||
"json_parse_error": {
|
||||
"message": "Error with Claude structured response."
|
||||
},
|
||||
"response_not_found": {
|
||||
"message": "Last content in chat log is not an AssistantContent."
|
||||
},
|
||||
"subentry_not_found": {
|
||||
"message": "Subentry not found."
|
||||
},
|
||||
"system_message_not_found": {
|
||||
"message": "First message must be a system message."
|
||||
},
|
||||
"unexpected_chat_log_content": {
|
||||
"message": "Unexpected content type in chat log: {type}."
|
||||
},
|
||||
"unexpected_stream_object": {
|
||||
"message": "Expected a stream of messages."
|
||||
},
|
||||
"unknown_issue_id": {
|
||||
"message": "Unknown issue ID."
|
||||
},
|
||||
"user_message_not_found": {
|
||||
"message": "Last message must be a user message to add attachments."
|
||||
},
|
||||
"wrong_file_path": {
|
||||
"message": "`{file_path}` does not exist."
|
||||
},
|
||||
"wrong_file_type": {
|
||||
"message": "Only images and PDF are supported by the Anthropic API, `{file_path}` ({mime_type}) is not an image file or PDF."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"model_deprecated": {
|
||||
"fix_flow": {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyblu==2.0.5"],
|
||||
"requirements": ["pyblu==2.0.6"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_musc._tcp.local."
|
||||
|
||||
@@ -11,7 +11,12 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.LIGHT,
|
||||
Platform.SELECT,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:
|
||||
|
||||
@@ -12,5 +12,7 @@ SORTED_BRIGHTNESS_LEVELS = sorted(BRIGHTNESS_LEVELS)
|
||||
|
||||
DEFAULT_DIMMING_TIME_MINUTES: int = DIMMING_TIME_MINUTES[0]
|
||||
|
||||
DIMMING_TIME_OPTIONS: tuple[str, ...] = tuple(str(m) for m in DIMMING_TIME_MINUTES)
|
||||
|
||||
# Interval between periodic state polls to catch externally-triggered changes.
|
||||
STATE_POLL_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.components.bluetooth.active_update_coordinator import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import STATE_POLL_INTERVAL
|
||||
from .const import SORTED_BRIGHTNESS_LEVELS, STATE_POLL_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,6 +51,15 @@ class CasperGlowCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
|
||||
)
|
||||
self.title = title
|
||||
|
||||
# The device API couples brightness and dimming time into a
|
||||
# single command (set_brightness_and_dimming_time), so both
|
||||
# values must be tracked here for cross-entity use.
|
||||
self.last_brightness_pct: int = (
|
||||
device.state.brightness_level
|
||||
if device.state.brightness_level is not None
|
||||
else SORTED_BRIGHTNESS_LEVELS[0]
|
||||
)
|
||||
|
||||
@callback
|
||||
def _needs_poll(
|
||||
self,
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
"resume": {
|
||||
"default": "mdi:play"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"dimming_time": {
|
||||
"default": "mdi:timer-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
if state.brightness_level is not None:
|
||||
self._attr_brightness = _device_pct_to_ha_brightness(state.brightness_level)
|
||||
self.coordinator.last_brightness_pct = state.brightness_level
|
||||
|
||||
@callback
|
||||
def _async_handle_state_update(self, state: GlowState) -> None:
|
||||
@@ -97,6 +98,7 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
|
||||
)
|
||||
)
|
||||
self._attr_brightness = _device_pct_to_ha_brightness(brightness_pct)
|
||||
self.coordinator.last_brightness_pct = brightness_pct
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
|
||||
@@ -52,8 +52,10 @@ rules:
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-category: done
|
||||
entity-device-class:
|
||||
status: exempt
|
||||
comment: No applicable device classes for binary_sensor, button, light, or select entities.
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
|
||||
92
homeassistant/components/casper_glow/select.py
Normal file
92
homeassistant/components/casper_glow/select.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Casper Glow integration select platform for dimming time."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.const import EntityCategory, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import DIMMING_TIME_OPTIONS
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
from .entity import CasperGlowEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: CasperGlowConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the select platform for Casper Glow."""
|
||||
async_add_entities([CasperGlowDimmingTimeSelect(entry.runtime_data)])
|
||||
|
||||
|
||||
class CasperGlowDimmingTimeSelect(CasperGlowEntity, SelectEntity, RestoreEntity):
|
||||
"""Select entity for Casper Glow dimming time."""
|
||||
|
||||
_attr_translation_key = "dimming_time"
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_options = list(DIMMING_TIME_OPTIONS)
|
||||
_attr_unit_of_measurement = UnitOfTime.MINUTES
|
||||
|
||||
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
|
||||
"""Initialize the dimming time select entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_dimming_time"
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the currently selected dimming time from the coordinator."""
|
||||
if self.coordinator.last_dimming_time_minutes is None:
|
||||
return None
|
||||
return str(self.coordinator.last_dimming_time_minutes)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last known dimming time and register state update callback."""
|
||||
await super().async_added_to_hass()
|
||||
if self.coordinator.last_dimming_time_minutes is None and (
|
||||
last_state := await self.async_get_last_state()
|
||||
):
|
||||
if last_state.state in DIMMING_TIME_OPTIONS:
|
||||
self.coordinator.last_dimming_time_minutes = int(last_state.state)
|
||||
self.async_on_remove(
|
||||
self._device.register_callback(self._async_handle_state_update)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_handle_state_update(self, state: GlowState) -> None:
|
||||
"""Handle a state update from the device."""
|
||||
if state.brightness_level is not None:
|
||||
self.coordinator.last_brightness_pct = state.brightness_level
|
||||
if (
|
||||
state.configured_dimming_time_minutes is not None
|
||||
and self.coordinator.last_dimming_time_minutes is None
|
||||
):
|
||||
self.coordinator.last_dimming_time_minutes = (
|
||||
state.configured_dimming_time_minutes
|
||||
)
|
||||
# Dimming time is not part of the device state
|
||||
# that is provided via BLE update. Therefore
|
||||
# we need to trigger a state update for the select entity
|
||||
# to update the current state.
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set the dimming time."""
|
||||
await self._async_command(
|
||||
self._device.set_brightness_and_dimming_time(
|
||||
self.coordinator.last_brightness_pct, int(option)
|
||||
)
|
||||
)
|
||||
self.coordinator.last_dimming_time_minutes = int(option)
|
||||
# Dimming time is not part of the device state
|
||||
# that is provided via BLE update. Therefore
|
||||
# we need to trigger a state update for the select entity
|
||||
# to update the current state.
|
||||
self.async_write_ha_state()
|
||||
@@ -39,6 +39,11 @@
|
||||
"resume": {
|
||||
"name": "Resume dimming"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"dimming_time": {
|
||||
"name": "Dimming time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -9,7 +9,6 @@ from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import CLIMATE
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
@@ -92,7 +91,7 @@ async def async_setup_entry(
|
||||
|
||||
entities: list[ClimateEntity] = []
|
||||
for device in coordinator.data[CLIMATE].values():
|
||||
values = load_api_data(device, CLIMATE_DOMAIN)
|
||||
values = load_api_data(device, "climate")
|
||||
if values[0] == 0 and values[4] == 0:
|
||||
# No climate data, device is only a humidifier/dehumidifier
|
||||
|
||||
@@ -140,7 +139,7 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
|
||||
def _update_attributes(self) -> None:
|
||||
"""Update class attributes."""
|
||||
device = self.coordinator.data[CLIMATE][self._device.index]
|
||||
values = load_api_data(device, CLIMATE_DOMAIN)
|
||||
values = load_api_data(device, "climate")
|
||||
|
||||
_active = values[1]
|
||||
_mode = values[2] # Values from API: "O", "L", "U"
|
||||
|
||||
@@ -9,7 +9,6 @@ from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import CLIMATE
|
||||
|
||||
from homeassistant.components.humidifier import (
|
||||
DOMAIN as HUMIDIFIER_DOMAIN,
|
||||
MODE_AUTO,
|
||||
MODE_NORMAL,
|
||||
HumidifierAction,
|
||||
@@ -68,7 +67,7 @@ async def async_setup_entry(
|
||||
|
||||
entities: list[ComelitHumidifierEntity] = []
|
||||
for device in coordinator.data[CLIMATE].values():
|
||||
values = load_api_data(device, HUMIDIFIER_DOMAIN)
|
||||
values = load_api_data(device, "humidifier")
|
||||
if values[0] == 0 and values[4] == 0:
|
||||
# No humidity data, device is only a climate
|
||||
|
||||
@@ -142,7 +141,7 @@ class ComelitHumidifierEntity(ComelitBridgeBaseEntity, HumidifierEntity):
|
||||
def _update_attributes(self) -> None:
|
||||
"""Update class attributes."""
|
||||
device = self.coordinator.data[CLIMATE][self._device.index]
|
||||
values = load_api_data(device, HUMIDIFIER_DOMAIN)
|
||||
values = load_api_data(device, "humidifier")
|
||||
|
||||
_active = values[1]
|
||||
_mode = values[2] # Values from API: "O", "L", "U"
|
||||
|
||||
@@ -113,9 +113,6 @@
|
||||
"humidity_while_off": {
|
||||
"message": "Cannot change humidity while off"
|
||||
},
|
||||
"invalid_clima_data": {
|
||||
"message": "Invalid 'clima' data"
|
||||
},
|
||||
"update_failed": {
|
||||
"message": "Failed to update data: {error}"
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Any, Concatenate
|
||||
from typing import TYPE_CHECKING, Any, Concatenate, Literal
|
||||
|
||||
from aiocomelit.api import ComelitSerialBridgeObject
|
||||
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
|
||||
from aiohttp import ClientSession, CookieJar
|
||||
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -30,17 +29,19 @@ async def async_client_session(hass: HomeAssistant) -> ClientSession:
|
||||
)
|
||||
|
||||
|
||||
def load_api_data(device: ComelitSerialBridgeObject, domain: str) -> list[Any]:
|
||||
def load_api_data(
|
||||
device: ComelitSerialBridgeObject,
|
||||
domain: Literal["climate", "humidifier"],
|
||||
) -> list[Any]:
|
||||
"""Load data from the API."""
|
||||
# This function is called when the data is loaded from the API
|
||||
if not isinstance(device.val, list):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=domain, translation_key="invalid_clima_data"
|
||||
)
|
||||
# This function is called when the data is loaded from the API.
|
||||
# For climate and humidifier device.val is always a list.
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(device.val, list)
|
||||
# CLIMATE has a 2 item tuple:
|
||||
# - first for Clima
|
||||
# - second for Humidifier
|
||||
return device.val[0] if domain == CLIMATE_DOMAIN else device.val[1]
|
||||
return device.val[0] if domain == "climate" else device.val[1]
|
||||
|
||||
|
||||
async def cleanup_stale_entity(
|
||||
|
||||
@@ -7,7 +7,7 @@ from homelink.mqtt_provider import MQTTProvider
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||
|
||||
from . import oauth2
|
||||
@@ -29,11 +29,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) ->
|
||||
hass, DOMAIN, auth_implementation
|
||||
)
|
||||
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
try:
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
)
|
||||
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
authenticated_session = oauth2.AsyncConfigEntryAuth(
|
||||
|
||||
@@ -49,5 +49,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GeocachingConfigEntry, GeocachingDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
@@ -14,7 +17,13 @@ PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: GeocachingConfigEntry) -> bool:
|
||||
"""Set up Geocaching from a config entry."""
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
|
||||
oauth_session = OAuth2Session(hass, entry, implementation)
|
||||
coordinator = GeocachingDataUpdateCoordinator(
|
||||
|
||||
@@ -65,5 +65,10 @@
|
||||
"unit_of_measurement": "souvenirs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.exceptions import (
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, discovery, intent
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
@@ -52,7 +53,13 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Google Assistant SDK from a config entry."""
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
try:
|
||||
await session.async_ensure_token_valid()
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
"grpc_error": {
|
||||
"message": "Failed to communicate with Google Assistant"
|
||||
},
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
},
|
||||
"reauth_required": {
|
||||
"message": "Credentials are invalid, re-authentication required"
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ from __future__ import annotations
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
@@ -34,7 +36,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) -> bool:
|
||||
"""Set up Google Mail from a config entry."""
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
auth = AsyncConfigEntryAuth(hass, session)
|
||||
await auth.check_and_refresh_token()
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"exceptions": {
|
||||
"missing_from_for_alias": {
|
||||
"message": "Missing 'from' email when setting an alias to show. You have to provide a 'from' email"
|
||||
},
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.exceptions import (
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
@@ -40,7 +41,13 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: GoogleSheetsConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Google Sheets from a config entry."""
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
try:
|
||||
await session.async_ensure_token_valid()
|
||||
|
||||
@@ -42,6 +42,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"append_sheet": {
|
||||
"description": "Appends data to a worksheet in Google Sheets.",
|
||||
|
||||
@@ -25,11 +25,17 @@ PLATFORMS: list[Platform] = [Platform.TODO]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: GoogleTasksConfigEntry) -> bool:
|
||||
"""Set up Google Tasks from a config entry."""
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
try:
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
)
|
||||
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
auth = api.AsyncConfigEntryAuth(hass, session)
|
||||
try:
|
||||
|
||||
@@ -42,5 +42,10 @@
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["python_qube_heatpump"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-qube-heatpump==1.7.0"]
|
||||
"requirements": ["python-qube-heatpump==1.8.0"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ from .const import DOMAIN
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
PLATFORMS = [Platform.NOTIFY]
|
||||
PLATFORMS = [Platform.EVENT, Platform.NOTIFY]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -7,3 +7,9 @@ SERVICE_DISMISS = "dismiss"
|
||||
ATTR_VAPID_PUB_KEY = "vapid_pub_key"
|
||||
ATTR_VAPID_PRV_KEY = "vapid_prv_key"
|
||||
ATTR_VAPID_EMAIL = "vapid_email"
|
||||
|
||||
REGISTRATIONS_FILE = "html5_push_registrations.conf"
|
||||
|
||||
ATTR_ACTION = "action"
|
||||
ATTR_DATA = "data"
|
||||
ATTR_TAG = "tag"
|
||||
|
||||
73
homeassistant/components/html5/entity.py
Normal file
73
homeassistant/components/html5/entity.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Base entities for HTML5 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NotRequired, TypedDict
|
||||
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class Keys(TypedDict):
|
||||
"""Types for keys."""
|
||||
|
||||
p256dh: str
|
||||
auth: str
|
||||
|
||||
|
||||
class Subscription(TypedDict):
|
||||
"""Types for subscription."""
|
||||
|
||||
endpoint: str
|
||||
expirationTime: int | None
|
||||
keys: Keys
|
||||
|
||||
|
||||
class Registration(TypedDict):
|
||||
"""Types for registration."""
|
||||
|
||||
subscription: Subscription
|
||||
browser: str
|
||||
name: NotRequired[str]
|
||||
|
||||
|
||||
class HTML5Entity(Entity):
|
||||
"""Base entity for HTML5 integration."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_key: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
target: str,
|
||||
registrations: dict[str, Registration],
|
||||
session: ClientSession,
|
||||
json_path: str,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.config_entry = config_entry
|
||||
self.target = target
|
||||
self.registrations = registrations
|
||||
self.registration = registrations[target]
|
||||
self.session = session
|
||||
self.json_path = json_path
|
||||
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{target}_{self._key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
name=target,
|
||||
model=self.registration["browser"].capitalize(),
|
||||
identifiers={(DOMAIN, f"{config_entry.entry_id}_{target}")},
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available and self.target in self.registrations
|
||||
67
homeassistant/components/html5/event.py
Normal file
67
homeassistant/components/html5/event.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Event platform for HTML5 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.event import EventEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import ATTR_ACTION, ATTR_DATA, ATTR_TAG, DOMAIN, REGISTRATIONS_FILE
|
||||
from .entity import HTML5Entity
|
||||
from .notify import _load_config
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the event entity platform."""
|
||||
|
||||
json_path = hass.config.path(REGISTRATIONS_FILE)
|
||||
registrations = await hass.async_add_executor_job(_load_config, json_path)
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
async_add_entities(
|
||||
HTML5EventEntity(config_entry, target, registrations, session, json_path)
|
||||
for target in registrations
|
||||
)
|
||||
|
||||
|
||||
class HTML5EventEntity(HTML5Entity, EventEntity):
|
||||
"""Representation of an event entity."""
|
||||
|
||||
_key = "event"
|
||||
_attr_event_types = ["clicked", "received", "closed"]
|
||||
_attr_translation_key = "event"
|
||||
|
||||
@callback
|
||||
def _async_handle_event(
|
||||
self, target: str, event_type: str, event_data: dict[str, Any]
|
||||
) -> None:
|
||||
"""Handle the event."""
|
||||
|
||||
if target == self.target:
|
||||
self._trigger_event(
|
||||
event_type,
|
||||
{
|
||||
**event_data.get(ATTR_DATA, {}),
|
||||
ATTR_ACTION: event_data.get(ATTR_ACTION),
|
||||
ATTR_TAG: event_data.get(ATTR_TAG),
|
||||
},
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register event callback."""
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, DOMAIN, self._async_handle_event)
|
||||
)
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"entity": {
|
||||
"event": {
|
||||
"event": {
|
||||
"default": "mdi:gesture-tap-button"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"dismiss": {
|
||||
"service": "mdi:bell-off"
|
||||
|
||||
@@ -8,7 +8,7 @@ from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, NotRequired, TypedDict, cast
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from urllib.parse import urlparse
|
||||
import uuid
|
||||
|
||||
@@ -38,7 +38,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.json import save_json
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -46,17 +46,19 @@ from homeassistant.util import ensure_unique_string
|
||||
from homeassistant.util.json import load_json_object
|
||||
|
||||
from .const import (
|
||||
ATTR_ACTION,
|
||||
ATTR_TAG,
|
||||
ATTR_VAPID_EMAIL,
|
||||
ATTR_VAPID_PRV_KEY,
|
||||
ATTR_VAPID_PUB_KEY,
|
||||
DOMAIN,
|
||||
REGISTRATIONS_FILE,
|
||||
SERVICE_DISMISS,
|
||||
)
|
||||
from .entity import HTML5Entity, Registration
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REGISTRATIONS_FILE = "html5_push_registrations.conf"
|
||||
|
||||
|
||||
ATTR_SUBSCRIPTION = "subscription"
|
||||
ATTR_BROWSER = "browser"
|
||||
@@ -67,8 +69,6 @@ ATTR_AUTH = "auth"
|
||||
ATTR_P256DH = "p256dh"
|
||||
ATTR_EXPIRATIONTIME = "expirationTime"
|
||||
|
||||
ATTR_TAG = "tag"
|
||||
ATTR_ACTION = "action"
|
||||
ATTR_ACTIONS = "actions"
|
||||
ATTR_TYPE = "type"
|
||||
ATTR_URL = "url"
|
||||
@@ -156,29 +156,6 @@ HTML5_SHOWNOTIFICATION_PARAMETERS = (
|
||||
)
|
||||
|
||||
|
||||
class Keys(TypedDict):
|
||||
"""Types for keys."""
|
||||
|
||||
p256dh: str
|
||||
auth: str
|
||||
|
||||
|
||||
class Subscription(TypedDict):
|
||||
"""Types for subscription."""
|
||||
|
||||
endpoint: str
|
||||
expirationTime: int | None
|
||||
keys: Keys
|
||||
|
||||
|
||||
class Registration(TypedDict):
|
||||
"""Types for registration."""
|
||||
|
||||
subscription: Subscription
|
||||
browser: str
|
||||
name: NotRequired[str]
|
||||
|
||||
|
||||
async def async_get_service(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
@@ -419,7 +396,15 @@ class HTML5PushCallbackView(HomeAssistantView):
|
||||
)
|
||||
|
||||
event_name = f"{NOTIFY_CALLBACK_EVENT}.{event_payload[ATTR_TYPE]}"
|
||||
request.app[KEY_HASS].bus.fire(event_name, event_payload)
|
||||
hass = request.app[KEY_HASS]
|
||||
hass.bus.fire(event_name, event_payload)
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
DOMAIN,
|
||||
event_payload[ATTR_TARGET],
|
||||
event_payload[ATTR_TYPE],
|
||||
event_payload,
|
||||
)
|
||||
return self.json({"status": "ok", "event": event_payload[ATTR_TYPE]})
|
||||
|
||||
|
||||
@@ -613,37 +598,11 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class HTML5NotifyEntity(NotifyEntity):
|
||||
class HTML5NotifyEntity(HTML5Entity, NotifyEntity):
|
||||
"""Representation of a notification entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
_attr_supported_features = NotifyEntityFeature.TITLE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
target: str,
|
||||
registrations: dict[str, Registration],
|
||||
session: ClientSession,
|
||||
json_path: str,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.config_entry = config_entry
|
||||
self.target = target
|
||||
self.registrations = registrations
|
||||
self.registration = registrations[target]
|
||||
self.session = session
|
||||
self.json_path = json_path
|
||||
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{target}_device"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
name=target,
|
||||
model=self.registration["browser"].capitalize(),
|
||||
identifiers={(DOMAIN, f"{config_entry.entry_id}_{target}")},
|
||||
)
|
||||
_key = "device"
|
||||
|
||||
async def async_send_message(self, message: str, title: str | None = None) -> None:
|
||||
"""Send a message to a device."""
|
||||
@@ -714,8 +673,3 @@ class HTML5NotifyEntity(NotifyEntity):
|
||||
translation_key="connection_error",
|
||||
translation_placeholders={"target": self.target},
|
||||
) from e
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available and self.target in self.registrations
|
||||
|
||||
@@ -20,6 +20,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"event": {
|
||||
"event": {
|
||||
"state_attributes": {
|
||||
"action": { "name": "Action" },
|
||||
"event_type": {
|
||||
"state": {
|
||||
"clicked": "Clicked",
|
||||
"closed": "Closed",
|
||||
"received": "Received"
|
||||
}
|
||||
},
|
||||
"tag": { "name": "Tag" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"channel_expired": {
|
||||
"message": "Notification channel for {target} has expired"
|
||||
|
||||
@@ -11,6 +11,9 @@ from homeassistant.helpers import (
|
||||
config_entry_oauth2_flow,
|
||||
config_validation as cv,
|
||||
)
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -42,11 +45,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AutomowerConfigEntry) -> bool:
|
||||
"""Set up this integration using UI."""
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
try:
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
api_api = api.AsyncConfigEntryAuth(
|
||||
aiohttp_client.async_get_clientsession(hass),
|
||||
|
||||
@@ -491,6 +491,9 @@
|
||||
"command_send_failed": {
|
||||
"message": "Failed to send command: {exception}"
|
||||
},
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
},
|
||||
"work_area_not_existing": {
|
||||
"message": "The selected work area does not exist."
|
||||
},
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/huum",
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["huum==0.8.2"]
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ rules:
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2025.9.0"]
|
||||
"requirements": ["pydrawise==2026.3.0"]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioimmich==0.12.0"]
|
||||
"requirements": ["aioimmich==0.12.1"]
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ import logging
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
IottyConfigEntry,
|
||||
IottyConfigEntryData,
|
||||
@@ -26,7 +29,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: IottyConfigEntry) -> boo
|
||||
"""Set up iotty from a config entry."""
|
||||
_LOGGER.debug("async_setup_entry entry_id=%s", entry.entry_id)
|
||||
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
|
||||
data_update_coordinator = IottyDataUpdateCoordinator(hass, entry, session)
|
||||
|
||||
@@ -25,5 +25,10 @@
|
||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/kaleidescape",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["pykaleidescape==1.1.3"],
|
||||
"requirements": ["pykaleidescape==1.1.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "schemas-upnp-org:device:Basic:1",
|
||||
|
||||
@@ -6,6 +6,7 @@ from aiolyric import Lyric
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_entry_oauth2_flow,
|
||||
@@ -27,11 +28,17 @@ PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LyricConfigEntry) -> bool:
|
||||
"""Set up Honeywell Lyric from a config entry."""
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
try:
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
)
|
||||
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
if not isinstance(implementation, LyricLocalOAuth2Implementation):
|
||||
raise TypeError("Unexpected auth implementation; can't find oauth client id")
|
||||
|
||||
|
||||
@@ -64,6 +64,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"set_hold_time": {
|
||||
"description": "Sets the time period to keep the temperature and override the schedule.",
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .const import PLATFORMS
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import MicroBeesUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -50,11 +50,17 @@ async def async_migrate_entry(hass: HomeAssistant, entry: MicroBeesConfigEntry)
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MicroBeesConfigEntry) -> bool:
|
||||
"""Set up microBees from a config entry."""
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
try:
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
)
|
||||
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
try:
|
||||
|
||||
@@ -35,5 +35,10 @@
|
||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,6 +440,7 @@ class WashingMachineProgramId(MieleEnum, missing_to_none=True):
|
||||
|
||||
no_program = 0, -1
|
||||
cottons = 1, 10001
|
||||
normal = 2
|
||||
minimum_iron = 3
|
||||
delicates = 4, 10022
|
||||
woollens = 8, 10040
|
||||
|
||||
@@ -6,13 +6,16 @@ import logging
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
|
||||
from .api import AuthenticatedMonzoAPI
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MonzoConfigEntry, MonzoCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -39,7 +42,13 @@ async def async_migrate_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> b
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> bool:
|
||||
"""Set up Monzo from a config entry."""
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
|
||||
|
||||
@@ -50,5 +50,10 @@
|
||||
"name": "Total balance"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import ClientError
|
||||
from pybotvac import Account
|
||||
from pybotvac.exceptions import NeatoException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
OAuth2TokenRequestError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
@@ -58,10 +63,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
try:
|
||||
await session.async_ensure_token_valid()
|
||||
except aiohttp.ClientResponseError as ex:
|
||||
_LOGGER.debug("API error: %s (%s)", ex.code, ex.message)
|
||||
if ex.code in (401, 403):
|
||||
raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex
|
||||
except OAuth2TokenRequestReauthError as ex:
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
except (OAuth2TokenRequestError, ClientError) as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
neato_session = api.ConfigEntryAuth(hass, entry, implementation)
|
||||
|
||||
@@ -40,7 +40,7 @@ from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.EVENT, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
MIGRATION_NAME_TO_KEY = {
|
||||
# Sensors
|
||||
|
||||
@@ -55,6 +55,10 @@ EVENT_STARTED_TORRENT = "transmission_started_torrent"
|
||||
EVENT_REMOVED_TORRENT = "transmission_removed_torrent"
|
||||
EVENT_DOWNLOADED_TORRENT = "transmission_downloaded_torrent"
|
||||
|
||||
EVENT_TYPE_STARTED = "started"
|
||||
EVENT_TYPE_REMOVED = "removed"
|
||||
EVENT_TYPE_DOWNLOADED = "downloaded"
|
||||
|
||||
STATE_UP_DOWN = "up_down"
|
||||
STATE_SEEDING = "seeding"
|
||||
STATE_DOWNLOADING = "downloading"
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
"""Coordinator for transmssion integration."""
|
||||
"""Coordinator for transmission integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import transmission_rpc
|
||||
from transmission_rpc.session import SessionStats
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import ATTR_ID, ATTR_NAME, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
ATTR_DOWNLOAD_PATH,
|
||||
ATTR_LABELS,
|
||||
CONF_LIMIT,
|
||||
CONF_ORDER,
|
||||
DEFAULT_LIMIT,
|
||||
@@ -23,13 +28,28 @@ from .const import (
|
||||
EVENT_DOWNLOADED_TORRENT,
|
||||
EVENT_REMOVED_TORRENT,
|
||||
EVENT_STARTED_TORRENT,
|
||||
EVENT_TYPE_DOWNLOADED,
|
||||
EVENT_TYPE_REMOVED,
|
||||
EVENT_TYPE_STARTED,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type EventCallback = Callable[[TransmissionEventData], None]
|
||||
type TransmissionConfigEntry = ConfigEntry[TransmissionDataUpdateCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransmissionEventData:
|
||||
"""Data for a single event."""
|
||||
|
||||
event_type: str
|
||||
name: str
|
||||
id: int
|
||||
download_path: str
|
||||
labels: list[str]
|
||||
|
||||
|
||||
class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
"""Transmission dataupdate coordinator class."""
|
||||
|
||||
@@ -49,6 +69,7 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
self._all_torrents: list[transmission_rpc.Torrent] = []
|
||||
self._completed_torrents: list[transmission_rpc.Torrent] = []
|
||||
self._started_torrents: list[transmission_rpc.Torrent] = []
|
||||
self._event_listeners: dict[str, EventCallback] = {}
|
||||
self.torrents: list[transmission_rpc.Torrent] = []
|
||||
super().__init__(
|
||||
hass,
|
||||
@@ -68,9 +89,32 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
"""Return order."""
|
||||
return self.config_entry.options.get(CONF_ORDER, DEFAULT_ORDER) # type: ignore[no-any-return]
|
||||
|
||||
@callback
|
||||
def async_add_event_listener(
|
||||
self, update_callback: EventCallback, target_event_id: str
|
||||
) -> Callable[[], None]:
|
||||
"""Listen for updates."""
|
||||
self._event_listeners[target_event_id] = update_callback
|
||||
return partial(self.__async_remove_listener_internal, target_event_id)
|
||||
|
||||
def __async_remove_listener_internal(self, listener_id: str) -> None:
|
||||
self._event_listeners.pop(listener_id, None)
|
||||
|
||||
@callback
|
||||
def _async_notify_event_listeners(self, event: TransmissionEventData) -> None:
|
||||
"""Notify event listeners in the event loop."""
|
||||
for listener in list(self._event_listeners.values()):
|
||||
listener(event)
|
||||
|
||||
async def _async_update_data(self) -> SessionStats:
|
||||
"""Update transmission data."""
|
||||
return await self.hass.async_add_executor_job(self.update)
|
||||
data = await self.hass.async_add_executor_job(self.update)
|
||||
|
||||
self.check_completed_torrent()
|
||||
self.check_started_torrent()
|
||||
self.check_removed_torrent()
|
||||
|
||||
return data
|
||||
|
||||
def update(self) -> SessionStats:
|
||||
"""Get the latest data from Transmission instance."""
|
||||
@@ -82,10 +126,6 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
except transmission_rpc.TransmissionError as err:
|
||||
raise UpdateFailed("Unable to connect to Transmission client") from err
|
||||
|
||||
self.check_completed_torrent()
|
||||
self.check_started_torrent()
|
||||
self.check_removed_torrent()
|
||||
|
||||
return data
|
||||
|
||||
def init_torrent_list(self) -> None:
|
||||
@@ -108,15 +148,24 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
|
||||
for torrent in current_completed_torrents:
|
||||
if torrent.id not in old_completed_torrents:
|
||||
self.hass.bus.fire(
|
||||
# Once event triggers are out of labs we can remove the bus event
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_DOWNLOADED_TORRENT,
|
||||
{
|
||||
"name": torrent.name,
|
||||
"id": torrent.id,
|
||||
"download_path": torrent.download_dir,
|
||||
"labels": torrent.labels,
|
||||
ATTR_NAME: torrent.name,
|
||||
ATTR_ID: torrent.id,
|
||||
ATTR_DOWNLOAD_PATH: torrent.download_dir,
|
||||
ATTR_LABELS: torrent.labels,
|
||||
},
|
||||
)
|
||||
event = TransmissionEventData(
|
||||
event_type=EVENT_TYPE_DOWNLOADED,
|
||||
name=torrent.name,
|
||||
id=torrent.id,
|
||||
download_path=torrent.download_dir or "",
|
||||
labels=torrent.labels,
|
||||
)
|
||||
self._async_notify_event_listeners(event)
|
||||
|
||||
self._completed_torrents = current_completed_torrents
|
||||
|
||||
@@ -130,15 +179,24 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
|
||||
for torrent in current_started_torrents:
|
||||
if torrent.id not in old_started_torrents:
|
||||
self.hass.bus.fire(
|
||||
# Once event triggers are out of labs we can remove the bus event
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_STARTED_TORRENT,
|
||||
{
|
||||
"name": torrent.name,
|
||||
"id": torrent.id,
|
||||
"download_path": torrent.download_dir,
|
||||
"labels": torrent.labels,
|
||||
ATTR_NAME: torrent.name,
|
||||
ATTR_ID: torrent.id,
|
||||
ATTR_DOWNLOAD_PATH: torrent.download_dir,
|
||||
ATTR_LABELS: torrent.labels,
|
||||
},
|
||||
)
|
||||
event = TransmissionEventData(
|
||||
event_type=EVENT_TYPE_STARTED,
|
||||
name=torrent.name,
|
||||
id=torrent.id,
|
||||
download_path=torrent.download_dir or "",
|
||||
labels=torrent.labels,
|
||||
)
|
||||
self._async_notify_event_listeners(event)
|
||||
|
||||
self._started_torrents = current_started_torrents
|
||||
|
||||
@@ -148,15 +206,24 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
|
||||
for torrent in self._all_torrents:
|
||||
if torrent.id not in current_torrents:
|
||||
self.hass.bus.fire(
|
||||
# Once event triggers are out of labs we can remove the bus event
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_REMOVED_TORRENT,
|
||||
{
|
||||
"name": torrent.name,
|
||||
"id": torrent.id,
|
||||
"download_path": torrent.download_dir,
|
||||
"labels": torrent.labels,
|
||||
ATTR_NAME: torrent.name,
|
||||
ATTR_ID: torrent.id,
|
||||
ATTR_DOWNLOAD_PATH: torrent.download_dir,
|
||||
ATTR_LABELS: torrent.labels,
|
||||
},
|
||||
)
|
||||
event = TransmissionEventData(
|
||||
event_type=EVENT_TYPE_REMOVED,
|
||||
name=torrent.name,
|
||||
id=torrent.id,
|
||||
download_path=torrent.download_dir or "",
|
||||
labels=torrent.labels,
|
||||
)
|
||||
self._async_notify_event_listeners(event)
|
||||
|
||||
self._all_torrents = self.torrents.copy()
|
||||
|
||||
|
||||
85
homeassistant/components/transmission/event.py
Normal file
85
homeassistant/components/transmission/event.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Define events for the Transmission integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.event import EventEntity, EventEntityDescription
|
||||
from homeassistant.const import ATTR_ID, ATTR_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
ATTR_DOWNLOAD_PATH,
|
||||
ATTR_LABELS,
|
||||
EVENT_TYPE_DOWNLOADED,
|
||||
EVENT_TYPE_REMOVED,
|
||||
EVENT_TYPE_STARTED,
|
||||
)
|
||||
from .coordinator import TransmissionConfigEntry, TransmissionEventData
|
||||
from .entity import TransmissionEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: TransmissionConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Transmission event platform."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
description = EventEntityDescription(
|
||||
key="torrent",
|
||||
translation_key="torrent",
|
||||
event_types=[
|
||||
EVENT_TYPE_STARTED,
|
||||
EVENT_TYPE_DOWNLOADED,
|
||||
EVENT_TYPE_REMOVED,
|
||||
],
|
||||
)
|
||||
|
||||
async_add_entities([TransmissionEvent(coordinator, description)])
|
||||
|
||||
|
||||
class TransmissionEvent(TransmissionEntity, EventEntity):
|
||||
"""Representation of a Transmission event entity."""
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self._attr_unique_id
|
||||
|
||||
self.async_on_remove(
|
||||
self.coordinator.async_add_event_listener(
|
||||
self._handle_event, self._attr_unique_id
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_event(self, event_data: TransmissionEventData) -> None:
|
||||
"""Handle the torrent events."""
|
||||
|
||||
event_type = event_data.event_type
|
||||
|
||||
if event_type not in self.event_types:
|
||||
_LOGGER.warning("Event type %s is not known", event_type)
|
||||
return
|
||||
|
||||
self._trigger_event(
|
||||
event_type,
|
||||
{
|
||||
ATTR_NAME: event_data.name,
|
||||
ATTR_ID: event_data.id,
|
||||
ATTR_DOWNLOAD_PATH: event_data.download_path,
|
||||
ATTR_LABELS: event_data.labels,
|
||||
},
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
@@ -8,6 +8,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"torrent": {
|
||||
"default": "mdi:folder-file-outline"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"active_torrents": {
|
||||
"default": "mdi:counter"
|
||||
|
||||
@@ -50,6 +50,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"torrent": {
|
||||
"name": "Torrent",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"downloaded": "Downloaded",
|
||||
"removed": "Removed",
|
||||
"started": "Started"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"active_torrents": {
|
||||
"name": "Active torrents",
|
||||
|
||||
@@ -16,6 +16,7 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.BUTTON,
|
||||
Platform.EVENT,
|
||||
Platform.IMAGE,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
@@ -15,7 +15,9 @@ from unifi_access_api import (
|
||||
ApiNotFoundError,
|
||||
Door,
|
||||
DoorLockRelayStatus,
|
||||
DoorLockRule,
|
||||
DoorLockRuleStatus,
|
||||
DoorLockRuleType,
|
||||
EmergencyStatus,
|
||||
UnifiAccessApiClient,
|
||||
WsMessageHandler,
|
||||
@@ -40,6 +42,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_LOCK_RULE_INTERVAL = 10
|
||||
|
||||
type UnifiAccessConfigEntry = ConfigEntry[UnifiAccessCoordinator]
|
||||
|
||||
@@ -102,6 +105,34 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]):
|
||||
self._event_listeners.append(event_callback)
|
||||
return _unsubscribe
|
||||
|
||||
async def async_set_lock_rule(self, door_id: str, rule_type: str) -> None:
|
||||
"""Set a temporary lock rule for a door."""
|
||||
if not rule_type:
|
||||
return
|
||||
lock_rule_type = DoorLockRuleType(rule_type)
|
||||
rule = DoorLockRule(type=lock_rule_type, interval=DEFAULT_LOCK_RULE_INTERVAL)
|
||||
await self.client.set_door_lock_rule(door_id, rule)
|
||||
if self.data is None or door_id not in self.data.doors:
|
||||
return
|
||||
new_status = DoorLockRuleStatus(
|
||||
type=DoorLockRuleType.NONE
|
||||
if lock_rule_type == DoorLockRuleType.RESET
|
||||
else lock_rule_type
|
||||
)
|
||||
updated_data = replace(
|
||||
self.data,
|
||||
door_lock_rules={
|
||||
**self.data.door_lock_rules,
|
||||
door_id: new_status,
|
||||
},
|
||||
)
|
||||
if self.last_update_success:
|
||||
self.async_set_updated_data(updated_data)
|
||||
else:
|
||||
# Preserve coordinator error state while updating cached data
|
||||
self.data = updated_data
|
||||
self.async_update_listeners()
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the WebSocket connection for push updates."""
|
||||
handlers: dict[str, WsMessageHandler] = {
|
||||
|
||||
102
homeassistant/components/unifi_access/select.py
Normal file
102
homeassistant/components/unifi_access/select.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Select platform for the UniFi Access integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unifi_access_api import Door, DoorLockRuleType, UnifiAccessError
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import UnifiAccessConfigEntry, UnifiAccessCoordinator
|
||||
from .entity import UnifiAccessEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: UnifiAccessConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up UniFi Access select entities."""
|
||||
coordinator = entry.runtime_data
|
||||
added_doors: set[str] = set()
|
||||
|
||||
@callback
|
||||
def _async_add_lock_rule_selects() -> None:
|
||||
new_door_ids = sorted(coordinator.get_lock_rule_sensor_door_ids() - added_doors)
|
||||
if not new_door_ids:
|
||||
return
|
||||
|
||||
async_add_entities(
|
||||
UnifiAccessDoorLockRuleSelectEntity(
|
||||
coordinator, coordinator.data.doors[door_id]
|
||||
)
|
||||
for door_id in new_door_ids
|
||||
if door_id in coordinator.data.doors
|
||||
)
|
||||
added_doors.update(new_door_ids)
|
||||
|
||||
_async_add_lock_rule_selects()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_add_lock_rule_selects))
|
||||
|
||||
|
||||
class UnifiAccessDoorLockRuleSelectEntity(UnifiAccessEntity, SelectEntity):
|
||||
"""Select entity for choosing the active temporary lock rule on a door."""
|
||||
|
||||
_attr_translation_key = "door_lock_rule"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: UnifiAccessCoordinator,
|
||||
door: Door,
|
||||
) -> None:
|
||||
"""Initialize the door lock rule select entity."""
|
||||
super().__init__(coordinator, door, "lock_rule_select")
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the currently active lock rule, or None if no rule is set."""
|
||||
rule_status = self.coordinator.get_lock_rule_status(self._door_id)
|
||||
if rule_status is None or rule_status.type in (
|
||||
DoorLockRuleType.NONE,
|
||||
DoorLockRuleType.RESET,
|
||||
DoorLockRuleType.LOCK_NOW,
|
||||
):
|
||||
return None
|
||||
value = rule_status.type.value
|
||||
return value if value in self.options else None
|
||||
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
"""Return the available lock rule options."""
|
||||
opts = ["keep_lock", "keep_unlock", "custom", "reset"]
|
||||
rule_status = self.coordinator.get_lock_rule_status(self._door_id)
|
||||
if rule_status is not None and rule_status.type in (
|
||||
DoorLockRuleType.SCHEDULE,
|
||||
DoorLockRuleType.LOCK_EARLY,
|
||||
):
|
||||
opts.extend(["schedule", "lock_early"])
|
||||
return opts
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return whether the select should currently be shown as available."""
|
||||
return super().available and (
|
||||
self._door_id in self.coordinator.get_lock_rule_sensor_door_ids()
|
||||
)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Apply the selected lock rule to the door."""
|
||||
if option == DoorLockRuleType.SCHEDULE.value:
|
||||
return
|
||||
try:
|
||||
await self.coordinator.async_set_lock_rule(self._door_id, option)
|
||||
except UnifiAccessError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="lock_rule_failed",
|
||||
) from err
|
||||
@@ -67,6 +67,19 @@
|
||||
"name": "Thumbnail"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"door_lock_rule": {
|
||||
"name": "Lock rule",
|
||||
"state": {
|
||||
"custom": "Custom",
|
||||
"keep_lock": "Keep locked",
|
||||
"keep_unlock": "Keep unlocked",
|
||||
"lock_early": "Lock early",
|
||||
"reset": "Reset",
|
||||
"schedule": "Schedule"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"door_lock_rule": {
|
||||
"name": "Lock rule",
|
||||
@@ -97,6 +110,9 @@
|
||||
"emergency_failed": {
|
||||
"message": "Failed to set emergency status."
|
||||
},
|
||||
"lock_rule_failed": {
|
||||
"message": "Failed to update the door lock rule."
|
||||
},
|
||||
"unlock_failed": {
|
||||
"message": "Failed to unlock the door."
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyvizio.api.apps import AppConfig, find_app_name
|
||||
from pyvizio.const import APP_HOME, INPUT_APPS, NO_APP_RUNNING, UNKNOWN_APP
|
||||
|
||||
@@ -14,10 +16,6 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_EXCLUDE, CONF_INCLUDE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -122,9 +120,7 @@ class VizioDevice(CoordinatorEntity[VizioDeviceCoordinator], MediaPlayerEntity):
|
||||
self._available_inputs: list[str] = []
|
||||
self._available_apps: list[str] = []
|
||||
|
||||
self._volume_step = config_entry.options[CONF_VOLUME_STEP]
|
||||
self._all_apps = apps_coordinator.data if apps_coordinator else None
|
||||
self._conf_apps = config_entry.options.get(CONF_APPS, {})
|
||||
self._additional_app_configs = config_entry.data.get(CONF_APPS, {}).get(
|
||||
CONF_ADDITIONAL_CONFIGS, []
|
||||
)
|
||||
@@ -142,6 +138,16 @@ class VizioDevice(CoordinatorEntity[VizioDeviceCoordinator], MediaPlayerEntity):
|
||||
self._attr_device_class = device_class
|
||||
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, unique_id)})
|
||||
|
||||
@property
|
||||
def _volume_step(self) -> int:
|
||||
"""Return the configured volume step."""
|
||||
return self._config_entry.options[CONF_VOLUME_STEP]
|
||||
|
||||
@property
|
||||
def _conf_apps(self) -> dict:
|
||||
"""Return the configured app filter options."""
|
||||
return self._config_entry.options.get(CONF_APPS, {})
|
||||
|
||||
def _apps_list(self, apps: list[str]) -> list[str]:
|
||||
"""Return process apps list based on configured filters."""
|
||||
if self._conf_apps.get(CONF_INCLUDE):
|
||||
@@ -225,22 +231,6 @@ class VizioDevice(CoordinatorEntity[VizioDeviceCoordinator], MediaPlayerEntity):
|
||||
additional_app["name"] for additional_app in self._additional_app_configs
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
async def _async_send_update_options_signal(
|
||||
hass: HomeAssistant, config_entry: VizioConfigEntry
|
||||
) -> None:
|
||||
"""Send update event when Vizio config entry is updated."""
|
||||
# Move this method to component level if another entity ever gets added for a
|
||||
# single config entry.
|
||||
# See here: https://github.com/home-assistant/core/pull/30653#discussion_r366426121
|
||||
async_dispatcher_send(hass, config_entry.entry_id, config_entry)
|
||||
|
||||
async def _async_update_options(self, config_entry: VizioConfigEntry) -> None:
|
||||
"""Update options if the update signal comes from this entity."""
|
||||
self._volume_step = config_entry.options[CONF_VOLUME_STEP]
|
||||
# Update so that CONF_ADDITIONAL_CONFIGS gets retained for imports
|
||||
self._conf_apps.update(config_entry.options.get(CONF_APPS, {}))
|
||||
|
||||
async def async_update_setting(
|
||||
self, setting_type: str, setting_name: str, new_value: int | str
|
||||
) -> None:
|
||||
@@ -259,19 +249,10 @@ class VizioDevice(CoordinatorEntity[VizioDeviceCoordinator], MediaPlayerEntity):
|
||||
# Process initial coordinator data
|
||||
self._handle_coordinator_update()
|
||||
|
||||
# Register callback for when config entry is updated.
|
||||
self.async_on_remove(
|
||||
self._config_entry.add_update_listener(
|
||||
self._async_send_update_options_signal
|
||||
)
|
||||
)
|
||||
async def _async_write_state(*_: Any) -> None:
|
||||
self._handle_coordinator_update()
|
||||
|
||||
# Register callback for update event
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, self._config_entry.entry_id, self._async_update_options
|
||||
)
|
||||
)
|
||||
self.async_on_remove(self._config_entry.add_update_listener(_async_write_state))
|
||||
|
||||
if not (apps_coordinator := self._apps_coordinator):
|
||||
return
|
||||
|
||||
@@ -81,8 +81,8 @@
|
||||
"usa_a": "Hall in USA A",
|
||||
"usa_b": "Hall in USA B",
|
||||
"vienna": "Hall in Vienna",
|
||||
"village_gate": "Village gate",
|
||||
"village_vanguard": "Village vanguard",
|
||||
"village_gate": "Village Gate",
|
||||
"village_vanguard": "Village Vanguard",
|
||||
"warehouse_loft": "Warehouse loft"
|
||||
}
|
||||
}
|
||||
|
||||
10
requirements_all.txt
generated
10
requirements_all.txt
generated
@@ -294,7 +294,7 @@ aiohue==4.8.0
|
||||
aioimaplib==2.0.1
|
||||
|
||||
# homeassistant.components.immich
|
||||
aioimmich==0.12.0
|
||||
aioimmich==0.12.1
|
||||
|
||||
# homeassistant.components.apache_kafka
|
||||
aiokafka==0.10.0
|
||||
@@ -1989,7 +1989,7 @@ pybbox==0.0.5-alpha
|
||||
pyblackbird==0.6
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
pyblu==2.0.5
|
||||
pyblu==2.0.6
|
||||
|
||||
# homeassistant.components.neato
|
||||
pybotvac==0.0.28
|
||||
@@ -2055,7 +2055,7 @@ pydiscovergy==3.0.2
|
||||
pydoods==1.0.2
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
pydrawise==2025.9.0
|
||||
pydrawise==2026.3.0
|
||||
|
||||
# homeassistant.components.android_ip_webcam
|
||||
pydroid-ipcam==3.0.0
|
||||
@@ -2209,7 +2209,7 @@ pyituran==0.1.5
|
||||
pyjvcprojector==2.0.3
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.1.3
|
||||
pykaleidescape==1.1.4
|
||||
|
||||
# homeassistant.components.kira
|
||||
pykira==0.1.1
|
||||
@@ -2654,7 +2654,7 @@ python-picnic-api2==1.3.1
|
||||
python-pooldose==0.9.0
|
||||
|
||||
# homeassistant.components.hr_energy_qube
|
||||
python-qube-heatpump==1.7.0
|
||||
python-qube-heatpump==1.8.0
|
||||
|
||||
# homeassistant.components.rabbitair
|
||||
python-rabbitair==0.0.8
|
||||
|
||||
10
requirements_test_all.txt
generated
10
requirements_test_all.txt
generated
@@ -282,7 +282,7 @@ aiohue==4.8.0
|
||||
aioimaplib==2.0.1
|
||||
|
||||
# homeassistant.components.immich
|
||||
aioimmich==0.12.0
|
||||
aioimmich==0.12.1
|
||||
|
||||
# homeassistant.components.apache_kafka
|
||||
aiokafka==0.10.0
|
||||
@@ -1723,7 +1723,7 @@ pybalboa==1.1.3
|
||||
pyblackbird==0.6
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
pyblu==2.0.5
|
||||
pyblu==2.0.6
|
||||
|
||||
# homeassistant.components.neato
|
||||
pybotvac==0.0.28
|
||||
@@ -1768,7 +1768,7 @@ pydexcom==0.5.1
|
||||
pydiscovergy==3.0.2
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
pydrawise==2025.9.0
|
||||
pydrawise==2026.3.0
|
||||
|
||||
# homeassistant.components.android_ip_webcam
|
||||
pydroid-ipcam==3.0.0
|
||||
@@ -1892,7 +1892,7 @@ pyituran==0.1.5
|
||||
pyjvcprojector==2.0.3
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.1.3
|
||||
pykaleidescape==1.1.4
|
||||
|
||||
# homeassistant.components.kira
|
||||
pykira==0.1.1
|
||||
@@ -2256,7 +2256,7 @@ python-picnic-api2==1.3.1
|
||||
python-pooldose==0.9.0
|
||||
|
||||
# homeassistant.components.hr_energy_qube
|
||||
python-qube-heatpump==1.7.0
|
||||
python-qube-heatpump==1.8.0
|
||||
|
||||
# homeassistant.components.rabbitair
|
||||
python-rabbitair==0.0.8
|
||||
|
||||
@@ -645,7 +645,7 @@ async def test_double_system_messages(
|
||||
assert result.response.error_code == "unknown"
|
||||
assert (
|
||||
result.response.speech["plain"]["speech"]
|
||||
== "Unexpected content type in chat log"
|
||||
== "Unexpected content type in chat log: SystemContent"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ from tests.common import MockConfigEntry
|
||||
),
|
||||
body={"type": "error", "error": {"type": "invalid_request_error"}},
|
||||
),
|
||||
"anthropic integration not ready yet: Your credit balance is too low to access the Claude API",
|
||||
"Your credit balance is too low to access the Claude API",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Casper Glow session fixtures."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Callable, Generator
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pycasperglow import GlowState
|
||||
@@ -51,6 +51,21 @@ def mock_casper_glow() -> Generator[MagicMock]:
|
||||
yield mock_device
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fire_callbacks(
|
||||
mock_casper_glow: MagicMock,
|
||||
) -> Callable[[GlowState], None]:
|
||||
"""Return a helper that fires all registered device callbacks with a given state."""
|
||||
|
||||
def _fire(state: GlowState) -> None:
|
||||
for cb in (
|
||||
call[0][0] for call in mock_casper_glow.register_callback.call_args_list
|
||||
):
|
||||
cb(state)
|
||||
|
||||
return _fire
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def config_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
67
tests/components/casper_glow/snapshots/test_select.ambr
Normal file
67
tests/components/casper_glow/snapshots/test_select.ambr
Normal file
@@ -0,0 +1,67 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[select.jar_dimming_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'15',
|
||||
'30',
|
||||
'45',
|
||||
'60',
|
||||
'90',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.jar_dimming_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Dimming time',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Dimming time',
|
||||
'platform': 'casper_glow',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'dimming_time',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_dimming_time',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[select.jar_dimming_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Jar Dimming time',
|
||||
'options': list([
|
||||
'15',
|
||||
'30',
|
||||
'45',
|
||||
'60',
|
||||
'90',
|
||||
]),
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.jar_dimming_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test the Casper Glow light platform."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pycasperglow import CasperGlowError, GlowState
|
||||
@@ -82,21 +83,19 @@ async def test_turn_off(
|
||||
async def test_state_update_via_callback(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test that the entity updates state when the device fires a callback."""
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
callback = mock_casper_glow.register_callback.call_args[0][0]
|
||||
|
||||
callback(GlowState(is_on=True))
|
||||
fire_callbacks(GlowState(is_on=True))
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
callback(GlowState(is_on=False))
|
||||
fire_callbacks(GlowState(is_on=False))
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_OFF
|
||||
@@ -115,25 +114,6 @@ async def test_color_mode(
|
||||
assert ColorMode.BRIGHTNESS in state.attributes["supported_color_modes"]
|
||||
|
||||
|
||||
async def test_turn_on_with_brightness(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
) -> None:
|
||||
"""Test turning on the light with brightness."""
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_BRIGHTNESS: 255},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_casper_glow.turn_on.assert_called_once_with()
|
||||
mock_casper_glow.set_brightness_and_dimming_time.assert_called_once_with(
|
||||
100, DEFAULT_DIMMING_TIME_MINUTES
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("ha_brightness", "device_pct"),
|
||||
[
|
||||
@@ -169,11 +149,10 @@ async def test_brightness_snap_to_nearest(
|
||||
async def test_brightness_update_via_callback(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test that brightness updates via device callback."""
|
||||
callback = mock_casper_glow.register_callback.call_args[0][0]
|
||||
callback(GlowState(is_on=True, brightness_level=80))
|
||||
fire_callbacks(GlowState(is_on=True, brightness_level=80))
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
@@ -181,18 +160,29 @@ async def test_brightness_update_via_callback(
|
||||
assert state.attributes.get(ATTR_BRIGHTNESS) == 153
|
||||
|
||||
|
||||
async def test_turn_on_error(
|
||||
@pytest.mark.usefixtures("config_entry")
|
||||
@pytest.mark.parametrize(
|
||||
("service", "mock_method"),
|
||||
[
|
||||
(SERVICE_TURN_ON, "turn_on"),
|
||||
(SERVICE_TURN_OFF, "turn_off"),
|
||||
],
|
||||
)
|
||||
async def test_command_error(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
service: str,
|
||||
mock_method: str,
|
||||
) -> None:
|
||||
"""Test that a turn on error raises HomeAssistantError without marking entity unavailable."""
|
||||
mock_casper_glow.turn_on.side_effect = CasperGlowError("Connection failed")
|
||||
"""Test that a device error raises HomeAssistantError without marking entity unavailable."""
|
||||
getattr(mock_casper_glow, mock_method).side_effect = CasperGlowError(
|
||||
"Connection failed"
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
@@ -202,27 +192,11 @@ async def test_turn_on_error(
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_turn_off_error(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that a turn off error raises HomeAssistantError."""
|
||||
mock_casper_glow.turn_off.side_effect = CasperGlowError("Connection failed")
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_state_update_via_callback_after_command_failure(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test that device callbacks correctly update state even after a command failure."""
|
||||
mock_casper_glow.turn_on.side_effect = CasperGlowError("Connection failed")
|
||||
@@ -241,8 +215,7 @@ async def test_state_update_via_callback_after_command_failure(
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# Device sends a push state update — entity reflects true device state
|
||||
callback = mock_casper_glow.register_callback.call_args[0][0]
|
||||
callback(GlowState(is_on=True))
|
||||
fire_callbacks(GlowState(is_on=True))
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
|
||||
199
tests/components/casper_glow/test_select.py
Normal file
199
tests/components/casper_glow/test_select.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""Test the Casper Glow select platform."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pycasperglow import CasperGlowError, GlowState
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.casper_glow.const import (
|
||||
DIMMING_TIME_OPTIONS,
|
||||
SORTED_BRIGHTNESS_LEVELS,
|
||||
)
|
||||
from homeassistant.components.select import (
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, mock_restore_cache, snapshot_platform
|
||||
|
||||
ENTITY_ID = "select.jar_dimming_time"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test all select entities match the snapshot."""
|
||||
with patch("homeassistant.components.casper_glow.PLATFORMS", [Platform.SELECT]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_select_state_from_callback(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test that the select entity shows dimming time reported by device callback."""
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
fire_callbacks(
|
||||
GlowState(configured_dimming_time_minutes=int(DIMMING_TIME_OPTIONS[2]))
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == DIMMING_TIME_OPTIONS[2]
|
||||
|
||||
|
||||
async def test_select_option(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test selecting a dimming time option."""
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, "option": DIMMING_TIME_OPTIONS[1]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_casper_glow.set_brightness_and_dimming_time.assert_called_once_with(
|
||||
SORTED_BRIGHTNESS_LEVELS[0], int(DIMMING_TIME_OPTIONS[1])
|
||||
)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == DIMMING_TIME_OPTIONS[1]
|
||||
|
||||
# A subsequent device callback must not overwrite the user's selection.
|
||||
fire_callbacks(
|
||||
GlowState(configured_dimming_time_minutes=int(DIMMING_TIME_OPTIONS[0]))
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == DIMMING_TIME_OPTIONS[1]
|
||||
|
||||
|
||||
async def test_select_option_error(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that a set_brightness_and_dimming_time error raises HomeAssistantError."""
|
||||
mock_casper_glow.set_brightness_and_dimming_time.side_effect = CasperGlowError(
|
||||
"Connection failed"
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, "option": DIMMING_TIME_OPTIONS[1]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_select_state_update_via_callback_after_command_failure(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test that device callbacks correctly update state even after a command failure."""
|
||||
mock_casper_glow.set_brightness_and_dimming_time.side_effect = CasperGlowError(
|
||||
"Connection failed"
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, "option": DIMMING_TIME_OPTIONS[1]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# Device sends a push state update — entity reflects true state
|
||||
fire_callbacks(
|
||||
GlowState(configured_dimming_time_minutes=int(DIMMING_TIME_OPTIONS[1]))
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == DIMMING_TIME_OPTIONS[1]
|
||||
|
||||
|
||||
async def test_select_ignores_remaining_time_updates(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test that callbacks with only remaining time do not change the select state."""
|
||||
fire_callbacks(GlowState(dimming_time_minutes=44))
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_restore_state(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the dimming time is restored from the last known state on restart."""
|
||||
mock_restore_cache(hass, (State(ENTITY_ID, DIMMING_TIME_OPTIONS[3]),))
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == DIMMING_TIME_OPTIONS[3]
|
||||
|
||||
# Coordinator should be seeded with the restored value.
|
||||
assert mock_config_entry.runtime_data.last_dimming_time_minutes == int(
|
||||
DIMMING_TIME_OPTIONS[3]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"restored_state",
|
||||
[STATE_UNKNOWN, "invalid", "999"],
|
||||
)
|
||||
async def test_restore_state_ignores_invalid(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
restored_state: str,
|
||||
) -> None:
|
||||
"""Test that invalid or unsupported restored states are ignored."""
|
||||
mock_restore_cache(hass, (State(ENTITY_ID, restored_state),))
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert mock_config_entry.runtime_data.last_dimming_time_minutes is None
|
||||
@@ -126,43 +126,6 @@ async def test_climate_data_update(
|
||||
assert state.attributes[ATTR_TEMPERATURE] == temp
|
||||
|
||||
|
||||
async def test_climate_data_update_bad_data(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_serial_bridge: AsyncMock,
|
||||
mock_serial_bridge_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test climate data update."""
|
||||
await setup_integration(hass, mock_serial_bridge_config_entry)
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.state == HVACMode.HEAT
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 5.0
|
||||
|
||||
mock_serial_bridge.get_all_devices.return_value[CLIMATE] = {
|
||||
0: ComelitSerialBridgeObject(
|
||||
index=0,
|
||||
name="Climate0",
|
||||
status=0,
|
||||
human_status="off",
|
||||
type="climate",
|
||||
val="bad_data", # type: ignore[arg-type]
|
||||
protected=0,
|
||||
zone="Living room",
|
||||
power=0.0,
|
||||
power_unit=WATT,
|
||||
),
|
||||
}
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.state == HVACMode.HEAT
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 5.0
|
||||
|
||||
|
||||
async def test_climate_set_temperature(
|
||||
hass: HomeAssistant,
|
||||
mock_serial_bridge: AsyncMock,
|
||||
|
||||
@@ -126,43 +126,6 @@ async def test_humidifier_data_update(
|
||||
assert state.attributes[ATTR_HUMIDITY] == humidity
|
||||
|
||||
|
||||
async def test_humidifier_data_update_bad_data(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_serial_bridge: AsyncMock,
|
||||
mock_serial_bridge_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test humidifier data update."""
|
||||
await setup_integration(hass, mock_serial_bridge_config_entry)
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_HUMIDITY] == 50.0
|
||||
|
||||
mock_serial_bridge.get_all_devices.return_value[CLIMATE] = {
|
||||
0: ComelitSerialBridgeObject(
|
||||
index=0,
|
||||
name="Climate0",
|
||||
status=0,
|
||||
human_status="off",
|
||||
type="climate",
|
||||
val="bad_data", # type: ignore[arg-type]
|
||||
protected=0,
|
||||
zone="Living room",
|
||||
power=0.0,
|
||||
power_unit=WATT,
|
||||
),
|
||||
}
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_HUMIDITY] == 50.0
|
||||
|
||||
|
||||
async def test_humidifier_set_humidity(
|
||||
hass: HomeAssistant,
|
||||
mock_serial_bridge: AsyncMock,
|
||||
|
||||
@@ -40,6 +40,15 @@ def mock_polling_interval_fixture() -> Generator[int]:
|
||||
yield polling_interval
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Mock setting up a config entry."""
|
||||
with patch(
|
||||
"homeassistant.components.energyid.async_setup_entry", return_value=True
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
||||
|
||||
|
||||
async def test_config_flow_user_step_success_claimed(hass: HomeAssistant) -> None:
|
||||
"""Test user step where device is already claimed."""
|
||||
mock_client = MagicMock()
|
||||
@@ -105,7 +114,6 @@ async def test_config_flow_auth_and_claim_step_success(hass: HomeAssistant) -> N
|
||||
"homeassistant.components.energyid.config_flow.WebhookClient",
|
||||
side_effect=mock_webhook_client,
|
||||
),
|
||||
patch("homeassistant.components.energyid.config_flow.asyncio.sleep"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
@@ -146,13 +154,13 @@ async def test_config_flow_claim_timeout(hass: HomeAssistant) -> None:
|
||||
"homeassistant.components.energyid.config_flow.WebhookClient",
|
||||
return_value=mock_unclaimed_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.energyid.config_flow.asyncio.sleep",
|
||||
) as mock_sleep,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert mock_unclaimed_client.authenticate.call_count == 0
|
||||
|
||||
result_external = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
@@ -160,8 +168,17 @@ async def test_config_flow_claim_timeout(hass: HomeAssistant) -> None:
|
||||
CONF_PROVISIONING_SECRET: TEST_PROVISIONING_SECRET,
|
||||
},
|
||||
)
|
||||
|
||||
assert result_external["type"] is FlowResultType.EXTERNAL_STEP
|
||||
|
||||
# Wait for the polling to time out.
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify polling actually ran the expected number of times
|
||||
# +1 for the initial attempt before polling starts
|
||||
assert mock_unclaimed_client.authenticate.call_count == MAX_POLLING_ATTEMPTS + 1
|
||||
mock_unclaimed_client.authenticate.reset_mock()
|
||||
|
||||
# Simulate polling timeout, then user continuing the flow
|
||||
result_after_timeout = await hass.config_entries.flow.async_configure(
|
||||
result_external["flow_id"]
|
||||
@@ -169,14 +186,10 @@ async def test_config_flow_claim_timeout(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# After timeout, polling stops and user continues - should see external step again
|
||||
assert mock_unclaimed_client.authenticate.call_count == 1
|
||||
assert result_after_timeout["type"] is FlowResultType.EXTERNAL_STEP
|
||||
assert result_after_timeout["step_id"] == "auth_and_claim"
|
||||
|
||||
# Verify polling actually ran the expected number of times
|
||||
# Sleep happens at beginning of polling loop, so MAX_POLLING_ATTEMPTS + 1 sleeps
|
||||
# but only MAX_POLLING_ATTEMPTS authentication attempts
|
||||
assert mock_sleep.call_count == MAX_POLLING_ATTEMPTS + 1
|
||||
|
||||
|
||||
async def test_duplicate_unique_id_prevented(hass: HomeAssistant) -> None:
|
||||
"""Test that duplicate device_id (unique_id) is detected and aborted."""
|
||||
@@ -546,7 +559,6 @@ async def test_config_flow_reauth_needs_claim(hass: HomeAssistant) -> None:
|
||||
"homeassistant.components.energyid.config_flow.WebhookClient",
|
||||
return_value=mock_client,
|
||||
),
|
||||
patch("homeassistant.components.energyid.config_flow.asyncio.sleep"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
@@ -607,7 +619,6 @@ async def test_polling_stops_on_invalid_auth_error(hass: HomeAssistant) -> None:
|
||||
"homeassistant.components.energyid.config_flow.WebhookClient",
|
||||
side_effect=mock_webhook_client,
|
||||
),
|
||||
patch("homeassistant.components.energyid.config_flow.asyncio.sleep"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
@@ -655,7 +666,6 @@ async def test_polling_stops_on_cannot_connect_error(hass: HomeAssistant) -> Non
|
||||
"homeassistant.components.energyid.config_flow.WebhookClient",
|
||||
side_effect=mock_webhook_client,
|
||||
),
|
||||
patch("homeassistant.components.energyid.config_flow.asyncio.sleep"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
@@ -707,7 +717,6 @@ async def test_auth_and_claim_subsequent_auth_error(hass: HomeAssistant) -> None
|
||||
"homeassistant.components.energyid.config_flow.WebhookClient",
|
||||
side_effect=mock_webhook_client,
|
||||
),
|
||||
patch("homeassistant.components.energyid.config_flow.asyncio.sleep"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
@@ -815,7 +824,6 @@ async def test_polling_cancellation_on_auth_failure(hass: HomeAssistant) -> None
|
||||
"homeassistant.components.energyid.config_flow.WebhookClient",
|
||||
side_effect=mock_webhook_client,
|
||||
),
|
||||
patch("homeassistant.components.energyid.config_flow.asyncio.sleep"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
@@ -888,7 +896,6 @@ async def test_polling_cancellation_on_success(hass: HomeAssistant) -> None:
|
||||
"homeassistant.components.energyid.config_flow.WebhookClient",
|
||||
side_effect=mock_webhook_client,
|
||||
),
|
||||
patch("homeassistant.components.energyid.config_flow.asyncio.sleep"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
@@ -911,25 +918,16 @@ async def test_polling_cancellation_on_success(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify polling made authentication attempt
|
||||
# auth_call_count should be 1 (polling detected device is claimed)
|
||||
assert auth_call_count >= 1
|
||||
claimed_auth_count = auth_call_count
|
||||
assert auth_call_count == 2 # One for polling, one for the final check
|
||||
|
||||
# User continues - device is already claimed, polling should be cancelled
|
||||
result_done = await hass.config_entries.flow.async_configure(
|
||||
result_external["flow_id"]
|
||||
)
|
||||
assert result_done["type"] is FlowResultType.EXTERNAL_STEP_DONE
|
||||
assert result_done["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
# Verify polling was cancelled - the auth count should only increase by 1
|
||||
# (for the manual check when user continues, not from polling)
|
||||
assert auth_call_count == claimed_auth_count + 1
|
||||
|
||||
# Final call to create entry
|
||||
final_result = await hass.config_entries.flow.async_configure(
|
||||
result_external["flow_id"]
|
||||
)
|
||||
assert final_result["type"] is FlowResultType.CREATE_ENTRY
|
||||
# Verify polling was cancelled - the auth count should not increase
|
||||
assert auth_call_count == 2
|
||||
|
||||
# Wait a bit and verify no further authentication attempts from polling
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -8,6 +8,9 @@ from syrupy.assertion import SnapshotAssertion
|
||||
from homeassistant.components.gentex_homelink.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
)
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
|
||||
from . import setup_integration, update_callback
|
||||
@@ -72,3 +75,21 @@ async def test_load_unload_entry(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("aioclient_mock_fixture")
|
||||
async def test_oauth_implementation_not_available(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
side_effect=ImplementationUnavailableError,
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
28
tests/components/geocaching/test_init.py
Normal file
28
tests/components/geocaching/test_init.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""Tests for the Geocaching integration."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_oauth_implementation_not_available(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.geocaching.async_get_config_entry_implementation",
|
||||
side_effect=ImplementationUnavailableError,
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
@@ -16,6 +16,9 @@ from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUA
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import ComponentSetup, ExpectedCredentials
|
||||
@@ -492,3 +495,20 @@ async def test_conversation_agent_language_changed(
|
||||
mock_text_assistant.assert_has_calls([call(ExpectedCredentials(), "es-ES")])
|
||||
mock_text_assistant.assert_has_calls([call().assist(text1)])
|
||||
mock_text_assistant.assert_has_calls([call().assist(text2)])
|
||||
|
||||
|
||||
async def test_oauth_implementation_not_available(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.google_assistant_sdk.async_get_config_entry_implementation",
|
||||
side_effect=ImplementationUnavailableError,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
@@ -11,9 +11,13 @@ from homeassistant.components.google_mail import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
)
|
||||
|
||||
from .conftest import GOOGLE_TOKEN_URI, ComponentSetup
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
@@ -134,3 +138,20 @@ async def test_device_info(
|
||||
assert device.identifiers == {(DOMAIN, entry.entry_id)}
|
||||
assert device.manufacturer == "Google, Inc."
|
||||
assert device.name == "example@gmail.com"
|
||||
|
||||
|
||||
async def test_oauth_implementation_not_available(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.google_mail.async_get_config_entry_implementation",
|
||||
side_effect=ImplementationUnavailableError,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
@@ -35,6 +35,9 @@ from homeassistant.exceptions import (
|
||||
OAuth2TokenRequestTransientError,
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -558,3 +561,20 @@ async def test_get_sheet_invalid_worksheet(
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_oauth_implementation_not_available(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.google_sheets.async_get_config_entry_implementation",
|
||||
side_effect=ImplementationUnavailableError,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
@@ -5,7 +5,7 @@ import http
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aiohttp import ClientError
|
||||
from httplib2 import Response
|
||||
@@ -15,6 +15,9 @@ from homeassistant.components.google_tasks import DOMAIN
|
||||
from homeassistant.components.google_tasks.const import OAUTH2_TOKEN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
)
|
||||
|
||||
from .conftest import LIST_TASK_LIST_RESPONSE, LIST_TASKS_RESPONSE_WATER
|
||||
|
||||
@@ -152,3 +155,20 @@ async def test_setup_error(
|
||||
|
||||
assert not await integration_setup()
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_oauth_implementation_not_available(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.google_tasks.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
side_effect=ImplementationUnavailableError,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
63
tests/components/html5/snapshots/test_event.ambr
Normal file
63
tests/components/html5/snapshots/test_event.ambr
Normal file
@@ -0,0 +1,63 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup[event.my_desktop-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'clicked',
|
||||
'received',
|
||||
'closed',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.my_desktop',
|
||||
'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': 'html5',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'event',
|
||||
'unique_id': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ_my-desktop_event',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.my_desktop-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'clicked',
|
||||
'received',
|
||||
'closed',
|
||||
]),
|
||||
'friendly_name': 'my-desktop',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.my_desktop',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
120
tests/components/html5/test_event.py
Normal file
120
tests/components/html5/test_event.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""Tests for the HTML5 event platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from aiohttp.hdrs import AUTHORIZATION
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.html5.notify import ATTR_ACTION, ATTR_TAG, ATTR_TYPE
|
||||
from homeassistant.components.notify import ATTR_DATA, ATTR_TARGET
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .test_notify import SUBSCRIPTION_1
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def event_only() -> Generator[None]:
|
||||
"""Enable only the event platform."""
|
||||
with patch(
|
||||
"homeassistant.components.html5.PLATFORMS",
|
||||
[Platform.EVENT],
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid", "event_only")
|
||||
@pytest.mark.freeze_time("1970-01-01T00:00:00.000Z")
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
load_config: MagicMock,
|
||||
) -> None:
|
||||
"""Snapshot test states of event platform."""
|
||||
load_config.return_value = {"my-desktop": SUBSCRIPTION_1}
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("event_payload"),
|
||||
[
|
||||
{
|
||||
ATTR_TARGET: "my-desktop",
|
||||
ATTR_TYPE: "clicked",
|
||||
ATTR_ACTION: "open_app",
|
||||
ATTR_TAG: "1234",
|
||||
ATTR_DATA: {"customKey": "Value"},
|
||||
},
|
||||
{
|
||||
ATTR_TARGET: "my-desktop",
|
||||
ATTR_TYPE: "received",
|
||||
ATTR_TAG: "1234",
|
||||
ATTR_DATA: {"customKey": "Value"},
|
||||
},
|
||||
{
|
||||
ATTR_TARGET: "my-desktop",
|
||||
ATTR_TYPE: "closed",
|
||||
ATTR_TAG: "1234",
|
||||
ATTR_DATA: {"customKey": "Value"},
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_jwt", "mock_vapid", "mock_uuid")
|
||||
@pytest.mark.freeze_time("1970-01-01T00:00:00.000Z")
|
||||
async def test_events(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
load_config: MagicMock,
|
||||
event_payload: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_jwt: MagicMock,
|
||||
) -> None:
|
||||
"""Test events."""
|
||||
load_config.return_value = {"my-desktop": SUBSCRIPTION_1}
|
||||
await async_setup_component(hass, "http", {})
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert (state := hass.states.get("event.my_desktop")) is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
mock_jwt.decode.return_value = {ATTR_TARGET: event_payload[ATTR_TARGET]}
|
||||
|
||||
resp = await client.post(
|
||||
"/api/notify.html5/callback",
|
||||
json=event_payload,
|
||||
headers={AUTHORIZATION: "Bearer JWT"},
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
|
||||
assert (state := hass.states.get("event.my_desktop"))
|
||||
assert state.state == "1970-01-01T00:00:00.000+00:00"
|
||||
assert state.attributes.get("action") == event_payload.get(ATTR_ACTION)
|
||||
assert state.attributes.get("tag") == event_payload[ATTR_TAG]
|
||||
assert state.attributes.get("customKey") == event_payload[ATTR_DATA]["customKey"]
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test HTML5 notify platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, mock_open, patch
|
||||
@@ -18,7 +19,12 @@ from homeassistant.components.notify import (
|
||||
SERVICE_SEND_MESSAGE,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -89,6 +95,16 @@ VAPID_HEADERS = {
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def notify_only() -> Generator[None]:
|
||||
"""Enable only the notify platform."""
|
||||
with patch(
|
||||
"homeassistant.components.html5.PLATFORMS",
|
||||
[Platform.NOTIFY],
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def test_get_service_with_no_json(hass: HomeAssistant) -> None:
|
||||
"""Test empty json file."""
|
||||
await async_setup_component(hass, "http", {})
|
||||
|
||||
@@ -24,6 +24,9 @@ from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERV
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import setup_integration
|
||||
@@ -722,3 +725,20 @@ async def test_websocket_watchdog(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_automower_client.get_status.call_count == 2
|
||||
|
||||
|
||||
async def test_oauth_implementation_not_available(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
side_effect=ImplementationUnavailableError,
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
32
tests/components/huum/snapshots/test_init.ambr
Normal file
32
tests/components/huum/snapshots/test_init.ambr
Normal file
@@ -0,0 +1,32 @@
|
||||
# serializer version: 1
|
||||
# name: test_device_entry
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'huum',
|
||||
'AABBCC112233',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Huum',
|
||||
'model': 'UKU WiFi',
|
||||
'model_id': None,
|
||||
'name': 'Huum sauna',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
@@ -5,6 +5,8 @@ from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from huum.const import SaunaStatus
|
||||
from huum.exceptions import SafetyException
|
||||
from huum.schemas import SaunaConfig
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -21,6 +23,7 @@ from homeassistant.components.huum.const import (
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
@@ -85,6 +88,62 @@ async def test_set_temperature(
|
||||
mock_huum_client.turn_on.assert_awaited_once_with(60)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_hvac_mode_off(
|
||||
hass: HomeAssistant,
|
||||
mock_huum_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setting HVAC mode to off."""
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.OFF},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_huum_client.turn_off.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_temperature_not_heating(
|
||||
hass: HomeAssistant,
|
||||
mock_huum_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setting temperature is ignored when not heating."""
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_TEMPERATURE: 60,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_huum_client.turn_on.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_turn_on_safety_exception(
|
||||
hass: HomeAssistant,
|
||||
mock_huum_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that SafetyException is raised as HomeAssistantError."""
|
||||
mock_huum_client.turn_on.side_effect = SafetyException("Door is open")
|
||||
mock_huum_client.status.return_value.status = SaunaStatus.ONLINE_HEATING
|
||||
|
||||
with pytest.raises(HomeAssistantError, match="Unable to turn on sauna"):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_TEMPERATURE: 60,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_temperature_range(
|
||||
hass: HomeAssistant,
|
||||
@@ -109,9 +168,27 @@ async def test_temperature_range(
|
||||
assert state.attributes["min_temp"] == CONFIG_DEFAULT_MIN_TEMP
|
||||
assert state.attributes["max_temp"] == CONFIG_DEFAULT_MAX_TEMP
|
||||
|
||||
# No sauna config should return default values.
|
||||
mock_huum_client.status.return_value.sauna_config = None
|
||||
|
||||
freezer.tick(timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes["min_temp"] == CONFIG_DEFAULT_MIN_TEMP
|
||||
assert state.attributes["max_temp"] == CONFIG_DEFAULT_MAX_TEMP
|
||||
|
||||
# Custom configured API response.
|
||||
mock_huum_client.status.return_value.sauna_config.min_temp = 50
|
||||
mock_huum_client.status.return_value.sauna_config.max_temp = 80
|
||||
mock_huum_client.status.return_value.sauna_config = SaunaConfig(
|
||||
child_lock="OFF",
|
||||
max_heating_time=3,
|
||||
min_heating_time=0,
|
||||
max_temp=80,
|
||||
min_temp=50,
|
||||
max_timer=0,
|
||||
min_timer=0,
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
"""Tests for the Huum __init__."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from huum.exceptions import Forbidden, NotAuthenticated
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from huum.exceptions import Forbidden, NotAuthenticated, RequestError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.huum.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
@@ -27,21 +30,88 @@ async def test_loading_and_unloading_config_entry(
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("side_effect", [Forbidden, NotAuthenticated])
|
||||
async def test_auth_error_triggers_reauth(
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
Forbidden("Forbidden"),
|
||||
NotAuthenticated("Not authenticated"),
|
||||
],
|
||||
)
|
||||
async def test_setup_entry_auth_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_huum_client: AsyncMock,
|
||||
side_effect: type[Exception],
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test that an auth error during coordinator refresh triggers reauth."""
|
||||
"""Test setup triggers reauth on auth errors."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_huum_client.status.side_effect = side_effect
|
||||
mock_huum_client.status.side_effect = exception
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
assert any(
|
||||
mock_config_entry.async_get_active_flows(hass, {config_entries.SOURCE_REAUTH})
|
||||
|
||||
flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["context"]["source"] == SOURCE_REAUTH
|
||||
|
||||
|
||||
async def test_setup_entry_connection_error(
|
||||
hass: HomeAssistant,
|
||||
mock_huum_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test setup retries on connection error."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_huum_client.status.side_effect = RequestError("Request error")
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
assert hass.config_entries.flow.async_progress_by_handler(DOMAIN) == []
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_device_entry(
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test device registry entry."""
|
||||
assert (
|
||||
device_entry := device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, mock_config_entry.entry_id)}
|
||||
)
|
||||
)
|
||||
assert device_entry == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"side_effect",
|
||||
[
|
||||
Forbidden("Forbidden"),
|
||||
NotAuthenticated("Not authenticated"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_coordinator_update_auth_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_huum_client: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
side_effect: Exception,
|
||||
) -> None:
|
||||
"""Test that an auth error during coordinator refresh triggers reauth."""
|
||||
mock_huum_client.status.side_effect = side_effect
|
||||
|
||||
freezer.tick(timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["context"]["source"] == SOURCE_REAUTH
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""Tests for the iotty integration."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from homeassistant.components.iotty.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -41,6 +44,23 @@ async def test_load_unload_coordinator_called(
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_oauth_implementation_not_available(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.iotty.async_get_config_entry_implementation",
|
||||
side_effect=ImplementationUnavailableError,
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_load_unload_iottyproxy_called(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
40
tests/components/lyric/test_init.py
Normal file
40
tests/components/lyric/test_init.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Tests for the Honeywell Lyric integration."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.lyric.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_oauth_implementation_not_available(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"auth_implementation": DOMAIN,
|
||||
"token": {
|
||||
"access_token": "mock-access-token",
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"expires_at": 9999999999,
|
||||
"token_type": "Bearer",
|
||||
},
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
side_effect=ImplementationUnavailableError,
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
@@ -99,6 +99,7 @@ FIXTURES = [
|
||||
"silabs_fan",
|
||||
"silabs_laundrywasher",
|
||||
"silabs_light_switch",
|
||||
"silabs_range_hood",
|
||||
"silabs_refrigerator",
|
||||
"silabs_water_heater",
|
||||
"switchbot_k11_plus",
|
||||
|
||||
676
tests/components/matter/fixtures/nodes/silabs_range_hood.json
Normal file
676
tests/components/matter/fixtures/nodes/silabs_range_hood.json
Normal file
@@ -0,0 +1,676 @@
|
||||
{
|
||||
"node_id": 114,
|
||||
"date_commissioned": "2026-03-28T13:40:57.299000",
|
||||
"last_interview": "2026-03-28T13:41:32.823000",
|
||||
"interview_version": 6,
|
||||
"available": true,
|
||||
"is_bridge": false,
|
||||
"attributes": {
|
||||
"0/65/0": [],
|
||||
"0/65/65533": 1,
|
||||
"0/65/65532": 0,
|
||||
"0/65/65531": [0, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/65/65529": [],
|
||||
"0/65/65528": [],
|
||||
"0/64/0": [],
|
||||
"0/64/65533": 1,
|
||||
"0/64/65532": 0,
|
||||
"0/64/65531": [0, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/64/65529": [],
|
||||
"0/64/65528": [],
|
||||
"0/63/0": [],
|
||||
"0/63/1": [],
|
||||
"0/63/2": 4,
|
||||
"0/63/3": 3,
|
||||
"0/63/65533": 2,
|
||||
"0/63/65532": 0,
|
||||
"0/63/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/63/65529": [0, 1, 3, 4],
|
||||
"0/63/65528": [5, 2],
|
||||
"0/62/0": [
|
||||
{
|
||||
"1": "FTABAQUkAgE3AyQTAhgmBFdVeS8mBdeLJkQ3BiQVAiQRchgkBwEkCAEwCUEEJOQ9kvNA6n0D7h8CVIGwLHqkyK1e16SizjGFxQXnlTPYX+2sFmp2ElaeNjEOaTKlQAtmUQCr7MPKb4gQnY1LVjcKNQEoARgkAgE2AwQCBAEYMAQU8zOoH6iO/qli6VOfyCglAd3NQlUwBRRa34d1hFPuca7UFWclq9cFnlPhShgwC0AFLtdLkSZTnoRLjiHfLIzlYc+GZeYZpvBZqheLaytsm3XrRvPFDELtX5SWUAv1VuKXcLKFwcQ/y7beTutRbieGGA==",
|
||||
"2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEAV5qZprx2HWOKSP2iCzsI7A0CHgZVtbwsQ/y4ssETfB9z00733STIN0AfD552Vi1h6fJSeEg0/pA82bJL/y0azcKNQEpARgkAmAwBBRa34d1hFPuca7UFWclq9cFnlPhSjAFFG9oKFV1nAO5dx/+jKvq8o8oKcZbGDALQDD8OnB1NcHRxx387f9wZeFDYf32VZ3ZENQrlWBTQZqEKP+K6XjWmjTWttDEeW1kiNtB1T5ZBIaJUxVdqMuNQx8Y",
|
||||
"254": 3
|
||||
}
|
||||
],
|
||||
"0/62/1": [
|
||||
{
|
||||
"1": "BG1FPaj8U/IZMJ0lkYRWnL7PNr67I7GmbKqrzOPKp92GWZkMLbxHskSpehAxMxW6nepQHWr+Eq9LLp7wy4CB7a4=",
|
||||
"2": 4939,
|
||||
"3": 2,
|
||||
"4": 114,
|
||||
"5": "ha",
|
||||
"254": 3
|
||||
}
|
||||
],
|
||||
"0/62/2": 5,
|
||||
"0/62/3": 3,
|
||||
"0/62/4": [
|
||||
"FTABAQAkAgE3AyYUyakYCSYVj6gLsxgmBH2IWjEkBQA3BiYUyakYCSYVj6gLsxgkBwEkCAEwCUEEgYwxrTB+tyiEGfrRwjlXTG34MiQtJXbg5Qqd0ohdRW7MfwYY7vZiX/0h9hI8MqUralFaVPcnghAP0MSJm1YrqTcKNQEpARgkAmAwBBS3BS9aJzt+p6i28Nj+trB2Uu+vdzAFFLcFL1onO36nqLbw2P62sHZS7693GDALQJ4lEwFc2hajQcqS2EERyUvKMaGClZUX11eBfZgrHewkwf3+xuFEUQ8duKOp0owZwSAiuYIJ8afw3R+dO6TGX54Y",
|
||||
"FTABAQAkAgE3AycUQhmZbaIbYjokFQIYJgRWZLcqJAUANwYnFEIZmW2iG2I6JBUCGCQHASQIATAJQQT2AlKGW/kOMjqayzeO0md523/fuhrhGEUU91uQpTiKo0I7wcPpKnmrwfQNPX6g0kEQl+VGaXa3e22lzfu5Tzp0Nwo1ASkBGCQCYDAEFOOMk13ScMKuT2hlaydi1yEJnhTqMAUU44yTXdJwwq5PaGVrJ2LXIQmeFOoYMAtAv2jJd1qd5miXbYesH1XrJ+vgyY0hzGuZ78N6Jw4Cb1oN1sLSpA+PNM0u7+hsEqcSvvn2eSV8EaRR+hg5YQjHDxg=",
|
||||
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEbUU9qPxT8hkwnSWRhFacvs82vrsjsaZsqqvM48qn3YZZmQwtvEeyRKl6EDEzFbqd6lAdav4Sr0sunvDLgIHtrjcKNQEpARgkAmAwBBRvaChVdZwDuXcf/oyr6vKPKCnGWzAFFG9oKFV1nAO5dx/+jKvq8o8oKcZbGDALQLa3jnnqN0/o6VG8wM4V9FDzrgDfKPd5cn3BBz77K80Jzo/aNotaTNOa6zX//yIvOkBZfGyq1Dh1vXZ4g2NKcXoY"
|
||||
],
|
||||
"0/62/5": 3,
|
||||
"0/62/65533": 2,
|
||||
"0/62/65532": 0,
|
||||
"0/62/65531": [0, 1, 2, 3, 4, 5, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11, 12, 13],
|
||||
"0/62/65528": [1, 3, 5, 8, 14],
|
||||
"0/60/0": 0,
|
||||
"0/60/1": null,
|
||||
"0/60/2": null,
|
||||
"0/60/65533": 1,
|
||||
"0/60/65532": 0,
|
||||
"0/60/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/60/65529": [0, 2],
|
||||
"0/60/65528": [],
|
||||
"0/52/0": [
|
||||
{
|
||||
"0": 10,
|
||||
"1": "UART",
|
||||
"3": 336
|
||||
},
|
||||
{
|
||||
"0": 9,
|
||||
"1": "Bluetoot",
|
||||
"3": 1224
|
||||
},
|
||||
{
|
||||
"0": 8,
|
||||
"1": "Bluetoot",
|
||||
"3": 600
|
||||
},
|
||||
{
|
||||
"0": 7,
|
||||
"1": "Bluetoot",
|
||||
"3": 380
|
||||
},
|
||||
{
|
||||
"0": 6,
|
||||
"1": "shell",
|
||||
"3": 1280
|
||||
},
|
||||
{
|
||||
"0": 5,
|
||||
"1": "OT Seria",
|
||||
"3": 3664
|
||||
},
|
||||
{
|
||||
"0": 4,
|
||||
"1": "RangeHoo",
|
||||
"3": 3076
|
||||
},
|
||||
{
|
||||
"0": 3,
|
||||
"1": "Tmr Svc",
|
||||
"3": 1060
|
||||
},
|
||||
{
|
||||
"0": 2,
|
||||
"1": "IDLE",
|
||||
"3": 1064
|
||||
},
|
||||
{
|
||||
"0": 1,
|
||||
"1": "OT Stack",
|
||||
"3": 2848
|
||||
},
|
||||
{
|
||||
"0": 0,
|
||||
"1": "CHIP",
|
||||
"3": 3096
|
||||
}
|
||||
],
|
||||
"0/52/1": 103512,
|
||||
"0/52/2": 26832,
|
||||
"0/52/65533": 1,
|
||||
"0/52/65532": 1,
|
||||
"0/52/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/52/65529": [0],
|
||||
"0/52/65528": [],
|
||||
"0/52/3": 36088,
|
||||
"0/51/0": [
|
||||
{
|
||||
"0": "MyHome",
|
||||
"1": true,
|
||||
"2": null,
|
||||
"3": null,
|
||||
"4": "XrM7nyFYrIQ=",
|
||||
"5": [],
|
||||
"6": [
|
||||
"/QANuACgAAAAAAD//gBsAA==",
|
||||
"/VX8YmMnAAF4u+orFSYj+A==",
|
||||
"/QANuACgAABiekSD+SLHxg==",
|
||||
"/oAAAAAAAABcszufIVishA=="
|
||||
],
|
||||
"7": 4
|
||||
}
|
||||
],
|
||||
"0/51/1": 5,
|
||||
"0/51/2": 118,
|
||||
"0/51/3": 13,
|
||||
"0/51/4": 1,
|
||||
"0/51/5": [],
|
||||
"0/51/6": [],
|
||||
"0/51/7": [],
|
||||
"0/51/8": false,
|
||||
"0/51/65533": 2,
|
||||
"0/51/65532": 0,
|
||||
"0/51/65531": [
|
||||
0, 1, 8, 3, 4, 5, 6, 7, 2, 65532, 65533, 65528, 65529, 65531
|
||||
],
|
||||
"0/51/65529": [0, 1],
|
||||
"0/51/65528": [2],
|
||||
"0/48/0": 0,
|
||||
"0/48/1": {
|
||||
"0": 60,
|
||||
"1": 900
|
||||
},
|
||||
"0/48/2": 0,
|
||||
"0/48/3": 0,
|
||||
"0/48/4": true,
|
||||
"0/48/65533": 2,
|
||||
"0/48/65532": 0,
|
||||
"0/48/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/48/65529": [0, 2, 4],
|
||||
"0/48/65528": [1, 3, 5],
|
||||
"0/40/0": 19,
|
||||
"0/40/1": "Silabs",
|
||||
"0/40/2": 65521,
|
||||
"0/40/3": "SL-RangeHood",
|
||||
"0/40/4": 32773,
|
||||
"0/40/5": "",
|
||||
"0/40/6": "**REDACTED**",
|
||||
"0/40/7": 1,
|
||||
"0/40/8": "TEST_VERSION",
|
||||
"0/40/9": 1,
|
||||
"0/40/10": "1",
|
||||
"0/40/11": "20200101",
|
||||
"0/40/12": "",
|
||||
"0/40/13": "",
|
||||
"0/40/14": "",
|
||||
"0/40/15": "",
|
||||
"0/40/16": false,
|
||||
"0/40/18": "CF23A858DDB010C9",
|
||||
"0/40/19": {
|
||||
"0": 3,
|
||||
"1": 3
|
||||
},
|
||||
"0/40/21": 17104896,
|
||||
"0/40/22": 1,
|
||||
"0/40/24": 1,
|
||||
"0/40/65533": 5,
|
||||
"0/40/65532": 0,
|
||||
"0/40/65531": [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 19, 21, 22, 11, 12, 13, 14, 15, 16, 18,
|
||||
65532, 65533, 65528, 65529, 65531
|
||||
],
|
||||
"0/40/65529": [],
|
||||
"0/40/65528": [],
|
||||
"0/31/0": [
|
||||
{
|
||||
"1": 5,
|
||||
"2": 2,
|
||||
"3": [112233],
|
||||
"4": null,
|
||||
"254": 3
|
||||
}
|
||||
],
|
||||
"0/31/2": 4,
|
||||
"0/31/3": 3,
|
||||
"0/31/4": 4,
|
||||
"0/31/65533": 2,
|
||||
"0/31/65532": 0,
|
||||
"0/31/65531": [0, 2, 3, 4, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/31/65529": [],
|
||||
"0/31/65528": [],
|
||||
"0/29/0": [
|
||||
{
|
||||
"0": 22,
|
||||
"1": 1
|
||||
}
|
||||
],
|
||||
"0/29/1": [65, 64, 63, 62, 60, 52, 51, 48, 40, 31, 29, 49, 42, 53],
|
||||
"0/29/2": [41],
|
||||
"0/29/3": [1, 2],
|
||||
"0/29/65533": 3,
|
||||
"0/29/65532": 0,
|
||||
"0/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/29/65529": [],
|
||||
"0/29/65528": [],
|
||||
"0/49/0": 1,
|
||||
"0/49/1": [
|
||||
{
|
||||
"0": "p0jbsOzJRNw=",
|
||||
"1": true
|
||||
}
|
||||
],
|
||||
"0/49/4": true,
|
||||
"0/49/5": 0,
|
||||
"0/49/6": "p0jbsOzJRNw=",
|
||||
"0/49/7": null,
|
||||
"0/49/2": 10,
|
||||
"0/49/3": 20,
|
||||
"0/49/9": 10,
|
||||
"0/49/10": 5,
|
||||
"0/49/65533": 2,
|
||||
"0/49/65532": 2,
|
||||
"0/49/65531": [
|
||||
0, 1, 4, 5, 6, 7, 2, 3, 9, 10, 65532, 65533, 65528, 65529, 65531
|
||||
],
|
||||
"0/49/65529": [0, 3, 4, 6, 8],
|
||||
"0/49/65528": [1, 5, 7],
|
||||
"0/42/0": [],
|
||||
"0/42/1": true,
|
||||
"0/42/2": 1,
|
||||
"0/42/3": null,
|
||||
"0/42/65533": 1,
|
||||
"0/42/65532": 0,
|
||||
"0/42/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
|
||||
"0/42/65529": [0],
|
||||
"0/42/65528": [],
|
||||
"0/53/0": 25,
|
||||
"0/53/1": 5,
|
||||
"0/53/2": "MyHome",
|
||||
"0/53/3": 4660,
|
||||
"0/53/4": 12054125955590472924,
|
||||
"0/53/5": "QP0ADbgAoAAA",
|
||||
"0/53/7": [
|
||||
{
|
||||
"0": 13438285129078731668,
|
||||
"1": 21,
|
||||
"2": 14336,
|
||||
"3": 1004376,
|
||||
"4": 210290,
|
||||
"5": 1,
|
||||
"6": -93,
|
||||
"7": -91,
|
||||
"8": 0,
|
||||
"9": 0,
|
||||
"10": true,
|
||||
"11": true,
|
||||
"12": true,
|
||||
"13": false
|
||||
},
|
||||
{
|
||||
"0": 2202349555917590819,
|
||||
"1": 17,
|
||||
"2": 25600,
|
||||
"3": 168304,
|
||||
"4": 58526,
|
||||
"5": 3,
|
||||
"6": -67,
|
||||
"7": -67,
|
||||
"8": 26,
|
||||
"9": 0,
|
||||
"10": true,
|
||||
"11": true,
|
||||
"12": true,
|
||||
"13": false
|
||||
},
|
||||
{
|
||||
"0": 10563246212816049995,
|
||||
"1": 11,
|
||||
"2": 26624,
|
||||
"3": 1951226,
|
||||
"4": 177215,
|
||||
"5": 3,
|
||||
"6": -37,
|
||||
"7": -33,
|
||||
"8": 0,
|
||||
"9": 0,
|
||||
"10": true,
|
||||
"11": true,
|
||||
"12": true,
|
||||
"13": false
|
||||
},
|
||||
{
|
||||
"0": 18079024453762862237,
|
||||
"1": 11,
|
||||
"2": 31744,
|
||||
"3": 3169619,
|
||||
"4": 21445,
|
||||
"5": 3,
|
||||
"6": -39,
|
||||
"7": -40,
|
||||
"8": 11,
|
||||
"9": 0,
|
||||
"10": true,
|
||||
"11": true,
|
||||
"12": true,
|
||||
"13": false
|
||||
},
|
||||
{
|
||||
"0": 14318601490803184919,
|
||||
"1": 11,
|
||||
"2": 33792,
|
||||
"3": 3478972,
|
||||
"4": 47143,
|
||||
"5": 3,
|
||||
"6": -41,
|
||||
"7": -41,
|
||||
"8": 0,
|
||||
"9": 0,
|
||||
"10": true,
|
||||
"11": true,
|
||||
"12": true,
|
||||
"13": false
|
||||
},
|
||||
{
|
||||
"0": 6498271992183290326,
|
||||
"1": 8,
|
||||
"2": 40960,
|
||||
"3": 363662,
|
||||
"4": 47746,
|
||||
"5": 3,
|
||||
"6": -55,
|
||||
"7": -56,
|
||||
"8": 25,
|
||||
"9": 0,
|
||||
"10": true,
|
||||
"11": true,
|
||||
"12": true,
|
||||
"13": false
|
||||
},
|
||||
{
|
||||
"0": 4206032556233211940,
|
||||
"1": 11,
|
||||
"2": 53248,
|
||||
"3": 936980,
|
||||
"4": 213306,
|
||||
"5": 2,
|
||||
"6": -82,
|
||||
"7": -85,
|
||||
"8": 0,
|
||||
"9": 0,
|
||||
"10": true,
|
||||
"11": true,
|
||||
"12": true,
|
||||
"13": false
|
||||
},
|
||||
{
|
||||
"0": 8265194500311707858,
|
||||
"1": 20,
|
||||
"2": 58368,
|
||||
"3": 126887,
|
||||
"4": 48540,
|
||||
"5": 3,
|
||||
"6": -63,
|
||||
"7": -64,
|
||||
"8": 0,
|
||||
"9": 0,
|
||||
"10": true,
|
||||
"11": true,
|
||||
"12": true,
|
||||
"13": false
|
||||
}
|
||||
],
|
||||
"0/53/8": [
|
||||
{
|
||||
"0": 13438285129078731668,
|
||||
"1": 14336,
|
||||
"2": 14,
|
||||
"3": 40,
|
||||
"4": 2,
|
||||
"5": 1,
|
||||
"6": 2,
|
||||
"7": 21,
|
||||
"8": true,
|
||||
"9": true
|
||||
},
|
||||
{
|
||||
"0": 2202349555917590819,
|
||||
"1": 25600,
|
||||
"2": 25,
|
||||
"3": 40,
|
||||
"4": 1,
|
||||
"5": 3,
|
||||
"6": 3,
|
||||
"7": 17,
|
||||
"8": true,
|
||||
"9": true
|
||||
},
|
||||
{
|
||||
"0": 10563246212816049995,
|
||||
"1": 26624,
|
||||
"2": 26,
|
||||
"3": 40,
|
||||
"4": 1,
|
||||
"5": 3,
|
||||
"6": 3,
|
||||
"7": 11,
|
||||
"8": true,
|
||||
"9": true
|
||||
},
|
||||
{
|
||||
"0": 6823863415041731716,
|
||||
"1": 27648,
|
||||
"2": 27,
|
||||
"3": 63,
|
||||
"4": 0,
|
||||
"5": 0,
|
||||
"6": 0,
|
||||
"7": 0,
|
||||
"8": true,
|
||||
"9": false
|
||||
},
|
||||
{
|
||||
"0": 18079024453762862237,
|
||||
"1": 31744,
|
||||
"2": 31,
|
||||
"3": 40,
|
||||
"4": 1,
|
||||
"5": 3,
|
||||
"6": 3,
|
||||
"7": 11,
|
||||
"8": true,
|
||||
"9": true
|
||||
},
|
||||
{
|
||||
"0": 14318601490803184919,
|
||||
"1": 33792,
|
||||
"2": 33,
|
||||
"3": 40,
|
||||
"4": 1,
|
||||
"5": 3,
|
||||
"6": 3,
|
||||
"7": 11,
|
||||
"8": true,
|
||||
"9": true
|
||||
},
|
||||
{
|
||||
"0": 6498271992183290326,
|
||||
"1": 40960,
|
||||
"2": 40,
|
||||
"3": 31,
|
||||
"4": 1,
|
||||
"5": 3,
|
||||
"6": 3,
|
||||
"7": 9,
|
||||
"8": true,
|
||||
"9": true
|
||||
},
|
||||
{
|
||||
"0": 12602498247788046175,
|
||||
"1": 47104,
|
||||
"2": 46,
|
||||
"3": 40,
|
||||
"4": 1,
|
||||
"5": 0,
|
||||
"6": 0,
|
||||
"7": 17,
|
||||
"8": true,
|
||||
"9": false
|
||||
},
|
||||
{
|
||||
"0": 4206032556233211940,
|
||||
"1": 53248,
|
||||
"2": 52,
|
||||
"3": 40,
|
||||
"4": 2,
|
||||
"5": 2,
|
||||
"6": 2,
|
||||
"7": 11,
|
||||
"8": true,
|
||||
"9": true
|
||||
},
|
||||
{
|
||||
"0": 8265194500311707858,
|
||||
"1": 58368,
|
||||
"2": 57,
|
||||
"3": 40,
|
||||
"4": 1,
|
||||
"5": 3,
|
||||
"6": 3,
|
||||
"7": 20,
|
||||
"8": true,
|
||||
"9": true
|
||||
}
|
||||
],
|
||||
"0/53/9": 1664395405,
|
||||
"0/53/10": 68,
|
||||
"0/53/11": 227,
|
||||
"0/53/12": 140,
|
||||
"0/53/13": 31,
|
||||
"0/53/56": 65536,
|
||||
"0/53/57": 0,
|
||||
"0/53/58": 0,
|
||||
"0/53/59": {
|
||||
"0": 672,
|
||||
"1": 143
|
||||
},
|
||||
"0/53/60": "AB//4A==",
|
||||
"0/53/61": {
|
||||
"0": true,
|
||||
"1": false,
|
||||
"2": true,
|
||||
"3": true,
|
||||
"4": true,
|
||||
"5": true,
|
||||
"6": false,
|
||||
"7": true,
|
||||
"8": true,
|
||||
"9": true,
|
||||
"10": true,
|
||||
"11": true
|
||||
},
|
||||
"0/53/62": [],
|
||||
"0/53/63": null,
|
||||
"0/53/64": null,
|
||||
"0/53/65533": 3,
|
||||
"0/53/65532": 15,
|
||||
"0/53/65531": [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
|
||||
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
|
||||
57, 58, 59, 60, 61, 62, 65532, 65533, 65528, 65529, 65531
|
||||
],
|
||||
"0/53/65529": [0],
|
||||
"0/53/65528": [],
|
||||
"0/53/6": 0,
|
||||
"0/53/14": 1,
|
||||
"0/53/15": 1,
|
||||
"0/53/16": 1,
|
||||
"0/53/17": 0,
|
||||
"0/53/18": 1,
|
||||
"0/53/19": 1,
|
||||
"0/53/20": 0,
|
||||
"0/53/21": 0,
|
||||
"0/53/22": 651,
|
||||
"0/53/23": 637,
|
||||
"0/53/24": 14,
|
||||
"0/53/25": 637,
|
||||
"0/53/26": 636,
|
||||
"0/53/27": 14,
|
||||
"0/53/28": 651,
|
||||
"0/53/29": 0,
|
||||
"0/53/30": 0,
|
||||
"0/53/31": 0,
|
||||
"0/53/32": 0,
|
||||
"0/53/33": 382,
|
||||
"0/53/34": 1,
|
||||
"0/53/35": 0,
|
||||
"0/53/36": 113,
|
||||
"0/53/37": 0,
|
||||
"0/53/38": 0,
|
||||
"0/53/39": 406,
|
||||
"0/53/40": 266,
|
||||
"0/53/41": 112,
|
||||
"0/53/42": 369,
|
||||
"0/53/43": 0,
|
||||
"0/53/44": 0,
|
||||
"0/53/45": 0,
|
||||
"0/53/46": 0,
|
||||
"0/53/47": 0,
|
||||
"0/53/48": 0,
|
||||
"0/53/49": 9,
|
||||
"0/53/50": 0,
|
||||
"0/53/51": 28,
|
||||
"0/53/52": 0,
|
||||
"0/53/53": 0,
|
||||
"0/53/54": 0,
|
||||
"0/53/55": 0,
|
||||
"1/29/0": [
|
||||
{
|
||||
"0": 122,
|
||||
"1": 1
|
||||
}
|
||||
],
|
||||
"1/29/1": [29, 514],
|
||||
"1/29/2": [],
|
||||
"1/29/3": [],
|
||||
"1/29/65533": 3,
|
||||
"1/29/65532": 0,
|
||||
"1/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
|
||||
"1/29/65529": [],
|
||||
"1/29/65528": [],
|
||||
"1/514/0": 0,
|
||||
"1/514/1": 0,
|
||||
"1/514/2": 0,
|
||||
"1/514/3": 0,
|
||||
"1/514/65533": 5,
|
||||
"1/514/65532": 0,
|
||||
"1/514/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
|
||||
"1/514/65529": [],
|
||||
"1/514/65528": [],
|
||||
"2/29/0": [
|
||||
{
|
||||
"0": 256,
|
||||
"1": 1
|
||||
}
|
||||
],
|
||||
"2/29/1": [29, 3, 4, 6],
|
||||
"2/29/2": [],
|
||||
"2/29/3": [],
|
||||
"2/29/65533": 3,
|
||||
"2/29/65532": 0,
|
||||
"2/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531],
|
||||
"2/29/65529": [],
|
||||
"2/29/65528": [],
|
||||
"2/3/0": 0,
|
||||
"2/3/1": 2,
|
||||
"2/3/65533": 6,
|
||||
"2/3/65532": 0,
|
||||
"2/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531],
|
||||
"2/3/65529": [0, 64],
|
||||
"2/3/65528": [],
|
||||
"2/4/0": 128,
|
||||
"2/4/65533": 4,
|
||||
"2/4/65532": 1,
|
||||
"2/4/65531": [0, 65532, 65533, 65528, 65529, 65531],
|
||||
"2/4/65529": [0, 1, 2, 3, 4, 5],
|
||||
"2/4/65528": [0, 1, 2, 3],
|
||||
"2/6/0": false,
|
||||
"2/6/65533": 6,
|
||||
"2/6/65532": 1,
|
||||
"2/6/65531": [0, 65532, 65533, 65528, 65529, 65531],
|
||||
"2/6/65529": [0, 1, 2],
|
||||
"2/6/65528": [],
|
||||
"2/6/16384": false,
|
||||
"2/6/16385": 0,
|
||||
"2/6/16386": 0,
|
||||
"2/6/16387": null
|
||||
},
|
||||
"attribute_subscriptions": []
|
||||
}
|
||||
@@ -4211,6 +4211,57 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_buttons[silabs_range_hood][button.sl_rangehood_identify-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'button.sl_rangehood_identify',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Identify',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Identify',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-0000000000000072-MatterNodeDevice-2-IdentifyButton-3-1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_buttons[silabs_range_hood][button.sl_rangehood_identify-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'identify',
|
||||
'friendly_name': 'SL-RangeHood Identify',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.sl_rangehood_identify',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_buttons[silabs_refrigerator][button.refrigerator_identify-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -413,3 +413,66 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_fans[silabs_range_hood][fan.sl_rangehood-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'preset_modes': list([
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'fan',
|
||||
'entity_category': None,
|
||||
'entity_id': 'fan.sl_rangehood',
|
||||
'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': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <FanEntityFeature: 56>,
|
||||
'translation_key': 'fan',
|
||||
'unique_id': '00000000000004D2-0000000000000072-MatterNodeDevice-1-MatterFan-514-0',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_fans[silabs_range_hood][fan.sl_rangehood-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'SL-RangeHood',
|
||||
'preset_mode': None,
|
||||
'preset_modes': list([
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
]),
|
||||
'supported_features': <FanEntityFeature: 56>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'fan.sl_rangehood',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -803,3 +803,62 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_lights[silabs_range_hood][light.sl_rangehood-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
}),
|
||||
'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.sl_rangehood',
|
||||
'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': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'light',
|
||||
'unique_id': '00000000000004D2-0000000000000072-MatterNodeDevice-2-MatterLight-6-0',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_lights[silabs_range_hood][light.sl_rangehood-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'color_mode': None,
|
||||
'friendly_name': 'SL-RangeHood',
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.sl_rangehood',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -4503,6 +4503,69 @@
|
||||
'state': 'Colors',
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[silabs_range_hood][select.sl_rangehood_power_on_behavior-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'on',
|
||||
'off',
|
||||
'toggle',
|
||||
'previous',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.sl_rangehood_power_on_behavior',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Power-on behavior',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power-on behavior',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'startup_on_off',
|
||||
'unique_id': '00000000000004D2-0000000000000072-MatterNodeDevice-2-MatterStartUpOnOff-6-16387',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[silabs_range_hood][select.sl_rangehood_power_on_behavior-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'SL-RangeHood Power-on behavior',
|
||||
'options': list([
|
||||
'on',
|
||||
'off',
|
||||
'toggle',
|
||||
'previous',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.sl_rangehood_power_on_behavior',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'previous',
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[silabs_refrigerator][select.refrigerator_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user