mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +00:00
Add Shelly RPC device trigger and logbook platforms (#56428)
* Add RPC device trigger and logbook platforms * Single input event for Block and RPC * Add device generation to shelly.click
This commit is contained in:
parent
542f637ac4
commit
47340802b3
@ -31,6 +31,7 @@ from .const import (
|
|||||||
ATTR_CHANNEL,
|
ATTR_CHANNEL,
|
||||||
ATTR_CLICK_TYPE,
|
ATTR_CLICK_TYPE,
|
||||||
ATTR_DEVICE,
|
ATTR_DEVICE,
|
||||||
|
ATTR_GENERATION,
|
||||||
BATTERY_DEVICES_WITH_PERMANENT_CONNECTION,
|
BATTERY_DEVICES_WITH_PERMANENT_CONNECTION,
|
||||||
BLOCK,
|
BLOCK,
|
||||||
CONF_COAP_PORT,
|
CONF_COAP_PORT,
|
||||||
@ -44,6 +45,7 @@ from .const import (
|
|||||||
REST,
|
REST,
|
||||||
REST_SENSORS_UPDATE_INTERVAL,
|
REST_SENSORS_UPDATE_INTERVAL,
|
||||||
RPC,
|
RPC,
|
||||||
|
RPC_INPUTS_EVENTS_TYPES,
|
||||||
RPC_RECONNECT_INTERVAL,
|
RPC_RECONNECT_INTERVAL,
|
||||||
SHBTN_MODELS,
|
SHBTN_MODELS,
|
||||||
SLEEP_PERIOD_MULTIPLIER,
|
SLEEP_PERIOD_MULTIPLIER,
|
||||||
@ -250,8 +252,8 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
|||||||
self.entry = entry
|
self.entry = entry
|
||||||
self.device = device
|
self.device = device
|
||||||
|
|
||||||
self._async_remove_device_updates_handler = self.async_add_listener(
|
entry.async_on_unload(
|
||||||
self._async_device_updates_handler
|
self.async_add_listener(self._async_device_updates_handler)
|
||||||
)
|
)
|
||||||
self._last_input_events_count: dict = {}
|
self._last_input_events_count: dict = {}
|
||||||
|
|
||||||
@ -306,6 +308,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
|||||||
ATTR_DEVICE: self.device.settings["device"]["hostname"],
|
ATTR_DEVICE: self.device.settings["device"]["hostname"],
|
||||||
ATTR_CHANNEL: channel,
|
ATTR_CHANNEL: channel,
|
||||||
ATTR_CLICK_TYPE: INPUTS_EVENTS_DICT[event_type],
|
ATTR_CLICK_TYPE: INPUTS_EVENTS_DICT[event_type],
|
||||||
|
ATTR_GENERATION: 1,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -356,7 +359,6 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
|||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
"""Shutdown the wrapper."""
|
"""Shutdown the wrapper."""
|
||||||
self.device.shutdown()
|
self.device.shutdown()
|
||||||
self._async_remove_device_updates_handler()
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_ha_stop(self, _event: Event) -> None:
|
def _handle_ha_stop(self, _event: Event) -> None:
|
||||||
@ -435,27 +437,40 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
def get_device_wrapper(
|
def get_block_device_wrapper(
|
||||||
hass: HomeAssistant, device_id: str
|
hass: HomeAssistant, device_id: str
|
||||||
) -> BlockDeviceWrapper | RpcDeviceWrapper | None:
|
) -> BlockDeviceWrapper | None:
|
||||||
"""Get a Shelly device wrapper for the given device id."""
|
"""Get a Shelly block device wrapper for the given device id."""
|
||||||
if not hass.data.get(DOMAIN):
|
if not hass.data.get(DOMAIN):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
for config_entry in hass.data[DOMAIN][DATA_CONFIG_ENTRY]:
|
dev_reg = device_registry.async_get(hass)
|
||||||
block_wrapper: BlockDeviceWrapper | None = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
|
if device := dev_reg.async_get(device_id):
|
||||||
config_entry
|
for config_entry in device.config_entries:
|
||||||
].get(BLOCK)
|
if not hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry):
|
||||||
|
continue
|
||||||
|
|
||||||
if block_wrapper and block_wrapper.device_id == device_id:
|
if wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(BLOCK):
|
||||||
return block_wrapper
|
return cast(BlockDeviceWrapper, wrapper)
|
||||||
|
|
||||||
rpc_wrapper: RpcDeviceWrapper | None = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
|
return None
|
||||||
config_entry
|
|
||||||
].get(RPC)
|
|
||||||
|
|
||||||
if rpc_wrapper and rpc_wrapper.device_id == device_id:
|
|
||||||
return rpc_wrapper
|
def get_rpc_device_wrapper(
|
||||||
|
hass: HomeAssistant, device_id: str
|
||||||
|
) -> RpcDeviceWrapper | None:
|
||||||
|
"""Get a Shelly RPC device wrapper for the given device id."""
|
||||||
|
if not hass.data.get(DOMAIN):
|
||||||
|
return None
|
||||||
|
|
||||||
|
dev_reg = device_registry.async_get(hass)
|
||||||
|
if device := dev_reg.async_get(device_id):
|
||||||
|
for config_entry in device.config_entries:
|
||||||
|
if not hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(RPC):
|
||||||
|
return cast(RpcDeviceWrapper, wrapper)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -479,10 +494,42 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
|||||||
self.entry = entry
|
self.entry = entry
|
||||||
self.device = device
|
self.device = device
|
||||||
|
|
||||||
|
entry.async_on_unload(
|
||||||
|
self.async_add_listener(self._async_device_updates_handler)
|
||||||
|
)
|
||||||
|
self._last_event: dict[str, Any] | None = None
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_device_updates_handler(self) -> None:
|
||||||
|
"""Handle device updates."""
|
||||||
|
if (
|
||||||
|
not self.device.initialized
|
||||||
|
or not self.device.event
|
||||||
|
or self.device.event == self._last_event
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._last_event = self.device.event
|
||||||
|
|
||||||
|
for event in self.device.event["events"]:
|
||||||
|
if event.get("event") not in RPC_INPUTS_EVENTS_TYPES:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.hass.bus.async_fire(
|
||||||
|
EVENT_SHELLY_CLICK,
|
||||||
|
{
|
||||||
|
ATTR_DEVICE_ID: self.device_id,
|
||||||
|
ATTR_DEVICE: self.device.hostname,
|
||||||
|
ATTR_CHANNEL: event["id"] + 1,
|
||||||
|
ATTR_CLICK_TYPE: event["event"],
|
||||||
|
ATTR_GENERATION: 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def _async_update_data(self) -> None:
|
async def _async_update_data(self) -> None:
|
||||||
"""Fetch data."""
|
"""Fetch data."""
|
||||||
if self.device.connected:
|
if self.device.connected:
|
||||||
|
@ -64,18 +64,28 @@ INPUTS_EVENTS_DICT: Final = {
|
|||||||
# List of battery devices that maintain a permanent WiFi connection
|
# List of battery devices that maintain a permanent WiFi connection
|
||||||
BATTERY_DEVICES_WITH_PERMANENT_CONNECTION: Final = ["SHMOS-01"]
|
BATTERY_DEVICES_WITH_PERMANENT_CONNECTION: Final = ["SHMOS-01"]
|
||||||
|
|
||||||
|
# Button/Click events for Block & RPC devices
|
||||||
EVENT_SHELLY_CLICK: Final = "shelly.click"
|
EVENT_SHELLY_CLICK: Final = "shelly.click"
|
||||||
|
|
||||||
ATTR_CLICK_TYPE: Final = "click_type"
|
ATTR_CLICK_TYPE: Final = "click_type"
|
||||||
ATTR_CHANNEL: Final = "channel"
|
ATTR_CHANNEL: Final = "channel"
|
||||||
ATTR_DEVICE: Final = "device"
|
ATTR_DEVICE: Final = "device"
|
||||||
|
ATTR_GENERATION: Final = "generation"
|
||||||
CONF_SUBTYPE: Final = "subtype"
|
CONF_SUBTYPE: Final = "subtype"
|
||||||
|
|
||||||
BASIC_INPUTS_EVENTS_TYPES: Final = {"single", "long"}
|
BASIC_INPUTS_EVENTS_TYPES: Final = {"single", "long"}
|
||||||
|
|
||||||
SHBTN_INPUTS_EVENTS_TYPES: Final = {"single", "double", "triple", "long"}
|
SHBTN_INPUTS_EVENTS_TYPES: Final = {"single", "double", "triple", "long"}
|
||||||
|
|
||||||
SUPPORTED_INPUTS_EVENTS_TYPES: Final = {
|
RPC_INPUTS_EVENTS_TYPES: Final = {
|
||||||
|
"btn_down",
|
||||||
|
"btn_up",
|
||||||
|
"single_push",
|
||||||
|
"double_push",
|
||||||
|
"long_push",
|
||||||
|
}
|
||||||
|
|
||||||
|
BLOCK_INPUTS_EVENTS_TYPES: Final = {
|
||||||
"single",
|
"single",
|
||||||
"double",
|
"double",
|
||||||
"triple",
|
"triple",
|
||||||
@ -84,9 +94,15 @@ SUPPORTED_INPUTS_EVENTS_TYPES: Final = {
|
|||||||
"long_single",
|
"long_single",
|
||||||
}
|
}
|
||||||
|
|
||||||
SHIX3_1_INPUTS_EVENTS_TYPES = SUPPORTED_INPUTS_EVENTS_TYPES
|
SHIX3_1_INPUTS_EVENTS_TYPES = BLOCK_INPUTS_EVENTS_TYPES
|
||||||
|
|
||||||
INPUTS_EVENTS_SUBTYPES: Final = {"button": 1, "button1": 1, "button2": 2, "button3": 3}
|
INPUTS_EVENTS_SUBTYPES: Final = {
|
||||||
|
"button": 1,
|
||||||
|
"button1": 1,
|
||||||
|
"button2": 2,
|
||||||
|
"button3": 3,
|
||||||
|
"button4": 4,
|
||||||
|
}
|
||||||
|
|
||||||
SHBTN_MODELS: Final = ["SHBTN-1", "SHBTN-2"]
|
SHBTN_MODELS: Final = ["SHBTN-1", "SHBTN-2"]
|
||||||
|
|
||||||
|
@ -25,28 +25,52 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from . import RpcDeviceWrapper, get_device_wrapper
|
from . import get_block_device_wrapper, get_rpc_device_wrapper
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_CHANNEL,
|
ATTR_CHANNEL,
|
||||||
ATTR_CLICK_TYPE,
|
ATTR_CLICK_TYPE,
|
||||||
|
BLOCK_INPUTS_EVENTS_TYPES,
|
||||||
CONF_SUBTYPE,
|
CONF_SUBTYPE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_SHELLY_CLICK,
|
EVENT_SHELLY_CLICK,
|
||||||
INPUTS_EVENTS_SUBTYPES,
|
INPUTS_EVENTS_SUBTYPES,
|
||||||
SHBTN_INPUTS_EVENTS_TYPES,
|
RPC_INPUTS_EVENTS_TYPES,
|
||||||
SHBTN_MODELS,
|
SHBTN_MODELS,
|
||||||
SUPPORTED_INPUTS_EVENTS_TYPES,
|
|
||||||
)
|
)
|
||||||
from .utils import get_input_triggers
|
from .utils import (
|
||||||
|
get_block_input_triggers,
|
||||||
|
get_rpc_input_triggers,
|
||||||
|
get_shbtn_input_triggers,
|
||||||
|
)
|
||||||
|
|
||||||
TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_TYPE): vol.In(SUPPORTED_INPUTS_EVENTS_TYPES),
|
vol.Required(CONF_TYPE): vol.In(
|
||||||
|
RPC_INPUTS_EVENTS_TYPES | BLOCK_INPUTS_EVENTS_TYPES
|
||||||
|
),
|
||||||
vol.Required(CONF_SUBTYPE): vol.In(INPUTS_EVENTS_SUBTYPES),
|
vol.Required(CONF_SUBTYPE): vol.In(INPUTS_EVENTS_SUBTYPES),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def append_input_triggers(
|
||||||
|
triggers: list[dict[str, Any]],
|
||||||
|
input_triggers: list[tuple[str, str]],
|
||||||
|
device_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Add trigger to triggers list."""
|
||||||
|
for trigger, subtype in input_triggers:
|
||||||
|
triggers.append(
|
||||||
|
{
|
||||||
|
CONF_PLATFORM: "device",
|
||||||
|
CONF_DEVICE_ID: device_id,
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_TYPE: trigger,
|
||||||
|
CONF_SUBTYPE: subtype,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_validate_trigger_config(
|
async def async_validate_trigger_config(
|
||||||
hass: HomeAssistant, config: dict[str, Any]
|
hass: HomeAssistant, config: dict[str, Any]
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@ -54,23 +78,29 @@ async def async_validate_trigger_config(
|
|||||||
config = TRIGGER_SCHEMA(config)
|
config = TRIGGER_SCHEMA(config)
|
||||||
|
|
||||||
# if device is available verify parameters against device capabilities
|
# if device is available verify parameters against device capabilities
|
||||||
wrapper = get_device_wrapper(hass, config[CONF_DEVICE_ID])
|
|
||||||
|
|
||||||
if isinstance(wrapper, RpcDeviceWrapper):
|
|
||||||
return config
|
|
||||||
|
|
||||||
if not wrapper or not wrapper.device.initialized:
|
|
||||||
return config
|
|
||||||
|
|
||||||
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
|
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
|
||||||
|
|
||||||
assert wrapper.device.blocks
|
if config[CONF_TYPE] in RPC_INPUTS_EVENTS_TYPES:
|
||||||
|
rpc_wrapper = get_rpc_device_wrapper(hass, config[CONF_DEVICE_ID])
|
||||||
|
if not rpc_wrapper or not rpc_wrapper.device.initialized:
|
||||||
|
return config
|
||||||
|
|
||||||
for block in wrapper.device.blocks:
|
input_triggers = get_rpc_input_triggers(rpc_wrapper.device)
|
||||||
input_triggers = get_input_triggers(wrapper.device, block)
|
|
||||||
if trigger in input_triggers:
|
if trigger in input_triggers:
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
elif config[CONF_TYPE] in BLOCK_INPUTS_EVENTS_TYPES:
|
||||||
|
block_wrapper = get_block_device_wrapper(hass, config[CONF_DEVICE_ID])
|
||||||
|
if not block_wrapper or not block_wrapper.device.initialized:
|
||||||
|
return config
|
||||||
|
|
||||||
|
assert block_wrapper.device.blocks
|
||||||
|
|
||||||
|
for block in block_wrapper.device.blocks:
|
||||||
|
input_triggers = get_block_input_triggers(block_wrapper.device, block)
|
||||||
|
if trigger in input_triggers:
|
||||||
|
return config
|
||||||
|
|
||||||
raise InvalidDeviceAutomationConfig(
|
raise InvalidDeviceAutomationConfig(
|
||||||
f"Invalid ({CONF_TYPE},{CONF_SUBTYPE}): {trigger}"
|
f"Invalid ({CONF_TYPE},{CONF_SUBTYPE}): {trigger}"
|
||||||
)
|
)
|
||||||
@ -80,45 +110,28 @@ async def async_get_triggers(
|
|||||||
hass: HomeAssistant, device_id: str
|
hass: HomeAssistant, device_id: str
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""List device triggers for Shelly devices."""
|
"""List device triggers for Shelly devices."""
|
||||||
wrapper = get_device_wrapper(hass, device_id)
|
triggers: list[dict[str, Any]] = []
|
||||||
if not wrapper:
|
|
||||||
raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}")
|
|
||||||
|
|
||||||
if isinstance(wrapper, RpcDeviceWrapper):
|
if rpc_wrapper := get_rpc_device_wrapper(hass, device_id):
|
||||||
return []
|
input_triggers = get_rpc_input_triggers(rpc_wrapper.device)
|
||||||
|
append_input_triggers(triggers, input_triggers, device_id)
|
||||||
triggers = []
|
|
||||||
|
|
||||||
if wrapper.model in SHBTN_MODELS:
|
|
||||||
for trigger in SHBTN_INPUTS_EVENTS_TYPES:
|
|
||||||
triggers.append(
|
|
||||||
{
|
|
||||||
CONF_PLATFORM: "device",
|
|
||||||
CONF_DEVICE_ID: device_id,
|
|
||||||
CONF_DOMAIN: DOMAIN,
|
|
||||||
CONF_TYPE: trigger,
|
|
||||||
CONF_SUBTYPE: "button",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return triggers
|
return triggers
|
||||||
|
|
||||||
assert wrapper.device.blocks
|
if block_wrapper := get_block_device_wrapper(hass, device_id):
|
||||||
|
if block_wrapper.model in SHBTN_MODELS:
|
||||||
|
input_triggers = get_shbtn_input_triggers()
|
||||||
|
append_input_triggers(triggers, input_triggers, device_id)
|
||||||
|
return triggers
|
||||||
|
|
||||||
for block in wrapper.device.blocks:
|
assert block_wrapper.device.blocks
|
||||||
input_triggers = get_input_triggers(wrapper.device, block)
|
|
||||||
|
|
||||||
for trigger, subtype in input_triggers:
|
for block in block_wrapper.device.blocks:
|
||||||
triggers.append(
|
input_triggers = get_block_input_triggers(block_wrapper.device, block)
|
||||||
{
|
append_input_triggers(triggers, input_triggers, device_id)
|
||||||
CONF_PLATFORM: "device",
|
|
||||||
CONF_DEVICE_ID: device_id,
|
|
||||||
CONF_DOMAIN: DOMAIN,
|
|
||||||
CONF_TYPE: trigger,
|
|
||||||
CONF_SUBTYPE: subtype,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return triggers
|
return triggers
|
||||||
|
|
||||||
|
raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}")
|
||||||
|
|
||||||
|
|
||||||
async def async_attach_trigger(
|
async def async_attach_trigger(
|
||||||
@ -137,6 +150,7 @@ async def async_attach_trigger(
|
|||||||
ATTR_CLICK_TYPE: config[CONF_TYPE],
|
ATTR_CLICK_TYPE: config[CONF_TYPE],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
event_config = event_trigger.TRIGGER_SCHEMA(event_config)
|
event_config = event_trigger.TRIGGER_SCHEMA(event_config)
|
||||||
return await event_trigger.async_attach_trigger(
|
return await event_trigger.async_attach_trigger(
|
||||||
hass, event_config, action, automation_info, platform_type="device"
|
hass, event_config, action, automation_info, platform_type="device"
|
||||||
|
@ -7,15 +7,17 @@ from homeassistant.const import ATTR_DEVICE_ID
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.typing import EventType
|
from homeassistant.helpers.typing import EventType
|
||||||
|
|
||||||
from . import RpcDeviceWrapper, get_device_wrapper
|
from . import get_block_device_wrapper, get_rpc_device_wrapper
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_CHANNEL,
|
ATTR_CHANNEL,
|
||||||
ATTR_CLICK_TYPE,
|
ATTR_CLICK_TYPE,
|
||||||
ATTR_DEVICE,
|
ATTR_DEVICE,
|
||||||
|
BLOCK_INPUTS_EVENTS_TYPES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_SHELLY_CLICK,
|
EVENT_SHELLY_CLICK,
|
||||||
|
RPC_INPUTS_EVENTS_TYPES,
|
||||||
)
|
)
|
||||||
from .utils import get_block_device_name
|
from .utils import get_block_device_name, get_rpc_entity_name
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -27,23 +29,27 @@ def async_describe_events(
|
|||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_describe_shelly_click_event(event: EventType) -> dict[str, str]:
|
def async_describe_shelly_click_event(event: EventType) -> dict[str, str]:
|
||||||
"""Describe shelly.click logbook event."""
|
"""Describe shelly.click logbook event (block device)."""
|
||||||
wrapper = get_device_wrapper(hass, event.data[ATTR_DEVICE_ID])
|
device_id = event.data[ATTR_DEVICE_ID]
|
||||||
|
|
||||||
if isinstance(wrapper, RpcDeviceWrapper):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
if wrapper and wrapper.device.initialized:
|
|
||||||
device_name = get_block_device_name(wrapper.device)
|
|
||||||
else:
|
|
||||||
device_name = event.data[ATTR_DEVICE]
|
|
||||||
|
|
||||||
channel = event.data[ATTR_CHANNEL]
|
|
||||||
click_type = event.data[ATTR_CLICK_TYPE]
|
click_type = event.data[ATTR_CLICK_TYPE]
|
||||||
|
channel = event.data[ATTR_CHANNEL]
|
||||||
|
input_name = f"{event.data[ATTR_DEVICE]} channel {channel}"
|
||||||
|
|
||||||
|
if click_type in RPC_INPUTS_EVENTS_TYPES:
|
||||||
|
rpc_wrapper = get_rpc_device_wrapper(hass, device_id)
|
||||||
|
if rpc_wrapper and rpc_wrapper.device.initialized:
|
||||||
|
key = f"input:{channel-1}"
|
||||||
|
input_name = get_rpc_entity_name(rpc_wrapper.device, key)
|
||||||
|
|
||||||
|
elif click_type in BLOCK_INPUTS_EVENTS_TYPES:
|
||||||
|
block_wrapper = get_block_device_wrapper(hass, device_id)
|
||||||
|
if block_wrapper and block_wrapper.device.initialized:
|
||||||
|
device_name = get_block_device_name(block_wrapper.device)
|
||||||
|
input_name = f"{device_name} channel {channel}"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"name": "Shelly",
|
"name": "Shelly",
|
||||||
"message": f"'{click_type}' click event for {device_name} channel {channel} was fired.",
|
"message": f"'{click_type}' click event for {input_name} Input was fired.",
|
||||||
}
|
}
|
||||||
|
|
||||||
async_describe_event(DOMAIN, EVENT_SHELLY_CLICK, async_describe_shelly_click_event)
|
async_describe_event(DOMAIN, EVENT_SHELLY_CLICK, async_describe_shelly_click_event)
|
||||||
|
@ -33,7 +33,8 @@
|
|||||||
"button": "Button",
|
"button": "Button",
|
||||||
"button1": "First button",
|
"button1": "First button",
|
||||||
"button2": "Second button",
|
"button2": "Second button",
|
||||||
"button3": "Third button"
|
"button3": "Third button",
|
||||||
|
"button4": "Fourth button"
|
||||||
},
|
},
|
||||||
"trigger_type": {
|
"trigger_type": {
|
||||||
"single": "{subtype} single clicked",
|
"single": "{subtype} single clicked",
|
||||||
@ -41,7 +42,12 @@
|
|||||||
"triple": "{subtype} triple clicked",
|
"triple": "{subtype} triple clicked",
|
||||||
"long": " {subtype} long clicked",
|
"long": " {subtype} long clicked",
|
||||||
"single_long": "{subtype} single clicked and then long clicked",
|
"single_long": "{subtype} single clicked and then long clicked",
|
||||||
"long_single": "{subtype} long clicked and then single clicked"
|
"long_single": "{subtype} long clicked and then single clicked",
|
||||||
|
"btn_down": "{subtype} button down",
|
||||||
|
"btn_up": "{subtype} button up",
|
||||||
|
"single_push": "{subtype} single push",
|
||||||
|
"double_push": "{subtype} double push",
|
||||||
|
"long_push": " {subtype} long push"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,8 @@
|
|||||||
"button": "Button",
|
"button": "Button",
|
||||||
"button1": "First button",
|
"button1": "First button",
|
||||||
"button2": "Second button",
|
"button2": "Second button",
|
||||||
"button3": "Third button"
|
"button3": "Third button",
|
||||||
|
"button4": "Fourth button"
|
||||||
},
|
},
|
||||||
"trigger_type": {
|
"trigger_type": {
|
||||||
"double": "{subtype} double clicked",
|
"double": "{subtype} double clicked",
|
||||||
@ -41,7 +42,12 @@
|
|||||||
"long_single": "{subtype} long clicked and then single clicked",
|
"long_single": "{subtype} long clicked and then single clicked",
|
||||||
"single": "{subtype} single clicked",
|
"single": "{subtype} single clicked",
|
||||||
"single_long": "{subtype} single clicked and then long clicked",
|
"single_long": "{subtype} single clicked and then long clicked",
|
||||||
"triple": "{subtype} triple clicked"
|
"triple": "{subtype} triple clicked",
|
||||||
|
"btn_down": "{subtype} button down",
|
||||||
|
"btn_up": "{subtype} button up",
|
||||||
|
"single_push": "{subtype} single push",
|
||||||
|
"double_push": "{subtype} double push",
|
||||||
|
"long_push": " {subtype} long push"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -22,6 +22,7 @@ from .const import (
|
|||||||
DEFAULT_COAP_PORT,
|
DEFAULT_COAP_PORT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
MAX_RPC_KEY_INSTANCES,
|
MAX_RPC_KEY_INSTANCES,
|
||||||
|
RPC_INPUTS_EVENTS_TYPES,
|
||||||
SHBTN_INPUTS_EVENTS_TYPES,
|
SHBTN_INPUTS_EVENTS_TYPES,
|
||||||
SHBTN_MODELS,
|
SHBTN_MODELS,
|
||||||
SHIX3_1_INPUTS_EVENTS_TYPES,
|
SHIX3_1_INPUTS_EVENTS_TYPES,
|
||||||
@ -162,7 +163,9 @@ def get_device_uptime(uptime: float, last_uptime: str | None) -> str:
|
|||||||
return last_uptime
|
return last_uptime
|
||||||
|
|
||||||
|
|
||||||
def get_input_triggers(device: BlockDevice, block: Block) -> list[tuple[str, str]]:
|
def get_block_input_triggers(
|
||||||
|
device: BlockDevice, block: Block
|
||||||
|
) -> list[tuple[str, str]]:
|
||||||
"""Return list of input triggers for block."""
|
"""Return list of input triggers for block."""
|
||||||
if "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids:
|
if "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids:
|
||||||
return []
|
return []
|
||||||
@ -191,6 +194,16 @@ def get_input_triggers(device: BlockDevice, block: Block) -> list[tuple[str, str
|
|||||||
return triggers
|
return triggers
|
||||||
|
|
||||||
|
|
||||||
|
def get_shbtn_input_triggers() -> list[tuple[str, str]]:
|
||||||
|
"""Return list of input triggers for SHBTN models."""
|
||||||
|
triggers = []
|
||||||
|
|
||||||
|
for trigger_type in SHBTN_INPUTS_EVENTS_TYPES:
|
||||||
|
triggers.append((trigger_type, "button"))
|
||||||
|
|
||||||
|
return triggers
|
||||||
|
|
||||||
|
|
||||||
@singleton.singleton("shelly_coap")
|
@singleton.singleton("shelly_coap")
|
||||||
async def get_coap_context(hass: HomeAssistant) -> COAP:
|
async def get_coap_context(hass: HomeAssistant) -> COAP:
|
||||||
"""Get CoAP context to be used in all Shelly devices."""
|
"""Get CoAP context to be used in all Shelly devices."""
|
||||||
@ -314,3 +327,21 @@ def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool:
|
|||||||
"""Return true if rpc channel consumption type is set to light."""
|
"""Return true if rpc channel consumption type is set to light."""
|
||||||
con_types = config["sys"]["ui_data"].get("consumption_types")
|
con_types = config["sys"]["ui_data"].get("consumption_types")
|
||||||
return con_types is not None and con_types[channel].lower().startswith("light")
|
return con_types is not None and con_types[channel].lower().startswith("light")
|
||||||
|
|
||||||
|
|
||||||
|
def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]:
|
||||||
|
"""Return list of input triggers for RPC device."""
|
||||||
|
triggers = []
|
||||||
|
|
||||||
|
key_ids = get_rpc_key_ids(device.config, "input")
|
||||||
|
|
||||||
|
for id_ in key_ids:
|
||||||
|
key = f"input:{id_}"
|
||||||
|
if not is_rpc_momentary_input(device.config, key):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for trigger_type in RPC_INPUTS_EVENTS_TYPES:
|
||||||
|
subtype = f"button{id_+1}"
|
||||||
|
triggers.append((trigger_type, subtype))
|
||||||
|
|
||||||
|
return triggers
|
||||||
|
@ -56,6 +56,7 @@ MOCK_BLOCKS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
MOCK_CONFIG = {
|
MOCK_CONFIG = {
|
||||||
|
"input:0": {"id": 0, "type": "button"},
|
||||||
"switch:0": {"name": "test switch_0"},
|
"switch:0": {"name": "test switch_0"},
|
||||||
"sys": {"ui_data": {}},
|
"sys": {"ui_data": {}},
|
||||||
"wifi": {
|
"wifi": {
|
||||||
@ -147,6 +148,7 @@ async def rpc_wrapper(hass):
|
|||||||
device = Mock(
|
device = Mock(
|
||||||
call_rpc=AsyncMock(),
|
call_rpc=AsyncMock(),
|
||||||
config=MOCK_CONFIG,
|
config=MOCK_CONFIG,
|
||||||
|
event={},
|
||||||
shelly=MOCK_SHELLY,
|
shelly=MOCK_SHELLY,
|
||||||
status=MOCK_STATUS,
|
status=MOCK_STATUS,
|
||||||
firmware_version="some fw string",
|
firmware_version="some fw string",
|
||||||
|
@ -30,8 +30,8 @@ from tests.common import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_get_triggers(hass, coap_wrapper):
|
async def test_get_triggers_block_device(hass, coap_wrapper):
|
||||||
"""Test we get the expected triggers from a shelly."""
|
"""Test we get the expected triggers from a shelly block device."""
|
||||||
assert coap_wrapper
|
assert coap_wrapper
|
||||||
expected_triggers = [
|
expected_triggers = [
|
||||||
{
|
{
|
||||||
@ -57,6 +57,54 @@ async def test_get_triggers(hass, coap_wrapper):
|
|||||||
assert_lists_same(triggers, expected_triggers)
|
assert_lists_same(triggers, expected_triggers)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_triggers_rpc_device(hass, rpc_wrapper):
|
||||||
|
"""Test we get the expected triggers from a shelly RPC device."""
|
||||||
|
assert rpc_wrapper
|
||||||
|
expected_triggers = [
|
||||||
|
{
|
||||||
|
CONF_PLATFORM: "device",
|
||||||
|
CONF_DEVICE_ID: rpc_wrapper.device_id,
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_TYPE: "btn_down",
|
||||||
|
CONF_SUBTYPE: "button1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_PLATFORM: "device",
|
||||||
|
CONF_DEVICE_ID: rpc_wrapper.device_id,
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_TYPE: "btn_up",
|
||||||
|
CONF_SUBTYPE: "button1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_PLATFORM: "device",
|
||||||
|
CONF_DEVICE_ID: rpc_wrapper.device_id,
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_TYPE: "single_push",
|
||||||
|
CONF_SUBTYPE: "button1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_PLATFORM: "device",
|
||||||
|
CONF_DEVICE_ID: rpc_wrapper.device_id,
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_TYPE: "double_push",
|
||||||
|
CONF_SUBTYPE: "button1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_PLATFORM: "device",
|
||||||
|
CONF_DEVICE_ID: rpc_wrapper.device_id,
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_TYPE: "long_push",
|
||||||
|
CONF_SUBTYPE: "button1",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
triggers = await async_get_device_automations(
|
||||||
|
hass, "trigger", rpc_wrapper.device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_lists_same(triggers, expected_triggers)
|
||||||
|
|
||||||
|
|
||||||
async def test_get_triggers_button(hass):
|
async def test_get_triggers_button(hass):
|
||||||
"""Test we get the expected triggers from a shelly button."""
|
"""Test we get the expected triggers from a shelly button."""
|
||||||
await async_setup_component(hass, "shelly", {})
|
await async_setup_component(hass, "shelly", {})
|
||||||
@ -136,8 +184,8 @@ async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper
|
|||||||
await async_get_device_automations(hass, "trigger", invalid_device.id)
|
await async_get_device_automations(hass, "trigger", invalid_device.id)
|
||||||
|
|
||||||
|
|
||||||
async def test_if_fires_on_click_event(hass, calls, coap_wrapper):
|
async def test_if_fires_on_click_event_block_device(hass, calls, coap_wrapper):
|
||||||
"""Test for click_event trigger firing."""
|
"""Test for click_event trigger firing for block device."""
|
||||||
assert coap_wrapper
|
assert coap_wrapper
|
||||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
@ -175,8 +223,47 @@ async def test_if_fires_on_click_event(hass, calls, coap_wrapper):
|
|||||||
assert calls[0].data["some"] == "test_trigger_single_click"
|
assert calls[0].data["some"] == "test_trigger_single_click"
|
||||||
|
|
||||||
|
|
||||||
async def test_validate_trigger_config_no_device(hass, calls, coap_wrapper):
|
async def test_if_fires_on_click_event_rpc_device(hass, calls, rpc_wrapper):
|
||||||
"""Test for click_event with no device."""
|
"""Test for click_event trigger firing for rpc device."""
|
||||||
|
assert rpc_wrapper
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: [
|
||||||
|
{
|
||||||
|
"trigger": {
|
||||||
|
CONF_PLATFORM: "device",
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_DEVICE_ID: rpc_wrapper.device_id,
|
||||||
|
CONF_TYPE: "single_push",
|
||||||
|
CONF_SUBTYPE: "button1",
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
"data_template": {"some": "test_trigger_single_push"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
message = {
|
||||||
|
CONF_DEVICE_ID: rpc_wrapper.device_id,
|
||||||
|
ATTR_CLICK_TYPE: "single_push",
|
||||||
|
ATTR_CHANNEL: 1,
|
||||||
|
}
|
||||||
|
hass.bus.async_fire(EVENT_SHELLY_CLICK, message)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data["some"] == "test_trigger_single_push"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_validate_trigger_block_device_not_ready(hass, calls, coap_wrapper):
|
||||||
|
"""Test validate trigger config when block device is not ready."""
|
||||||
assert coap_wrapper
|
assert coap_wrapper
|
||||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
@ -189,7 +276,7 @@ async def test_validate_trigger_config_no_device(hass, calls, coap_wrapper):
|
|||||||
"trigger": {
|
"trigger": {
|
||||||
CONF_PLATFORM: "device",
|
CONF_PLATFORM: "device",
|
||||||
CONF_DOMAIN: DOMAIN,
|
CONF_DOMAIN: DOMAIN,
|
||||||
CONF_DEVICE_ID: "no_device",
|
CONF_DEVICE_ID: "device_not_ready",
|
||||||
CONF_TYPE: "single",
|
CONF_TYPE: "single",
|
||||||
CONF_SUBTYPE: "button1",
|
CONF_SUBTYPE: "button1",
|
||||||
},
|
},
|
||||||
@ -201,7 +288,11 @@ async def test_validate_trigger_config_no_device(hass, calls, coap_wrapper):
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
message = {CONF_DEVICE_ID: "no_device", ATTR_CLICK_TYPE: "single", ATTR_CHANNEL: 1}
|
message = {
|
||||||
|
CONF_DEVICE_ID: "device_not_ready",
|
||||||
|
ATTR_CLICK_TYPE: "single",
|
||||||
|
ATTR_CHANNEL: 1,
|
||||||
|
}
|
||||||
hass.bus.async_fire(EVENT_SHELLY_CLICK, message)
|
hass.bus.async_fire(EVENT_SHELLY_CLICK, message)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
@ -209,6 +300,44 @@ async def test_validate_trigger_config_no_device(hass, calls, coap_wrapper):
|
|||||||
assert calls[0].data["some"] == "test_trigger_single_click"
|
assert calls[0].data["some"] == "test_trigger_single_click"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_validate_trigger_rpc_device_not_ready(hass, calls, rpc_wrapper):
|
||||||
|
"""Test validate trigger config when RPC device is not ready."""
|
||||||
|
assert rpc_wrapper
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: [
|
||||||
|
{
|
||||||
|
"trigger": {
|
||||||
|
CONF_PLATFORM: "device",
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_DEVICE_ID: "device_not_ready",
|
||||||
|
CONF_TYPE: "single_push",
|
||||||
|
CONF_SUBTYPE: "button1",
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
"data_template": {"some": "test_trigger_single_push"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
message = {
|
||||||
|
CONF_DEVICE_ID: "device_not_ready",
|
||||||
|
ATTR_CLICK_TYPE: "single_push",
|
||||||
|
ATTR_CHANNEL: 1,
|
||||||
|
}
|
||||||
|
hass.bus.async_fire(EVENT_SHELLY_CLICK, message)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data["some"] == "test_trigger_single_push"
|
||||||
|
|
||||||
|
|
||||||
async def test_validate_trigger_invalid_triggers(hass, coap_wrapper):
|
async def test_validate_trigger_invalid_triggers(hass, coap_wrapper):
|
||||||
"""Test for click_event with invalid triggers."""
|
"""Test for click_event with invalid triggers."""
|
||||||
assert coap_wrapper
|
assert coap_wrapper
|
||||||
|
@ -13,8 +13,8 @@ from homeassistant.setup import async_setup_component
|
|||||||
from tests.components.logbook.test_init import MockLazyEventPartialState
|
from tests.components.logbook.test_init import MockLazyEventPartialState
|
||||||
|
|
||||||
|
|
||||||
async def test_humanify_shelly_click_event(hass, coap_wrapper):
|
async def test_humanify_shelly_click_event_block_device(hass, coap_wrapper):
|
||||||
"""Test humanifying Shelly click event."""
|
"""Test humanifying Shelly click event for block device."""
|
||||||
assert coap_wrapper
|
assert coap_wrapper
|
||||||
hass.config.components.add("recorder")
|
hass.config.components.add("recorder")
|
||||||
assert await async_setup_component(hass, "logbook", {})
|
assert await async_setup_component(hass, "logbook", {})
|
||||||
@ -51,12 +51,63 @@ async def test_humanify_shelly_click_event(hass, coap_wrapper):
|
|||||||
assert event1["name"] == "Shelly"
|
assert event1["name"] == "Shelly"
|
||||||
assert event1["domain"] == DOMAIN
|
assert event1["domain"] == DOMAIN
|
||||||
assert (
|
assert (
|
||||||
event1["message"] == "'single' click event for Test name channel 1 was fired."
|
event1["message"]
|
||||||
|
== "'single' click event for Test name channel 1 Input was fired."
|
||||||
)
|
)
|
||||||
|
|
||||||
assert event2["name"] == "Shelly"
|
assert event2["name"] == "Shelly"
|
||||||
assert event2["domain"] == DOMAIN
|
assert event2["domain"] == DOMAIN
|
||||||
assert (
|
assert (
|
||||||
event2["message"]
|
event2["message"]
|
||||||
== "'long' click event for shellyswitch25-12345678 channel 2 was fired."
|
== "'long' click event for shellyswitch25-12345678 channel 2 Input was fired."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_humanify_shelly_click_event_rpc_device(hass, rpc_wrapper):
|
||||||
|
"""Test humanifying Shelly click event for rpc device."""
|
||||||
|
assert rpc_wrapper
|
||||||
|
hass.config.components.add("recorder")
|
||||||
|
assert await async_setup_component(hass, "logbook", {})
|
||||||
|
entity_attr_cache = logbook.EntityAttributeCache(hass)
|
||||||
|
|
||||||
|
event1, event2 = list(
|
||||||
|
logbook.humanify(
|
||||||
|
hass,
|
||||||
|
[
|
||||||
|
MockLazyEventPartialState(
|
||||||
|
EVENT_SHELLY_CLICK,
|
||||||
|
{
|
||||||
|
ATTR_DEVICE_ID: rpc_wrapper.device_id,
|
||||||
|
ATTR_DEVICE: "shellyplus1pm-12345678",
|
||||||
|
ATTR_CLICK_TYPE: "single_push",
|
||||||
|
ATTR_CHANNEL: 1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MockLazyEventPartialState(
|
||||||
|
EVENT_SHELLY_CLICK,
|
||||||
|
{
|
||||||
|
ATTR_DEVICE_ID: "no_device_id",
|
||||||
|
ATTR_DEVICE: "shellypro4pm-12345678",
|
||||||
|
ATTR_CLICK_TYPE: "btn_down",
|
||||||
|
ATTR_CHANNEL: 2,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
entity_attr_cache,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert event1["name"] == "Shelly"
|
||||||
|
assert event1["domain"] == DOMAIN
|
||||||
|
assert (
|
||||||
|
event1["message"]
|
||||||
|
== "'single_push' click event for test switch_0 Input was fired."
|
||||||
|
)
|
||||||
|
|
||||||
|
assert event2["name"] == "Shelly"
|
||||||
|
assert event2["domain"] == DOMAIN
|
||||||
|
assert (
|
||||||
|
event2["message"]
|
||||||
|
== "'btn_down' click event for shellypro4pm-12345678 channel 2 Input was fired."
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user