From 9fae4e7e1f742efc666aa8d0dc65d59b2751d34a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:00:54 +0200 Subject: [PATCH] Add support for Tuya bzyd category (white noise machine) (#152025) --- homeassistant/components/tuya/const.py | 2 + homeassistant/components/tuya/entity.py | 2 +- homeassistant/components/tuya/light.py | 11 +- homeassistant/components/tuya/number.py | 8 + homeassistant/components/tuya/strings.json | 6 + homeassistant/components/tuya/switch.py | 25 +++ tests/components/tuya/conftest.py | 10 +- .../components/tuya/snapshots/test_init.ambr | 4 +- .../components/tuya/snapshots/test_light.ambr | 132 ++++++++++++++++ .../tuya/snapshots/test_number.ambr | 116 ++++++++++++++ .../tuya/snapshots/test_switch.ambr | 146 ++++++++++++++++++ 11 files changed, 453 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index a1ad046692d..19c7ffac7dd 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -331,6 +331,7 @@ class DPCode(StrEnum): SMOKE_SENSOR_STATE = "smoke_sensor_state" SMOKE_SENSOR_STATUS = "smoke_sensor_status" SMOKE_SENSOR_VALUE = "smoke_sensor_value" + SNOOZE = "snooze" SOS = "sos" # Emergency State SOS_STATE = "sos_state" # Emergency mode SPEED = "speed" # Speed level @@ -371,6 +372,7 @@ class DPCode(StrEnum): SWITCH_MODE7 = "switch_mode7" SWITCH_MODE8 = "switch_mode8" SWITCH_MODE9 = "switch_mode9" + SWITCH_MUSIC = "switch_music" SWITCH_NIGHT_LIGHT = "switch_night_light" SWITCH_SAVE_ENERGY = "switch_save_energy" SWITCH_SOUND = "switch_sound" # Voice switch diff --git a/homeassistant/components/tuya/entity.py b/homeassistant/components/tuya/entity.py index 7d51a006877..1ed9aae1f22 100644 --- a/homeassistant/components/tuya/entity.py +++ b/homeassistant/components/tuya/entity.py @@ -126,7 +126,7 @@ class TuyaEntity(Entity): return None def get_dptype( - self, dpcode: DPCode | None, prefer_function: bool = False + self, dpcode: DPCode | None, *, prefer_function: bool = False ) -> DPType | None: """Find a matching DPCode data type available on for this device.""" if dpcode is None: diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 673e9b1ffb3..9dba24ec490 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -73,6 +73,15 @@ class TuyaLightEntityDescription(LightEntityDescription): LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { + # White noise machine + "bzyd": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + name=None, + color_mode=DPCode.WORK_MODE, + color_data=DPCode.COLOUR_DATA, + ), + ), # Curtain Switch # https://developer.tuya.com/en/docs/iot/category-clkg?id=Kaiuz0gitil39 "clkg": ( @@ -531,7 +540,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity): if ( dpcode := get_dpcode(self.device, description.color_data) - ) and self.get_dptype(dpcode) == DPType.JSON: + ) and self.get_dptype(dpcode, prefer_function=True) == DPType.JSON: self._color_data_dpcode = dpcode color_modes.add(ColorMode.HS) if dpcode in self.device.function: diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 3ee6900d228..6a4482821ba 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -65,6 +65,14 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # White noise machine + "bzyd": ( + NumberEntityDescription( + key=DPCode.VOLUME_SET, + translation_key="volume", + entity_category=EntityCategory.CONFIG, + ), + ), # CO2 Detector # https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy "co2bj": ( diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index d470492e9d7..7781fc926ca 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -981,6 +981,12 @@ }, "output_power_limit": { "name": "Output power limit" + }, + "music": { + "name": "Music" + }, + "snooze": { + "name": "Snooze" } }, "valve": { diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 62ea4d86b3d..208cd3e19b7 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -37,6 +37,31 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # White noise machine + "bzyd": ( + SwitchEntityDescription( + key=DPCode.SWITCH, + name=None, + ), + SwitchEntityDescription( + key=DPCode.CHILD_LOCK, + translation_key="child_lock", + icon="mdi:account-lock", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_MUSIC, + translation_key="music", + icon="mdi:music", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.SNOOZE, + translation_key="snooze", + icon="mdi:alarm-snooze", + entity_category=EntityCategory.CONFIG, + ), + ), # Curtain # https://developer.tuya.com/en/docs/iot/f?id=K9gf46o5mtfyc "cl": ( diff --git a/tests/components/tuya/conftest.py b/tests/components/tuya/conftest.py index a699eb7846c..21e558b7192 100644 --- a/tests/components/tuya/conftest.py +++ b/tests/components/tuya/conftest.py @@ -208,11 +208,11 @@ async def _create_device(hass: HomeAssistant, mock_device_code: str) -> Customer } device.status = details["status"] for key, value in device.status.items(): - # Some devices to not provide a status_range for all status DPs - dp_type = device.status_range.get(key) - if dp_type is None: - dp_type = device.function[key] - if dp_type.type == "Json": + # Some devices do not provide a status_range for all status DPs + # Others set the type as String in status_range and as Json in function + if ((dp_type := device.status_range.get(key)) and dp_type.type == "Json") or ( + (dp_type := device.function.get(key)) and dp_type.type == "Json" + ): device.status[key] = json_dumps(value) return device diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index a3b0b0b10c8..533aee7d687 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -2004,7 +2004,7 @@ 'labels': set({ }), 'manufacturer': 'Tuya', - 'model': 'BlissRadia (unsupported)', + 'model': 'BlissRadia ', 'model_id': 'ssimhf6r8kgwepfb', 'name': 'BlissRadia ', 'name_by_user': None, @@ -5755,7 +5755,7 @@ 'labels': set({ }), 'manufacturer': 'Tuya', - 'model': 'Smart White Noise Machine (unsupported)', + 'model': 'Smart White Noise Machine', 'model_id': '45idzfufidgee7ir', 'name': 'Smart White Noise Machine', 'name_by_user': None, diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index 54c4b8784d6..c8d7556fa11 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -345,6 +345,67 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[light.blissradia-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': None, + 'entity_id': 'light.blissradia', + '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': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfpewgk8r6fhmissdyzbswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.blissradia-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'BlissRadia ', + 'hs_color': None, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.blissradia', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[light.cam_garage_indicator_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2957,6 +3018,77 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[light.smart_white_noise_machine-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': None, + 'entity_id': 'light.smart_white_noise_machine', + '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': 0, + 'translation_key': None, + 'unique_id': 'tuya.ri7eegdifufzdi54dyzbswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.smart_white_noise_machine-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 1003, + 'color_mode': , + 'friendly_name': 'Smart White Noise Machine', + 'hs_color': tuple( + 239.666, + 393.307, + ), + 'rgb_color': tuple( + -748, + -742, + 255, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + -0.03, + -0.215, + ), + }), + 'context': , + 'entity_id': 'light.smart_white_noise_machine', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[light.solar_zijpad-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_number.ambr b/tests/components/tuya/snapshots/test_number.ambr index 73dab1877e1..15003c65db0 100644 --- a/tests/components/tuya/snapshots/test_number.ambr +++ b/tests/components/tuya/snapshots/test_number.ambr @@ -58,6 +58,64 @@ 'state': '1.0', }) # --- +# name: test_platform_setup_and_discovery[number.blissradia_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 5.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.blissradia_volume', + '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': 'Volume', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume', + 'unique_id': 'tuya.bfpewgk8r6fhmissdyzbvolume_set', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[number.blissradia_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'BlissRadia Volume', + 'max': 100.0, + 'min': 5.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'number.blissradia_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- # name: test_platform_setup_and_discovery[number.boiler_temperature_controller_temperature_correction-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2223,6 +2281,64 @@ 'state': '-2.0', }) # --- +# name: test_platform_setup_and_discovery[number.smart_white_noise_machine_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.smart_white_noise_machine_volume', + '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': 'Volume', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume', + 'unique_id': 'tuya.ri7eegdifufzdi54dyzbvolume_set', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[number.smart_white_noise_machine_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart White Noise Machine Volume', + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'number.smart_white_noise_machine_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17.0', + }) +# --- # name: test_platform_setup_and_discovery[number.sous_vide_cooking_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 1b481daa945..7df3249aa67 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -1070,6 +1070,55 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[switch.blissradia_snooze-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': , + 'entity_id': 'switch.blissradia_snooze', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:alarm-snooze', + 'original_name': 'Snooze', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'snooze', + 'unique_id': 'tuya.bfpewgk8r6fhmissdyzbsnooze', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.blissradia_snooze-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'BlissRadia Snooze', + 'icon': 'mdi:alarm-snooze', + }), + 'context': , + 'entity_id': 'switch.blissradia_snooze', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[switch.bree_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -7403,6 +7452,103 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[switch.smart_white_noise_machine-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.smart_white_noise_machine', + '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': 0, + 'translation_key': None, + 'unique_id': 'tuya.ri7eegdifufzdi54dyzbswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_white_noise_machine-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart White Noise Machine', + }), + 'context': , + 'entity_id': 'switch.smart_white_noise_machine', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_white_noise_machine_music-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': , + 'entity_id': 'switch.smart_white_noise_machine_music', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:music', + 'original_name': 'Music', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'music', + 'unique_id': 'tuya.ri7eegdifufzdi54dyzbswitch_music', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_white_noise_machine_music-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart White Noise Machine Music', + 'icon': 'mdi:music', + }), + 'context': , + 'entity_id': 'switch.smart_white_noise_machine_music', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[switch.smoke_alarm_mute-entry] EntityRegistryEntrySnapshot({ 'aliases': set({