Skip to content
Snippets Groups Projects
Unverified Commit b8ce687e authored by TimL's avatar TimL Committed by GitHub
Browse files

Add server side events to Smlight integration (#125553)


* Register SSE client

* Add switch events for settings changes

* Mock sse settings events

* Apply suggestions from code review

Co-authored-by: default avatarPaarth Shah <mail@shahpaarth.com>

* access callbacks from mock call_args

---------

Co-authored-by: default avatarPaarth Shah <mail@shahpaarth.com>
parent 2f68bbd2
No related branches found
No related tags found
No related merge requests found
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from pysmlight import Api2, Info, Sensors from pysmlight import Api2, Info, Sensors
from pysmlight.const import Settings, SettingsProp
from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
...@@ -44,6 +45,10 @@ class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]): ...@@ -44,6 +45,10 @@ class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]):
self.client = Api2(host=host, session=async_get_clientsession(hass)) self.client = Api2(host=host, session=async_get_clientsession(hass))
self.legacy_api: int = 0 self.legacy_api: int = 0
self.config_entry.async_create_background_task(
hass, self.client.sse.client(), "smlight-sse-client"
)
async def _async_setup(self) -> None: async def _async_setup(self) -> None:
"""Authenticate if needed during initial setup.""" """Authenticate if needed during initial setup."""
if await self.client.check_auth_needed(): if await self.client.check_auth_needed():
...@@ -78,6 +83,13 @@ class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]): ...@@ -78,6 +83,13 @@ class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]):
translation_key="unsupported_firmware", translation_key="unsupported_firmware",
) )
def update_setting(self, setting: Settings, value: bool | int) -> None:
"""Update the sensor value from event."""
prop = SettingsProp[setting.name].value
setattr(self.data.sensors, prop, value)
self.async_set_updated_data(self.data)
async def _async_update_data(self) -> SmData: async def _async_update_data(self) -> SmData:
"""Fetch data from the SMLIGHT device.""" """Fetch data from the SMLIGHT device."""
try: try:
......
...@@ -7,7 +7,7 @@ from dataclasses import dataclass ...@@ -7,7 +7,7 @@ from dataclasses import dataclass
import logging import logging
from typing import Any from typing import Any
from pysmlight import Sensors from pysmlight import Sensors, SettingsEvent
from pysmlight.const import Settings from pysmlight.const import Settings
from homeassistant.components.switch import ( from homeassistant.components.switch import (
...@@ -16,7 +16,7 @@ from homeassistant.components.switch import ( ...@@ -16,7 +16,7 @@ from homeassistant.components.switch import (
SwitchEntityDescription, SwitchEntityDescription,
) )
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import SmConfigEntry from . import SmConfigEntry
...@@ -86,22 +86,33 @@ class SmSwitch(SmEntity, SwitchEntity): ...@@ -86,22 +86,33 @@ class SmSwitch(SmEntity, SwitchEntity):
self._page, self._toggle = description.setting.value self._page, self._toggle = description.setting.value
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.coordinator.client.sse.register_settings_cb(
self.entity_description.setting, self.event_callback
)
)
async def set_smlight(self, state: bool) -> None: async def set_smlight(self, state: bool) -> None:
"""Set the state on SLZB device.""" """Set the state on SLZB device."""
await self.coordinator.client.set_toggle(self._page, self._toggle, state) await self.coordinator.client.set_toggle(self._page, self._toggle, state)
@callback
def event_callback(self, event: SettingsEvent) -> None:
"""Handle switch events from the SLZB device."""
if event.setting is not None:
self.coordinator.update_setting(
self.entity_description.setting, event.setting[self._toggle]
)
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on.""" """Turn the switch on."""
self._attr_is_on = True
self.async_write_ha_state()
await self.set_smlight(True) await self.set_smlight(True)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off.""" """Turn the switch off."""
self._attr_is_on = False
self.async_write_ha_state()
await self.set_smlight(False) await self.set_smlight(False)
@property @property
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
from collections.abc import AsyncGenerator, Generator from collections.abc import AsyncGenerator, Generator
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from pysmlight.sse import sseClient
from pysmlight.web import CmdWrapper, Info, Sensors from pysmlight.web import CmdWrapper, Info, Sensors
import pytest import pytest
...@@ -89,6 +90,7 @@ def mock_smlight_client(request: pytest.FixtureRequest) -> Generator[MagicMock]: ...@@ -89,6 +90,7 @@ def mock_smlight_client(request: pytest.FixtureRequest) -> Generator[MagicMock]:
api.cmds = AsyncMock(spec_set=CmdWrapper) api.cmds = AsyncMock(spec_set=CmdWrapper)
api.set_toggle = AsyncMock() api.set_toggle = AsyncMock()
api.sse = MagicMock(spec_set=sseClient)
yield api yield api
......
"""Tests for the SMLIGHT switch platform.""" """Tests for the SMLIGHT switch platform."""
from collections.abc import Callable
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory from pysmlight import SettingsEvent
from pysmlight import Sensors
from pysmlight.const import Settings from pysmlight.const import Settings
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.components.smlight.const import SCAN_INTERVAL
from homeassistant.components.switch import ( from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN, DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
...@@ -20,7 +19,7 @@ from homeassistant.helpers import entity_registry as er ...@@ -20,7 +19,7 @@ from homeassistant.helpers import entity_registry as er
from .conftest import setup_integration from .conftest import setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform from tests.common import MockConfigEntry, snapshot_platform
pytestmark = [ pytestmark = [
pytest.mark.usefixtures( pytest.mark.usefixtures(
...@@ -48,18 +47,16 @@ async def test_switch_setup( ...@@ -48,18 +47,16 @@ async def test_switch_setup(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("entity", "setting", "field"), ("entity", "setting"),
[ [
("disable_leds", Settings.DISABLE_LEDS, "disable_leds"), ("disable_leds", Settings.DISABLE_LEDS),
("led_night_mode", Settings.NIGHT_MODE, "night_mode"), ("led_night_mode", Settings.NIGHT_MODE),
("auto_zigbee_update", Settings.ZB_AUTOUPDATE, "auto_zigbee"), ("auto_zigbee_update", Settings.ZB_AUTOUPDATE),
], ],
) )
async def test_switches( async def test_switches(
hass: HomeAssistant, hass: HomeAssistant,
entity: str, entity: str,
field: str,
freezer: FrozenDateTimeFactory,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
mock_smlight_client: MagicMock, mock_smlight_client: MagicMock,
setting: Settings, setting: Settings,
...@@ -82,11 +79,21 @@ async def test_switches( ...@@ -82,11 +79,21 @@ async def test_switches(
assert len(mock_smlight_client.set_toggle.mock_calls) == 1 assert len(mock_smlight_client.set_toggle.mock_calls) == 1
mock_smlight_client.set_toggle.assert_called_once_with(_page, _toggle, True) mock_smlight_client.set_toggle.assert_called_once_with(_page, _toggle, True)
mock_smlight_client.get_sensors.return_value = Sensors(**{field: True})
freezer.tick(SCAN_INTERVAL) event_function: Callable[[SettingsEvent], None] = next(
async_fire_time_changed(hass) (
await hass.async_block_till_done() call_args[0][1]
for call_args in mock_smlight_client.sse.register_settings_cb.call_args_list
if setting == call_args[0][0]
),
None,
)
async def _call_event_function(state: bool = True):
event_function(SettingsEvent(page=_page, origin="ha", setting={_toggle: state}))
await hass.async_block_till_done()
await _call_event_function(state=True)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.state == STATE_ON assert state.state == STATE_ON
...@@ -100,11 +107,8 @@ async def test_switches( ...@@ -100,11 +107,8 @@ async def test_switches(
assert len(mock_smlight_client.set_toggle.mock_calls) == 2 assert len(mock_smlight_client.set_toggle.mock_calls) == 2
mock_smlight_client.set_toggle.assert_called_with(_page, _toggle, False) mock_smlight_client.set_toggle.assert_called_with(_page, _toggle, False)
mock_smlight_client.get_sensors.return_value = Sensors(**{field: False})
freezer.tick(SCAN_INTERVAL) await _call_event_function(state=False)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.state == STATE_OFF assert state.state == STATE_OFF
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment