diff --git a/README.md b/README.md
index 7f1c25f8d..e54de7d20 100644
--- a/README.md
+++ b/README.md
@@ -155,6 +155,32 @@ You can obtain Energy monitoring (voltage, current) in two different ways:
unit_of_measurement: 'W'
```
+# Climates
+
+There are a multitude of Tuya based climates out there, both heaters,
+thermostats and ACs. The all seems to be integrated in different ways and it's
+hard to find a common DP mapping. Below are a table of DP to product mapping
+which are currently seen working. Use it as a guide for your own mapping and
+please contribute to the list if you have the possibility.
+
+| DP | Moes BHT 002 | Qlima WMS S + SC52 (AB;AF) | Avatto |
+|-----|---------------------------------------------------------|---------------------------------------------------------|--------------------------------------------|
+| 1 | ID: On/Off
{true, false} | ID: On/Off
{true, false} | ID: On/Off
{true, false} |
+| 2 | Target temperature
Integer, scaling: 0.5 | Target temperature
Integer, scaling 1 | Target temperature
Integer, scaling 1 |
+| 3 | Current temperature
Integer, scaling: 0.5 | Current temperature
Integer, scaling: 1 | Current temperature
Integer, scaling: 1 |
+| 4 | Mode
{0, 1} | Mode
{"hot", "wind", "wet", "cold", "auto"} | ? |
+| 5 | Eco mode
? | Fan mode
{"strong", "high", "middle", "low", "auto"} | ? |
+| 15 | Not supported | Supported, unknown
{true, false} | ? |
+| 19 | Not supported | Temperature unit
{"c", "f"} | ? |
+| 23 | Not supported | Supported, unknown
Integer, eg. 68 | ? |
+| 24 | Not supported | Supported, unknown
Integer, eg. 64 | ? |
+| 101 | Not supported | Outdoor temperature
Integer. Scaling: 1 | ? |
+| 102 | Temperature of external sensor
Integer, scaling: 0.5 | Supported, unknown
Integer, eg. 34 | ? |
+| 104 | Supported, unknown
{true, false(?)} | Not supported | ? |
+
+[Moes BHT 002](https://community.home-assistant.io/t/moes-bht-002-thermostat-local-control-tuya-based/151953/47)
+[Avatto thermostat](https://pl.aliexpress.com/item/1005001605377377.html?gatewayAdapt=glo2pol)
+
# Debugging
Whenever you write a bug report, it helps tremendously if you include debug logs directly (otherwise we will just ask for them and it will take longer). So please enable debug logs like this and include them in your issue:
diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py
index 78a944307..b126c2fd5 100644
--- a/custom_components/localtuya/__init__.py
+++ b/custom_components/localtuya/__init__.py
@@ -139,15 +139,16 @@ def _device_discovered(device):
)
new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000))
hass.config_entries.async_update_entry(entry, data=new_data)
- device = hass.data[DOMAIN][TUYA_DEVICES][device_id]
- if not device.connected:
- device.async_connect()
+
elif device_id in hass.data[DOMAIN][TUYA_DEVICES]:
- # _LOGGER.debug("Device %s found with IP %s", device_id, device_ip)
+ _LOGGER.debug("Device %s found with IP %s", device_id, device_ip)
+
+ device = hass.data[DOMAIN][TUYA_DEVICES].get(device_id)
+ if not device:
+ _LOGGER.warning(f"Could not find device for device_id {device_id}")
+ elif not device.connected:
+ device.async_connect()
- device = hass.data[DOMAIN][TUYA_DEVICES][device_id]
- if not device.connected:
- device.async_connect()
def _shutdown(event):
"""Clean up resources when shutting down."""
diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py
index 76675d38c..da23cfaf9 100644
--- a/custom_components/localtuya/climate.py
+++ b/custom_components/localtuya/climate.py
@@ -11,16 +11,20 @@
ClimateEntity,
)
from homeassistant.components.climate.const import (
- CURRENT_HVAC_HEAT,
- CURRENT_HVAC_IDLE,
- HVAC_MODE_AUTO,
- HVAC_MODE_HEAT,
- HVAC_MODE_OFF,
+ HVACAction,
+ HVACMode,
PRESET_AWAY,
PRESET_ECO,
PRESET_HOME,
PRESET_NONE,
ClimateEntityFeature,
+ FAN_AUTO,
+ FAN_LOW,
+ FAN_MEDIUM,
+ FAN_HIGH,
+ FAN_TOP,
+ SWING_ON,
+ SWING_OFF,
)
from homeassistant.const import (
ATTR_TEMPERATURE,
@@ -34,6 +38,8 @@
from .common import LocalTuyaEntity, async_setup_entry
from .const import (
CONF_CURRENT_TEMPERATURE_DP,
+ CONF_TEMP_MAX,
+ CONF_TEMP_MIN,
CONF_ECO_DP,
CONF_ECO_VALUE,
CONF_HEURISTIC_ACTION,
@@ -49,53 +55,79 @@
CONF_TARGET_PRECISION,
CONF_TARGET_TEMPERATURE_DP,
CONF_TEMPERATURE_STEP,
+ CONF_HVAC_FAN_MODE_DP,
+ CONF_HVAC_FAN_MODE_SET,
+ CONF_HVAC_SWING_MODE_DP,
+ CONF_HVAC_SWING_MODE_SET,
)
_LOGGER = logging.getLogger(__name__)
HVAC_MODE_SETS = {
"manual/auto": {
- HVAC_MODE_HEAT: "manual",
- HVAC_MODE_AUTO: "auto",
+ HVACMode.HEAT: "manual",
+ HVACMode.AUTO: "auto",
},
"Manual/Auto": {
- HVAC_MODE_HEAT: "Manual",
- HVAC_MODE_AUTO: "Auto",
+ HVACMode.HEAT: "Manual",
+ HVACMode.AUTO: "Auto",
},
"Manual/Program": {
- HVAC_MODE_HEAT: "Manual",
- HVAC_MODE_AUTO: "Program",
+ HVACMode.HEAT: "Manual",
+ HVACMode.AUTO: "Program",
},
"m/p": {
- HVAC_MODE_HEAT: "m",
- HVAC_MODE_AUTO: "p",
+ HVACMode.HEAT: "m",
+ HVACMode.AUTO: "p",
},
"True/False": {
- HVAC_MODE_HEAT: True,
+ HVACMode.HEAT: True,
+ },
+ "Auto/Cold/Dry/Wind/Hot": {
+ HVACMode.HEAT: "hot",
+ HVACMode.FAN_ONLY: "wind",
+ HVACMode.DRY: "wet",
+ HVACMode.COOL: "cold",
+ HVACMode.AUTO: "auto",
},
"1/0": {
- HVAC_MODE_HEAT: "1",
- HVAC_MODE_AUTO: "0",
+ HVACMode.HEAT: "1",
+ HVACMode.AUTO: "0",
},
}
HVAC_ACTION_SETS = {
"True/False": {
- CURRENT_HVAC_HEAT: True,
- CURRENT_HVAC_IDLE: False,
+ HVACAction.HEATING: True,
+ HVACAction.IDLE: False,
},
"open/close": {
- CURRENT_HVAC_HEAT: "open",
- CURRENT_HVAC_IDLE: "close",
+ HVACAction.HEATING: "open",
+ HVACAction.IDLE: "close",
},
"heating/no_heating": {
- CURRENT_HVAC_HEAT: "heating",
- CURRENT_HVAC_IDLE: "no_heating",
+ HVACAction.HEATING: "heating",
+ HVACAction.IDLE: "no_heating",
},
"Heat/Warming": {
- CURRENT_HVAC_HEAT: "Heat",
- CURRENT_HVAC_IDLE: "Warming",
+ HVACAction.HEATING: "Heat",
+ HVACAction.IDLE: "Warming",
},
}
+HVAC_FAN_MODE_SETS = {
+ "Auto/Low/Middle/High/Strong": {
+ FAN_AUTO: "auto",
+ FAN_LOW: "low",
+ FAN_MEDIUM: "middle",
+ FAN_HIGH: "high",
+ FAN_TOP: "strong",
+ }
+}
+HVAC_SWING_MODE_SETS = {
+ "True/False": {
+ SWING_ON: True,
+ SWING_OFF: False,
+ }
+}
PRESET_SETS = {
"Manual/Holiday/Program": {
PRESET_AWAY: "Holiday",
@@ -118,16 +150,20 @@ def flow_schema(dps):
return {
vol.Optional(CONF_TARGET_TEMPERATURE_DP): vol.In(dps),
vol.Optional(CONF_CURRENT_TEMPERATURE_DP): vol.In(dps),
- vol.Optional(CONF_TEMPERATURE_STEP): vol.In(
+ vol.Optional(CONF_TEMPERATURE_STEP, default=PRECISION_WHOLE): vol.In(
[PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS]
),
+ vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
+ vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP_DP): vol.In(dps),
vol.Optional(CONF_MIN_TEMP_DP): vol.In(dps),
- vol.Optional(CONF_PRECISION): vol.In(
+ vol.Optional(CONF_PRECISION, default=PRECISION_WHOLE): vol.In(
[PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS]
),
vol.Optional(CONF_HVAC_MODE_DP): vol.In(dps),
vol.Optional(CONF_HVAC_MODE_SET): vol.In(list(HVAC_MODE_SETS.keys())),
+ vol.Optional(CONF_HVAC_FAN_MODE_DP): vol.In(dps),
+ vol.Optional(CONF_HVAC_FAN_MODE_SET): vol.In(list(HVAC_FAN_MODE_SETS.keys())),
vol.Optional(CONF_HVAC_ACTION_DP): vol.In(dps),
vol.Optional(CONF_HVAC_ACTION_SET): vol.In(list(HVAC_ACTION_SETS.keys())),
vol.Optional(CONF_ECO_DP): vol.In(dps),
@@ -137,7 +173,7 @@ def flow_schema(dps):
vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(
[TEMPERATURE_CELSIUS, TEMPERATURE_FAHRENHEIT]
),
- vol.Optional(CONF_TARGET_PRECISION): vol.In(
+ vol.Optional(CONF_TARGET_PRECISION, default=PRECISION_WHOLE): vol.In(
[PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS]
),
vol.Optional(CONF_HEURISTIC_ACTION): bool,
@@ -160,6 +196,8 @@ def __init__(
self._target_temperature = None
self._current_temperature = None
self._hvac_mode = None
+ self._fan_mode = None
+ self._swing_mode = None
self._preset_mode = None
self._hvac_action = None
self._precision = self._config.get(CONF_PRECISION, DEFAULT_PRECISION)
@@ -170,6 +208,14 @@ def __init__(
self._conf_hvac_mode_set = HVAC_MODE_SETS.get(
self._config.get(CONF_HVAC_MODE_SET), {}
)
+ self._conf_hvac_fan_mode_dp = self._config.get(CONF_HVAC_FAN_MODE_DP)
+ self._conf_hvac_fan_mode_set = HVAC_FAN_MODE_SETS.get(
+ self._config.get(CONF_HVAC_FAN_MODE_SET), {}
+ )
+ self._conf_hvac_swing_mode_dp = self._config.get(CONF_HVAC_SWING_MODE_DP)
+ self._conf_hvac_swing_mode_set = HVAC_SWING_MODE_SETS.get(
+ self._config.get(CONF_HVAC_SWING_MODE_SET), {}
+ )
self._conf_preset_dp = self._config.get(CONF_PRESET_DP)
self._conf_preset_set = PRESET_SETS.get(self._config.get(CONF_PRESET_SET), {})
self._conf_hvac_action_dp = self._config.get(CONF_HVAC_ACTION_DP)
@@ -186,13 +232,17 @@ def __init__(
@property
def supported_features(self):
"""Flag supported features."""
- supported_features = 0
+ supported_features = ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
if self.has_config(CONF_TARGET_TEMPERATURE_DP):
supported_features = supported_features | ClimateEntityFeature.TARGET_TEMPERATURE
if self.has_config(CONF_MAX_TEMP_DP):
supported_features = supported_features | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
if self.has_config(CONF_PRESET_DP) or self.has_config(CONF_ECO_DP):
supported_features = supported_features | ClimateEntityFeature.PRESET_MODE
+ if self.has_config(CONF_HVAC_FAN_MODE_DP) and self.has_config(CONF_HVAC_FAN_MODE_SET):
+ supported_features = supported_features | ClimateEntityFeature.FAN_MODE
+ if self.has_config(CONF_HVAC_SWING_MODE_DP):
+ supported_features = supported_features | ClimateEntityFeature.SWING_MODE
return supported_features
@property
@@ -225,7 +275,7 @@ def hvac_modes(self):
"""Return the list of available operation modes."""
if not self.has_config(CONF_HVAC_MODE_DP):
return None
- return list(self._conf_hvac_mode_set) + [HVAC_MODE_OFF]
+ return list(self._conf_hvac_mode_set) + [HVACMode.OFF]
@property
def hvac_action(self):
@@ -234,22 +284,22 @@ def hvac_action(self):
Need to be one of CURRENT_HVAC_*.
"""
if self._config.get(CONF_HEURISTIC_ACTION, False):
- if self._hvac_mode == HVAC_MODE_HEAT:
+ if self._hvac_mode == HVACMode.HEAT:
if self._current_temperature < (
self._target_temperature - self._precision
):
- self._hvac_action = CURRENT_HVAC_HEAT
+ self._hvac_action = HVACAction.HEATING
if self._current_temperature == (
self._target_temperature - self._precision
):
- if self._hvac_action == CURRENT_HVAC_HEAT:
- self._hvac_action = CURRENT_HVAC_HEAT
- if self._hvac_action == CURRENT_HVAC_IDLE:
- self._hvac_action = CURRENT_HVAC_IDLE
+ if self._hvac_action == HVACAction.HEATING:
+ self._hvac_action = HVACAction.HEATING
+ if self._hvac_action == HVACAction.IDLE:
+ self._hvac_action = HVACAction.IDLE
if (
self._current_temperature + self._precision
) > self._target_temperature:
- self._hvac_action = CURRENT_HVAC_IDLE
+ self._hvac_action = HVACAction.IDLE
return self._hvac_action
return self._hvac_action
@@ -286,12 +336,26 @@ def target_temperature_step(self):
@property
def fan_mode(self):
"""Return the fan setting."""
- return NotImplementedError()
+ return self._fan_mode
@property
def fan_modes(self):
"""Return the list of available fan modes."""
- return NotImplementedError()
+ if not self.has_config(CONF_HVAC_FAN_MODE_DP):
+ return None
+ return list(self._conf_hvac_fan_mode_set)
+
+ @property
+ def swing_mode(self):
+ """Return the swing setting."""
+ return self._swing_mode
+
+ @property
+ def swing_modes(self):
+ """Return the list of available swing modes."""
+ if not self.has_config(CONF_HVAC_SWING_MODE_DP):
+ return None
+ return list(self._conf_hvac_swing_mode_set)
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
@@ -301,13 +365,21 @@ 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 self._conf_hvac_fan_mode_dp is None:
+ _LOGGER.error("Fan speed unsupported (no DP)")
+ return
+ if fan_mode not in self._conf_hvac_fan_mode_set:
+ _LOGGER.error("Unsupported fan_mode: %s" % fan_mode)
+ return
+ await self._device.set_dp(
+ self._conf_hvac_fan_mode_set[fan_mode], self._conf_hvac_fan_mode_dp
+ )
async def async_set_hvac_mode(self, hvac_mode):
"""Set new target operation mode."""
- if hvac_mode == HVAC_MODE_OFF:
+ 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:
@@ -318,6 +390,18 @@ async def async_set_hvac_mode(self, hvac_mode):
self._conf_hvac_mode_set[hvac_mode], self._conf_hvac_mode_dp
)
+ async def async_set_swing_mode(self, swing_mode):
+ """Set new target swing operation."""
+ if self._conf_hvac_swing_mode_dp is None:
+ _LOGGER.error("Swing mode unsupported (no DP)")
+ return
+ if swing_mode not in self._conf_hvac_swing_mode_set:
+ _LOGGER.error("Unsupported swing_mode: %s" % swing_mode)
+ return
+ await self._device.set_dp(
+ self._conf_hvac_swing_mode_set[swing_mode], self._conf_hvac_swing_mode_dp
+ )
+
async def async_turn_on(self) -> None:
"""Turn the entity on."""
await self._device.set_dp(True, self._dp_id)
@@ -340,22 +424,14 @@ def min_temp(self):
"""Return the minimum temperature."""
if self.has_config(CONF_MIN_TEMP_DP):
return self.dps_conf(CONF_MIN_TEMP_DP)
- # DEFAULT_MIN_TEMP is in C
- if self.temperature_unit == UnitOfTemperature.FAHRENHEIT:
- return DEFAULT_MIN_TEMP * 1.8 + 32
- else:
- return DEFAULT_MIN_TEMP
+ return self._config[CONF_TEMP_MIN]
@property
def max_temp(self):
"""Return the maximum temperature."""
if self.has_config(CONF_MAX_TEMP_DP):
return self.dps_conf(CONF_MAX_TEMP_DP)
- # DEFAULT_MAX_TEMP is in C
- if self.temperature_unit == UnitOfTemperature.FAHRENHEIT:
- return DEFAULT_MAX_TEMP * 1.8 + 32
- else:
- return DEFAULT_MAX_TEMP
+ return self._config[CONF_TEMP_MAX]
def status_updated(self):
"""Device status was updated."""
@@ -388,7 +464,7 @@ def status_updated(self):
# Update the HVAC status
if self.has_config(CONF_HVAC_MODE_DP):
if not self._state:
- self._hvac_mode = HVAC_MODE_OFF
+ self._hvac_mode = HVACMode.OFF
else:
for mode, value in self._conf_hvac_mode_set.items():
if self.dps_conf(CONF_HVAC_MODE_DP) == value:
@@ -396,7 +472,28 @@ def status_updated(self):
break
else:
# in case hvac mode and preset share the same dp
- self._hvac_mode = HVAC_MODE_AUTO
+ self._hvac_mode = HVACMode.AUTO
+
+ # Update the fan status
+ if self.has_config(CONF_HVAC_FAN_MODE_DP):
+ for mode, value in self._conf_hvac_fan_mode_set.items():
+ if self.dps_conf(CONF_HVAC_FAN_MODE_DP) == value:
+ self._fan_mode = mode
+ break
+ else:
+ # in case fan mode and preset share the same dp
+ _LOGGER.debug("Unknown fan mode %s" % self.dps_conf(CONF_HVAC_FAN_MODE_DP))
+ self._fan_mode = FAN_AUTO
+
+ # Update the swing status
+ if self.has_config(CONF_HVAC_SWING_MODE_DP):
+ for mode, value in self._conf_hvac_swing_mode_set.items():
+ if self.dps_conf(CONF_HVAC_SWING_MODE_DP) == value:
+ self._swing_mode = mode
+ break
+ else:
+ _LOGGER.debug("Unknown swing mode %s" % self.dps_conf(CONF_HVAC_SWING_MODE_DP))
+ self._swing_mode = SWING_OFF
# Update the current action
for action, value in self._conf_hvac_action_set.items():
diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py
index cd503c2af..4ad46f6df 100644
--- a/custom_components/localtuya/common.py
+++ b/custom_components/localtuya/common.py
@@ -124,8 +124,10 @@ def async_config_entry_by_device_id(hass, device_id):
"""Look up config entry by device id."""
current_entries = hass.config_entries.async_entries(DOMAIN)
for entry in current_entries:
- if device_id in entry.data[CONF_DEVICES]:
+ if device_id in entry.data.get(CONF_DEVICES, []):
return entry
+ else:
+ _LOGGER.warning(f"Missing device configuration for device_id {device_id}")
return None
diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py
index 37ac0b0ed..fc0a47683 100644
--- a/custom_components/localtuya/const.py
+++ b/custom_components/localtuya/const.py
@@ -93,10 +93,16 @@
CONF_TEMPERATURE_STEP = "temperature_step"
CONF_MAX_TEMP_DP = "max_temperature_dp"
CONF_MIN_TEMP_DP = "min_temperature_dp"
+CONF_TEMP_MAX = "max_temperature_const"
+CONF_TEMP_MIN = "min_temperature_const"
CONF_PRECISION = "precision"
CONF_TARGET_PRECISION = "target_precision"
CONF_HVAC_MODE_DP = "hvac_mode_dp"
CONF_HVAC_MODE_SET = "hvac_mode_set"
+CONF_HVAC_FAN_MODE_DP = "hvac_fan_mode_dp"
+CONF_HVAC_FAN_MODE_SET = "hvac_fan_mode_set"
+CONF_HVAC_SWING_MODE_DP = "hvac_swing_mode_dp"
+CONF_HVAC_SWING_MODE_SET = "hvac_swing_mode_set"
CONF_PRESET_DP = "preset_dp"
CONF_PRESET_SET = "preset_set"
CONF_HEURISTIC_ACTION = "heuristic_action"
diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json
index d420d5970..efbc2e64a 100644
--- a/custom_components/localtuya/translations/en.json
+++ b/custom_components/localtuya/translations/en.json
@@ -72,7 +72,13 @@
"title": "Edit a new device",
"description": "Pick the configured device you wish to edit.",
"data": {
- "selected_device": "Configured Devices"
+ "selected_device": "Configured Devices",
+ "max_temperature_const": "Max Temperature Constant (optional)",
+ "min_temperature_const": "Min Temperature Constant (optional)",
+ "hvac_fan_mode_dp": "HVAC Fan Mode DP (optional)",
+ "hvac_fan_mode_set": "HVAC Fan Mode Set (optional)",
+ "hvac_swing_mode_dp": "HVAC Swing Mode DP (optional)",
+ "hvac_swing_mode_set": "HVAC Swing Mode Set (optional)"
}
},
"cloud_setup": {
@@ -176,13 +182,19 @@
"current_temperature_dp": "Current Temperature",
"target_temperature_dp": "Target Temperature",
"temperature_step": "Temperature Step (optional)",
- "max_temperature_dp": "Max Temperature (optional)",
- "min_temperature_dp": "Min Temperature (optional)",
+ "max_temperature_dp": "Max Temperature DP (optional)",
+ "min_temperature_dp": "Min Temperature DP (optional)",
+ "max_temperature_const": "Max Temperature Constant (optional)",
+ "min_temperature_const": "Min Temperature Constant (optional)",
"precision": "Precision (optional, for DPs values)",
"target_precision": "Target Precision (optional, for DPs values)",
"temperature_unit": "Temperature Unit (optional)",
"hvac_mode_dp": "HVAC Mode DP (optional)",
"hvac_mode_set": "HVAC Mode Set (optional)",
+ "hvac_fan_mode_dp": "HVAC Fan Mode DP (optional)",
+ "hvac_fan_mode_set": "HVAC Fan Mode Set (optional)",
+ "hvac_swing_mode_dp": "HVAC Swing Mode DP (optional)",
+ "hvac_swing_mode_set": "HVAC Swing Mode Set (optional)",
"hvac_action_dp": "HVAC Current Action DP (optional)",
"hvac_action_set": "HVAC Current Action Set (optional)",
"preset_dp": "Presets DP (optional)",