Add WLED reverse effect switch (#59778)

This commit is contained in:
Franck Nijhof 2021-11-20 15:57:47 +01:00 committed by GitHub
parent bf79db4226
commit 25f491ad16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 163 additions and 11 deletions

View file

@ -1,12 +1,13 @@
"""Support for WLED switches."""
from __future__ import annotations
from functools import partial
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ENTITY_CATEGORY_CONFIG
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
@ -31,12 +32,22 @@ async def async_setup_entry(
"""Set up WLED switch based on a config entry."""
coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
switches = [
WLEDNightlightSwitch(coordinator),
WLEDSyncSendSwitch(coordinator),
WLEDSyncReceiveSwitch(coordinator),
]
async_add_entities(switches)
async_add_entities(
[
WLEDNightlightSwitch(coordinator),
WLEDSyncSendSwitch(coordinator),
WLEDSyncReceiveSwitch(coordinator),
]
)
update_segments = partial(
async_update_segments,
coordinator,
set(),
async_add_entities,
)
coordinator.async_add_listener(update_segments)
update_segments()
class WLEDNightlightSwitch(WLEDEntity, SwitchEntity):
@ -140,3 +151,69 @@ class WLEDSyncReceiveSwitch(WLEDEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the WLED sync receive switch."""
await self.coordinator.wled.sync(receive=True)
class WLEDReverseSwitch(WLEDEntity, SwitchEntity):
"""Defines a WLED reverse effect switch."""
_attr_icon = "mdi:swap-horizontal-bold"
_attr_entity_category = ENTITY_CATEGORY_CONFIG
_segment: int
def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None:
"""Initialize WLED reverse effect switch."""
super().__init__(coordinator=coordinator)
# Segment 0 uses a simpler name, which is more natural for when using
# a single segment / using WLED with one big LED strip.
self._attr_name = f"{coordinator.data.info.name} Segment {segment} Reverse"
if segment == 0:
self._attr_name = f"{coordinator.data.info.name} Reverse"
self._attr_unique_id = f"{coordinator.data.info.mac_address}_reverse_{segment}"
self._segment = segment
@property
def available(self) -> bool:
"""Return True if entity is available."""
try:
self.coordinator.data.state.segments[self._segment]
except IndexError:
return False
return super().available
@property
def is_on(self) -> bool:
"""Return the state of the switch."""
return self.coordinator.data.state.segments[self._segment].reverse
@wled_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the WLED reverse effect switch."""
await self.coordinator.wled.segment(segment_id=self._segment, reverse=False)
@wled_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the WLED reverse effect switch."""
await self.coordinator.wled.segment(segment_id=self._segment, reverse=True)
@callback
def async_update_segments(
coordinator: WLEDDataUpdateCoordinator,
current_ids: set[int],
async_add_entities,
) -> None:
"""Update segments."""
segment_ids = {segment.segment_id for segment in coordinator.data.state.segments}
new_entities = []
# Process new segments, add them to Home Assistant
for segment_id in segment_ids - current_ids:
current_ids.add(segment_id)
new_entities.append(WLEDReverseSwitch(coordinator, segment_id))
if new_entities:
async_add_entities(new_entities)

View file

@ -41,7 +41,7 @@
"ix": 64,
"pal": 1,
"sel": true,
"rev": false,
"rev": true,
"cln": -1
}
]

View file

@ -76,7 +76,7 @@ async def test_rgb_light_state(
assert state.attributes.get(ATTR_INTENSITY) == 64
assert state.attributes.get(ATTR_PALETTE) == "Random Cycle"
assert state.attributes.get(ATTR_PRESET) is None
assert state.attributes.get(ATTR_REVERSE) is False
assert state.attributes.get(ATTR_REVERSE) is True
assert state.attributes.get(ATTR_SPEED) == 16
assert state.state == STATE_ON

View file

@ -1,8 +1,9 @@
"""Tests for the WLED switch platform."""
import json
from unittest.mock import MagicMock
import pytest
from wled import WLEDConnectionError, WLEDError
from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.wled.const import (
@ -10,6 +11,7 @@ from homeassistant.components.wled.const import (
ATTR_FADE,
ATTR_TARGET_BRIGHTNESS,
ATTR_UDP_PORT,
SCAN_INTERVAL,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
@ -23,8 +25,9 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.util.dt as dt_util
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture
async def test_switch_state(
@ -68,6 +71,16 @@ async def test_switch_state(
assert entry.unique_id == "aabbccddeeff_sync_receive"
assert entry.entity_category == ENTITY_CATEGORY_CONFIG
state = hass.states.get("switch.wled_rgb_light_reverse")
assert state
assert state.attributes.get(ATTR_ICON) == "mdi:swap-horizontal-bold"
assert state.state == STATE_OFF
entry = entity_registry.async_get("switch.wled_rgb_light_reverse")
assert entry
assert entry.unique_id == "aabbccddeeff_reverse_0"
assert entry.entity_category == ENTITY_CATEGORY_CONFIG
async def test_switch_change_state(
hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock
@ -137,6 +150,26 @@ async def test_switch_change_state(
assert mock_wled.sync.call_count == 4
mock_wled.sync.assert_called_with(receive=True)
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.wled_rgb_light_reverse"},
blocking=True,
)
await hass.async_block_till_done()
assert mock_wled.segment.call_count == 1
mock_wled.segment.assert_called_with(segment_id=0, reverse=True)
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.wled_rgb_light_reverse"},
blocking=True,
)
await hass.async_block_till_done()
assert mock_wled.segment.call_count == 2
mock_wled.segment.assert_called_with(segment_id=0, reverse=False)
async def test_switch_error(
hass: HomeAssistant,
@ -182,3 +215,45 @@ async def test_switch_connection_error(
assert state
assert state.state == STATE_UNAVAILABLE
assert "Error communicating with API" in caplog.text
@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True)
async def test_switch_dynamically_handle_segments(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
) -> None:
"""Test if a new/deleted segment is dynamically added/removed."""
segment0 = hass.states.get("switch.wled_rgb_light_reverse")
segment1 = hass.states.get("switch.wled_rgb_light_segment_1_reverse")
assert segment0
assert segment0.state == STATE_OFF
assert not segment1
# Test adding a segment dynamically...
return_value = mock_wled.update.return_value
mock_wled.update.return_value = WLEDDevice(
json.loads(load_fixture("wled/rgb.json"))
)
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
segment0 = hass.states.get("switch.wled_rgb_light_reverse")
segment1 = hass.states.get("switch.wled_rgb_light_segment_1_reverse")
assert segment0
assert segment0.state == STATE_OFF
assert segment1
assert segment1.state == STATE_ON
# Test remove segment again...
mock_wled.update.return_value = return_value
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
segment0 = hass.states.get("switch.wled_rgb_light_reverse")
segment1 = hass.states.get("switch.wled_rgb_light_segment_1_reverse")
assert segment0
assert segment0.state == STATE_OFF
assert segment1
assert segment1.state == STATE_UNAVAILABLE