mirror of
https://github.com/home-assistant/core.git
synced 2026-05-19 14:51:46 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 635669278c | |||
| 8536f2b4cb | |||
| 387bf83ba8 | |||
| 18b0f54a3e | |||
| f76e295204 | |||
| e63b17cd58 | |||
| 05e23f0fc7 | |||
| fca4ef3b1e | |||
| 1c5eb92c9c | |||
| 3337dd4ed7 | |||
| f1d21685e6 | |||
| 73f27549e4 | |||
| 1882b914dc | |||
| 06f99dc9ba | |||
| 2e2c718d94 | |||
| b8f56a6ed6 | |||
| db37dbec03 | |||
| 579f44468e | |||
| d452e957c9 | |||
| 5f9bcd583b | |||
| c0c508c7a2 | |||
| 13f5adfa84 | |||
| a07a3a61bf | |||
| 848162debd | |||
| 07cd669bc1 |
@@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.CLIMATE,
|
||||
Platform.EVENT,
|
||||
Platform.FAN,
|
||||
Platform.LIGHT,
|
||||
Platform.SELECT,
|
||||
@@ -28,7 +29,6 @@ PLATFORMS = [
|
||||
Platform.TIME,
|
||||
]
|
||||
|
||||
|
||||
KEEP_ALIVE_INTERVAL = timedelta(minutes=1)
|
||||
SYNC_TIME_INTERVAL = timedelta(hours=1)
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Support for Balboa events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from pybalboa import EVENT_UPDATE, SpaClient
|
||||
|
||||
from homeassistant.components.event import EventEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from . import BalboaConfigEntry
|
||||
from .entity import BalboaEntity
|
||||
|
||||
FAULT = "fault"
|
||||
FAULT_DATE = "fault_date"
|
||||
REQUEST_FAULT_LOG_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
FAULT_MESSAGE_CODE_MAP: dict[int, str] = {
|
||||
15: "sensor_out_of_sync",
|
||||
16: "low_flow",
|
||||
17: "flow_failed",
|
||||
18: "settings_reset",
|
||||
19: "priming_mode",
|
||||
20: "clock_failed",
|
||||
21: "settings_reset",
|
||||
22: "memory_failure",
|
||||
26: "service_sensor_sync",
|
||||
27: "heater_dry",
|
||||
28: "heater_may_be_dry",
|
||||
29: "water_too_hot",
|
||||
30: "heater_too_hot",
|
||||
31: "sensor_a_fault",
|
||||
32: "sensor_b_fault",
|
||||
34: "pump_stuck",
|
||||
35: "hot_fault",
|
||||
36: "gfci_test_failed",
|
||||
37: "standby_mode",
|
||||
}
|
||||
FAULT_EVENT_TYPES = sorted(set(FAULT_MESSAGE_CODE_MAP.values()))
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: BalboaConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the spa's events."""
|
||||
async_add_entities([BalboaEventEntity(entry.runtime_data)])
|
||||
|
||||
|
||||
class BalboaEventEntity(BalboaEntity, EventEntity):
|
||||
"""Representation of a Balboa event entity."""
|
||||
|
||||
_attr_event_types = FAULT_EVENT_TYPES
|
||||
_attr_translation_key = FAULT
|
||||
|
||||
def __init__(self, spa: SpaClient) -> None:
|
||||
"""Initialize a Balboa event entity."""
|
||||
super().__init__(spa, FAULT)
|
||||
|
||||
@callback
|
||||
def _async_handle_event(self) -> None:
|
||||
"""Handle the fault event."""
|
||||
if not (fault := self._client.fault):
|
||||
return
|
||||
fault_date = fault.fault_datetime.isoformat()
|
||||
if self.state_attributes.get(FAULT_DATE) != fault_date:
|
||||
self._trigger_event(
|
||||
FAULT_MESSAGE_CODE_MAP.get(fault.message_code, fault.message),
|
||||
{FAULT_DATE: fault_date, "code": fault.message_code},
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(self._client.on(EVENT_UPDATE, self._async_handle_event))
|
||||
|
||||
async def request_fault_log(now: datetime | None = None) -> None:
|
||||
"""Request the most recent fault log."""
|
||||
await self._client.request_fault_log()
|
||||
|
||||
await request_fault_log()
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass, request_fault_log, REQUEST_FAULT_LOG_INTERVAL
|
||||
)
|
||||
)
|
||||
@@ -57,6 +57,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"fault": {
|
||||
"name": "Fault",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"sensor_out_of_sync": "Sensors are out of sync",
|
||||
"low_flow": "The water flow is low",
|
||||
"flow_failed": "The water flow has failed",
|
||||
"settings_reset": "The settings have been reset",
|
||||
"priming_mode": "Priming mode",
|
||||
"clock_failed": "The clock has failed",
|
||||
"memory_failure": "Program memory failure",
|
||||
"service_sensor_sync": "Sensors are out of sync -- call for service",
|
||||
"heater_dry": "The heater is dry",
|
||||
"heater_may_be_dry": "The heater may be dry",
|
||||
"water_too_hot": "The water is too hot",
|
||||
"heater_too_hot": "The heater is too hot",
|
||||
"sensor_a_fault": "Sensor A fault",
|
||||
"sensor_b_fault": "Sensor B fault",
|
||||
"pump_stuck": "A pump may be stuck on",
|
||||
"hot_fault": "Hot fault",
|
||||
"gfci_test_failed": "The GFCI test failed",
|
||||
"standby_mode": "Standby mode (hold mode)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"pump": {
|
||||
"name": "Pump {index}"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==29.3.1",
|
||||
"aioesphomeapi==29.3.2",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==2.9.0"
|
||||
],
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
},
|
||||
"services": {
|
||||
"add_all_link": {
|
||||
"name": "Add all link",
|
||||
"name": "Add All-Link",
|
||||
"description": "Tells the Insteon Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.",
|
||||
"fields": {
|
||||
"group": {
|
||||
@@ -120,13 +120,13 @@
|
||||
},
|
||||
"mode": {
|
||||
"name": "[%key:common::config_flow::data::mode%]",
|
||||
"description": "Linking mode controller - IM is controller responder - IM is responder."
|
||||
"description": "Linking mode of the Insteon Modem."
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete_all_link": {
|
||||
"name": "Delete all link",
|
||||
"description": "Tells the Insteon Modem (IM) to remove an All-Link record from the All-Link Database of the IM and a device. Once the IM is set to delete the link, press the link button on the corresponding device to complete the process.",
|
||||
"name": "Delete All-Link",
|
||||
"description": "Tells the Insteon Modem (IM) to remove an All-Link record from the All-Link database of the IM and a device. Once the IM is set to delete the link, press the link button on the corresponding device to complete the process.",
|
||||
"fields": {
|
||||
"group": {
|
||||
"name": "Group",
|
||||
@@ -135,8 +135,8 @@
|
||||
}
|
||||
},
|
||||
"load_all_link_database": {
|
||||
"name": "Load all link database",
|
||||
"description": "Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistent. This may take a LONG time and may need to be repeated to obtain all records.",
|
||||
"name": "Load All-Link database",
|
||||
"description": "Loads the All-Link database for a device. WARNING - Loading a device All-Link database is very time consuming and inconsistent. This may take a LONG time and may need to be repeated to obtain all records.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@@ -149,8 +149,8 @@
|
||||
}
|
||||
},
|
||||
"print_all_link_database": {
|
||||
"name": "Print all link database",
|
||||
"description": "Prints the All-Link Database for a device. Requires that the All-Link Database is loaded into memory.",
|
||||
"name": "Print All-Link database",
|
||||
"description": "Prints the All-Link database for a device. Requires that the All-Link database is loaded into memory.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@@ -159,8 +159,8 @@
|
||||
}
|
||||
},
|
||||
"print_im_all_link_database": {
|
||||
"name": "Print IM all link database",
|
||||
"description": "Prints the All-Link Database for the INSTEON Modem (IM)."
|
||||
"name": "Print IM All-Link database",
|
||||
"description": "Prints the All-Link database for the INSTEON Modem (IM)."
|
||||
},
|
||||
"x10_all_units_off": {
|
||||
"name": "X10 all units off",
|
||||
|
||||
@@ -63,6 +63,10 @@ COMMAND_TEST_FAILURE_STOP = "test.failure.stop"
|
||||
COMMAND_TEST_PANEL_START = "test.panel.start"
|
||||
COMMAND_TEST_PANEL_STOP = "test.panel.stop"
|
||||
COMMAND_TEST_SYSTEM_START = "test.system.start"
|
||||
COMMAND_OUTLET1_OFF = "outlet.1.load.off"
|
||||
COMMAND_OUTLET1_ON = "outlet.1.load.on"
|
||||
COMMAND_OUTLET2_OFF = "outlet.2.load.off"
|
||||
COMMAND_OUTLET2_ON = "outlet.2.load.on"
|
||||
|
||||
INTEGRATION_SUPPORTED_COMMANDS = {
|
||||
COMMAND_BEEPER_DISABLE,
|
||||
@@ -91,4 +95,8 @@ INTEGRATION_SUPPORTED_COMMANDS = {
|
||||
COMMAND_TEST_PANEL_START,
|
||||
COMMAND_TEST_PANEL_STOP,
|
||||
COMMAND_TEST_SYSTEM_START,
|
||||
COMMAND_OUTLET1_OFF,
|
||||
COMMAND_OUTLET1_ON,
|
||||
COMMAND_OUTLET2_OFF,
|
||||
COMMAND_OUTLET2_ON,
|
||||
}
|
||||
|
||||
@@ -74,7 +74,11 @@
|
||||
"test_failure_stop": "Stop simulating a power failure",
|
||||
"test_panel_start": "Start testing the UPS panel",
|
||||
"test_panel_stop": "Stop a UPS panel test",
|
||||
"test_system_start": "Start a system test"
|
||||
"test_system_start": "Start a system test",
|
||||
"outlet_1_load_on": "Power outlet 1 on",
|
||||
"outlet_1_load_off": "Power outlet 1 off",
|
||||
"outlet_2_load_on": "Power outlet 2 on",
|
||||
"outlet_2_load_off": "Power outlet 2 off"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -34,7 +34,7 @@ class SmartThingsConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Create an entry for SmartThings."""
|
||||
if data[CONF_TOKEN]["scope"].split() != SCOPES:
|
||||
if not set(data[CONF_TOKEN]["scope"].split()) >= set(SCOPES):
|
||||
return self.async_abort(reason="missing_scopes")
|
||||
client = SmartThings(session=async_get_clientsession(self.hass))
|
||||
client.authenticate(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
|
||||
|
||||
@@ -196,7 +196,10 @@ class ViCareFan(ViCareEntity, FanEntity):
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the entity is on."""
|
||||
if self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY):
|
||||
if (
|
||||
self._attr_supported_features & FanEntityFeature.TURN_OFF
|
||||
and self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY)
|
||||
):
|
||||
return False
|
||||
|
||||
return self.percentage is not None and self.percentage > 0
|
||||
@@ -209,7 +212,10 @@ class ViCareFan(ViCareEntity, FanEntity):
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon to use in the frontend."""
|
||||
if self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY):
|
||||
if (
|
||||
self._attr_supported_features & FanEntityFeature.TURN_OFF
|
||||
and self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY)
|
||||
):
|
||||
return "mdi:fan-off"
|
||||
if hasattr(self, "_attr_preset_mode"):
|
||||
if self._attr_preset_mode == VentilationMode.VENTILATION:
|
||||
|
||||
@@ -121,6 +121,7 @@ class TriggerBaseEntity(Entity):
|
||||
self._rendered = dict(self._static_rendered)
|
||||
self._parse_result = {CONF_AVAILABILITY}
|
||||
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
|
||||
self._render_error = False
|
||||
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
@@ -146,7 +147,7 @@ class TriggerBaseEntity(Entity):
|
||||
def available(self) -> bool:
|
||||
"""Return availability of the entity."""
|
||||
return (
|
||||
self._rendered is not self._static_rendered
|
||||
self._render_error is False
|
||||
and
|
||||
# Check against False so `None` is ok
|
||||
self._rendered.get(CONF_AVAILABILITY) is not False
|
||||
@@ -176,12 +177,34 @@ class TriggerBaseEntity(Entity):
|
||||
extra_state_attributes[attr] = last_state.attributes[attr]
|
||||
self._rendered[CONF_ATTRIBUTES] = extra_state_attributes
|
||||
|
||||
def _render_availability_template(self, variables: dict[str, Any]) -> None:
|
||||
"""Render availability template."""
|
||||
self._render_error = False
|
||||
rendered = {**self._static_rendered, **self._rendered}
|
||||
key = CONF_AVAILABILITY
|
||||
try:
|
||||
if key in self._to_render_simple:
|
||||
rendered[key] = self._config[key].async_render(
|
||||
variables,
|
||||
parse_result=key in self._parse_result,
|
||||
)
|
||||
except TemplateError as err:
|
||||
logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error(
|
||||
"Error rendering %s template for %s: %s", key, self.entity_id, err
|
||||
)
|
||||
self._render_error = True
|
||||
self._rendered = rendered
|
||||
|
||||
def _render_templates(self, variables: dict[str, Any]) -> None:
|
||||
"""Render templates."""
|
||||
self._render_availability_template(variables)
|
||||
rendered = dict(self._rendered)
|
||||
if CONF_AVAILABILITY in rendered and rendered[CONF_AVAILABILITY] is False:
|
||||
return
|
||||
try:
|
||||
rendered = dict(self._static_rendered)
|
||||
|
||||
for key in self._to_render_simple:
|
||||
if key == CONF_AVAILABILITY:
|
||||
continue
|
||||
rendered[key] = self._config[key].async_render(
|
||||
variables,
|
||||
parse_result=key in self._parse_result,
|
||||
@@ -198,13 +221,13 @@ class TriggerBaseEntity(Entity):
|
||||
self._config[CONF_ATTRIBUTES],
|
||||
variables,
|
||||
)
|
||||
|
||||
self._rendered = rendered
|
||||
except TemplateError as err:
|
||||
logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error(
|
||||
"Error rendering %s template for %s: %s", key, self.entity_id, err
|
||||
)
|
||||
self._rendered = self._static_rendered
|
||||
self._render_error = True
|
||||
return
|
||||
self._rendered = rendered
|
||||
|
||||
|
||||
class ManualTriggerEntity(TriggerBaseEntity):
|
||||
@@ -230,16 +253,15 @@ class ManualTriggerEntity(TriggerBaseEntity):
|
||||
Implementing class should call this last in update method to render templates.
|
||||
Ex: self._process_manual_data(payload)
|
||||
"""
|
||||
|
||||
run_variables: dict[str, Any] = {"value": value}
|
||||
# Silently try if variable is a json and store result in `value_json` if it is.
|
||||
with contextlib.suppress(*JSON_DECODE_EXCEPTIONS):
|
||||
run_variables["value_json"] = json_loads(run_variables["value"])
|
||||
|
||||
variables = {
|
||||
"this": TemplateStateFromEntityId(self.hass, self.entity_id),
|
||||
**(run_variables or {}),
|
||||
}
|
||||
|
||||
self._render_templates(variables)
|
||||
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -243,7 +243,7 @@ aioelectricitymaps==0.4.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==29.3.1
|
||||
aioesphomeapi==29.3.2
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
|
||||
Generated
+1
-1
@@ -231,7 +231,7 @@ aioelectricitymaps==0.4.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==29.3.1
|
||||
aioesphomeapi==29.3.2
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
|
||||
@@ -68,4 +68,6 @@ def client_fixture() -> Generator[MagicMock]:
|
||||
client.pumps = []
|
||||
client.temperature_range.state = LowHighRange.LOW
|
||||
|
||||
client.fault = None
|
||||
|
||||
yield client
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# serializer version: 1
|
||||
# name: test_events[event.fakespa_fault-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'clock_failed',
|
||||
'flow_failed',
|
||||
'gfci_test_failed',
|
||||
'heater_dry',
|
||||
'heater_may_be_dry',
|
||||
'heater_too_hot',
|
||||
'hot_fault',
|
||||
'low_flow',
|
||||
'memory_failure',
|
||||
'priming_mode',
|
||||
'pump_stuck',
|
||||
'sensor_a_fault',
|
||||
'sensor_b_fault',
|
||||
'sensor_out_of_sync',
|
||||
'service_sensor_sync',
|
||||
'settings_reset',
|
||||
'standby_mode',
|
||||
'water_too_hot',
|
||||
]),
|
||||
}),
|
||||
'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.fakespa_fault',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Fault',
|
||||
'platform': 'balboa',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'fault',
|
||||
'unique_id': 'FakeSpa-fault-c0ffee',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_events[event.fakespa_fault-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'clock_failed',
|
||||
'flow_failed',
|
||||
'gfci_test_failed',
|
||||
'heater_dry',
|
||||
'heater_may_be_dry',
|
||||
'heater_too_hot',
|
||||
'hot_fault',
|
||||
'low_flow',
|
||||
'memory_failure',
|
||||
'priming_mode',
|
||||
'pump_stuck',
|
||||
'sensor_a_fault',
|
||||
'sensor_b_fault',
|
||||
'sensor_out_of_sync',
|
||||
'service_sensor_sync',
|
||||
'settings_reset',
|
||||
'standby_mode',
|
||||
'water_too_hot',
|
||||
]),
|
||||
'friendly_name': 'FakeSpa Fault',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.fakespa_fault',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Tests of the events of the balboa integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.event import ATTR_EVENT_TYPE
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import init_integration
|
||||
|
||||
from tests.common import snapshot_platform
|
||||
|
||||
ENTITY_EVENT = "event.fakespa_fault"
|
||||
FAULT_DATE = "fault_date"
|
||||
|
||||
|
||||
async def test_events(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test spa events."""
|
||||
with patch("homeassistant.components.balboa.PLATFORMS", [Platform.EVENT]):
|
||||
entry = await init_integration(hass)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
|
||||
|
||||
|
||||
async def test_event(hass: HomeAssistant, client: MagicMock) -> None:
|
||||
"""Test spa fault event."""
|
||||
await init_integration(hass)
|
||||
|
||||
# check the state is unknown
|
||||
state = hass.states.get(ENTITY_EVENT)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# set a fault
|
||||
client.fault = MagicMock(
|
||||
fault_datetime=datetime(2025, 2, 15, 13, 0), message_code=16
|
||||
)
|
||||
client.emit("")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# check new state is what we expect
|
||||
state = hass.states.get(ENTITY_EVENT)
|
||||
assert state.attributes[ATTR_EVENT_TYPE] == "low_flow"
|
||||
assert state.attributes[FAULT_DATE] == "2025-02-15T13:00:00"
|
||||
assert state.attributes["code"] == 16
|
||||
|
||||
# set fault to None
|
||||
client.fault = None
|
||||
client.emit("")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# validate state remains unchanged
|
||||
state = hass.states.get(ENTITY_EVENT)
|
||||
assert state.attributes[ATTR_EVENT_TYPE] == "low_flow"
|
||||
assert state.attributes[FAULT_DATE] == "2025-02-15T13:00:00"
|
||||
assert state.attributes["code"] == 16
|
||||
|
||||
# set fault to an unknown one
|
||||
client.fault = MagicMock(
|
||||
fault_datetime=datetime(2025, 2, 15, 14, 0), message_code=-1
|
||||
)
|
||||
# validate a ValueError is raises
|
||||
with pytest.raises(ValueError):
|
||||
client.emit("")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# validate state remains unchanged
|
||||
state = hass.states.get(ENTITY_EVENT)
|
||||
assert state.attributes[ATTR_EVENT_TYPE] == "low_flow"
|
||||
assert state.attributes[FAULT_DATE] == "2025-02-15T13:00:00"
|
||||
assert state.attributes["code"] == 16
|
||||
@@ -808,3 +808,52 @@ async def test_availability(
|
||||
entity_state = hass.states.get("sensor.test")
|
||||
assert entity_state
|
||||
assert entity_state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"get_config",
|
||||
[
|
||||
{
|
||||
"command_line": [
|
||||
{
|
||||
"sensor": {
|
||||
"name": "Test",
|
||||
"command": "echo {{ states.sensor.input_sensor.state }}",
|
||||
"availability": "{{ value|is_number}}",
|
||||
"unit_of_measurement": " ",
|
||||
"state_class": "measurement",
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
)
|
||||
async def test_template_render_not_break_for_availability(
|
||||
hass: HomeAssistant, load_yaml_integration: None
|
||||
) -> None:
|
||||
"""Ensure command with templates get rendered properly."""
|
||||
hass.states.async_set("sensor.input_sensor", "sensor_value")
|
||||
|
||||
# Give time for template to load
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(minutes=1),
|
||||
)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
entity_state = hass.states.get("sensor.test")
|
||||
assert entity_state
|
||||
assert entity_state.state == STATE_UNAVAILABLE
|
||||
|
||||
hass.states.async_set("sensor.input_sensor", "1")
|
||||
|
||||
# Give time for template to load
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(minutes=1),
|
||||
)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
entity_state = hass.states.get("sensor.test")
|
||||
assert entity_state
|
||||
assert entity_state.state == "1"
|
||||
|
||||
@@ -1054,3 +1054,54 @@ async def test_availability_in_config(hass: HomeAssistant) -> None:
|
||||
|
||||
state = hass.states.get("sensor.rest_sensor")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_json_response_with_availability(hass: HomeAssistant) -> None:
|
||||
"""Test availability with complex json."""
|
||||
|
||||
respx.get("http://localhost").respond(
|
||||
status_code=HTTPStatus.OK,
|
||||
json={"heartbeatList": {"1": [{"status": 1, "ping": 21.4}]}},
|
||||
)
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
DOMAIN: [
|
||||
{
|
||||
"resource": "http://localhost",
|
||||
"sensor": [
|
||||
{
|
||||
"unique_id": "complex_json",
|
||||
"name": "complex_json",
|
||||
"value_template": '{% set v = value_json.heartbeatList["1"][-1] %}{{ v.ping }}',
|
||||
"availability": '{% set v = value_json.heartbeatList["1"][-1] %}{{ v.status == 1 and is_number(v.ping) }}',
|
||||
"unit_of_measurement": "ms",
|
||||
"state_class": "measurement",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
await async_setup_component(hass, "homeassistant", {})
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1
|
||||
|
||||
state = hass.states.get("sensor.complex_json")
|
||||
assert state.state == "21.4"
|
||||
|
||||
respx.get("http://localhost").respond(
|
||||
status_code=HTTPStatus.OK,
|
||||
json={"heartbeatList": {"1": [{"status": 0, "ping": None}]}},
|
||||
)
|
||||
await hass.services.async_call(
|
||||
"homeassistant",
|
||||
"update_entity",
|
||||
{ATTR_ENTITY_ID: ["sensor.complex_json"]},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.complex_json")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
@@ -261,7 +261,7 @@ async def test_reauthentication(
|
||||
"expires_in": 82806,
|
||||
"scope": "r:devices:* w:devices:* x:devices:* r:hubs:* "
|
||||
"r:locations:* w:locations:* x:locations:* "
|
||||
"r:scenes:* x:scenes:* r:rules:* w:rules:* sse",
|
||||
"r:scenes:* x:scenes:* r:rules:* sse w:rules:*",
|
||||
"access_tier": 0,
|
||||
"installed_app_id": "5aaaa925-2be1-4e40-b257-e4ef59083324",
|
||||
},
|
||||
@@ -279,7 +279,7 @@ async def test_reauthentication(
|
||||
"expires_in": 82806,
|
||||
"scope": "r:devices:* w:devices:* x:devices:* r:hubs:* "
|
||||
"r:locations:* w:locations:* x:locations:* "
|
||||
"r:scenes:* x:scenes:* r:rules:* w:rules:* sse",
|
||||
"r:scenes:* x:scenes:* r:rules:* sse w:rules:*",
|
||||
"access_tier": 0,
|
||||
"installed_app_id": "5aaaa925-2be1-4e40-b257-e4ef59083324",
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import sensor
|
||||
from homeassistant.components.template import template_entity
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import template
|
||||
|
||||
@@ -22,3 +24,46 @@ async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None:
|
||||
entity.add_template_attribute("_hello", tpl_with_hass)
|
||||
|
||||
assert len(entity._template_attrs.get(tpl_with_hass, [])) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{
|
||||
"sensor": {
|
||||
"platform": "template",
|
||||
"sensors": {
|
||||
"test_template_sensor": {
|
||||
"value_template": "{{ states.sensor.test_sensor.state }}",
|
||||
"availability_template": "{{ is_state('sensor.test_sensor', 'on') }}",
|
||||
"icon_template": "{% if states.sensor.test_sensor.state == 'on' %}mdi:on{% else %}mdi:off{% endif %}",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("start_ha")
|
||||
async def test_unavailable_does_not_render_other_state_attributes(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test when entity goes unavailable, other state attributes are not rendered."""
|
||||
hass.states.async_set("sensor.test_sensor", STATE_OFF)
|
||||
|
||||
# When template returns true..
|
||||
hass.states.async_set("sensor.test_sensor", STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Device State should not be unavailable
|
||||
assert hass.states.get("sensor.test_template_sensor").state != STATE_UNAVAILABLE
|
||||
assert hass.states.get("sensor.test_template_sensor").attributes["icon"] == "mdi:on"
|
||||
|
||||
# When Availability template returns false
|
||||
hass.states.async_set("sensor.test_sensor", STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# device state should be unavailable
|
||||
assert hass.states.get("sensor.test_template_sensor").state == STATE_UNAVAILABLE
|
||||
# Icon should be mdi:on as going unavailable does not render state attributes
|
||||
assert hass.states.get("sensor.test_template_sensor").attributes["icon"] == "mdi:on"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,69 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[fan.model0_ventilation-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'preset_modes': list([
|
||||
<VentilationMode.PERMANENT: 'permanent'>,
|
||||
<VentilationMode.VENTILATION: 'ventilation'>,
|
||||
<VentilationMode.SENSOR_DRIVEN: 'sensor_driven'>,
|
||||
<VentilationMode.SENSOR_OVERRIDE: 'sensor_override'>,
|
||||
]),
|
||||
}),
|
||||
'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.model0_ventilation',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:fan',
|
||||
'original_name': 'Ventilation',
|
||||
'platform': 'vicare',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <FanEntityFeature: 9>,
|
||||
'translation_key': 'ventilation',
|
||||
'unique_id': 'gateway0_deviceSerialViAir300F-ventilation',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fan.model0_ventilation-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'model0 Ventilation',
|
||||
'icon': 'mdi:fan',
|
||||
'percentage': 0,
|
||||
'percentage_step': 25.0,
|
||||
'preset_mode': None,
|
||||
'preset_modes': list([
|
||||
<VentilationMode.PERMANENT: 'permanent'>,
|
||||
<VentilationMode.VENTILATION: 'ventilation'>,
|
||||
<VentilationMode.SENSOR_DRIVEN: 'sensor_driven'>,
|
||||
<VentilationMode.SENSOR_OVERRIDE: 'sensor_override'>,
|
||||
]),
|
||||
'supported_features': <FanEntityFeature: 9>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'fan.model0_ventilation',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fan.model1_ventilation-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -62,3 +127,64 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fan.model2_ventilation-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'preset_modes': list([
|
||||
<VentilationMode.VENTILATION: 'ventilation'>,
|
||||
<VentilationMode.STANDBY: 'standby'>,
|
||||
<VentilationMode.STANDARD: 'standard'>,
|
||||
]),
|
||||
}),
|
||||
'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.model2_ventilation',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:fan',
|
||||
'original_name': 'Ventilation',
|
||||
'platform': 'vicare',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <FanEntityFeature: 8>,
|
||||
'translation_key': 'ventilation',
|
||||
'unique_id': 'gateway2_################-ventilation',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fan.model2_ventilation-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'model2 Ventilation',
|
||||
'icon': 'mdi:fan',
|
||||
'preset_mode': None,
|
||||
'preset_modes': list([
|
||||
<VentilationMode.VENTILATION: 'ventilation'>,
|
||||
<VentilationMode.STANDBY: 'standby'>,
|
||||
<VentilationMode.STANDARD: 'standard'>,
|
||||
]),
|
||||
'supported_features': <FanEntityFeature: 8>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'fan.model2_ventilation',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -23,7 +23,9 @@ async def test_all_entities(
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
fixtures: list[Fixture] = [Fixture({"type:boiler"}, "vicare/Vitodens300W.json")]
|
||||
fixtures: list[Fixture] = [
|
||||
Fixture({"type:boiler"}, "vicare/Vitodens300W.json"),
|
||||
]
|
||||
with (
|
||||
patch(f"{MODULE}.login", return_value=MockPyViCare(fixtures)),
|
||||
patch(f"{MODULE}.PLATFORMS", [Platform.CLIMATE]),
|
||||
|
||||
@@ -26,6 +26,7 @@ async def test_all_entities(
|
||||
fixtures: list[Fixture] = [
|
||||
Fixture({"type:ventilation"}, "vicare/ViAir300F.json"),
|
||||
Fixture({"type:ventilation"}, "vicare/VitoPure.json"),
|
||||
Fixture({"type:heatpump"}, "vicare/Vitocal222G_Vitovent300W.json"),
|
||||
]
|
||||
with (
|
||||
patch(f"{MODULE}.login", return_value=MockPyViCare(fixtures)),
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
"""Test template trigger entity."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.const import CONF_ICON, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity
|
||||
from homeassistant.helpers.trigger_template_entity import (
|
||||
CONF_AVAILABILITY,
|
||||
CONF_PICTURE,
|
||||
ManualTriggerEntity,
|
||||
)
|
||||
|
||||
|
||||
async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None:
|
||||
"""Test manual trigger template entity."""
|
||||
config = {
|
||||
"name": template.Template("test_entity", hass),
|
||||
"icon": template.Template(
|
||||
CONF_NAME: template.Template("test_entity", hass),
|
||||
CONF_ICON: template.Template(
|
||||
'{% if value=="on" %} mdi:on {% else %} mdi:off {% endif %}', hass
|
||||
),
|
||||
"picture": template.Template(
|
||||
CONF_PICTURE: template.Template(
|
||||
'{% if value=="on" %} /local/picture_on {% else %} /local/picture_off {% endif %}',
|
||||
hass,
|
||||
),
|
||||
@@ -20,21 +29,137 @@ async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None:
|
||||
|
||||
entity = ManualTriggerEntity(hass, config)
|
||||
entity.entity_id = "test.entity"
|
||||
hass.states.async_set("test.entity", "on")
|
||||
hass.states.async_set("test.entity", STATE_ON)
|
||||
await entity.async_added_to_hass()
|
||||
|
||||
entity._process_manual_data("on")
|
||||
entity._process_manual_data(STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity.name == "test_entity"
|
||||
assert entity.icon == "mdi:on"
|
||||
assert entity.entity_picture == "/local/picture_on"
|
||||
|
||||
hass.states.async_set("test.entity", "off")
|
||||
hass.states.async_set("test.entity", STATE_OFF)
|
||||
await entity.async_added_to_hass()
|
||||
entity._process_manual_data("off")
|
||||
entity._process_manual_data(STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity.name == "test_entity"
|
||||
assert entity.icon == "mdi:off"
|
||||
assert entity.entity_picture == "/local/picture_off"
|
||||
|
||||
|
||||
async def test_trigger_template_availability(hass: HomeAssistant) -> None:
|
||||
"""Test manual trigger template entity availability template."""
|
||||
config = {
|
||||
CONF_NAME: template.Template("test_entity", hass),
|
||||
CONF_ICON: template.Template(
|
||||
'{% if value=="on" %} mdi:on {% else %} mdi:off {% endif %}', hass
|
||||
),
|
||||
CONF_PICTURE: template.Template(
|
||||
'{% if value=="on" %} /local/picture_on {% else %} /local/picture_off {% endif %}',
|
||||
hass,
|
||||
),
|
||||
CONF_AVAILABILITY: template.Template('{{ has_value("test.entity") }}', hass),
|
||||
}
|
||||
|
||||
entity = ManualTriggerEntity(hass, config)
|
||||
entity.entity_id = "test.entity"
|
||||
hass.states.async_set("test.entity", STATE_ON)
|
||||
await entity.async_added_to_hass()
|
||||
|
||||
entity._process_manual_data(STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity.name == "test_entity"
|
||||
assert entity.icon == "mdi:on"
|
||||
assert entity.entity_picture == "/local/picture_on"
|
||||
assert entity.available is True
|
||||
|
||||
hass.states.async_set("test.entity", STATE_OFF)
|
||||
await entity.async_added_to_hass()
|
||||
entity._process_manual_data(STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity.name == "test_entity"
|
||||
assert entity.icon == "mdi:off"
|
||||
assert entity.entity_picture == "/local/picture_off"
|
||||
assert entity.available is True
|
||||
|
||||
hass.states.async_set("test.entity", STATE_UNKNOWN)
|
||||
await entity.async_added_to_hass()
|
||||
entity._process_manual_data(STATE_UNKNOWN)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity.name == "test_entity"
|
||||
assert entity.icon == "mdi:off"
|
||||
assert entity.entity_picture == "/local/picture_off"
|
||||
assert entity.available is False
|
||||
|
||||
|
||||
async def test_trigger_template_availability_fails(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test manual trigger template entity when availability render fails."""
|
||||
config = {
|
||||
CONF_NAME: template.Template("test_entity", hass),
|
||||
CONF_ICON: template.Template(
|
||||
'{% if value=="on" %} mdi:on {% else %} mdi:off {% endif %}', hass
|
||||
),
|
||||
CONF_PICTURE: template.Template(
|
||||
'{% if value=="on" %} /local/picture_on {% else %} /local/picture_off {% endif %}',
|
||||
hass,
|
||||
),
|
||||
CONF_AVAILABILITY: template.Template("{{ incorrect ", hass),
|
||||
}
|
||||
|
||||
entity = ManualTriggerEntity(hass, config)
|
||||
entity.entity_id = "test.entity"
|
||||
hass.states.async_set("test.entity", STATE_ON)
|
||||
await entity.async_added_to_hass()
|
||||
|
||||
entity._process_manual_data(STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "Error rendering availability template for test.entity" in caplog.text
|
||||
|
||||
|
||||
async def test_trigger_template_complex(hass: HomeAssistant) -> None:
|
||||
"""Test manual trigger template entity complex template."""
|
||||
complex_template = """
|
||||
{% set d = {'test_key':'test_data'} %}
|
||||
{{ dict(d) }}
|
||||
|
||||
"""
|
||||
config = {
|
||||
CONF_NAME: template.Template("test_entity", hass),
|
||||
CONF_ICON: template.Template(
|
||||
'{% if value=="on" %} mdi:on {% else %} mdi:off {% endif %}', hass
|
||||
),
|
||||
CONF_PICTURE: template.Template(
|
||||
'{% if value=="on" %} /local/picture_on {% else %} /local/picture_off {% endif %}',
|
||||
hass,
|
||||
),
|
||||
CONF_AVAILABILITY: template.Template('{{ has_value("test.entity") }}', hass),
|
||||
"other_key": template.Template(complex_template, hass),
|
||||
}
|
||||
|
||||
class TestEntity(ManualTriggerEntity):
|
||||
"""Test entity class."""
|
||||
|
||||
extra_template_keys_complex = ("other_key",)
|
||||
|
||||
@property
|
||||
def some_other_key(self) -> dict[str, Any] | None:
|
||||
"""Return extra attributes."""
|
||||
return self._rendered.get("other_key")
|
||||
|
||||
entity = TestEntity(hass, config)
|
||||
entity.entity_id = "test.entity"
|
||||
hass.states.async_set("test.entity", STATE_ON)
|
||||
await entity.async_added_to_hass()
|
||||
|
||||
entity._process_manual_data(STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity.some_other_key == {"test_key": "test_data"}
|
||||
|
||||
Reference in New Issue
Block a user