Use attributes in wilight (#73898)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
epenet 2022-06-25 00:55:01 +02:00 committed by GitHub
parent 15b7564171
commit 9b88b77b66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 88 additions and 99 deletions

View File

@ -1,5 +1,9 @@
"""The WiLight integration.""" """The WiLight integration."""
from typing import Any
from pywilight.wilight_device import Device as PyWiLightDevice
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -51,61 +55,43 @@ class WiLightDevice(Entity):
Contains the common logic for WiLight entities. Contains the common logic for WiLight entities.
""" """
def __init__(self, api_device, index, item_name): _attr_should_poll = False
def __init__(self, api_device: PyWiLightDevice, index: str, item_name: str) -> None:
"""Initialize the device.""" """Initialize the device."""
# WiLight specific attributes for every component type # WiLight specific attributes for every component type
self._device_id = api_device.device_id self._device_id = api_device.device_id
self._sw_version = api_device.swversion
self._client = api_device.client self._client = api_device.client
self._model = api_device.model
self._name = item_name
self._index = index self._index = index
self._unique_id = f"{self._device_id}_{self._index}" self._status: dict[str, Any] = {}
self._status = {}
@property self._attr_name = item_name
def should_poll(self): self._attr_unique_id = f"{self._device_id}_{index}"
"""No polling needed.""" self._attr_device_info = DeviceInfo(
return False name=item_name,
identifiers={(DOMAIN, self._attr_unique_id)},
@property model=api_device.model,
def name(self):
"""Return a name for this WiLight item."""
return self._name
@property
def unique_id(self):
"""Return the unique ID for this WiLight item."""
return self._unique_id
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
name=self._name,
identifiers={(DOMAIN, self._unique_id)},
model=self._model,
manufacturer="WiLight", manufacturer="WiLight",
sw_version=self._sw_version, sw_version=api_device.swversion,
via_device=(DOMAIN, self._device_id), via_device=(DOMAIN, self._device_id),
) )
@property @property
def available(self): def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return bool(self._client.is_connected) return bool(self._client.is_connected)
@callback @callback
def handle_event_callback(self, states): def handle_event_callback(self, states: dict[str, Any]) -> None:
"""Propagate changes through ha.""" """Propagate changes through ha."""
self._status = states self._status = states
self.async_write_ha_state() self.async_write_ha_state()
async def async_update(self): async def async_update(self) -> None:
"""Synchronize state with api_device.""" """Synchronize state with api_device."""
await self._client.status(self._index) await self._client.status(self._index)
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""Register update callback.""" """Register update callback."""
self._client.register_status_callback(self.handle_event_callback, self._index) self._client.register_status_callback(self.handle_event_callback, self._index)
await self._client.status(self._index) await self._client.status(self._index)

View File

@ -1,4 +1,6 @@
"""Support for WiLight Cover.""" """Support for WiLight Cover."""
from __future__ import annotations
from typing import Any from typing import Any
from pywilight.const import ( from pywilight.const import (
@ -18,16 +20,18 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN, WiLightDevice from . import DOMAIN, WiLightDevice
from .parent_device import WiLightParent
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up WiLight covers from a config entry.""" """Set up WiLight covers from a config entry."""
parent = hass.data[DOMAIN][entry.entry_id] parent: WiLightParent = hass.data[DOMAIN][entry.entry_id]
# Handle a discovered WiLight device. # Handle a discovered WiLight device.
entities = [] entities = []
assert parent.api
for item in parent.api.items: for item in parent.api.items:
if item["type"] != ITEM_COVER: if item["type"] != ITEM_COVER:
continue continue
@ -35,18 +39,17 @@ async def async_setup_entry(
item_name = item["name"] item_name = item["name"]
if item["sub_type"] != COVER_V1: if item["sub_type"] != COVER_V1:
continue continue
entity = WiLightCover(parent.api, index, item_name) entities.append(WiLightCover(parent.api, index, item_name))
entities.append(entity)
async_add_entities(entities) async_add_entities(entities)
def wilight_to_hass_position(value): def wilight_to_hass_position(value: int) -> int:
"""Convert wilight position 1..255 to hass format 0..100.""" """Convert wilight position 1..255 to hass format 0..100."""
return min(100, round((value * 100) / 255)) return min(100, round((value * 100) / 255))
def hass_to_wilight_position(value): def hass_to_wilight_position(value: int) -> int:
"""Convert hass position 0..100 to wilight 1..255 scale.""" """Convert hass position 0..100 to wilight 1..255 scale."""
return min(255, round((value * 255) / 100)) return min(255, round((value * 255) / 100))
@ -55,7 +58,7 @@ class WiLightCover(WiLightDevice, CoverEntity):
"""Representation of a WiLights cover.""" """Representation of a WiLights cover."""
@property @property
def current_cover_position(self): def current_cover_position(self) -> int | None:
"""Return current position of cover. """Return current position of cover.
None is unknown, 0 is closed, 100 is fully open. None is unknown, 0 is closed, 100 is fully open.
@ -65,21 +68,21 @@ class WiLightCover(WiLightDevice, CoverEntity):
return None return None
@property @property
def is_opening(self): def is_opening(self) -> bool | None:
"""Return if the cover is opening or not.""" """Return if the cover is opening or not."""
if "motor_state" not in self._status: if "motor_state" not in self._status:
return None return None
return self._status["motor_state"] == WL_OPENING return self._status["motor_state"] == WL_OPENING
@property @property
def is_closing(self): def is_closing(self) -> bool | None:
"""Return if the cover is closing or not.""" """Return if the cover is closing or not."""
if "motor_state" not in self._status: if "motor_state" not in self._status:
return None return None
return self._status["motor_state"] == WL_CLOSING return self._status["motor_state"] == WL_CLOSING
@property @property
def is_closed(self): def is_closed(self) -> bool | None:
"""Return if the cover is closed or not.""" """Return if the cover is closed or not."""
if "motor_state" not in self._status or "position_current" not in self._status: if "motor_state" not in self._status or "position_current" not in self._status:
return None return None

View File

@ -13,6 +13,7 @@ from pywilight.const import (
WL_SPEED_LOW, WL_SPEED_LOW,
WL_SPEED_MEDIUM, WL_SPEED_MEDIUM,
) )
from pywilight.wilight_device import Device as PyWiLightDevice
from homeassistant.components.fan import DIRECTION_FORWARD, FanEntity, FanEntityFeature from homeassistant.components.fan import DIRECTION_FORWARD, FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -24,6 +25,7 @@ from homeassistant.util.percentage import (
) )
from . import DOMAIN, WiLightDevice from . import DOMAIN, WiLightDevice
from .parent_device import WiLightParent
ORDERED_NAMED_FAN_SPEEDS = [WL_SPEED_LOW, WL_SPEED_MEDIUM, WL_SPEED_HIGH] ORDERED_NAMED_FAN_SPEEDS = [WL_SPEED_LOW, WL_SPEED_MEDIUM, WL_SPEED_HIGH]
@ -32,10 +34,11 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up WiLight lights from a config entry.""" """Set up WiLight lights from a config entry."""
parent = hass.data[DOMAIN][entry.entry_id] parent: WiLightParent = hass.data[DOMAIN][entry.entry_id]
# Handle a discovered WiLight device. # Handle a discovered WiLight device.
entities = [] entities = []
assert parent.api
for item in parent.api.items: for item in parent.api.items:
if item["type"] != ITEM_FAN: if item["type"] != ITEM_FAN:
continue continue
@ -43,8 +46,7 @@ async def async_setup_entry(
item_name = item["name"] item_name = item["name"]
if item["sub_type"] != FAN_V1: if item["sub_type"] != FAN_V1:
continue continue
entity = WiLightFan(parent.api, index, item_name) entities.append(WiLightFan(parent.api, index, item_name))
entities.append(entity)
async_add_entities(entities) async_add_entities(entities)
@ -52,19 +54,16 @@ async def async_setup_entry(
class WiLightFan(WiLightDevice, FanEntity): class WiLightFan(WiLightDevice, FanEntity):
"""Representation of a WiLights fan.""" """Representation of a WiLights fan."""
_attr_icon = "mdi:fan"
_attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS)
_attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION
def __init__(self, api_device, index, item_name): def __init__(self, api_device: PyWiLightDevice, index: str, item_name: str) -> None:
"""Initialize the device.""" """Initialize the device."""
super().__init__(api_device, index, item_name) super().__init__(api_device, index, item_name)
# Initialize the WiLights fan. # Initialize the WiLights fan.
self._direction = WL_DIRECTION_FORWARD self._direction = WL_DIRECTION_FORWARD
@property
def icon(self):
"""Return the icon of device based on its type."""
return "mdi:fan"
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if device is on.""" """Return true if device is on."""
@ -83,11 +82,6 @@ class WiLightFan(WiLightDevice, FanEntity):
return None return None
return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, wl_speed) return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, wl_speed)
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return len(ORDERED_NAMED_FAN_SPEEDS)
@property @property
def current_direction(self) -> str: def current_direction(self) -> str:
"""Return the current direction of the fan.""" """Return the current direction of the fan."""

View File

@ -1,5 +1,10 @@
"""Support for WiLight lights.""" """Support for WiLight lights."""
from __future__ import annotations
from typing import Any
from pywilight.const import ITEM_LIGHT, LIGHT_COLOR, LIGHT_DIMMER, LIGHT_ON_OFF from pywilight.const import ITEM_LIGHT, LIGHT_COLOR, LIGHT_DIMMER, LIGHT_ON_OFF
from pywilight.wilight_device import Device as PyWiLightDevice
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
@ -12,25 +17,23 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN, WiLightDevice from . import DOMAIN, WiLightDevice
from .parent_device import WiLightParent
def entities_from_discovered_wilight(hass, api_device): def entities_from_discovered_wilight(api_device: PyWiLightDevice) -> list[LightEntity]:
"""Parse configuration and add WiLight light entities.""" """Parse configuration and add WiLight light entities."""
entities = [] entities: list[LightEntity] = []
for item in api_device.items: for item in api_device.items:
if item["type"] != ITEM_LIGHT: if item["type"] != ITEM_LIGHT:
continue continue
index = item["index"] index = item["index"]
item_name = item["name"] item_name = item["name"]
if item["sub_type"] == LIGHT_ON_OFF: if item["sub_type"] == LIGHT_ON_OFF:
entity = WiLightLightOnOff(api_device, index, item_name) entities.append(WiLightLightOnOff(api_device, index, item_name))
elif item["sub_type"] == LIGHT_DIMMER: elif item["sub_type"] == LIGHT_DIMMER:
entity = WiLightLightDimmer(api_device, index, item_name) entities.append(WiLightLightDimmer(api_device, index, item_name))
elif item["sub_type"] == LIGHT_COLOR: elif item["sub_type"] == LIGHT_COLOR:
entity = WiLightLightColor(api_device, index, item_name) entities.append(WiLightLightColor(api_device, index, item_name))
else:
continue
entities.append(entity)
return entities return entities
@ -39,10 +42,11 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up WiLight lights from a config entry.""" """Set up WiLight lights from a config entry."""
parent = hass.data[DOMAIN][entry.entry_id] parent: WiLightParent = hass.data[DOMAIN][entry.entry_id]
# Handle a discovered WiLight device. # Handle a discovered WiLight device.
entities = entities_from_discovered_wilight(hass, parent.api) assert parent.api
entities = entities_from_discovered_wilight(parent.api)
async_add_entities(entities) async_add_entities(entities)
@ -53,15 +57,15 @@ class WiLightLightOnOff(WiLightDevice, LightEntity):
_attr_supported_color_modes = {ColorMode.ONOFF} _attr_supported_color_modes = {ColorMode.ONOFF}
@property @property
def is_on(self): def is_on(self) -> bool | None:
"""Return true if device is on.""" """Return true if device is on."""
return self._status.get("on") return self._status.get("on")
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on.""" """Turn the device on."""
await self._client.turn_on(self._index) await self._client.turn_on(self._index)
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off.""" """Turn the device off."""
await self._client.turn_off(self._index) await self._client.turn_off(self._index)
@ -73,16 +77,16 @@ class WiLightLightDimmer(WiLightDevice, LightEntity):
_attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
@property @property
def brightness(self): def brightness(self) -> int:
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
return int(self._status.get("brightness", 0)) return int(self._status.get("brightness", 0))
@property @property
def is_on(self): def is_on(self) -> bool | None:
"""Return true if device is on.""" """Return true if device is on."""
return self._status.get("on") return self._status.get("on")
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on,set brightness if needed.""" """Turn the device on,set brightness if needed."""
# Dimmer switches use a range of [0, 255] to control # Dimmer switches use a range of [0, 255] to control
# brightness. Level 255 might mean to set it to previous value # brightness. Level 255 might mean to set it to previous value
@ -92,27 +96,27 @@ class WiLightLightDimmer(WiLightDevice, LightEntity):
else: else:
await self._client.turn_on(self._index) await self._client.turn_on(self._index)
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off.""" """Turn the device off."""
await self._client.turn_off(self._index) await self._client.turn_off(self._index)
def wilight_to_hass_hue(value): def wilight_to_hass_hue(value: int) -> float:
"""Convert wilight hue 1..255 to hass 0..360 scale.""" """Convert wilight hue 1..255 to hass 0..360 scale."""
return min(360, round((value * 360) / 255, 3)) return min(360, round((value * 360) / 255, 3))
def hass_to_wilight_hue(value): def hass_to_wilight_hue(value: float) -> int:
"""Convert hass hue 0..360 to wilight 1..255 scale.""" """Convert hass hue 0..360 to wilight 1..255 scale."""
return min(255, round((value * 255) / 360)) return min(255, round((value * 255) / 360))
def wilight_to_hass_saturation(value): def wilight_to_hass_saturation(value: int) -> float:
"""Convert wilight saturation 1..255 to hass 0..100 scale.""" """Convert wilight saturation 1..255 to hass 0..100 scale."""
return min(100, round((value * 100) / 255, 3)) return min(100, round((value * 100) / 255, 3))
def hass_to_wilight_saturation(value): def hass_to_wilight_saturation(value: float) -> int:
"""Convert hass saturation 0..100 to wilight 1..255 scale.""" """Convert hass saturation 0..100 to wilight 1..255 scale."""
return min(255, round((value * 255) / 100)) return min(255, round((value * 255) / 100))
@ -124,24 +128,24 @@ class WiLightLightColor(WiLightDevice, LightEntity):
_attr_supported_color_modes = {ColorMode.HS} _attr_supported_color_modes = {ColorMode.HS}
@property @property
def brightness(self): def brightness(self) -> int:
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
return int(self._status.get("brightness", 0)) return int(self._status.get("brightness", 0))
@property @property
def hs_color(self): def hs_color(self) -> tuple[float, float]:
"""Return the hue and saturation color value [float, float].""" """Return the hue and saturation color value [float, float]."""
return [ return (
wilight_to_hass_hue(int(self._status.get("hue", 0))), wilight_to_hass_hue(int(self._status.get("hue", 0))),
wilight_to_hass_saturation(int(self._status.get("saturation", 0))), wilight_to_hass_saturation(int(self._status.get("saturation", 0))),
] )
@property @property
def is_on(self): def is_on(self) -> bool | None:
"""Return true if device is on.""" """Return true if device is on."""
return self._status.get("on") return self._status.get("on")
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on,set brightness if needed.""" """Turn the device on,set brightness if needed."""
# Brightness use a range of [0, 255] to control # Brightness use a range of [0, 255] to control
# Hue use a range of [0, 360] to control # Hue use a range of [0, 360] to control
@ -161,6 +165,6 @@ class WiLightLightColor(WiLightDevice, LightEntity):
else: else:
await self._client.turn_on(self._index) await self._client.turn_on(self._index)
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off.""" """Turn the device off."""
await self._client.turn_off(self._index) await self._client.turn_off(self._index)

View File

@ -1,12 +1,16 @@
"""The WiLight Device integration.""" """The WiLight Device integration."""
from __future__ import annotations
import asyncio import asyncio
import logging import logging
import pywilight import pywilight
from pywilight.wilight_device import Device as PyWiLightDevice
import requests import requests
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -15,23 +19,23 @@ _LOGGER = logging.getLogger(__name__)
class WiLightParent: class WiLightParent:
"""Manages a single WiLight Parent Device.""" """Manages a single WiLight Parent Device."""
def __init__(self, hass, config_entry): def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize the system.""" """Initialize the system."""
self._host = config_entry.data[CONF_HOST] self._host: str = config_entry.data[CONF_HOST]
self._hass = hass self._hass = hass
self._api = None self._api: PyWiLightDevice | None = None
@property @property
def host(self): def host(self) -> str:
"""Return the host of this parent.""" """Return the host of this parent."""
return self._host return self._host
@property @property
def api(self): def api(self) -> PyWiLightDevice | None:
"""Return the api of this parent.""" """Return the api of this parent."""
return self._api return self._api
async def async_setup(self): async def async_setup(self) -> bool:
"""Set up a WiLight Parent Device based on host parameter.""" """Set up a WiLight Parent Device based on host parameter."""
host = self._host host = self._host
hass = self._hass hass = self._hass
@ -42,7 +46,7 @@ class WiLightParent:
return False return False
@callback @callback
def disconnected(): def disconnected() -> None:
# Schedule reconnect after connection has been lost. # Schedule reconnect after connection has been lost.
_LOGGER.warning("WiLight %s disconnected", api_device.device_id) _LOGGER.warning("WiLight %s disconnected", api_device.device_id)
async_dispatcher_send( async_dispatcher_send(
@ -50,14 +54,14 @@ class WiLightParent:
) )
@callback @callback
def reconnected(): def reconnected() -> None:
# Schedule reconnect after connection has been lost. # Schedule reconnect after connection has been lost.
_LOGGER.warning("WiLight %s reconnect", api_device.device_id) _LOGGER.warning("WiLight %s reconnect", api_device.device_id)
async_dispatcher_send( async_dispatcher_send(
hass, f"wilight_device_available_{api_device.device_id}", True hass, f"wilight_device_available_{api_device.device_id}", True
) )
async def connect(api_device): async def connect(api_device: PyWiLightDevice) -> None:
# Set up connection and hook it into HA for reconnect/shutdown. # Set up connection and hook it into HA for reconnect/shutdown.
_LOGGER.debug("Initiating connection to %s", api_device.device_id) _LOGGER.debug("Initiating connection to %s", api_device.device_id)
@ -81,7 +85,7 @@ class WiLightParent:
return True return True
async def async_reset(self): async def async_reset(self) -> None:
"""Reset api.""" """Reset api."""
# If the initialization was not wrong. # If the initialization was not wrong.
@ -89,15 +93,13 @@ class WiLightParent:
self._api.client.stop() self._api.client.stop()
def create_api_device(host): def create_api_device(host: str) -> PyWiLightDevice:
"""Create an API Device.""" """Create an API Device."""
try: try:
device = pywilight.device_from_host(host) return pywilight.device_from_host(host)
except ( except (
requests.exceptions.ConnectionError, requests.exceptions.ConnectionError,
requests.exceptions.Timeout, requests.exceptions.Timeout,
) as err: ) as err:
_LOGGER.error("Unable to access WiLight at %s (%s)", host, err) _LOGGER.error("Unable to access WiLight at %s (%s)", host, err)
return None return None
return device