Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
d372b9c
Update manifest.json
azerty9971 Jan 8, 2026
11fc526
Update cover.py
azerty9971 Jan 8, 2026
b423a82
Merge pull request #697 from azerty9971/alpha
azerty9971 Jan 8, 2026
8a45eee
Update climate.py
azerty9971 Jan 8, 2026
54dfbff
Update climate.py
azerty9971 Jan 8, 2026
cd9a689
Merge pull request #698 from azerty9971/alpha
azerty9971 Jan 8, 2026
01cf799
Update shared_classes.py
azerty9971 Jan 8, 2026
0e8219e
Update shared_classes.py
azerty9971 Jan 8, 2026
c6217e4
Update cover.py
azerty9971 Jan 8, 2026
7df8117
Update shared_classes.py
azerty9971 Jan 8, 2026
b718520
Update cover.py
azerty9971 Jan 8, 2026
188b683
Update cover.py
azerty9971 Jan 8, 2026
b532b01
Update cover.py
azerty9971 Jan 8, 2026
4c16551
Merge pull request #699 from azerty9971/alpha
azerty9971 Jan 8, 2026
d71cba5
Update shared_classes.py
azerty9971 Jan 8, 2026
82f74c9
Update openapi.py
azerty9971 Jan 8, 2026
e88e7fa
Merge pull request #701 from azerty9971/alpha
azerty9971 Jan 8, 2026
8b0be85
Rewrite send_command to use all API as fallback
azerty9971 Jan 9, 2026
feafaab
Merge common interface in XTDeviceFunction and XTDeviceStatusRange
azerty9971 Jan 9, 2026
07fc963
Make hvac_modes unique
azerty9971 Jan 9, 2026
f175c86
Update device.py
azerty9971 Jan 9, 2026
f2b1988
Update init.py
azerty9971 Jan 9, 2026
bee67e0
Update device.py
azerty9971 Jan 9, 2026
04895c0
Update device.py
azerty9971 Jan 9, 2026
79039c1
Update device.py
azerty9971 Jan 10, 2026
f4f6d51
Update device.py
azerty9971 Jan 10, 2026
c5539ce
Update device.py
azerty9971 Jan 10, 2026
dd37ddd
Update climate.py
azerty9971 Jan 10, 2026
7b74dc5
Update climate.py
azerty9971 Jan 10, 2026
636e7be
Update xt_tuya_iot_manager.py
azerty9971 Jan 10, 2026
20dc518
Update climate.py
azerty9971 Jan 10, 2026
001beeb
Update climate.py
azerty9971 Jan 10, 2026
8be80c6
Update climate.py
azerty9971 Jan 10, 2026
711e262
Update climate.py
azerty9971 Jan 10, 2026
cdc7515
Merge pull request #705 from azerty9971/alpha
azerty9971 Jan 10, 2026
4ad56e1
Support for HA2026.2
azerty9971 Jan 29, 2026
531777d
Update shared_classes.py
azerty9971 Jan 29, 2026
42ec5e4
More 2026.02 fixes
azerty9971 Jan 29, 2026
f5642ca
Update multi_device_listener.py
azerty9971 Jan 29, 2026
edffd7e
More fixes for 2026.2
azerty9971 Jan 29, 2026
20bce7c
More fixes for 2026.2
azerty9971 Jan 29, 2026
cdc336e
More fixes for 2026.2
azerty9971 Jan 29, 2026
6513cd8
Merge pull request #722 from azerty9971/alpha
azerty9971 Jan 31, 2026
c4d9b6a
Add stack trace to request logging
azerty9971 Feb 2, 2026
e1ad387
new logic to handle MQTT connections
azerty9971 Feb 3, 2026
cd35656
Update openmq.py
azerty9971 Feb 3, 2026
b380cbb
Update openmq.py
azerty9971 Feb 3, 2026
da24c98
Update openmq.py
azerty9971 Feb 3, 2026
30ff4de
Merge pull request #724 from azerty9971/alpha
azerty9971 Feb 3, 2026
8174e77
Update openmq.py
azerty9971 Feb 3, 2026
306c98a
Merge pull request #725 from azerty9971/alpha
azerty9971 Feb 3, 2026
ccf90e8
Update xt_tuya_iot_manager.py
azerty9971 Feb 4, 2026
1b2e2e5
Update cloud_fix.py
azerty9971 Feb 4, 2026
fc6d58f
Update cloud_fix.py
azerty9971 Feb 4, 2026
d645d70
Update cloud_fix.py
azerty9971 Feb 4, 2026
090c8e0
Update climate.py
azerty9971 Feb 4, 2026
7f484ab
Update climate.py
azerty9971 Feb 4, 2026
78d3eab
Update cloud_fix.py
azerty9971 Feb 4, 2026
f9f9e81
Update climate.py
azerty9971 Feb 4, 2026
2415450
Update climate.py
azerty9971 Feb 4, 2026
6f8367b
Update climate.py
azerty9971 Feb 4, 2026
4b24b18
Merge pull request #726 from azerty9971/alpha
azerty9971 Feb 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion custom_components/xtend_tuya/climate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Support for XT Climate."""

from __future__ import annotations
import collections
from dataclasses import dataclass
from enum import StrEnum
from typing import cast, Self
Expand Down Expand Up @@ -29,6 +30,7 @@
XTDPCode,
XT_CELSIUS_ALIASES,
XT_FAHRENHEIT_ALIASES,
LOGGER, # noqa: F401
)
from .ha_tuya_integration.tuya_integration_imports import (
TuyaClimateEntity,
Expand All @@ -41,6 +43,10 @@
tuya_climate_get_temperature_wrapper,
TuyaClimateSwingModeWrapper,
TuyaCustomerDevice,
TuyaDeviceWrapper,
TuyaClimatePresetWrapper,
TuyaClimateHvacModeWrapper,
TuyaEnumTypeInformation,
)
from .entity import (
XTEntity,
Expand All @@ -54,9 +60,11 @@
"dehumidify": HVACMode.DRY,
"freeze": HVACMode.COOL,
"heat": HVACMode.HEAT,
"home": HVACMode.HEAT_COOL,
"hot": HVACMode.HEAT,
"manual": HVACMode.HEAT_COOL,
"smartcool": HVACMode.HEAT_COOL,
"temporary": HVACMode.HEAT_COOL,
"wet": HVACMode.DRY,
"wind": HVACMode.FAN_ONLY,
}
Expand Down Expand Up @@ -165,6 +173,7 @@ def get_entity_instance(
current_humidity_wrapper: TuyaClimateRoundedIntegerWrapper | None,
current_temperature_wrapper: TuyaDPCodeIntegerWrapper | None,
fan_mode_wrapper: TuyaDPCodeEnumWrapper | None,
preset_wrapper: TuyaDeviceWrapper[str] | None,
hvac_mode_wrapper: TuyaDPCodeEnumWrapper | None,
set_temperature_wrapper: TuyaDPCodeIntegerWrapper | None,
swing_wrapper: TuyaClimateSwingModeWrapper | None,
Expand All @@ -179,6 +188,7 @@ def get_entity_instance(
current_humidity_wrapper=current_humidity_wrapper,
current_temperature_wrapper=current_temperature_wrapper,
fan_mode_wrapper=fan_mode_wrapper,
preset_wrapper=preset_wrapper,
hvac_mode_wrapper=hvac_mode_wrapper,
set_temperature_wrapper=set_temperature_wrapper,
swing_wrapper=swing_wrapper,
Expand All @@ -203,6 +213,50 @@ def get_entity_instance(
),
}

def _filter_hvac_mode_mappings(tuya_range: list[str]) -> dict[str, HVACMode | None]:
"""Filter TUYA_HVAC_TO_HA modes that are not in the range.

If multiple Tuya modes map to the same HA mode, set the mapping to None to avoid
ambiguity when converting back from HA to Tuya modes.
"""
modes_in_range = {
tuya_mode: XT_HVAC_TO_HA.get(tuya_mode) for tuya_mode in tuya_range
}
modes_occurrences = collections.Counter(modes_in_range.values())
for key, value in modes_in_range.items():
if value is not None and modes_occurrences[value] > 1:
modes_in_range[key] = None
return modes_in_range

class XTClimatePresetWrapper(TuyaClimatePresetWrapper):
def __init__(self, dpcode: str, type_information: TuyaEnumTypeInformation) -> None:
"""Init _PresetWrapper."""
super().__init__(dpcode, type_information)
mappings = _filter_hvac_mode_mappings(type_information.range)
self.options = [
tuya_mode for tuya_mode, ha_mode in mappings.items() if ha_mode is None
]

def read_device_status(self, device: TuyaCustomerDevice) -> str | None:
"""Read the device status."""
if (raw := super(TuyaClimatePresetWrapper, self).read_device_status(device)) in XT_HVAC_TO_HA:
return None
return raw

class XTClimateHvacModeWrapper(TuyaClimateHvacModeWrapper):
def __init__(self, dpcode: str, type_information: TuyaEnumTypeInformation) -> None:
"""Init _HvacModeWrapper."""
super().__init__(dpcode, type_information)
self._mappings = _filter_hvac_mode_mappings(type_information.range)
self.options = [
ha_mode for ha_mode in self._mappings.values() if ha_mode is not None
]

def read_device_status(self, device: TuyaCustomerDevice) -> HVACMode | None:
"""Read the device status."""
if (raw := super(TuyaClimateHvacModeWrapper, self).read_device_status(device)) not in XT_HVAC_TO_HA:
return None
return XT_HVAC_TO_HA[raw]

class XTClimateSwingModeWrapper(TuyaClimateSwingModeWrapper):
@classmethod
Expand Down Expand Up @@ -360,7 +414,12 @@ def async_discover_device(device_map, restrict_dpcode: str | None = None) -> Non
XT_CLIMATE_FAN_SPEED_DPCODES, # type: ignore
prefer_function=True,
),
hvac_mode_wrapper=TuyaDPCodeEnumWrapper.find_dpcode(
preset_wrapper=XTClimatePresetWrapper.find_dpcode(
device,
XT_CLIMATE_MODE_DPCODES,
prefer_function=True,
),
hvac_mode_wrapper=XTClimateHvacModeWrapper.find_dpcode(
device,
XT_CLIMATE_MODE_DPCODES, # type: ignore
prefer_function=True,
Expand Down Expand Up @@ -417,6 +476,7 @@ def __init__(
current_humidity_wrapper: TuyaClimateRoundedIntegerWrapper | None,
current_temperature_wrapper: TuyaDPCodeIntegerWrapper | None,
fan_mode_wrapper: TuyaDPCodeEnumWrapper | None,
preset_wrapper: TuyaDeviceWrapper[str] | None,
hvac_mode_wrapper: TuyaDPCodeEnumWrapper | None,
set_temperature_wrapper: TuyaDPCodeIntegerWrapper | None,
swing_wrapper: TuyaClimateSwingModeWrapper | None,
Expand All @@ -437,6 +497,7 @@ def __init__(
current_humidity_wrapper=current_humidity_wrapper,
current_temperature_wrapper=current_temperature_wrapper,
fan_mode_wrapper=fan_mode_wrapper,
preset_wrapper=preset_wrapper,
hvac_mode_wrapper=hvac_mode_wrapper,
set_temperature_wrapper=set_temperature_wrapper,
swing_wrapper=swing_wrapper,
Expand All @@ -457,6 +518,7 @@ def get_entity_instance(
current_humidity_wrapper: TuyaClimateRoundedIntegerWrapper | None,
current_temperature_wrapper: TuyaDPCodeIntegerWrapper | None,
fan_mode_wrapper: TuyaDPCodeEnumWrapper | None,
preset_wrapper: TuyaDeviceWrapper[str] | None,
hvac_mode_wrapper: TuyaDPCodeEnumWrapper | None,
set_temperature_wrapper: TuyaDPCodeIntegerWrapper | None,
swing_wrapper: TuyaClimateSwingModeWrapper | None,
Expand All @@ -474,6 +536,7 @@ def get_entity_instance(
current_humidity_wrapper=current_humidity_wrapper,
current_temperature_wrapper=current_temperature_wrapper,
fan_mode_wrapper=fan_mode_wrapper,
preset_wrapper=preset_wrapper,
hvac_mode_wrapper=hvac_mode_wrapper,
set_temperature_wrapper=set_temperature_wrapper,
swing_wrapper=swing_wrapper,
Expand All @@ -488,6 +551,7 @@ def get_entity_instance(
current_humidity_wrapper=current_humidity_wrapper,
current_temperature_wrapper=current_temperature_wrapper,
fan_mode_wrapper=fan_mode_wrapper,
preset_wrapper=preset_wrapper,
hvac_mode_wrapper=hvac_mode_wrapper,
set_temperature_wrapper=set_temperature_wrapper,
swing_wrapper=swing_wrapper,
Expand Down
5 changes: 5 additions & 0 deletions custom_components/xtend_tuya/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,11 @@ class XTDPCode(StrEnum):
UNLOCK_PASSWORD = "unlock_password"
UNLOCK_PHONE_REMOTE = "unlock_phone_remote"
UNLOCK_VOICE_REMOTE = "unlock_voice_remote"
CARD_UNLOCK_USER = "card_unlock_user"
FACE_UNLOCK_USER = "face_unlock_user"
HAND_UNLOCK_USER = "hand_unlock_user"
FINGERPRINT_UNLOCK_USER = "fingerprint_unlock_user"
PASSWORD_UNLOCK_USER = "password_unlock_user"
USAGE_TIMES = "usage_times"
USE_TIME_1 = "use_time_1"
USE_TIME_2 = "use_time_2"
Expand Down
50 changes: 42 additions & 8 deletions custom_components/xtend_tuya/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
TuyaDPCode,
TuyaDPType,
TuyaCoverDPCodePercentageMappingWrapper,
TuyaCoverIsClosedWrapper,
TuyaCoverIsClosedEnumWrapper,
TuyaCoverIsClosedInvertedWrapper,
tuya_cover_get_instruction_wrapper,
TuyaDeviceWrapper,
TuyaRemapHelper,
Expand Down Expand Up @@ -77,7 +78,7 @@ def get_entity_instance(
hass: HomeAssistant,
*,
current_position: XTCoverDPCodePercentageMappingWrapper | None,
current_state_wrapper: TuyaCoverIsClosedWrapper | None,
current_state_wrapper: TuyaCoverIsClosedInvertedWrapper | TuyaCoverIsClosedEnumWrapper | None,
instruction_wrapper: TuyaDeviceWrapper | None,
set_position: XTCoverDPCodePercentageMappingWrapper | None,
tilt_position: TuyaCoverDPCodePercentageMappingWrapper | None,
Expand Down Expand Up @@ -305,12 +306,13 @@ def __init__(
hass: HomeAssistant,
*,
current_position: XTCoverDPCodePercentageMappingWrapper | None,
current_state_wrapper: TuyaCoverIsClosedWrapper | None,
current_state_wrapper: TuyaCoverIsClosedInvertedWrapper | TuyaCoverIsClosedEnumWrapper | None,
instruction_wrapper: TuyaDeviceWrapper | None,
set_position: XTCoverDPCodePercentageMappingWrapper | None,
tilt_position: TuyaCoverDPCodePercentageMappingWrapper | None,
) -> None:
"""Initialize the cover entity."""
device_manager.device_watcher.report_message(device.id, f"Initializing cover entity {device.name}: current_position: {current_position.dpcode if current_position else None}, set_position: {set_position.dpcode if set_position else None}", device)
super(XTCoverEntity, self).__init__(device, device_manager, description)
super(XTEntity, self).__init__(
device,
Expand Down Expand Up @@ -408,18 +410,50 @@ def current_cover_position(self) -> int | None:
)
return current_cover_position

async def _async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
if self._set_position is not None:
await self._async_send_commands(
self._set_position.get_update_commands(self.device, 100)
)
return

if (
self._instruction_wrapper
and (options := self._instruction_wrapper.options)
and "open" in options
):
await self._async_send_wrapper_updates(self._instruction_wrapper, "open")
return

async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
if self.is_cover_control_inverted:
await super().async_close_cover(**kwargs)
await self._async_close_cover(**kwargs)
else:
await super().async_open_cover(**kwargs)
await self._async_open_cover(**kwargs)

async def _async_close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
if self._set_position is not None:
await self._async_send_commands(
self._set_position.get_update_commands(self.device, 0)
)
return

if (
self._instruction_wrapper
and (options := self._instruction_wrapper.options)
and "close" in options
):
await self._async_send_wrapper_updates(self._instruction_wrapper, "close")
return

async def async_close_cover(self, **kwargs: Any) -> None:
if self.is_cover_control_inverted:
await super().async_open_cover(**kwargs)
await self._async_open_cover(**kwargs)
else:
await super().async_close_cover(**kwargs)
await self._async_close_cover(**kwargs)

async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
Expand All @@ -437,7 +471,7 @@ def get_entity_instance(
hass: HomeAssistant,
*,
current_position: XTCoverDPCodePercentageMappingWrapper | None,
current_state_wrapper: TuyaCoverIsClosedWrapper | None,
current_state_wrapper: TuyaCoverIsClosedInvertedWrapper | TuyaCoverIsClosedEnumWrapper | None,
instruction_wrapper: TuyaDeviceWrapper | None,
set_position: XTCoverDPCodePercentageMappingWrapper | None,
tilt_position: TuyaCoverDPCodePercentageMappingWrapper | None,
Expand Down
40 changes: 35 additions & 5 deletions custom_components/xtend_tuya/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
from .const import (
TUYA_DISCOVERY_NEW,
LOGGER,
XTDPCode,
)
from .ha_tuya_integration.tuya_integration_imports import (
TuyaEventEntity,
TuyaEventEntityDescription,
TuyaEventDPCodeEventWrapper,
TuyaDeviceWrapper,
)
from .entity import (
XTEntity,
Expand All @@ -45,7 +46,7 @@ def get_entity_instance(
device: XTDevice,
device_manager: MultiManager,
description: XTEventEntityDescription,
dpcode_wrapper: TuyaEventDPCodeEventWrapper,
dpcode_wrapper: TuyaDeviceWrapper,
) -> XTEventEntity:
return XTEventEntity(
device=device,
Expand All @@ -59,7 +60,36 @@ def get_entity_instance(
# default status set of each category (that don't have a set instruction)
# end up being events.
# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq
EVENTS: dict[str, tuple[XTEventEntityDescription, ...]] = {}
EVENTS: dict[str, tuple[XTEventEntityDescription, ...]] = {
# Smart Lock - Track who unlocked the door
"jtmspro": (
XTEventEntityDescription(
key=XTDPCode.CARD_UNLOCK_USER,
translation_key="card_unlock_user",
device_class=None,
),
XTEventEntityDescription(
key=XTDPCode.FACE_UNLOCK_USER,
translation_key="face_unlock_user",
device_class=None,
),
XTEventEntityDescription(
key=XTDPCode.HAND_UNLOCK_USER,
translation_key="hand_unlock_user",
device_class=None,
),
XTEventEntityDescription(
key=XTDPCode.FINGERPRINT_UNLOCK_USER,
translation_key="fingerprint_unlock_user",
device_class=None,
),
XTEventEntityDescription(
key=XTDPCode.PASSWORD_UNLOCK_USER,
translation_key="password_unlock_user",
device_class=None,
),
),
}


async def async_setup_entry(
Expand Down Expand Up @@ -179,7 +209,7 @@ def __init__(
device: XTDevice,
device_manager: MultiManager,
description: XTEventEntityDescription,
dpcode_wrapper: TuyaEventDPCodeEventWrapper,
dpcode_wrapper: TuyaDeviceWrapper[tuple[str, dict[str, Any] | None]],
) -> None:
"""Init Tuya event entity."""
try:
Expand All @@ -201,7 +231,7 @@ def get_entity_instance(
description: XTEventEntityDescription,
device: XTDevice,
device_manager: MultiManager,
dpcode_wrapper: TuyaEventDPCodeEventWrapper,
dpcode_wrapper: TuyaDeviceWrapper,
) -> XTEventEntity:
if hasattr(description, "get_entity_instance") and callable(
getattr(description, "get_entity_instance")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@
_RoundedIntegerWrapper as TuyaClimateRoundedIntegerWrapper, # noqa: F401
_get_temperature_wrapper as tuya_climate_get_temperature_wrapper, # noqa: F401
_SwingModeWrapper as TuyaClimateSwingModeWrapper, # noqa: F401
_PresetWrapper as TuyaClimatePresetWrapper, # noqa: F401
_HvacModeWrapper as TuyaClimateHvacModeWrapper, # noqa: F401
)
from homeassistant.components.tuya.cover import (
COVERS as COVERS_TUYA, # noqa: F401
TuyaCoverEntity as TuyaCoverEntity,
TuyaCoverEntityDescription as TuyaCoverEntityDescription,
_DPCodePercentageMappingWrapper as TuyaCoverDPCodePercentageMappingWrapper, # noqa: F401
_IsClosedWrapper as TuyaCoverIsClosedWrapper, # noqa: F401
_IsClosedEnumWrapper as TuyaCoverIsClosedEnumWrapper, # noqa: F401
_IsClosedInvertedWrapper as TuyaCoverIsClosedInvertedWrapper, # noqa: F401
_get_instruction_wrapper as tuya_cover_get_instruction_wrapper, # noqa: F401
)
from homeassistant.components.event import (
Expand All @@ -49,7 +52,6 @@
EVENTS as EVENTS_TUYA, # noqa: F401 # type: ignore
TuyaEventEntity as TuyaEventEntity,
TuyaEventEntityDescription as TuyaEventEntityDescription,
_DPCodeEventWrapper as TuyaEventDPCodeEventWrapper, # noqa: F401
)
except Exception:
EVENTS_TUYA: dict[str, tuple[EventEntityDescription, ...]] = {}
Expand Down Expand Up @@ -120,6 +122,8 @@
)
from homeassistant.components.tuya.vacuum import (
TuyaVacuumEntity as TuyaVacuumEntity,
_VacuumActionWrapper as TuyaVacuumActionWrapper, # noqa: F401
_VacuumActivityWrapper as TuyaVacuumActivityWrapper, # noqa: F401
)
import homeassistant.components.tuya as tuya_integration # noqa: F401

Expand Down
Loading