Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions homeassistant/components/insteon/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,10 @@
"p",
]

EVENT_GROUP_ON = "insteon.button_on"
EVENT_GROUP_OFF = "insteon.button_off"
EVENT_GROUP_ON_FAST = "insteon.button_on_fast"
EVENT_GROUP_OFF_FAST = "insteon.button_off_fast"
EVENT_CONF_BUTTON = "button"
EVENT_CONF_BATTERY = "battery"
EVENT_CONF_HEARTBEAT = "heartbeat"
EVENT_CONF_MOISTURE = "moisture"

STATE_NAME_LABEL_MAP = {
DIMMABLE_LIGHT_MAIN: "Main",
Expand Down
84 changes: 60 additions & 24 deletions homeassistant/components/insteon/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@
from pyinsteon.address import Address
from pyinsteon.constants import ALDBStatus, DeviceAction
from pyinsteon.device_types.device_base import Device
from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT, Event
from pyinsteon.events import (
HEARTBEAT_EVENT,
LEAK_DRY_EVENT,
LEAK_WET_EVENT,
LOW_BATTERY_EVENT,
OFF_EVENT,
OFF_FAST_EVENT,
ON_EVENT,
ON_FAST_EVENT,
Event,
)

from homeassistant.components import usb
from homeassistant.const import CONF_ADDRESS, Platform
Expand All @@ -21,11 +31,10 @@

from .const import (
DOMAIN,
EVENT_CONF_BATTERY,
EVENT_CONF_BUTTON,
EVENT_GROUP_OFF,
EVENT_GROUP_OFF_FAST,
EVENT_GROUP_ON,
EVENT_GROUP_ON_FAST,
EVENT_CONF_HEARTBEAT,
EVENT_CONF_MOISTURE,
SIGNAL_ADD_ENTITIES,
)
from .ipdb import get_device_platform_groups, get_device_platforms
Expand All @@ -52,30 +61,57 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:

@callback
def async_fire_insteon_event(
name: str, address: Address, group: int, button: str | None = None
name: str,
address: Address,
group: int,
button: str | None = None,
low_battery: bool | None = None,
heartbeat_missed: bool | None = None,
dry: bool | None = None,
):
# Firing an event when a button is pressed.
if button and button[-2] == "_":
button_id = button[-1].lower()
else:
button_id = None

event = name
schema = {CONF_ADDRESS: address, "group": group}
if button_id:
schema[EVENT_CONF_BUTTON] = button_id
if name == ON_EVENT:
event = EVENT_GROUP_ON
elif name == OFF_EVENT:
event = EVENT_GROUP_OFF
elif name == ON_FAST_EVENT:
event = EVENT_GROUP_ON_FAST
elif name == OFF_FAST_EVENT:
event = EVENT_GROUP_OFF_FAST
else:
event = f"insteon.{name}"

# Prefix button events with "button_" and add the button number to the event data.
if name in (ON_EVENT, OFF_EVENT, ON_FAST_EVENT, OFF_FAST_EVENT):
event = f"button_{event}"
if button is not None and len(button) >= 2 and button[-2] == "_":
schema[EVENT_CONF_BUTTON] = button[-1].lower()

# Low battery
if name == LOW_BATTERY_EVENT and low_battery is not None:
schema[EVENT_CONF_BATTERY] = "low" if low_battery else "ok"

# Heartbeat missed
if name == HEARTBEAT_EVENT and heartbeat_missed is not None:
schema[EVENT_CONF_HEARTBEAT] = "missed" if heartbeat_missed else "received"

# Wet / dry for leak sensors
if name in (LEAK_WET_EVENT, LEAK_DRY_EVENT) and dry is not None:
schema[EVENT_CONF_MOISTURE] = "wet" if not dry else "dry"

# Prefix the event name with "insteon." to avoid conflicts with other integrations and to make it clear that the event is related to Insteon devices.
event = f"insteon.{event}"
legacy_event = event

event = event.removesuffix("_event")

Comment thread
gogglespisano marked this conversation as resolved.
# Fire the event along with the data about the device address, group, button, etc
_LOGGER.debug("Firing event %s with %s", event, schema)
hass.bus.async_fire(event, schema)

if legacy_event != event:
# For backward compatibility with custom automations that may be using the old event names,
# we will also fire an event with the old event name.
legacy_schema = {
**schema,
"deprecated": (
"Use the event name without the '_event' suffix instead, as the old event name will eventually be removed in a future release."
),
}
_LOGGER.debug("Firing event %s with %s", legacy_event, legacy_schema)
hass.bus.async_fire(legacy_event, legacy_schema)
Comment thread
gogglespisano marked this conversation as resolved.

if str(device.address).startswith("X10"):
return

Expand Down
152 changes: 152 additions & 0 deletions tests/components/insteon/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Tests for Insteon utils."""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import MagicMock

from pyinsteon.address import Address
from pyinsteon.events import (
HEARTBEAT_EVENT,
LEAK_DRY_EVENT,
LEAK_WET_EVENT,
LOW_BATTERY_EVENT,
OFF_EVENT,
ON_EVENT,
Event,
)
import pytest

from homeassistant.components.insteon.const import (
EVENT_CONF_BATTERY,
EVENT_CONF_BUTTON,
EVENT_CONF_HEARTBEAT,
EVENT_CONF_MOISTURE,
)
from homeassistant.components.insteon.utils import add_insteon_events
from homeassistant.const import CONF_ADDRESS
from homeassistant.core import HomeAssistant

from tests.common import async_capture_events


def test_add_insteon_events_skips_x10_devices(hass: HomeAssistant) -> None:
"""X10 devices should not register Insteon event subscribers."""
mock_event = MagicMock()
x10_address = MagicMock()
x10_address.__str__ = MagicMock(return_value="X10.A.1")

device = SimpleNamespace(address=x10_address, events={"on": mock_event})

add_insteon_events(hass, device)

mock_event.subscribe.assert_not_called()


def test_add_insteon_events_registers_string_keyed_events(hass: HomeAssistant) -> None:
"""Events keyed by name should subscribe the HA bus callback."""
addr = Address("11.11.11")
mock_event = MagicMock()
device = SimpleNamespace(
address=addr,
events={"any_key": mock_event},
)

add_insteon_events(hass, device)

mock_event.subscribe.assert_called_once()
(listener,), kwargs = mock_event.subscribe.call_args
assert kwargs.get("force_strong_ref") is True
assert callable(listener)


def test_add_insteon_events_registers_grouped_events(hass: HomeAssistant) -> None:
"""Events nested under an integer group should each be registered."""
addr = Address("22.22.22")
ev_a = MagicMock()
ev_b = MagicMock()
device = SimpleNamespace(address=addr, events={3: {"a": ev_a, "b": ev_b}})

add_insteon_events(hass, device)

ev_a.subscribe.assert_called_once()
ev_b.subscribe.assert_called_once()


async def test_add_insteon_events_on_event_fires_bus(hass: HomeAssistant) -> None:
"""ON events should fire new and legacy Insteon bus event names."""
new_events = async_capture_events(hass, "insteon.button_on")
legacy_events = async_capture_events(hass, "insteon.button_on_event")

addr = Address("33.33.33")
on_event = Event(ON_EVENT, addr, group=4, button="button_2")
device = SimpleNamespace(address=addr, events={"on": on_event})

add_insteon_events(hass, device)
on_event.trigger(255)
await hass.async_block_till_done()

assert len(new_events) == 1
assert new_events[0].data[CONF_ADDRESS] == addr.id
assert new_events[0].data["group"] == 4
assert new_events[0].data[EVENT_CONF_BUTTON] == "2"

assert len(legacy_events) == 1
assert "deprecated" in legacy_events[0].data


async def test_add_insteon_events_off_event_fires_bus(hass: HomeAssistant) -> None:
"""OFF events use the button_ prefix and suffix stripping."""
captured = async_capture_events(hass, "insteon.button_off")

addr = Address("44.44.44")
off_event = Event(OFF_EVENT, addr, group=0, button="xy")
device = SimpleNamespace(address=addr, events={"off": off_event})

add_insteon_events(hass, device)
off_event.trigger(0)
await hass.async_block_till_done()

assert len(captured) == 1
assert EVENT_CONF_BUTTON not in captured[0].data


@pytest.mark.parametrize(
("event_name", "kwargs", "expected_key", "expected_value"),
[
(LOW_BATTERY_EVENT, {"low_battery": True}, EVENT_CONF_BATTERY, "low"),
(LOW_BATTERY_EVENT, {"low_battery": False}, EVENT_CONF_BATTERY, "ok"),
(HEARTBEAT_EVENT, {"heartbeat_missed": True}, EVENT_CONF_HEARTBEAT, "missed"),
(
HEARTBEAT_EVENT,
{"heartbeat_missed": False},
EVENT_CONF_HEARTBEAT,
"received",
),
(LEAK_WET_EVENT, {"dry": False}, EVENT_CONF_MOISTURE, "wet"),
(LEAK_DRY_EVENT, {"dry": True}, EVENT_CONF_MOISTURE, "dry"),
],
)
async def test_add_insteon_events_listener_optional_fields(
hass: HomeAssistant,
event_name: str,
kwargs: dict[str, bool],
expected_key: str,
expected_value: str,
) -> None:
"""Optional subscriber kwargs map to Insteon event schema fields."""
mock_event = MagicMock()
addr = Address("55.55.55")
device = SimpleNamespace(address=addr, events={"evt": mock_event})

add_insteon_events(hass, device)
(listener,) = mock_event.subscribe.call_args[0]

captured = async_capture_events(
hass, f"insteon.{event_name.removesuffix('_event')}"
)
listener(event_name, addr.id, 1, **kwargs)
await hass.async_block_till_done()

assert len(captured) == 1
assert captured[0].data[expected_key] == expected_value
Loading