Add light platform for switchbee integration (#78382)

* Added Light platform for switchbee integration

* added light to .coveragerc

* Applied code review feedback from other PR

* Fixes based on previous reviewes

* fixed small mistake

* added test coverage for light

* aligned code with other PR

* rebased

* fixes

* removed unecessary changes

* Update homeassistant/components/switchbee/light.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* Update homeassistant/components/switchbee/light.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* more fixes

* more fixes

* adjusting switch with the new change

* more fixes

* Update homeassistant/components/switchbee/light.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* Update homeassistant/components/switchbee/light.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* Update homeassistant/components/switchbee/light.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
This commit is contained in:
Jafar Atili 2022-09-28 17:30:33 +03:00 committed by GitHub
parent 24c26dc032
commit de3a1f444c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 179 additions and 28 deletions

View file

@ -1232,6 +1232,7 @@ omit =
homeassistant/components/switchbee/button.py
homeassistant/components/switchbee/coordinator.py
homeassistant/components/switchbee/entity.py
homeassistant/components/switchbee/light.py
homeassistant/components/switchbee/switch.py
homeassistant/components/switchbot/__init__.py
homeassistant/components/switchbot/binary_sensor.py

View file

@ -13,7 +13,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SWITCH]
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.LIGHT, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View file

@ -61,6 +61,7 @@ class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevic
DeviceType.GroupSwitch,
DeviceType.TimedPowerSwitch,
DeviceType.Scenario,
DeviceType.Dimmer,
]
)
except SwitchBeeError as exp:

View file

@ -2,6 +2,7 @@
from typing import Generic, TypeVar
from switchbee import SWITCHBEE_BRAND
from switchbee.api import SwitchBeeDeviceOfflineError, SwitchBeeError
from switchbee.device import SwitchBeeBaseDevice
from homeassistant.helpers.entity import DeviceInfo
@ -40,6 +41,7 @@ class SwitchBeeDeviceEntity(SwitchBeeEntity[_DeviceTypeT]):
) -> None:
"""Initialize the Switchbee device."""
super().__init__(device, coordinator)
self._is_online: bool = True
self._attr_device_info = DeviceInfo(
name=f"SwitchBee {device.unit_id}",
identifiers={
@ -56,3 +58,28 @@ class SwitchBeeDeviceEntity(SwitchBeeEntity[_DeviceTypeT]):
f"{coordinator.api.name} ({coordinator.api.mac})",
),
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._is_online and super().available
async def async_refresh_state(self) -> None:
"""Refresh the device state in the Central Unit.
This function addresses issue of a device that came online back but still report
unavailable state (-1).
Such device (offline device) will keep reporting unavailable state (-1)
until it has been actuated by the user (state changed to on/off).
With this code we keep trying setting dummy state for the device
in order for it to start reporting its real state back (assuming it came back online)
"""
try:
await self.coordinator.api.set_state(self._device.id, "dummy")
except SwitchBeeDeviceOfflineError:
return
except SwitchBeeError:
return

View file

@ -0,0 +1,148 @@
"""Support for SwitchBee light."""
from __future__ import annotations
import logging
from typing import Any, cast
from switchbee.api import SwitchBeeDeviceOfflineError, SwitchBeeError
from switchbee.device import ApiStateCommand, DeviceType, SwitchBeeDimmer
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
from .entity import SwitchBeeDeviceEntity
MAX_BRIGHTNESS = 255
_LOGGER = logging.getLogger(__name__)
def _hass_brightness_to_switchbee(value: int) -> int:
"""Convert hass brightness to SwitchBee."""
sb_brightness = int(100 * value / MAX_BRIGHTNESS)
# SwitchBee maximum brightness is 99
return sb_brightness if sb_brightness != 100 else 99
def _switchbee_brightness_to_hass(value: int) -> int:
"""Convert SwitchBee brightness to hass."""
if value == 99:
value = 100
return round(value * MAX_BRIGHTNESS / 100)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up SwitchBee light."""
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SwitchBeeLightEntity(switchbee_device, coordinator)
for switchbee_device in coordinator.data.values()
if switchbee_device.type == DeviceType.Dimmer
)
class SwitchBeeLightEntity(SwitchBeeDeviceEntity[SwitchBeeDimmer], LightEntity):
"""Representation of a SwitchBee light."""
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
def __init__(
self,
device: SwitchBeeDimmer,
coordinator: SwitchBeeCoordinator,
) -> None:
"""Initialize the SwitchBee light."""
super().__init__(device, coordinator)
self._attr_is_on = False
self._attr_brightness = 0
self._update_attrs_from_coordinator()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_attrs_from_coordinator()
super()._handle_coordinator_update()
def _update_attrs_from_coordinator(self) -> None:
coordinator_device = cast(
SwitchBeeDimmer, self.coordinator.data[self._device.id]
)
brightness = coordinator_device.brightness
# module is offline
if brightness == -1:
# This specific call will refresh the state of the device in the CU
self.hass.async_create_task(self.async_refresh_state())
# if the device was online (now offline), log message and mark it as Unavailable
if self._is_online:
_LOGGER.warning(
"%s light is not responding, check the status in the SwitchBee mobile app",
self.name,
)
self._is_online = False
return
# check if the device was offline (now online) and bring it back
if not self._is_online:
_LOGGER.info(
"%s light is now responding",
self.name,
)
self._is_online = True
self._attr_is_on = bool(brightness != 0)
# 1-99 is the only valid SwitchBee brightness range
if 0 < brightness < 100:
self._attr_brightness = _switchbee_brightness_to_hass(brightness)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Async function to set on to light."""
if ATTR_BRIGHTNESS in kwargs:
state: int | str = _hass_brightness_to_switchbee(kwargs[ATTR_BRIGHTNESS])
else:
state = ApiStateCommand.ON
if self.brightness:
state = _hass_brightness_to_switchbee(self.brightness)
try:
await self.coordinator.api.set_state(self._device.id, state)
except (SwitchBeeError, SwitchBeeDeviceOfflineError) as exp:
raise HomeAssistantError(
f"Failed to set {self.name} state {state}, {str(exp)}"
) from exp
if not isinstance(state, int):
# We just turned the light on, still don't know the last brightness known the Central Unit (yet)
# the brightness will be learned and updated in the next coordinator refresh
return
# update the coordinator data manually we already know the Central Unit brightness data for this light
cast(SwitchBeeDimmer, self.coordinator.data[self._device.id]).brightness = state
self.coordinator.async_set_updated_data(self.coordinator.data)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off SwitchBee light."""
try:
await self.coordinator.api.set_state(self._device.id, ApiStateCommand.OFF)
except (SwitchBeeError, SwitchBeeDeviceOfflineError) as exp:
raise HomeAssistantError(
f"Failed to turn off {self._attr_name}, {str(exp)}"
) from exp
# update the coordinator manually
cast(SwitchBeeDimmer, self.coordinator.data[self._device.id]).brightness = 0
self.coordinator.async_set_updated_data(self.coordinator.data)

View file

@ -69,12 +69,6 @@ class SwitchBeeSwitchEntity(SwitchBeeDeviceEntity[_DeviceTypeT], SwitchEntity):
"""Initialize the Switchbee switch."""
super().__init__(device, coordinator)
self._attr_is_on = False
self._is_online = True
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._is_online and super().available
@callback
def _handle_coordinator_update(self) -> None:
@ -85,32 +79,12 @@ class SwitchBeeSwitchEntity(SwitchBeeDeviceEntity[_DeviceTypeT], SwitchEntity):
def _update_from_coordinator(self) -> None:
"""Update the entity attributes from the coordinator data."""
async def async_refresh_state() -> None:
"""Refresh the device state in the Central Unit.
This function addresses issue of a device that came online back but still report
unavailable state (-1).
Such device (offline device) will keep reporting unavailable state (-1)
until it has been actuated by the user (state changed to on/off).
With this code we keep trying setting dummy state for the device
in order for it to start reporting its real state back (assuming it came back online)
"""
try:
await self.coordinator.api.set_state(self._device.id, "dummy")
except SwitchBeeDeviceOfflineError:
return
except SwitchBeeError:
return
coordinator_device = cast(_DeviceTypeT, self.coordinator.data[self._device.id])
if coordinator_device.state == -1:
# This specific call will refresh the state of the device in the CU
self.hass.async_create_task(async_refresh_state())
self.hass.async_create_task(self.async_refresh_state())
# if the device was online (now offline), log message and mark it as Unavailable
if self._is_online: