From d8de6e34dde374c3dec8edde22185db5358c1400 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 16 Jul 2025 09:24:20 +0200 Subject: [PATCH] Add support for Tuya ks category (tower fan) (#148811) --- homeassistant/components/tuya/fan.py | 22 +++- homeassistant/components/tuya/light.py | 9 ++ homeassistant/components/tuya/switch.py | 8 ++ tests/components/tuya/__init__.py | 6 + .../tuya/fixtures/ks_tower_fan.json | 107 ++++++++++++++++++ tests/components/tuya/snapshots/test_fan.ambr | 64 +++++++++++ .../components/tuya/snapshots/test_light.ambr | 57 ++++++++++ .../tuya/snapshots/test_switch.ambr | 48 ++++++++ 8 files changed, 316 insertions(+), 5 deletions(-) create mode 100644 tests/components/tuya/fixtures/ks_tower_fan.json diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index f96ea2c0a65..90f4132cef0 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -26,11 +26,23 @@ from .entity import TuyaEntity from .models import EnumTypeData, IntegerTypeData TUYA_SUPPORT_TYPE = { - "cs", # Dehumidifier - "fs", # Fan - "fsd", # Fan with Light - "fskg", # Fan wall switch - "kj", # Air Purifier + # Dehumidifier + # https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha + "cs", + # Fan + # https://developer.tuya.com/en/docs/iot/categoryfs?id=Kaiuz1xweel1c + "fs", + # Ceiling Fan Light + # https://developer.tuya.com/en/docs/iot/fsd?id=Kaof8eiei4c2v + "fsd", + # Fan wall switch + "fskg", + # Air Purifier + # https://developer.tuya.com/en/docs/iot/f?id=K9gf46h2s6dzm + "kj", + # Undocumented tower fan + # https://github.com/orgs/home-assistant/discussions/329 + "ks", } diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 3f8fc7d0fb9..b6d0332e03a 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -242,6 +242,15 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Undocumented tower fan + # https://github.com/orgs/home-assistant/discussions/329 + "ks": ( + TuyaLightEntityDescription( + key=DPCode.LIGHT, + translation_key="backlight", + entity_category=EntityCategory.CONFIG, + ), + ), # Unknown light product # Found as VECINO RGBW as provided by diagnostics # Not documented diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index f455424c2c1..bfe80ec67bf 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -431,6 +431,14 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Undocumented tower fan + # https://github.com/orgs/home-assistant/discussions/329 + "ks": ( + SwitchEntityDescription( + key=DPCode.ANION, + translation_key="ionizer", + ), + ), # Alarm Host # https://developer.tuya.com/en/docs/iot/alarm-hosts?id=K9gf48r87hyjk "mal": ( diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 129930b810f..6427a69cdea 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -79,6 +79,12 @@ DEVICE_MOCKS = { Platform.SELECT, Platform.SWITCH, ], + "ks_tower_fan": [ + # https://github.com/orgs/home-assistant/discussions/329 + Platform.FAN, + Platform.LIGHT, + Platform.SWITCH, + ], "mal_alarm_host": [ # Alarm Host support Platform.ALARM_CONTROL_PANEL, diff --git a/tests/components/tuya/fixtures/ks_tower_fan.json b/tests/components/tuya/fixtures/ks_tower_fan.json new file mode 100644 index 00000000000..071596e8e6c --- /dev/null +++ b/tests/components/tuya/fixtures/ks_tower_fan.json @@ -0,0 +1,107 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "terminal_id": "mock_terminal_id", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "id": "mock_device_id", + "name": "Tower Fan CA-407G Smart", + "category": "ks", + "product_id": "j9fa8ahzac8uvlfl", + "product_name": "Tower Fan CA-407G Smart", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-07-14T11:22:54+00:00", + "create_time": "2025-07-14T11:22:54+00:00", + "update_time": "2025-07-14T11:22:54+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "fan_speed": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 12, + "scale": 0, + "step": 1 + } + }, + "mode": { + "type": "Enum", + "value": { + "range": ["ordinary", "nature", "sleep"] + } + }, + "switch_horizontal": { + "type": "Boolean", + "value": {} + }, + "anion": { + "type": "Boolean", + "value": {} + }, + "light": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "fan_speed": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 12, + "scale": 0, + "step": 1 + } + }, + "mode": { + "type": "Enum", + "value": { + "range": ["ordinary", "nature", "sleep"] + } + }, + "switch_horizontal": { + "type": "Boolean", + "value": {} + }, + "anion": { + "type": "Boolean", + "value": {} + }, + "light": { + "type": "Boolean", + "value": {} + }, + "countdown_left": { + "type": "Integer", + "value": { + "unit": "min", + "min": 0, + "max": 721, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": false, + "fan_speed": 5, + "mode": "ordinary", + "switch_horizontal": true, + "anion": false, + "light": true, + "countdown_left": 0 + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_fan.ambr b/tests/components/tuya/snapshots/test_fan.ambr index 2a7ea120dd5..ff795c150c9 100644 --- a/tests/components/tuya/snapshots/test_fan.ambr +++ b/tests/components/tuya/snapshots/test_fan.ambr @@ -210,3 +210,67 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[ks_tower_fan][fan.tower_fan_ca_407g_smart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': list([ + 'ordinary', + 'nature', + 'sleep', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.tower_fan_ca_407g_smart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.mock_device_id', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[ks_tower_fan][fan.tower_fan_ca_407g_smart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tower Fan CA-407G Smart', + 'oscillating': True, + 'percentage': 37, + 'percentage_step': 1.0, + 'preset_mode': 'ordinary', + 'preset_modes': list([ + 'ordinary', + 'nature', + 'sleep', + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.tower_fan_ca_407g_smart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index b9395b3d682..b83e9484853 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -56,3 +56,60 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[ks_tower_fan][light.tower_fan_ca_407g_smart_backlight-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': , + 'entity_id': 'light.tower_fan_ca_407g_smart_backlight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Backlight', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'backlight', + 'unique_id': 'tuya.mock_device_idlight', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[ks_tower_fan][light.tower_fan_ca_407g_smart_backlight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'Tower Fan CA-407G Smart Backlight', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.tower_fan_ca_407g_smart_backlight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 1ba823e192d..4e6af0fa7d3 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -677,6 +677,54 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[ks_tower_fan][switch.tower_fan_ca_407g_smart_ionizer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.tower_fan_ca_407g_smart_ionizer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Ionizer', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ionizer', + 'unique_id': 'tuya.mock_device_idanion', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[ks_tower_fan][switch.tower_fan_ca_407g_smart_ionizer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tower Fan CA-407G Smart Ionizer', + }), + 'context': , + 'entity_id': 'switch.tower_fan_ca_407g_smart_ionizer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[mal_alarm_host][switch.multifunction_alarm_arm_beep-entry] EntityRegistryEntrySnapshot({ 'aliases': set({