From 72880ae5852e6ae95f6b24503325f837b61f27f8 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 21 Oct 2023 01:05:56 +0200 Subject: [PATCH] Improve Yeelight by using common facilities (#1846) Add color and color temperature descriptors always and let downstreams handle filtering. Remove custom cli_format_yeelight in favor of common status reporting. * Expose "toggle" and "set_default" as actions. * Expose "color_mode" sensor. --- miio/integrations/yeelight/light/yeelight.py | 87 ++++---------------- 1 file changed, 16 insertions(+), 71 deletions(-) diff --git a/miio/integrations/yeelight/light/yeelight.py b/miio/integrations/yeelight/light/yeelight.py index ce5d58c85..71a84eb1c 100644 --- a/miio/integrations/yeelight/light/yeelight.py +++ b/miio/integrations/yeelight/light/yeelight.py @@ -1,13 +1,13 @@ import logging from enum import IntEnum -from typing import Dict, List, Optional, Tuple +from typing import List, Optional, Tuple import click from miio.click_common import command, format_output -from miio.descriptors import PropertyDescriptor, RangeDescriptor, ValidSettingRange +from miio.descriptors import ValidSettingRange from miio.device import Device, DeviceStatus -from miio.devicestatus import sensor, setting +from miio.devicestatus import action, sensor, setting from miio.identifiers import LightId from miio.utils import int_to_rgb, rgb_to_int @@ -26,37 +26,6 @@ } -def cli_format_yeelight(result) -> str: - """Return human readable sub lights string.""" - s = f"Name: {result.name}\n" - s += f"Update default on change: {result.save_state_on_change}\n" - s += f"Delay in minute before off: {result.delay_off}\n" - if result.music_mode is not None: - s += f"Music mode: {result.music_mode}\n" - if result.developer_mode is not None: - s += f"Developer mode: {result.developer_mode}\n" - for light in result.lights: - s += f"{light.type.name} light\n" - s += f" Power: {light.is_on}\n" - s += f" Brightness: {light.brightness}\n" - s += f" Color mode: {light.color_mode}\n" - if light.color_mode == YeelightMode.RGB: - s += f" RGB: {light.rgb}\n" - elif light.color_mode == YeelightMode.HSV: - s += f" HSV: {light.hsv}\n" - else: - s += f" Temperature: {light.color_temp}\n" - s += f" Color flowing mode: {light.color_flowing}\n" - if light.color_flowing: - s += f" Color flowing parameters: {light.color_flow_params}\n" - if result.moonlight_mode is not None: - s += "Moonlight\n" - s += f" Is in mode: {result.moonlight_mode}\n" - s += f" Moonlight mode brightness: {result.moonlight_mode_brightness}\n" - s += "\n" - return s - - class YeelightMode(IntEnum): RGB = 1 ColorTemperature = 2 @@ -176,11 +145,13 @@ def rgb(self) -> Optional[Tuple[int, int, int]]: return self.lights[0].rgb @property + @setting("Color", id=LightId.Color, setter_name="set_rgb_int") def rgb_int(self) -> Optional[int]: """Return color as single integer if RGB mode is active.""" return self.lights[0].rgb_int @property + @sensor("Color mode") def color_mode(self) -> Optional[YeelightMode]: """Return current color mode.""" return self.lights[0].color_mode @@ -194,6 +165,13 @@ def hsv(self) -> Optional[Tuple[int, int, int]]: return self.lights[0].hsv @property + @setting( + "Color temperature", + id=LightId.ColorTemperature, + setter_name="set_color_temperature", + range_attribute="color_temperature_range", + unit="K", + ) def color_temp(self) -> Optional[int]: """Return current color temperature, if applicable.""" return self.lights[0].color_temp @@ -311,7 +289,7 @@ def __init__( self._light_type = YeelightSubLightType.Main self._light_info = self._model_info.lamps[self._light_type] - @command(default_output=format_output("", result_msg_fmt=cli_format_yeelight)) + @command() def status(self) -> YeelightStatus: """Retrieve properties.""" properties = [ @@ -350,41 +328,6 @@ def status(self) -> YeelightStatus: return YeelightStatus(dict(zip(properties, values))) - def properties(self) -> Dict[str, PropertyDescriptor]: - """Return properties. - - This is overridden to inject the color temperature and color settings, if they - are supported by the device. - """ - # TODO: unclear semantics on settings, as making changes here will affect other instances of the class... - settings = super().properties().copy() - ct = self._light_info.color_temp - if ct.min_value != ct.max_value: - _LOGGER.debug("Got ct for %s: %s", self.model, ct) - settings[LightId.ColorTemperature.value] = RangeDescriptor( - name="Color temperature", - id=LightId.ColorTemperature.value, - status_attribute="color_temp", - setter=self.set_color_temperature, - min_value=self.color_temperature_range.min_value, - max_value=self.color_temperature_range.max_value, - step=1, - unit="kelvin", - ) - if self._light_info.supports_color: - _LOGGER.debug("Got color for %s", self.model) - settings[LightId.Color.value] = RangeDescriptor( - name="Color", - id=LightId.Color.value, - status_attribute="rgb_int", - setter=self.set_rgb_int, - min_value=1, - max_value=0xFFFFFF, - step=1, - ) - - return settings - @property def color_temperature_range(self) -> ValidSettingRange: """Return supported color temperature range.""" @@ -514,11 +457,13 @@ def set_name(self, name: str) -> bool: return self.send("set_name", [name]) @command(default_output=format_output("Toggling the bulb")) - def toggle(self): + @action("Toggle") + def toggle(self, *args): """Toggle bulb state.""" return self.send("toggle") @command(default_output=format_output("Setting current settings to default")) + @action("Set current as default") def set_default(self): """Set current state as default.""" return self.send("set_default")