DavidIlie/tuya-smart-ir-ac

Need feedback for edited code

Opened this issue · 3 comments

All credits goes to ChatGPT, I just tell him what to do.

Fix and improvements made:

  • Can change modes
  • Can change fan to Automatic
  • Two+ units can be added
  • Debug/log and traceability

Findings over testing:

  • Status of unit comes from Tuya app remote, if temp adjusted with physical remote control then changes will not reflect on thermostat in HA and will cause further issues.

  • With log you can see issues, as example for unknown reasons when I try set Fan Only mode it sends '3' but in return:
    "Failed to send command mode with value 3: {'code': 30100, 'msg': '没有查询到码库', 'success': False, 't': 1717948574454, 'tid': 'cc998e5d267811efb9130e1a774ae1f3'}"
    I assume issue is because of remote control setup in Tuya app, will test other remotes. To test working modes of unit its possible to change modes in Tuya app and see HA log of status update.

  • Flowing warning changes if fixed, but anyway replaces wit other warning about HVAC modes, I assume its something in HA, may be gone after they cut it out.
    "Entity None (<class 'custom_components.tuya_smart_ir_ac.climate.TuyaThermostat'>) implements HVACMode(s): cool, heat, auto, fan_only, dry, off and therefore implicitly supports the turn_on/turn_off methods without setting the proper ClimateEntityFeature. Please report it to the author of the 'tuya_smart_ir_ac' custom integration"

Big thanks to David! Save my started budget smart home project.


Add to configuration.yaml this code too:

logger: default: warning logs: custom_components.tuya_smart_ir_ac: debug tuya_hack: debug

/homeassistant/custom_components/tuya_smart_ir_ac/climate.py
`import logging
from typing import Any, Dict

import voluptuous as vol
from pprint import pformat

from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.components.climate.const import HVACMode
from homeassistant.const import UnitOfTemperature, STATE_UNKNOWN
from homeassistant.components.climate import ClimateEntity, ClimateEntityFeature

from .const import VALID_MODES
from .api import TuyaAPI

_LOGGER = logging.getLogger("tuya_hack")

Define constants for configuration keys

ACCESS_ID = "access_id"
ACCESS_SECRET = "access_secret"
REMOTE_ID = "remote_id"
AC_ID = "ac_id"
NAME = "name"
SENSOR = "sensor"

Schema for platform configuration

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(ACCESS_ID): cv.string,
vol.Required(ACCESS_SECRET): cv.string,
vol.Required(REMOTE_ID): cv.string,
vol.Required(AC_ID): cv.string,
vol.Required(NAME): cv.string,
vol.Required(SENSOR): cv.string,
}
)

def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Tuya thermostat platform."""
_LOGGER.info("Setting up Tuya thermostat platform")
climate_config = {
"access_id": config[ACCESS_ID],
"access_secret": config[ACCESS_SECRET],
"remote_id": config[REMOTE_ID],
"ac_id": config[AC_ID],
"name": config[NAME],
"sensor": config[SENSOR]
}

_LOGGER.debug(f"Platform configuration: {pformat(climate_config)}")
add_entities([TuyaThermostat(climate_config, hass)])

class TuyaThermostat(ClimateEntity):
"""Representation of a Tuya Thermostat."""

def __init__(self, climate: Dict[str, Any], hass: HomeAssistant) -> None:
    """Initialize the Tuya thermostat entity."""
    _LOGGER.info("Initializing Tuya thermostat")
    _LOGGER.debug(pformat(climate))
    self._api = TuyaAPI(
        hass,
        climate[ACCESS_ID],
        climate[ACCESS_SECRET],
        climate[AC_ID],
        climate[REMOTE_ID],
    )
    self._sensor_name = climate[SENSOR]
    self._name = climate[NAME]
    self.hass = hass

@property
def name(self) -> str:
    """Return the name of the thermostat."""
    return self._name

@property
def unique_id(self) -> str:
    """Return the unique ID of the thermostat."""
    return f"tuya_thermostat_{self._api.thermostat_device_id}"

@property
def temperature_unit(self) -> str:
    """Return the unit of temperature measurement."""
    return UnitOfTemperature.CELSIUS

@property
def supported_features(self) -> int:
    """Return the supported features of the thermostat."""
    return (
        ClimateEntityFeature.TARGET_TEMPERATURE |
        ClimateEntityFeature.FAN_MODE
    )

@property
def min_temp(self) -> float:
    """Return the minimum temperature that can be set."""
    return 15.0

@property
def max_temp(self) -> float:
    """Return the maximum temperature that can be set."""
    return 30.0

@property
def current_temperature(self) -> float | None:
    """Return the current temperature."""
    sensor_state = self.hass.states.get(self._sensor_name)
    _LOGGER.debug(f"Current sensor state: {sensor_state}")
    if sensor_state and sensor_state.state != STATE_UNKNOWN:
        try:
            return float(sensor_state.state)
        except ValueError:
            _LOGGER.error(f"Invalid sensor state: {sensor_state.state}")
    return float(self._api._temperature) if self._api._temperature else None

@property
def target_temperature(self) -> float | None:
    """Return the temperature we try to reach."""
    return float(self._api._temperature) if self._api._temperature else None

@property
def hvac_mode(self) -> str | None:
    """Return current operation (heat, cool, idle)."""
    if self._api._power == "0":
        return HVACMode.OFF
    return VALID_MODES.get(str(self._api._mode), None)

@property
def hvac_modes(self) -> list[str]:
    """Return the list of available operation modes."""
    return list(VALID_MODES.values())

@property
def fan_mode(self) -> str | None:
    """Return the fan setting."""
    return (
        "Low" if self._api._wind == "1" else
        "Medium" if self._api._wind == "2" else
        "High" if self._api._wind == "3" else
        "Automatic" if self._api._wind == "0" else None
    )

@property
def fan_modes(self) -> list[str]:
    """Return the list of available fan modes."""
    return ["Low", "Medium", "High", "Automatic"]

async def async_set_fan_mode(self, fan_mode: str) -> None:
    """Set new target fan mode."""
    _LOGGER.info(f"Setting fan mode to: {fan_mode}")
    command_map = {
        "Low": "1",
        "Medium": "2",
        "High": "3",
        "Automatic": "0",
    }
    if fan_mode in command_map:
        await self._api.send_command("wind", command_map[fan_mode])
    else:
        _LOGGER.warning(f"Invalid fan mode: {fan_mode}")

async def async_update(self) -> None:
    """Update the state of the thermostat."""
    _LOGGER.info("Updating Tuya thermostat state")
    await self._api.async_update()
    self.async_write_ha_state()

async def async_set_temperature(self, **kwargs: Any) -> None:
    """Set new target temperature."""
    temperature = kwargs.get("temperature")
    if temperature is not None:
        _LOGGER.info(f"Setting target temperature to: {temperature}")
        await self._api.async_set_temperature(float(temperature))

async def async_set_hvac_mode(self, hvac_mode: str) -> None:
    """Set new target operation mode."""
    _LOGGER.info(f"Setting HVAC mode to: {hvac_mode}")
    for mode, mode_name in VALID_MODES.items():
        if hvac_mode == mode_name:
            if mode == "5":
                await self._api.async_power_off()
            else:
                if self._api._power == "0":
                    await self._api.async_power_on()
                await self._api.async_set_hvac_mode(mode)
            break
    else:
        _LOGGER.warning(f"Invalid HVAC mode: {hvac_mode}")

`

/homeassistant/custom_components/tuya_smart_ir_ac/api.py
`from tuya_connector import TuyaOpenAPI
from .const import VALID_MODES
from homeassistant.core import HomeAssistant
import logging
from pprint import pformat

_LOGGER = logging.getLogger("tuya_hack")

class TuyaAPI:
"""
Interface to interact with Tuya devices.
"""

def __init__(
    self,
    hass: HomeAssistant,
    access_id,
    access_secret,
    thermostat_device_id,
    ir_remote_device_id,
):
    """
    Initialize Tuya API.
    """
    self.access_id = access_id
    self.access_secret = access_secret
    self.thermostat_device_id = thermostat_device_id
    self.ir_remote_device_id = ir_remote_device_id
    self.hass = hass
    
    """
    Change address according used server.
    China Data Center	https://openapi.tuyacn.com
    *** Western America Data Center	https://openapi.tuyaus.com
    Eastern America Data Center	https://openapi-ueaz.tuyaus.com
    Central Europe Data Center	https://openapi.tuyaeu.com
    Western Europe Data Center	https://openapi-weaz.tuyaeu.com
    India Data Center	https://openapi.tuyain.com
    """
    openapi = TuyaOpenAPI("https://openapi.tuyaus.com", access_id, access_secret)
    openapi.connect()
    self.openapi = openapi

    self._temperature = "0"
    self._mode = "0"
    self._power = "0"
    self._wind = "0"

async def async_init(self):
    """
    Asynchronously initialize Tuya API.
    """
    await self.async_update()

async def async_update(self):
    """
    Asynchronously update status from the device.
    """
    status = await self.get_status()
    if status:
        self._temperature = status.get("temp")
        self._mode = status.get("mode")
        self._power = status.get("power")
        self._wind = status.get("wind")
    _LOGGER.info(f"ASYNC_UPDATE: {status}")

async def async_set_fan_speed(self, fan_speed):
    """
    Asynchronously set fan speed.
    """
    _LOGGER.info(f"Setting fan speed to {fan_speed}")
    await self.send_command("wind", str(fan_speed))

async def async_set_temperature(self, temperature):
    """
    Asynchronously set temperature.
    """
    _LOGGER.info(f"Setting temperature to {temperature}")
    await self.send_command("temp", str(temperature))

async def async_power_on(self):
    """
    Asynchronously turn on the device.
    """
    _LOGGER.info("Turning on")
    await self.send_command("power", "1")

async def async_power_off(self):
    """
    Asynchronously turn off the device.
    """
    _LOGGER.info("Turning off")
    await self.send_command("power", "0")

async def async_set_hvac_mode(self, hvac_mode):
    """
    Asynchronously set HVAC mode.
    """
    _LOGGER.info(f"Setting HVAC mode to {hvac_mode}")
    await self.send_command("mode", str(hvac_mode))

async def get_status(self):
    """
    Asynchronously fetch device status.
    """
    url = f"/v2.0/infrareds/{self.ir_remote_device_id}/remotes/{self.thermostat_device_id}/ac/status"
    _LOGGER.info(f"Fetching status from URL: {url}")
    try:
        data = await self.hass.async_add_executor_job(self.openapi.get, url)
        _LOGGER.debug(f"Full response data: {pformat(data)}")
        if data.get("success"):
            _LOGGER.info(f"GET_STATUS: {data.get('result')}")
            return data.get("result")
        else:
            _LOGGER.warning(f"Failed to fetch status: {data}")
    except Exception as e:
        _LOGGER.error(f"Error fetching status: {e}")
    return None

async def send_command(self, code, value):
    """
    Asynchronously send command to the device.
    """
    url = f"/v2.0/infrareds/{self.ir_remote_device_id}/air-conditioners/{self.thermostat_device_id}/command"
    _LOGGER.info(f"Sending command to URL: {url}")
    _LOGGER.info(f"SEND_COMMAND_CODE_THEN_VAL: {code} {value}")
    try:
        data = await self.hass.async_add_executor_job(
            self.openapi.post,
            url,
            {
                "code": code,
                "value": value,
            },
        )
        _LOGGER.debug(f"Full response data: {pformat(data)}")
        if data.get("success"):
            _LOGGER.info(f"Command {code} with value {value} sent successfully")
        else:
            _LOGGER.warning(f"Failed to send command {code} with value {value}: {data}")
        return data
    except Exception as e:
        _LOGGER.error(f"Error sending command: {e}")
        return False`

/homeassistant/custom_components/tuya_smart_ir_ac/const.py
`from homeassistant.components.climate.const import HVACMode
import logging

_LOGGER = logging.getLogger(name)

VALID_MODES = {
"0": HVACMode.COOL,
"1": HVACMode.HEAT,
"2": HVACMode.AUTO,
"3": HVACMode.FAN_ONLY,
"4": HVACMode.DRY,
"5": HVACMode.OFF,
}

_LOGGER.debug(f"Valid HVAC modes: {VALID_MODES}")`

Need Help....
I understood that you solved the problem of - Two+ units can be added
I couldn't figure out how to fix the problem according to your solution.
What to copy to my files
Thanks

Need Help.... I understood that you solved the problem of - Two+ units can be added I couldn't figure out how to fix the problem according to your solution. What to copy to my files Thanks

I'm new on Git so don't know how things around here works.

https://github.com/Ovi8392/tuya_custom/tree/DavidIlie-/-tuya-smart-ir-ac-/-(edited-by-me)

Files you interested in:

init.py
api.py
climate.py
const.py
manifest.json

to add in configuration.yaml

climate:

  • platform: tuya_smart_ir_ac
    name: "Name AC 1"
    sensor: "sensor.temperature_humidity_sensor_temperature" ### Replace with your sensor
    access_id: "xxxxx" ### from Tuya developer site
    access_secret: "xxxxx" ### from Tuya developer site
    remote_id: "xxxxx" ### Smart IR device
    ac_id: "xxxxx" ### Remote created with Smart IR

  • platform: tuya_smart_ir_ac
    name: "Name AC 2"
    sensor: "sensor.temperature_humidity_sensor_temperature" ### Replace with your sensor
    access_id: "xxxxx"
    access_secret: "xxxxx"
    remote_id: "xxxxx"
    ac_id: "xxxxx"

logger:
default: info
logs:
custom_components.tuya_smart_ir_ac: debug tuya_hack: debug

end of part to add in configuration.yaml

Remember change in api.py correct server you use 
openapi = TuyaOpenAPI("https://openapi.tuyaus.com", access_id, access_secret)

Ignore my project [tuya_custom] and its files, it's my playground, I add Fan with it, but still testing.

Need Help.... I understood that you solved the problem of - Two+ units can be added I couldn't figure out how to fix the problem according to your solution. What to copy to my files Thanks

I'm new on Git so don't know how things around here works.

https://github.com/Ovi8392/tuya_custom/tree/DavidIlie-/-tuya-smart-ir-ac-/-(edited-by-me)

Files you interested in:

init.py api.py climate.py const.py manifest.json

to add in configuration.yaml

climate:

  • platform: tuya_smart_ir_ac
    name: "Name AC 1"
    sensor: "sensor.temperature_humidity_sensor_temperature" ### Replace with your sensor
    access_id: "xxxxx" ### from Tuya developer site
    access_secret: "xxxxx" ### from Tuya developer site
    remote_id: "xxxxx" ### Smart IR device
    ac_id: "xxxxx" ### Remote created with Smart IR
  • platform: tuya_smart_ir_ac
    name: "Name AC 2"
    sensor: "sensor.temperature_humidity_sensor_temperature" ### Replace with your sensor
    access_id: "xxxxx"
    access_secret: "xxxxx"
    remote_id: "xxxxx"
    ac_id: "xxxxx"

logger: default: info logs: custom_components.tuya_smart_ir_ac: debug tuya_hack: debug

end of part to add in configuration.yaml

Remember change in api.py correct server you use 
openapi = TuyaOpenAPI("https://openapi.tuyaus.com", access_id, access_secret)

Ignore my project [tuya_custom] and its files, it's my playground, I add Fan with it, but still testing.

tanks man this solved my problem, now i can add more then one ac wihtout any problem