Skip to content
Merged
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
1 change: 1 addition & 0 deletions homeassistant/components/switcher_kis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
Platform.BUTTON,
Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT,
Platform.SENSOR,
Platform.SWITCH,
]
Expand Down
129 changes: 129 additions & 0 deletions homeassistant/components/switcher_kis/light.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Switcher integration Light platform."""

from __future__ import annotations

import logging
from typing import Any, cast

from aioswitcher.api import SwitcherBaseResponse, SwitcherType2Api
from aioswitcher.device import (
DeviceCategory,
DeviceState,
SwitcherSingleShutterDualLight,
)

from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import SIGNAL_DEVICE_ADD
from .coordinator import SwitcherDataUpdateCoordinator
from .entity import SwitcherEntity

_LOGGER = logging.getLogger(__name__)

API_SET_LIGHT = "set_light"


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Switcher light from a config entry."""

@callback
def async_add_light(coordinator: SwitcherDataUpdateCoordinator) -> None:
"""Add light from Switcher device."""
if (
coordinator.data.device_type.category
== DeviceCategory.SINGLE_SHUTTER_DUAL_LIGHT
):
async_add_entities(
[
SwitcherLightEntity(coordinator, 0),
SwitcherLightEntity(coordinator, 1),
]
)

config_entry.async_on_unload(
async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_light)
)


class SwitcherLightEntity(SwitcherEntity, LightEntity):
"""Representation of a Switcher light entity."""

_attr_color_mode = ColorMode.ONOFF
_attr_supported_color_modes = {ColorMode.ONOFF}
_attr_translation_key = "light"

def __init__(
self, coordinator: SwitcherDataUpdateCoordinator, light_id: int
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._light_id = light_id
self.control_result: bool | None = None

# Entity class attributes
self._attr_translation_placeholders = {"light_id": str(light_id + 1)}
self._attr_unique_id = (
f"{coordinator.device_id}-{coordinator.mac_address}-{light_id}"
)

@callback
def _handle_coordinator_update(self) -> None:
"""When device updates, clear control result that overrides state."""
self.control_result = None
self.async_write_ha_state()

@property
def is_on(self) -> bool:
"""Return True if entity is on."""
if self.control_result is not None:
return self.control_result

data = cast(SwitcherSingleShutterDualLight, self.coordinator.data)
return bool(data.lights[self._light_id] == DeviceState.ON)

async def _async_call_api(self, api: str, *args: Any) -> None:
"""Call Switcher API."""
_LOGGER.debug("Calling api for %s, api: '%s', args: %s", self.name, api, args)
response: SwitcherBaseResponse | None = None
error = None

try:
async with SwitcherType2Api(
self.coordinator.data.device_type,
self.coordinator.data.ip_address,
self.coordinator.data.device_id,
self.coordinator.data.device_key,
self.coordinator.token,
) as swapi:
response = await getattr(swapi, api)(*args)
except (TimeoutError, OSError, RuntimeError) as err:
error = repr(err)

if error or not response or not response.successful:
self.coordinator.last_update_success = False
self.async_write_ha_state()
raise HomeAssistantError(
f"Call api for {self.name} failed, api: '{api}', "
f"args: {args}, response/error: {response or error}"
)

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
await self._async_call_api(API_SET_LIGHT, DeviceState.ON, self._light_id)
self.control_result = True
self.async_write_ha_state()

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
await self._async_call_api(API_SET_LIGHT, DeviceState.OFF, self._light_id)
self.control_result = False
self.async_write_ha_state()
5 changes: 5 additions & 0 deletions homeassistant/components/switcher_kis/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
"name": "Vertical swing off"
}
},
"light": {
"light": {
"name": "Light {light_id}"
}
},
"sensor": {
"remaining_time": {
"name": "Remaining time"
Expand Down
154 changes: 154 additions & 0 deletions tests/components/switcher_kis/test_light.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Test the Switcher light platform."""

from unittest.mock import patch

from aioswitcher.api import SwitcherBaseResponse
from aioswitcher.device import DeviceState
import pytest

from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import slugify

from . import init_integration
from .consts import (
DUMMY_SINGLE_SHUTTER_DUAL_LIGHT_DEVICE as DEVICE,
DUMMY_TOKEN as TOKEN,
DUMMY_USERNAME as USERNAME,
)

ENTITY_ID = f"{LIGHT_DOMAIN}.{slugify(DEVICE.name)}_light_1"
ENTITY_ID2 = f"{LIGHT_DOMAIN}.{slugify(DEVICE.name)}_light_2"


@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True)
@pytest.mark.parametrize(
("entity_id", "light_id", "device_state"),
[
(ENTITY_ID, 0, [DeviceState.OFF, DeviceState.ON]),
(ENTITY_ID2, 1, [DeviceState.ON, DeviceState.OFF]),
],
)
async def test_light(
hass: HomeAssistant,
mock_bridge,
mock_api,
monkeypatch: pytest.MonkeyPatch,
entity_id: str,
light_id: int,
device_state: list[DeviceState],
) -> None:
"""Test the light."""
await init_integration(hass, USERNAME, TOKEN)
assert mock_bridge

# Test initial state - light on
state = hass.states.get(entity_id)
assert state.state == STATE_ON

# Test state change on --> off for light
monkeypatch.setattr(DEVICE, "lights", device_state)
mock_bridge.mock_callbacks([DEVICE])
await hass.async_block_till_done()

state = hass.states.get(entity_id)
assert state.state == STATE_OFF

# Test turning on light
with patch(
"homeassistant.components.switcher_kis.light.SwitcherType2Api.set_light",
) as mock_set_light:
await hass.services.async_call(
LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
)

assert mock_api.call_count == 2
mock_set_light.assert_called_once_with(DeviceState.ON, light_id)
state = hass.states.get(entity_id)
assert state.state == STATE_ON

# Test turning off light
with patch(
"homeassistant.components.switcher_kis.light.SwitcherType2Api.set_light"
) as mock_set_light:
await hass.services.async_call(
LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True
)

assert mock_api.call_count == 4
mock_set_light.assert_called_once_with(DeviceState.OFF, light_id)
state = hass.states.get(entity_id)
assert state.state == STATE_OFF


@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True)
async def test_light_control_fail(
hass: HomeAssistant,
mock_bridge,
mock_api,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test light control fail."""
await init_integration(hass, USERNAME, TOKEN)
assert mock_bridge

# Test initial state - light off
monkeypatch.setattr(DEVICE, "lights", [DeviceState.OFF, DeviceState.ON])
mock_bridge.mock_callbacks([DEVICE])
await hass.async_block_till_done()

state = hass.states.get(ENTITY_ID)
assert state.state == STATE_OFF

# Test exception during turn on
with patch(
"homeassistant.components.switcher_kis.cover.SwitcherType2Api.set_light",
side_effect=RuntimeError("fake error"),
) as mock_control_device:
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)

assert mock_api.call_count == 2
mock_control_device.assert_called_once_with(DeviceState.ON, 0)
state = hass.states.get(ENTITY_ID)
assert state.state == STATE_UNAVAILABLE

# Make device available again
mock_bridge.mock_callbacks([DEVICE])
await hass.async_block_till_done()

state = hass.states.get(ENTITY_ID)
assert state.state == STATE_OFF

# Test error response during turn on
with patch(
"homeassistant.components.switcher_kis.cover.SwitcherType2Api.set_light",
return_value=SwitcherBaseResponse(None),
) as mock_control_device:
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)

assert mock_api.call_count == 4
mock_control_device.assert_called_once_with(DeviceState.ON, 0)
state = hass.states.get(ENTITY_ID)
assert state.state == STATE_UNAVAILABLE