Add Switch platform to Smlight integration (#125292)

* Add switch platform to Smlight

* Add strings for switch platform

* Add tests for Smlight switch platform

* Regenerate snapshot

* Address review comments

* Use is_on property for updating switch state

* Address review comments

---------

Co-authored-by: Tim Lunn <tim@feathertop.org>
This commit is contained in:
TimL 2024-09-06 22:25:55 +10:00 committed by GitHub
parent 8f38b7191a
commit 0eda451c24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 376 additions and 1 deletions

View File

@ -12,6 +12,7 @@ PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.SENSOR,
Platform.SWITCH,
]
type SmConfigEntry = ConfigEntry[SmDataUpdateCoordinator]

View File

@ -80,6 +80,17 @@
"zigbee_flash_mode": {
"name": "Zigbee flash mode"
}
},
"switch": {
"auto_zigbee_update": {
"name": "Auto Zigbee update"
},
"disable_led": {
"name": "Disable LEDs"
},
"night_mode": {
"name": "LED night mode"
}
}
}
}

View File

@ -0,0 +1,110 @@
"""Support for SLZB-06 switches."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any
from pysmlight import Sensors
from pysmlight.const import Settings
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import SmConfigEntry
from .coordinator import SmDataUpdateCoordinator
from .entity import SmEntity
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class SmSwitchEntityDescription(SwitchEntityDescription):
"""Class to describe a Switch entity."""
setting: Settings
state_fn: Callable[[Sensors], bool | None]
SWITCHES: list[SmSwitchEntityDescription] = [
SmSwitchEntityDescription(
key="disable_led",
translation_key="disable_led",
setting=Settings.DISABLE_LEDS,
state_fn=lambda x: x.disable_leds,
),
SmSwitchEntityDescription(
key="night_mode",
translation_key="night_mode",
setting=Settings.NIGHT_MODE,
state_fn=lambda x: x.night_mode,
),
SmSwitchEntityDescription(
key="auto_zigbee_update",
translation_key="auto_zigbee_update",
entity_category=EntityCategory.CONFIG,
setting=Settings.ZB_AUTOUPDATE,
state_fn=lambda x: x.auto_zigbee,
),
]
async def async_setup_entry(
hass: HomeAssistant,
entry: SmConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize switches for SLZB-06 device."""
coordinator = entry.runtime_data
async_add_entities(SmSwitch(coordinator, switch) for switch in SWITCHES)
class SmSwitch(SmEntity, SwitchEntity):
"""Representation of a SLZB-06 switch."""
entity_description: SmSwitchEntityDescription
_attr_device_class = SwitchDeviceClass.SWITCH
def __init__(
self,
coordinator: SmDataUpdateCoordinator,
description: SmSwitchEntityDescription,
) -> None:
"""Initialize the switch."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.unique_id}-{description.key}"
self._page, self._toggle = description.setting.value
async def set_smlight(self, state: bool) -> None:
"""Set the state on SLZB device."""
await self.coordinator.client.set_toggle(self._page, self._toggle, state)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
self._attr_is_on = True
self.async_write_ha_state()
await self.set_smlight(True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
self._attr_is_on = False
self.async_write_ha_state()
await self.set_smlight(False)
@property
def is_on(self) -> bool | None:
"""Return the state of the switch."""
return self.entity_description.state_fn(self.coordinator.data.sensors)

View File

@ -88,6 +88,7 @@ def mock_smlight_client(request: pytest.FixtureRequest) -> Generator[MagicMock]:
api.authenticate.return_value = True
api.cmds = AsyncMock(spec_set=CmdWrapper)
api.set_toggle = AsyncMock()
yield api

View File

@ -9,6 +9,6 @@
"wifi_connected": false,
"wifi_status": 255,
"disable_leds": false,
"night_mode": false,
"night_mode": true,
"auto_zigbee": false
}

View File

@ -0,0 +1,142 @@
# serializer version: 1
# name: test_switch_setup[switch.mock_title_auto_zigbee_update-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.mock_title_auto_zigbee_update',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Auto Zigbee update',
'platform': 'smlight',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'auto_zigbee_update',
'unique_id': 'aa:bb:cc:dd:ee:ff-auto_zigbee_update',
'unit_of_measurement': None,
})
# ---
# name: test_switch_setup[switch.mock_title_auto_zigbee_update-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Mock Title Auto Zigbee update',
}),
'context': <ANY>,
'entity_id': 'switch.mock_title_auto_zigbee_update',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_setup[switch.mock_title_disable_leds-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.mock_title_disable_leds',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Disable LEDs',
'platform': 'smlight',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'disable_led',
'unique_id': 'aa:bb:cc:dd:ee:ff-disable_led',
'unit_of_measurement': None,
})
# ---
# name: test_switch_setup[switch.mock_title_disable_leds-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Mock Title Disable LEDs',
}),
'context': <ANY>,
'entity_id': 'switch.mock_title_disable_leds',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_setup[switch.mock_title_led_night_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.mock_title_led_night_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'LED night mode',
'platform': 'smlight',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'night_mode',
'unique_id': 'aa:bb:cc:dd:ee:ff-night_mode',
'unit_of_measurement': None,
})
# ---
# name: test_switch_setup[switch.mock_title_led_night_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Mock Title LED night mode',
}),
'context': <ANY>,
'entity_id': 'switch.mock_title_led_night_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -0,0 +1,110 @@
"""Tests for the SMLIGHT switch platform."""
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory
from pysmlight import Sensors
from pysmlight.const import Settings
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.smlight.const import SCAN_INTERVAL
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .conftest import setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
pytestmark = [
pytest.mark.usefixtures(
"mock_smlight_client",
)
]
@pytest.fixture
def platforms() -> list[Platform]:
"""Platforms, which should be loaded during the test."""
return [Platform.SWITCH]
async def test_switch_setup(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test setup of SMLIGHT switches."""
entry = await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
@pytest.mark.parametrize(
("entity", "setting", "field"),
[
("disable_leds", Settings.DISABLE_LEDS, "disable_leds"),
("led_night_mode", Settings.NIGHT_MODE, "night_mode"),
("auto_zigbee_update", Settings.ZB_AUTOUPDATE, "auto_zigbee"),
],
)
async def test_switches(
hass: HomeAssistant,
entity: str,
field: str,
freezer: FrozenDateTimeFactory,
mock_config_entry: MockConfigEntry,
mock_smlight_client: MagicMock,
setting: Settings,
) -> None:
"""Test the SMLIGHT switches."""
await setup_integration(hass, mock_config_entry)
_page, _toggle = setting.value
entity_id = f"switch.mock_title_{entity}"
state = hass.states.get(entity_id)
assert state is not None
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
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.get_sensors.return_value = Sensors(**{field: True})
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_ON
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert len(mock_smlight_client.set_toggle.mock_calls) == 2
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)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_OFF