So now you are able to get the keys from tyua developer interface, that’s great! It makes local tuya much easier to setup.
At one point the only way to get them was to run a buggy old version of the tuya android app in an emulator on a PC.
I’m using local tuya for a couple of power measuring sockets, should probably move the thermostats over as well.
Hi Yozen,
I have the same problem of values of the temperatures in my system.
I run HA on Respberry PI3 B (Raspberry Pi - Home Assistant).
HA version 2022.12.08.
I tried to find the file: /usr/src/homeassistant/homeassistant/components/tuya/climate.py
But probably because of the different environment (your Docker vs my Raspberry’s OS) it was not successful.
Do you have an idea where I can find this climate.py file for modifying that line?
Thank you for your help.
Karoly.
Hungary.
Something like this?
sudo apt install mlocate
sudo updatedb
locate climate.py
I can see Local Tuya and Tuya Local, both seem recently updated, any recommendations for one over the other?
@midstar can you please update your fork and patch. I’ve manually patched 2023.2.3 and target temp keeps incorrect scaling, while current temp is fine. Thanks
HI,
i use home assistant SO on virtualbox/win10, i read this and many other guides but i can’t change the temperature of my tuya thermostats. I used Tuya integration and the devices have been acquired, but the temperature is not correct (with this mode the \HOMEASSISTANT\config\custom_components folder is not generated).
I tried with HACS\Local Tuya but I don’t know how to set the parameters and the devices are not detected. With this mode the custom_components\local tuya folder is generated, I modified the climate.py file as instructed, restarted HA but no effect.
help help help
Tuya:
TuyaLocal:
The trick and complication to get local tuya to work is finding out the device key and ip address. The details are in the link given above
Finding your device ID and local key
The easiest way to find your local key is with the Tuya Developer portal. If you have previously configured the built in Tuya cloud integration, or localtuya, you probably already have a developer account with the Tuya app linked. Note that you need to use Tuya’s own branded “Tuya Smart” or “SmartLife” apps to access devices through the developer portal. For most devices, your device will work identically with those apps as it does with your manufacturer’s branded app, but there are a few devices where that is not the case and you will need to decide whether you are willing to potentially lose access to some functionality (such as mapping for some vacuum cleaners).
If you log on to your Developer Portal account, under Cloud you should be able to get a list of your devices, which contains the “Device ID”. If you don’t see them, check your server is set correctly at the top of the page. Make a note of the Device IDs for all your devices, then select Cloud on the side bar again and go to the API Explorer.
Under General Device Capabilities / General Devices Management, select the “Get Device Information” function, and enter your Device ID. In the results you should see your local_key.
The IP address you should be able to get from your router. Using a command line Tuya client like tuyaapi/cli or tinytuya you may also be able to scan your network for Tuya devices to find the IP address and also automate the above process of connecting to the portal and getting the local key.
So i got the same problem and localtuya worked for me for some time and got unavailable.
i switched to original tuya cloud integration and made a workaround using the post above but with some difference (maybe its a version change) so here is how i did it.
Im not a super docker user, and didnt open the ssh on hassio, so here is how the easy way to find climate.py
- you install portainer as addon to homeassistant (it stoped be officialy supported so you have to add a repository and then it appears and can be installed, google for that)
- make a new container Filebrowser (it adds a filebrowser over all host)
Docker Container: filebrowser [Portainer Installation] - YouTube
here is an explanation how - open filebrowser with admin admin login password
- search for the file climate.py it will give you loads of stuff you should scroll find the one with a "tuya/"in front of it.
in my case it was like
./mnt/data/docker/overlay2/51a2a977709474a71165ca2d8e133e4f901e5aba8ade60398eb39362ab3b3dba/merged/usr/src/homeassistant/homeassistant/components/tuya/climate.py
- open it and close - youre in the folder
- make a copy of original one in case you mess it up
- take the one i have
"""Support for Tuya Climate."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from tuya_iot import TuyaDevice, TuyaDeviceManager
from homeassistant.components.climate import (
SWING_BOTH,
SWING_HORIZONTAL,
SWING_OFF,
SWING_ON,
SWING_VERTICAL,
ClimateEntity,
ClimateEntityDescription,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantTuyaData
from .base import IntegerTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType
TUYA_HVAC_TO_HA = {
"auto": HVACMode.HEAT_COOL,
"cold": HVACMode.COOL,
"freeze": HVACMode.COOL,
"heat": HVACMode.HEAT,
"hot": HVACMode.HEAT,
"manual": HVACMode.HEAT_COOL,
"wet": HVACMode.DRY,
"wind": HVACMode.FAN_ONLY,
}
@dataclass
class TuyaClimateSensorDescriptionMixin:
"""Define an entity description mixin for climate entities."""
switch_only_hvac_mode: HVACMode
@dataclass
class TuyaClimateEntityDescription(
ClimateEntityDescription, TuyaClimateSensorDescriptionMixin
):
"""Describe an Tuya climate entity."""
CLIMATE_DESCRIPTIONS: dict[str, TuyaClimateEntityDescription] = {
# Air conditioner
# https://developer.tuya.com/en/docs/iot/categorykt?id=Kaiuz0z71ov2n
"kt": TuyaClimateEntityDescription(
key="kt",
switch_only_hvac_mode=HVACMode.COOL,
),
# Heater
# https://developer.tuya.com/en/docs/iot/f?id=K9gf46epy4j82
"qn": TuyaClimateEntityDescription(
key="qn",
switch_only_hvac_mode=HVACMode.HEAT,
),
# Heater
# https://developer.tuya.com/en/docs/iot/categoryrs?id=Kaiuz0nfferyx
"rs": TuyaClimateEntityDescription(
key="rs",
switch_only_hvac_mode=HVACMode.HEAT,
),
# Thermostat
# https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9
"wk": TuyaClimateEntityDescription(
key="wk",
switch_only_hvac_mode=HVACMode.HEAT_COOL,
),
# Thermostatic Radiator Valve
# Not documented
"wkf": TuyaClimateEntityDescription(
key="wkf",
switch_only_hvac_mode=HVACMode.HEAT,
),
}
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Tuya climate dynamically through Tuya discovery."""
hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id]
@callback
def async_discover_device(device_ids: list[str]) -> None:
"""Discover and add a discovered Tuya climate."""
entities: list[TuyaClimateEntity] = []
for device_id in device_ids:
device = hass_data.device_manager.device_map[device_id]
if device and device.category in CLIMATE_DESCRIPTIONS:
entities.append(
TuyaClimateEntity(
device,
hass_data.device_manager,
CLIMATE_DESCRIPTIONS[device.category],
)
)
async_add_entities(entities)
async_discover_device([*hass_data.device_manager.device_map])
entry.async_on_unload(
async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device)
)
class TuyaClimateEntity(TuyaEntity, ClimateEntity):
"""Tuya Climate Device."""
_current_humidity: IntegerTypeData | None = None
_current_temperature: IntegerTypeData | None = None
_hvac_to_tuya: dict[str, str]
_set_humidity: IntegerTypeData | None = None
_set_temperature: IntegerTypeData | None = None
entity_description: TuyaClimateEntityDescription
def __init__(
self,
device: TuyaDevice,
device_manager: TuyaDeviceManager,
description: TuyaClimateEntityDescription,
) -> None:
"""Determine which values to use."""
self._attr_target_temperature_step = 1.0
self.entity_description = description
super().__init__(device, device_manager)
# If both temperature values for celsius and fahrenheit are present,
# use whatever the device is set to, with a fallback to celsius.
prefered_temperature_unit = None
if all(
dpcode in device.status
for dpcode in (DPCode.TEMP_CURRENT, DPCode.TEMP_CURRENT_F)
) or all(
dpcode in device.status for dpcode in (DPCode.TEMP_SET, DPCode.TEMP_SET_F)
):
prefered_temperature_unit = UnitOfTemperature.CELSIUS
if any(
"f" in device.status[dpcode].lower()
for dpcode in (DPCode.C_F, DPCode.TEMP_UNIT_CONVERT)
if isinstance(device.status.get(dpcode), str)
):
prefered_temperature_unit = UnitOfTemperature.FAHRENHEIT
# Default to Celsius
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
# Figure out current temperature, use preferred unit or what is available
celsius_type = self.find_dpcode(
(DPCode.TEMP_CURRENT, DPCode.UPPER_TEMP), dptype=DPType.INTEGER
)
fahrenheit_type = self.find_dpcode(
(DPCode.TEMP_CURRENT_F, DPCode.UPPER_TEMP_F), dptype=DPType.INTEGER
)
if fahrenheit_type and (
prefered_temperature_unit == UnitOfTemperature.FAHRENHEIT
or (
prefered_temperature_unit == UnitOfTemperature.CELSIUS
and not celsius_type
)
):
self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
self._current_temperature = fahrenheit_type
elif celsius_type:
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._current_temperature = celsius_type
# Figure out setting temperature, use preferred unit or what is available
celsius_type = self.find_dpcode(
DPCode.TEMP_SET, dptype=DPType.INTEGER, prefer_function=True
)
fahrenheit_type = self.find_dpcode(
DPCode.TEMP_SET_F, dptype=DPType.INTEGER, prefer_function=True
)
if fahrenheit_type and (
prefered_temperature_unit == UnitOfTemperature.FAHRENHEIT
or (
prefered_temperature_unit == UnitOfTemperature.CELSIUS
and not celsius_type
)
):
self._set_temperature = fahrenheit_type
elif celsius_type:
self._set_temperature = celsius_type
# Get integer type data for the dpcode to set temperature, use
# it to define min, max & step temperatures
if self._set_temperature:
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
self._attr_max_temp = self._set_temperature.max_scaled * 5
self._attr_min_temp = self._set_temperature.min_scaled
self._attr_target_temperature_step = self._set_temperature.step_scaled
# Determine HVAC modes
self._attr_hvac_modes: list[str] = []
self._hvac_to_tuya = {}
if enum_type := self.find_dpcode(
DPCode.MODE, dptype=DPType.ENUM, prefer_function=True
):
self._attr_hvac_modes = [HVACMode.OFF]
unknown_hvac_modes: list[str] = []
for tuya_mode in enum_type.range:
if tuya_mode in TUYA_HVAC_TO_HA:
ha_mode = TUYA_HVAC_TO_HA[tuya_mode]
self._hvac_to_tuya[ha_mode] = tuya_mode
self._attr_hvac_modes.append(ha_mode)
else:
unknown_hvac_modes.append(tuya_mode)
if unknown_hvac_modes: # Tuya modes are presets instead of hvac_modes
self._attr_hvac_modes.append(description.switch_only_hvac_mode)
self._attr_preset_modes = unknown_hvac_modes
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
elif self.find_dpcode(DPCode.SWITCH, prefer_function=True):
self._attr_hvac_modes = [
HVACMode.OFF,
description.switch_only_hvac_mode,
]
# Determine dpcode to use for setting the humidity
if int_type := self.find_dpcode(
DPCode.HUMIDITY_SET, dptype=DPType.INTEGER, prefer_function=True
):
self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY
self._set_humidity = int_type
self._attr_min_humidity = int(int_type.min_scaled)
self._attr_max_humidity = int(int_type.max_scaled)
# Determine dpcode to use for getting the current humidity
self._current_humidity = self.find_dpcode(
DPCode.HUMIDITY_CURRENT, dptype=DPType.INTEGER
)
# Determine fan modes
if enum_type := self.find_dpcode(
(DPCode.FAN_SPEED_ENUM, DPCode.WINDSPEED),
dptype=DPType.ENUM,
prefer_function=True,
):
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
self._attr_fan_modes = enum_type.range
# Determine swing modes
if self.find_dpcode(
(
DPCode.SHAKE,
DPCode.SWING,
DPCode.SWITCH_HORIZONTAL,
DPCode.SWITCH_VERTICAL,
),
prefer_function=True,
):
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
self._attr_swing_modes = [SWING_OFF]
if self.find_dpcode((DPCode.SHAKE, DPCode.SWING), prefer_function=True):
self._attr_swing_modes.append(SWING_ON)
if self.find_dpcode(DPCode.SWITCH_HORIZONTAL, prefer_function=True):
self._attr_swing_modes.append(SWING_HORIZONTAL)
if self.find_dpcode(DPCode.SWITCH_VERTICAL, prefer_function=True):
self._attr_swing_modes.append(SWING_VERTICAL)
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
commands = [{"code": DPCode.SWITCH, "value": hvac_mode != HVACMode.OFF}]
if hvac_mode in self._hvac_to_tuya:
commands.append(
{"code": DPCode.MODE, "value": self._hvac_to_tuya[hvac_mode]}
)
self._send_command(commands)
def set_preset_mode(self, preset_mode):
"""Set new target preset mode."""
commands = [{"code": DPCode.MODE, "value": preset_mode}]
self._send_command(commands)
def set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
self._send_command([{"code": DPCode.FAN_SPEED_ENUM, "value": fan_mode}])
def set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
if self._set_humidity is None:
raise RuntimeError(
"Cannot set humidity, device doesn't provide methods to set it"
)
self._send_command(
[
{
"code": self._set_humidity.dpcode,
"value": self._set_humidity.scale_value_back(humidity),
}
]
)
def set_swing_mode(self, swing_mode: str) -> None:
"""Set new target swing operation."""
# The API accepts these all at once and will ignore the codes
# that don't apply to the device being controlled.
self._send_command(
[
{
"code": DPCode.SHAKE,
"value": swing_mode == SWING_ON,
},
{
"code": DPCode.SWING,
"value": swing_mode == SWING_ON,
},
{
"code": DPCode.SWITCH_VERTICAL,
"value": swing_mode in (SWING_BOTH, SWING_VERTICAL),
},
{
"code": DPCode.SWITCH_HORIZONTAL,
"value": swing_mode in (SWING_BOTH, SWING_HORIZONTAL),
},
]
)
def set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if self._set_temperature is None:
raise RuntimeError(
"Cannot set target temperature, device doesn't provide methods to"
" set it"
)
self._send_command(
[
{
"code": self._set_temperature.dpcode,
"value": round(
self._set_temperature.scale_value_back(kwargs["temperature"]) / 5
)
}
]
)
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if self._current_temperature is None:
return None
temperature = self.device.status.get(self._current_temperature.dpcode)
if temperature is None:
return None
if self._current_temperature.scale == 0 and self._current_temperature.step != 1:
# The current temperature can have a scale of 0 or 1 and is used for
# rounding, Home Assistant doesn't need to round but we will always
# need to divide the value by 10^1 in case of 0 as scale.
# https://developer.tuya.com/en/docs/iot/shift-temperature-scale-follow-the-setting-of-app-account-center?id=Ka9
temperature = temperature / 10
temperature = temperature * 5
return self._current_temperature.scale_value(temperature)
@property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
if self._current_humidity is None:
return None
humidity = self.device.status.get(self._current_humidity.dpcode)
if humidity is None:
return None
return round(self._current_humidity.scale_value(humidity))
@property
def target_temperature(self) -> float | None:
"""Return the temperature currently set to be reached."""
if self._set_temperature is None:
return None
temperature = self.device.status.get(self._set_temperature.dpcode)
if temperature is None:
return None
temperature = temperature * 5
return self._set_temperature.scale_value(temperature)
@property
def target_humidity(self) -> int | None:
"""Return the humidity currently set to be reached."""
if self._set_humidity is None:
return None
humidity = self.device.status.get(self._set_humidity.dpcode)
if humidity is None:
return None
return round(self._set_humidity.scale_value(humidity))
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac mode."""
# If the switch off, hvac mode is off as well. Unless the switch
# the switch is on or doesn't exists of course...
if not self.device.status.get(DPCode.SWITCH, True):
return HVACMode.OFF
if DPCode.MODE not in self.device.function:
if self.device.status.get(DPCode.SWITCH, False):
return self.entity_description.switch_only_hvac_mode
return HVACMode.OFF
if (
mode := self.device.status.get(DPCode.MODE)
) is not None and mode in TUYA_HVAC_TO_HA:
return TUYA_HVAC_TO_HA[mode]
# If the switch is on, and the mode does not match any hvac mode.
if self.device.status.get(DPCode.SWITCH, False):
return self.entity_description.switch_only_hvac_mode
return HVACMode.OFF
@property
def preset_mode(self) -> str | None:
"""Return preset mode."""
if DPCode.MODE not in self.device.function:
return None
mode = self.device.status.get(DPCode.MODE)
if mode in TUYA_HVAC_TO_HA:
return None
return mode
@property
def fan_mode(self) -> str | None:
"""Return fan mode."""
return self.device.status.get(DPCode.FAN_SPEED_ENUM)
@property
def swing_mode(self) -> str:
"""Return swing mode."""
if any(
self.device.status.get(dpcode) for dpcode in (DPCode.SHAKE, DPCode.SWING)
):
return SWING_ON
horizontal = self.device.status.get(DPCode.SWITCH_HORIZONTAL)
vertical = self.device.status.get(DPCode.SWITCH_VERTICAL)
if horizontal and vertical:
return SWING_BOTH
if horizontal:
return SWING_HORIZONTAL
if vertical:
return SWING_VERTICAL
return SWING_OFF
def turn_on(self) -> None:
"""Turn the device on, retaining current HVAC (if supported)."""
if DPCode.SWITCH in self.device.function:
self._send_command([{"code": DPCode.SWITCH, "value": True}])
return
# Fake turn on
for mode in (HVACMode.HEAT_COOL, HVACMode.HEAT, HVACMode.COOL):
if mode not in self.hvac_modes:
continue
self.set_hvac_mode(mode)
break
def turn_off(self) -> None:
"""Turn the device on, retaining current HVAC (if supported)."""
if DPCode.SWITCH in self.device.function:
self._send_command([{"code": DPCode.SWITCH, "value": False}])
return
# Fake turn off
if HVACMode.OFF in self.hvac_modes:
self.set_hvac_mode(HVACMode.OFF)
- make a copy of the modified one ( in case you update tuya integration and it will be a copy to fill)
- reboot youre done.
Its not the best option if you have a thermostat that is showing correct (other model) and a thermostat bugged, if you fix one, the other one will become buggy
not working in latest core
use make-all/tuya-local hacs integration that is what i did
I did installed tuya local and one of my thermostat got the right temperature, but only one of the two I have, and they’re exactly the same brand and model, I even bought them at the same time, so there shouldn’t be much difference, any suggestions from someone? if I would edit the climate.py that would edit the temperature for both thermostats isn’t it?
my tuya thermostat cloud integration hack stopeed working after HA core update.
i did my path once again and it is not working
cant apply localtuya thermostat integration because integration doesnt know my device.
oh my god those china manufacturers have a hard time for me
same for me, i got 3 thermostats, all look the same and brought same time from a single batch i guess.
it is Beca bht-002
2 thermostats connected by localtuya and 1 im unable to do that
update:
i managed to add that 3rd device using tuya local integration
the thing is that i had to specify the IP and not use auto (previous two of them went on auto)
and use 3.1 protocol and not use auto
Hi, i lost all the configuration and have to start from scratch again. I´m stucked at step 3 of your custom component guide.
"Step 3 - Download the modified Tuya integration
Clone [the modified files](https://github.com/midstar/homeassistant_core/tree/tuya_BHT-002_thermostat_workaround) from the tuya_BHT-002_thermostat_workaround branch.
joel@penguin:~/temp$ git clone -b tuya_BHT-002_thermostat_workaround [email protected]:midstar/homeassistant_core.git
joel@penguin:~/temp/homeassistant_core$ cd homeassistant_core/homeassistant/components/tuya/"
I just don´t know how to download de modified tuya integration, or what you mean by “clone de modified files”, and the link leads me to a general home assistant folder. I´m lost here. Any help would be appreciated.
Hi folks, still have the issue with original Tuya Integration and changing the climate.py file under custom_components doesn’t work. Is there any fix using original tuya integration?
Thx
Hi.
I’m using:
Home Assistant 2023.10.0
Supervisor 2023.10.0
Operating System 10.5
Frontend 20231002.0 - latest
Sorry for asking about this after so long, but I can’t find where can I edit the climate.py of the tuya component, I only can’t find access to custom components using the file editor, if you can give a little explanation about this it would be great, because HA shows temperatures of this thermostat divided by 5.
Thanks in advance.
Hi guys, some issue here:
Home Assistant 2023.10.3
Supervisor 2023.10.0
Operating System 11.0
Frontend 20231005.0 - latest
Home assistant installed on rpi4-64.
With a Tuya Wifi thermostat the temperature of this thermostat is divided by 5 using the ufficial Tuya addons ( Others addons not work for me).
In my OS files there isn’t a file called climate.py or tuya.py in custom_components.
Была пошаговая инструкция да устрела