Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
rospogrigio authored Jan 14, 2025
2 parents f9d7266 + 1369245 commit 27dbb57
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 40 deletions.
8 changes: 4 additions & 4 deletions custom_components/localtuya/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@
HVACMode.AUTO: "Auto",
},
"MANUAL/AUTO": {
HVAC_MODE_HEAT: "MANUAL",
HVAC_MODE_AUTO: "AUTO",
HVACMode.HEAT: "MANUAL",
HVACMode.AUTO: "AUTO",
},
"Manual/Program": {
HVACMode.HEAT: "Manual",
Expand Down Expand Up @@ -117,8 +117,8 @@
HVACAction.IDLE: "Warming",
},
"heating/warming": {
CURRENT_HVAC_HEAT: "heating",
CURRENT_HVAC_IDLE: "warming",
HVACAction.HEATING: "heating",
HVACAction.IDLE: "warming",
},
}
HVAC_FAN_MODE_SETS = {
Expand Down
1 change: 1 addition & 0 deletions custom_components/localtuya/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
CONF_BRIGHTNESS_UPPER = "brightness_upper"
CONF_COLOR = "color"
CONF_COLOR_MODE = "color_mode"
CONF_COLOR_MODE_SET = "color_mode_set"
CONF_COLOR_TEMP_MIN_KELVIN = "color_temp_min_kelvin"
CONF_COLOR_TEMP_MAX_KELVIN = "color_temp_max_kelvin"
CONF_COLOR_TEMP_REVERSE = "color_temp_reverse"
Expand Down
10 changes: 7 additions & 3 deletions custom_components/localtuya/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
DIRECTION_FORWARD,
DIRECTION_REVERSE,
DOMAIN,
FanEntity, FanEntityFeature,
FanEntityFeature,
FanEntity,
)
from homeassistant.util.percentage import (
int_states_in_range,
Expand Down Expand Up @@ -186,9 +187,9 @@ async def async_set_direction(self, direction):
self.schedule_update_ha_state()

@property
def supported_features(self) -> int:
def supported_features(self) -> FanEntityFeature:
"""Flag supported features."""
features = 0
features = FanEntityFeature(0)

if self.has_config(CONF_FAN_OSCILLATING_CONTROL):
features |= FanEntityFeature.OSCILLATE
Expand All @@ -199,6 +200,9 @@ def supported_features(self) -> int:
if self.has_config(CONF_FAN_DIRECTION):
features |= FanEntityFeature.DIRECTION

features |= FanEntityFeature.TURN_OFF
features |= FanEntityFeature.TURN_ON

return features

@property
Expand Down
125 changes: 92 additions & 33 deletions custom_components/localtuya/light.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
"""Platform to locally control Tuya-based light devices."""
import logging
import textwrap
from dataclasses import dataclass
from functools import partial

import homeassistant.util.color as color_util
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_EFFECT,
ATTR_HS_COLOR,
DOMAIN,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT,
LightEntity,
LightEntityFeature,
ColorMode,
)
from homeassistant.const import CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_SCENE

Expand All @@ -28,7 +26,7 @@
CONF_COLOR_TEMP_MAX_KELVIN,
CONF_COLOR_TEMP_MIN_KELVIN,
CONF_COLOR_TEMP_REVERSE,
CONF_MUSIC_MODE,
CONF_MUSIC_MODE, CONF_COLOR_MODE_SET,
)

_LOGGER = logging.getLogger(__name__)
Expand All @@ -41,6 +39,7 @@
DEFAULT_LOWER_BRIGHTNESS = 29
DEFAULT_UPPER_BRIGHTNESS = 1000

MODE_MANUAL = "manual"
MODE_COLOR = "colour"
MODE_MUSIC = "music"
MODE_SCENE = "scene"
Expand All @@ -49,6 +48,8 @@
SCENE_CUSTOM = "Custom"
SCENE_MUSIC = "Music"

MODES_SET = {"Colour, Music, Scene and White": 0, "Manual, Music, Scene and White": 1}

SCENE_LIST_RGBW_1000 = {
"Night": "000e0d0000000000000000c80000",
"Read": "010e0d0000000000000003e801f4",
Expand Down Expand Up @@ -91,6 +92,22 @@
+ "0000000",
}

@dataclass(frozen=True)
class Mode:
color: str = MODE_COLOR
music: str = MODE_MUSIC
scene: str = MODE_SCENE
white: str = MODE_WHITE

def as_list(self) -> list:
return [self.color, self.music, self.scene, self.white]

def as_dict(self) -> dict[str, str]:
default = {"Default": self.white}
return {**default, "Mode Color": self.color, "Mode Scene": self.scene}

MAP_MODE_SET = {0: Mode(), 1: Mode(color=MODE_MANUAL)}


def map_range(value, from_lower, from_upper, to_lower, to_upper):
"""Map a value in one range to another."""
Expand Down Expand Up @@ -162,10 +179,12 @@ def __init__(
self._color_temp_reverse = self._config.get(
CONF_COLOR_TEMP_REVERSE, DEFAULT_COLOR_TEMP_REVERSE
)
self._modes = MAP_MODE_SET[int(self._config.get(CONF_COLOR_MODE_SET, 0))]
self._hs = None
self._effect = None
self._effect_list = []
self._scenes = None
self._scenes = {}

if self.has_config(CONF_SCENE):
if self._config.get(CONF_SCENE) < 20:
self._scenes = SCENE_LIST_RGBW_255
Expand All @@ -174,6 +193,7 @@ def __init__(
else:
self._scenes = SCENE_LIST_RGBW_1000
self._effect_list = list(self._scenes.keys())

if self._config.get(CONF_MUSIC_MODE):
self._effect_list.append(SCENE_MUSIC)

Expand All @@ -197,8 +217,8 @@ def hs_color(self):
if self.is_color_mode:
return self._hs
if (
self.supported_features & SUPPORT_COLOR
and not self.supported_features & SUPPORT_COLOR_TEMP
ColorMode.HS in self.supported_color_modes
and not ColorMode.COLOR_TEMP in self.supported_color_modes
):
return [0, 0]
return None
Expand Down Expand Up @@ -241,45 +261,76 @@ def effect(self):
@property
def effect_list(self):
"""Return the list of supported effects for this light."""
return self._effect_list
if self.is_scene_mode or self.is_music_mode:
return self._effect
elif (color_mode := self.__get_color_mode()) in self._scenes.values():
return self.__find_scene_by_scene_data(color_mode)
return None

@property
def supported_features(self):
"""Flag supported features."""
supports = 0
if self.has_config(CONF_BRIGHTNESS):
supports |= SUPPORT_BRIGHTNESS
def supported_color_modes(self) -> set[ColorMode] | set[str] | None:
"""Flag supported color modes."""
color_modes: set[ColorMode] = set()

if self.has_config(CONF_COLOR_TEMP):
supports |= SUPPORT_COLOR_TEMP
color_modes.add(ColorMode.COLOR_TEMP)
if self.has_config(CONF_COLOR):
supports |= SUPPORT_COLOR | SUPPORT_BRIGHTNESS
color_modes.add(ColorMode.HS)

if not color_modes and self.has_config(CONF_BRIGHTNESS):
return {ColorMode.BRIGHTNESS}

if not color_modes:
return {ColorMode.ONOFF}

return color_modes

@property
def supported_features(self) -> LightEntityFeature:
"""Flag supported features."""
supports = LightEntityFeature(0)
if self.has_config(CONF_SCENE) or self.has_config(CONF_MUSIC_MODE):
supports |= SUPPORT_EFFECT
supports |= LightEntityFeature.EFFECT
return supports

@property
def color_mode(self) -> ColorMode:
"""Return the color_mode of the light."""
if len(self.supported_color_modes) == 1:
return next(iter(self.supported_color_modes))

if self.is_color_mode:
return ColorMode.HS
if self.is_white_mode:
return ColorMode.COLOR_TEMP
if self._brightness:
return ColorMode.BRIGHTNESS

return ColorMode.ONOFF

@property
def is_white_mode(self):
"""Return true if the light is in white mode."""
color_mode = self.__get_color_mode()
return color_mode is None or color_mode == MODE_WHITE
return color_mode is None or color_mode == self._modes.white

@property
def is_color_mode(self):
"""Return true if the light is in color mode."""
color_mode = self.__get_color_mode()
return color_mode is not None and color_mode == MODE_COLOR
return color_mode is not None and color_mode == self._modes.color

@property
def is_scene_mode(self):
"""Return true if the light is in scene mode."""
color_mode = self.__get_color_mode()
return color_mode is not None and color_mode.startswith(MODE_SCENE)
return color_mode is not None and color_mode.startswith(self._modes.scene)

@property
def is_music_mode(self):
"""Return true if the light is in music mode."""
color_mode = self.__get_color_mode()
return color_mode is not None and color_mode == MODE_MUSIC
return color_mode is not None and color_mode == self._modes.music

def __is_color_rgb_encoded(self):
return len(self.dps_conf(CONF_COLOR)) > 12
Expand All @@ -294,7 +345,7 @@ def __get_color_mode(self):
return (
self.dps_conf(CONF_COLOR_MODE)
if self.has_config(CONF_COLOR_MODE)
else MODE_WHITE
else self._modes.white
)

async def async_turn_on(self, **kwargs):
Expand All @@ -304,7 +355,7 @@ async def async_turn_on(self, **kwargs):
states[self._dp_id] = True
features = self.supported_features
brightness = None
if ATTR_EFFECT in kwargs and (features & SUPPORT_EFFECT):
if ATTR_EFFECT in kwargs and (features & LightEntityFeature.EFFECT):
scene = self._scenes.get(kwargs[ATTR_EFFECT])
if scene is not None:
if scene.startswith(MODE_SCENE):
Expand All @@ -315,7 +366,11 @@ async def async_turn_on(self, **kwargs):
elif kwargs[ATTR_EFFECT] == SCENE_MUSIC:
states[self._config.get(CONF_COLOR_MODE)] = MODE_MUSIC

if ATTR_BRIGHTNESS in kwargs and (features & SUPPORT_BRIGHTNESS):
if ATTR_BRIGHTNESS in kwargs and (
ColorMode.BRIGHTNESS in self.supported_color_modes
or self.has_config(CONF_BRIGHTNESS)
or self.has_config(CONF_COLOR)
):
brightness = map_range(
int(kwargs[ATTR_BRIGHTNESS]),
0,
Expand Down Expand Up @@ -347,7 +402,7 @@ async def async_turn_on(self, **kwargs):
states[self._config.get(CONF_COLOR)] = color
states[self._config.get(CONF_COLOR_MODE)] = MODE_COLOR

if ATTR_HS_COLOR in kwargs and (features & SUPPORT_COLOR):
if ATTR_HS_COLOR in kwargs and ColorMode.HS in self.supported_color_modes:
if brightness is None:
brightness = self._brightness
hs = kwargs[ATTR_HS_COLOR]
Expand All @@ -374,10 +429,10 @@ async def async_turn_on(self, **kwargs):
states[self._config.get(CONF_COLOR)] = color
states[self._config.get(CONF_COLOR_MODE)] = MODE_COLOR

if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP):
if ColorMode.COLOR_TEMP in kwargs and ColorMode.COLOR_TEMP in self.supported_color_modes:
if brightness is None:
brightness = self._brightness
mired = int(kwargs[ATTR_COLOR_TEMP])
mired = int(kwargs[ColorMode.COLOR_TEMP])
if self._color_temp_reverse:
mired = self._max_mired - (mired - self._min_mired)
if mired < self._min_mired:
Expand All @@ -403,10 +458,14 @@ def status_updated(self):
self._state = self.dps(self._dp_id)
supported = self.supported_features
self._effect = None
if supported & SUPPORT_BRIGHTNESS and self.has_config(CONF_BRIGHTNESS):

if (ColorMode.BRIGHTNESS in self.supported_color_modes
or self.has_config(CONF_BRIGHTNESS)
or self.has_config(CONF_COLOR)
):
self._brightness = self.dps_conf(CONF_BRIGHTNESS)

if supported & SUPPORT_COLOR:
if ColorMode.HS in self.supported_color_modes:
color = self.dps_conf(CONF_COLOR)
if color is not None and not self.is_white_mode:
if self.__is_color_rgb_encoded():
Expand All @@ -422,10 +481,10 @@ def status_updated(self):
self._hs = [hue, sat / 10.0]
self._brightness = value

if supported & SUPPORT_COLOR_TEMP:
if ColorMode.COLOR_TEMP in self.supported_color_modes:
self._color_temp = self.dps_conf(CONF_COLOR_TEMP)

if self.is_scene_mode and supported & SUPPORT_EFFECT:
if self.is_scene_mode and supported & LightEntityFeature.EFFECT:
if self.dps_conf(CONF_COLOR_MODE) != MODE_SCENE:
self._effect = self.__find_scene_by_scene_data(
self.dps_conf(CONF_COLOR_MODE)
Expand All @@ -440,7 +499,7 @@ def status_updated(self):
elif SCENE_CUSTOM in self._effect_list:
self._effect_list.remove(SCENE_CUSTOM)

if self.is_music_mode and supported & SUPPORT_EFFECT:
if self.is_music_mode and supported & LightEntityFeature.EFFECT:
self._effect = SCENE_MUSIC


Expand Down

0 comments on commit 27dbb57

Please sign in to comment.