Skip to content

Commit

Permalink
Refactor Climate entity and auto configure fixes.
Browse files Browse the repository at this point in the history
* Auto configure: Fix HVAC Modes.
* Add FAN Support.
* Unsupported HVAC Modes will automatically added as preset.
  • Loading branch information
xZetsubou committed Feb 21, 2024
1 parent 75e8b3a commit b82cd16
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 55 deletions.
127 changes: 88 additions & 39 deletions custom_components/localtuya/climate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Platform to locally control Tuya-based climate devices.
# PRESETS and HVAC_MODE Needs to be handle in better way.
"""

import asyncio
import logging
from functools import partial
Expand Down Expand Up @@ -51,6 +52,8 @@
CONF_MIN_TEMP,
CONF_MAX_TEMP,
CONF_HVAC_ADD_OFF,
CONF_FAN_SPEED_DP,
CONF_FAN_SPEED_LIST,
)

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -106,6 +109,8 @@
# Empirically tested to work for AVATTO thermostat
MODE_WAIT = 0.1

FAN_SPEEDS_DEFAULT = "auto,low,middle,high"


def flow_schema(dps):
"""Return schema used in config flow."""
Expand All @@ -132,6 +137,8 @@ def flow_schema(dps):
vol.Optional(CONF_ECO_VALUE): str,
vol.Optional(CONF_PRESET_DP): _col_to_select(dps, is_dps=True),
vol.Optional(CONF_PRESET_SET, default={}): selector.ObjectSelector(),
vol.Optional(CONF_FAN_SPEED_DP): _col_to_select(dps, is_dps=True),
vol.Optional(CONF_FAN_SPEED_LIST, default=FAN_SPEEDS_DEFAULT): str,
vol.Optional(CONF_TEMPERATURE_UNIT): _col_to_select(
[TEMPERATURE_CELSIUS, TEMPERATURE_FAHRENHEIT]
),
Expand Down Expand Up @@ -160,31 +167,55 @@ def __init__(
self._preset_mode = None
self._hvac_action = None
self._precision = float(self._config.get(CONF_PRECISION, DEFAULT_PRECISION))
self._conf_hvac_mode_dp = self._config.get(CONF_HVAC_MODE_DP)

# HVAC Modes
self._hvac_mode_dp = self._config.get(CONF_HVAC_MODE_DP)
if modes_set := self._config.get(CONF_HVAC_MODE_SET, {}):
# HA HVAC Modes are all lower case.
modes_set = {k.lower(): v for k, v in modes_set.copy().items()}
self._conf_hvac_mode_set = modes_set
self._conf_preset_dp = self._config.get(CONF_PRESET_DP)
self._conf_preset_set: dict = self._config.get(CONF_PRESET_SET, {})
self._hvac_mode_set = modes_set

# Presets
self._preset_dp = self._config.get(CONF_PRESET_DP)
self._preset_set: dict = self._config.get(CONF_PRESET_SET, {})

# Sort Modes If the HVAC isn't supported by HA then we add it as preset.
if self._preset_dp == self._hvac_mode_dp or not self._preset_dp:
for k, v in self._hvac_mode_set.copy().items():
if k not in HVACMode:
self._preset_dp = self._hvac_mode_dp
self._preset_set[k] = self._hvac_mode_set.pop(k)

self._preset_name_to_value = {v: k for k, v in self._preset_set.items()}

# HVAC Actions
self._conf_hvac_action_dp = self._config.get(CONF_HVAC_ACTION_DP)
if actions_set := self._config.get(CONF_HVAC_ACTION_SET, {}):
actions_set = {k.lower(): v for k, v in actions_set.copy().items()}
self._conf_hvac_action_set = actions_set
self._conf_eco_dp = self._config.get(CONF_ECO_DP)
self._conf_eco_value = self._config.get(CONF_ECO_VALUE, "ECO")
self._has_presets = self.has_config(CONF_ECO_DP) or self.has_config(
CONF_PRESET_DP
)

# Fan
self._fan_speed_dp = self._config.get(CONF_FAN_SPEED_DP)
if fan_speeds := self._config.get(CONF_FAN_SPEED_LIST, []):
fan_speeds = [v.lstrip() for v in fan_speeds.split(",")]
self._fan_supported_speeds = fan_speeds
self._has_fan_mode = self._fan_speed_dp and self._fan_supported_speeds

# Eco!?
self._eco_dp = self._config.get(CONF_ECO_DP)
self._eco_value = self._config.get(CONF_ECO_VALUE, "ECO")
self._has_presets = self._eco_dp or (self._preset_dp and self._preset_set)

@property
def supported_features(self):
"""Flag supported features."""
supported_features = ClimateEntityFeature(0)
if self.has_config(CONF_TARGET_TEMPERATURE_DP):
supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
if self.has_config(CONF_PRESET_DP) or self.has_config(CONF_ECO_DP):
if self._has_presets:
supported_features |= ClimateEntityFeature.PRESET_MODE
if self._has_fan_mode:
supported_features |= ClimateEntityFeature.FAN_MODE

try: # requires HA >= 2024.2.1
supported_features |= ClimateEntityFeature.TURN_OFF
Expand Down Expand Up @@ -229,17 +260,25 @@ def hvac_modes(self):
"""Return the list of available operation modes."""
if not self.has_config(CONF_HVAC_MODE_DP):
return None
modes = list(self._conf_hvac_mode_set)
modes = list(self._hvac_mode_set)
if self._config.get(CONF_HVAC_ADD_OFF, True) and HVACMode.OFF not in modes:
modes.append(HVACMode.OFF)
return modes

@property
def hvac_action(self):
"""Return the current running hvac operation if supported.
"""Return the current running hvac operation if supported."""
if not self._conf_hvac_action_dp:
if self._hvac_mode == HVACMode.COOL:
self._hvac_action = HVACAction.COOLING
if self._hvac_mode == HVACMode.HEAT:
self._hvac_action = HVACAction.HEATING
if self._hvac_mode == HVACMode.OFF:
self._hvac_action = HVACAction.IDLE
if self._hvac_mode == HVACMode.DRY:
self._hvac_action = HVACAction.DRYING

Need to be one of CURRENT_HVAC_*.
"""
# This exists from upstream, not sure the use case of this.
if self._config.get(CONF_HEURISTIC_ACTION, False):
if self._hvac_mode == HVACMode.HEAT:
if self._current_temperature < (
Expand All @@ -263,15 +302,20 @@ def hvac_action(self):
@property
def preset_mode(self):
"""Return current preset."""
mode = self.dp_value(CONF_HVAC_MODE_DP)
if mode in list(self._hvac_mode_set.values()):
return None

return self._preset_mode

@property
def preset_modes(self):
"""Return the list of available presets modes."""
if not self._has_presets:
return None
presets = list(self._conf_preset_set.values())
if self._conf_eco_dp:

presets = list(self._preset_set.values())
if self._eco_dp:
presets.append(PRESET_ECO)
return presets

Expand All @@ -294,12 +338,14 @@ def target_temperature_step(self):
@property
def fan_mode(self):
"""Return the fan setting."""
return NotImplementedError()
if not (fan_value := self.dp_value(self._fan_speed_dp)):
return None
return fan_value

@property
def fan_modes(self):
def fan_modes(self) -> list:
"""Return the list of available fan modes."""
return NotImplementedError()
return self._fan_supported_speeds

async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
Expand All @@ -309,23 +355,24 @@ async def async_set_temperature(self, **kwargs):
temperature, self._config[CONF_TARGET_TEMPERATURE_DP]
)

def set_fan_mode(self, fan_mode):
async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
return NotImplementedError()
if not self._state:
await self._device.set_dp(True, self._dp_id)

await self._device.set_dp(fan_mode, self._fan_speed_dp)

async def async_set_hvac_mode(self, hvac_mode):
"""Set new target operation mode."""
if hvac_mode == HVACMode.OFF:
await self._device.set_dp(False, self._dp_id)
return
if not self._state and self._conf_hvac_mode_dp != self._dp_id:
if not self._state and self._hvac_mode_dp != self._dp_id:
await self._device.set_dp(True, self._dp_id)
# Some thermostats need a small wait before sending another update
await asyncio.sleep(MODE_WAIT)

await self._device.set_dp(
self._conf_hvac_mode_set[hvac_mode], self._conf_hvac_mode_dp
)
await self._device.set_dp(self._hvac_mode_set[hvac_mode], self._hvac_mode_dp)

async def async_turn_on(self) -> None:
"""Turn the entity on."""
Expand All @@ -338,55 +385,57 @@ async def async_turn_off(self) -> None:
async def async_set_preset_mode(self, preset_mode):
"""Set new target preset mode."""
if preset_mode == PRESET_ECO:
await self._device.set_dp(self._conf_eco_value, self._conf_eco_dp)
await self._device.set_dp(self._eco_value, self._eco_dp)
return
await self._device.set_dp(preset_mode, self._conf_preset_dp)

preset_value = self._preset_name_to_value.get(preset_mode)
await self._device.set_dp(preset_value, self._preset_dp)

def status_updated(self):
"""Device status was updated."""
self._state = self.dp_value(self._dp_id)

# Update target temperature
if self.has_config(CONF_TARGET_TEMPERATURE_DP):
self._target_temperature = (
self.dp_value(CONF_TARGET_TEMPERATURE_DP) * self._precision
)

# Update current temperature
if self.has_config(CONF_CURRENT_TEMPERATURE_DP):
self._current_temperature = (
self.dp_value(CONF_CURRENT_TEMPERATURE_DP) * self._precision
)

# Update preset states
if self._has_presets:
if (
self.has_config(CONF_ECO_DP)
and self.dp_value(CONF_ECO_DP) == self._conf_eco_value
):
if self.dp_value(CONF_ECO_DP) == self._eco_value:
self._preset_mode = PRESET_ECO
else:
for preset_value, preset_name in self._conf_preset_set.items():
for preset_value, preset_name in self._preset_set.items():
if self.dp_value(CONF_PRESET_DP) == preset_value:
self._preset_mode = preset_name
break
else:
self._preset_mode = PRESET_NONE

# Update the HVAC status
# Update the HVAC Mode
if self.has_config(CONF_HVAC_MODE_DP):
if not self._state:
self._hvac_mode = HVACMode.OFF
else:
for mode, value in self._conf_hvac_mode_set.items():
if self.dp_value(CONF_HVAC_MODE_DP) == value:
self._hvac_mode = mode
for ha_hvac, tuya_value in self._hvac_mode_set.items():
if self.dp_value(CONF_HVAC_MODE_DP) == tuya_value:
self._hvac_mode = ha_hvac
break
else:
# in case hvac mode and preset share the same dp
self._hvac_mode = HVACMode.AUTO

# Update the current action
for action, value in self._conf_hvac_action_set.items():
if self.dp_value(CONF_HVAC_ACTION_DP) == value:
self._hvac_action = action
for ha_action, tuya_value in self._conf_hvac_action_set.items():
if self.dp_value(CONF_HVAC_ACTION_DP) == tuya_value:
self._hvac_action = ha_action


async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaClimate, flow_schema)
2 changes: 2 additions & 0 deletions custom_components/localtuya/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Constants for localtuya integration."""

from homeassistant.const import EntityCategory, Platform

DOMAIN = "localtuya"
Expand Down Expand Up @@ -124,6 +125,7 @@
CONF_HVAC_ADD_OFF = "hvac_add_off"
CONF_ECO_DP = "eco_dp"
CONF_ECO_VALUE = "eco_value"
CONF_FAN_SPEED_LIST = "fan_speed_list"

# vacuum
CONF_POWERGO_DP = "powergo_dp"
Expand Down
11 changes: 6 additions & 5 deletions custom_components/localtuya/core/ha_entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,12 @@ def convert_list(_list: list, req_info: CLOUD_VALUE = str):
# Return dict {value_1: Value 1, value_2: Value 2, value_3: Value 3}
to_dict = {}
for k in _list:
k_name = k.replace("_", " ").capitalize() # Default name
if isinstance(req_info.default_value, dict):
k_name = req_info.default_value.get(k, k_name)
if remaped_value := req_info.remap_values.get(k):
k_name = remaped_value
if k in req_info.remap_values:
k_name = req_info.remap_values.get(k)
else:
k_name = k.replace("_", " ").capitalize() # Default name
if isinstance(req_info.default_value, dict):
k_name = req_info.default_value.get(k, k_name)

if req_info.reverse_dict:
to_dict.update({k_name: k})
Expand Down
8 changes: 8 additions & 0 deletions custom_components/localtuya/core/ha_entities/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class DPCode(StrEnum):
ADD_ELE1 = "add_ele1"
ADD_ELE2 = "add_ele2"
AIR_QUALITY = "air_quality"
AIR_RETURN = "air_return"
ALARM_DELAY_TIME = "alarm_delay_time"
ALARM_LOCK = "alarm_lock"
ALARM_MESSAGE = "alarm_message"
Expand All @@ -111,6 +112,7 @@ class DPCode(StrEnum):
ALARM_TIME = "alarm_time" # Alarm time
ALARM_VOLUME = "alarm_volume" # Alarm volume
ALL_ENERGY = "all_energy"
AMBIEN = "ambien"
ANGLE_HORIZONTAL = "angle_horizontal"
ANGLE_VERTICAL = "angle_vertical"
ANION = "anion" # Ionizer unit
Expand Down Expand Up @@ -167,11 +169,13 @@ class DPCode(StrEnum):
CLOUD_RECIPE_NUMBER = "cloud_recipe_number"
CO2_STATE = "co2_state"
CO2_VALUE = "co2_value" # CO2 concentration
COIL_OUT = "coil_out"
COLLECTION_MODE = "collection_mode"
COLOR_DATA_V2 = "color_data_v2"
COLOUR_DATA = "colour_data" # Colored light mode
COLOUR_DATA_HSV = "colour_data_hsv" # Colored light mode
COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode
COMPRESSOR_COMMAND = "compressor_command"
CONCENTRATION_SET = "concentration_set" # Concentration setting
CONTROL = "control"
CONTROL_2 = "control_2"
Expand Down Expand Up @@ -254,6 +258,7 @@ class DPCode(StrEnum):
FLOODLIGHT_LIGHTNESS = "floodlight_lightness"
FLOODLIGHT_SWITCH = "floodlight_switch"
FORWARD_ENERGY_TOTAL = "forward_energy_total"
FOUT_WAY_VALVE = "fout_way_valve"
GAS_SENSOR_STATE = "gas_sensor_state"
GAS_SENSOR_STATUS = "gas_sensor_status"
GAS_SENSOR_VALUE = "gas_sensor_value"
Expand All @@ -265,6 +270,7 @@ class DPCode(StrEnum):
HUMIDITY_INDOOR = "humidity_indoor" # Indoor humidity
HUMIDITY_SET = "humidity_set" # Humidity setting
HUMIDITY_VALUE = "humidity_value" # Humidity
IDU_ERROR = "idu_error"
IPC_WORK_MODE = "ipc_work_mode"
LED_TYPE_1 = "led_type_1"
LED_TYPE_2 = "led_type_2"
Expand Down Expand Up @@ -307,6 +313,7 @@ class DPCode(StrEnum):
NEAR_DETECTION = "near_detection"
NET_STATE = "net_state"
NORMAL_OPEN_SWITCH = "normal_open_switch"
ODU_FAN_SPEED = "odu_fan_speed"
OPPOSITE = "opposite"
OPTIMUMSTART = "optimumstart"
OTHEREVENT = "OtherEvent"
Expand Down Expand Up @@ -375,6 +382,7 @@ class DPCode(StrEnum):
RESET_MAP = "reset_map"
RESET_ROLL_BRUSH = "reset_roll_brush"
ROLL_BRUSH = "roll_brush"
RUNNING_FAN_SPEED = "running_fan_speed"
SCENE_1 = "scene_1"
SCENE_10 = "scene_10"
SCENE_11 = "scene_11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ def localtuya_binarySensor(state_on="1"):
entity_category=EntityCategory.DIAGNOSTIC,
custom_configs=ON_1,
),
LocalTuyaEntity(
id=DPCode.IDU_ERROR,
name="IDU Error",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
custom_configs=ON_1,
),
# CZ - Energy monitor?
LocalTuyaEntity(
id=DPCode.POWER_TYPE,
Expand Down Expand Up @@ -412,3 +419,4 @@ def localtuya_binarySensor(state_on="1"):
BINARY_SENSORS["cz"] = FAULT_SENSOR
BINARY_SENSORS["cs"] = FAULT_SENSOR
BINARY_SENSORS["jsq"] = FAULT_SENSOR
BINARY_SENSORS["kt"] = FAULT_SENSOR
Loading

0 comments on commit b82cd16

Please sign in to comment.