From 483f2db07cc3845687f28e2d9dc0ff6a691bd2de Mon Sep 17 00:00:00 2001 From: Bander <46300268+xZetsubou@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:38:34 +0300 Subject: [PATCH 1/7] attempt to fix RF commands --- custom_components/localtuya/remote.py | 90 ++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 17 deletions(-) diff --git a/custom_components/localtuya/remote.py b/custom_components/localtuya/remote.py index 0aa177e05..7bd458ae6 100644 --- a/custom_components/localtuya/remote.py +++ b/custom_components/localtuya/remote.py @@ -57,6 +57,25 @@ class RemoteDP(StrEnum): DP_RECIEVE = "202" +MODE_IR_TO_RF = { + ControlMode.SEND_IR: "rfstudy_send", + ControlMode.STUDY: "rf_study", + ControlMode.STUDY_EXIT: "rfstudy_exit", + ControlMode.STUDY_KEY: "rf_study", +} + +MODE_RF_TO_SHORT = { + MODE_IR_TO_RF[ControlMode.STUDY]: "rf_shortstudy", + MODE_IR_TO_RF[ControlMode.STUDY_EXIT]: "rfstudy_exit", +} +ATTR_FEQ = "feq" +ATTR_VER = "ver" +ATTR_RF_TYPE = "rf_type" +ATTR_TIMES = "times" +ATTR_DELAY = "delay" +ATTR_INTERVALS = "intervals" +ATTR_STUDY_FREQ = "study_feq" + CODE_STORAGE_VERSION = 1 SOTRAGE_KEY = "localtuya_remotes_codes" @@ -114,6 +133,15 @@ def _ir_control_type(self): else: return ControlType.JSON + @staticmethod + def rf_decode_button(base64_code): + try: + jstr = base64.b64decode + jdata = json.loads(jstr) + return jdata + except: + return None + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the remote.""" self._attr_is_on = True @@ -150,13 +178,13 @@ async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> Non # pulses = self.pronto_to_pulses(option_value) # base64_code = "1" + self.pulses_to_base64(pulses) for command in commands: - code = self._get_code(device, command) + code, is_rf = self._get_code(device, command) base64_code = "1" + code if repeats: current_repeat = 0 while current_repeat < repeats: - await self.send_signal(ControlMode.SEND_IR, base64_code) + await self.send_signal(ControlMode.SEND_IR, base64_code, rf=is_rf) if repeats_delay: await asyncio.sleep(repeats_delay) current_repeat += 1 @@ -174,6 +202,8 @@ async def async_learn_command(self, **kwargs: Any) -> None: device = kwargs.get(ATTR_DEVICE) commands = kwargs.get(ATTR_COMMAND) + + is_rf = kwargs.get(ATTR_COMMAND_TYPE) == "rf" # command_type = kwargs.get(ATTR_COMMAND_TYPE) for req in [device, commands]: if not req: @@ -188,7 +218,7 @@ async def async_learn_command(self, **kwargs: Any) -> None: async with self._lock: for command in commands: last_code = self._last_code - await self.send_signal(ControlMode.STUDY) + await self.send_signal(ControlMode.STUDY, rf=is_rf) persistent_notification.async_create( self.hass, f"Press the '{command}' button.", @@ -199,23 +229,20 @@ async def async_learn_command(self, **kwargs: Any) -> None: try: self.debug(f"Waiting for code from DP: {self._dp_recieve}") while now < timeout: - if ( - last_code != (dp_code := self.dp_value(self._dp_recieve)) - and dp_code is not None - ): + if last_code != (dp_code := self.dp_value(self._dp_recieve)): self._last_code = dp_code sucess = True - await self.send_signal(ControlMode.STUDY_EXIT) + await self.send_signal(ControlMode.STUDY_EXIT, rf=is_rf) break now += 1 await asyncio.sleep(1) if not sucess: - await self.send_signal(ControlMode.STUDY_EXIT) raise ServiceValidationError(f"Failed to learn: {command}") finally: + await self.send_signal(ControlMode.STUDY_EXIT, rf=is_rf) persistent_notification.async_dismiss( self.hass, notification_id="learn_command" ) @@ -242,7 +269,7 @@ async def async_delete_command(self, **kwargs: Any) -> None: for command in commands: await self._delete_command(device, command) - async def send_signal(self, control, base64_code=None): + async def send_signal(self, control, base64_code=None, rf=False): if self._ir_control_type == ControlType.ENUM: command = {self._dp_id: control} if control == ControlMode.SEND_IR: @@ -250,14 +277,40 @@ async def send_signal(self, control, base64_code=None): command[self._dp_key_study] = base64_code command["13"] = 0 else: - command = {NSDP_CONTROL: control} - if control == ControlMode.SEND_IR: - command[NSDP_TYPE] = 0 - command[NSDP_HEAD] = "" # also known as ir_code - command[NSDP_KEY1] = base64_code # also code: key_code + command = {NSDP_CONTROL: control if not rf else MODE_IR_TO_RF.get(control)} + if rf: + for attr, default_value in ( + (ATTR_RF_TYPE, "sub_2g"), + (ATTR_VER, "2"), + ): + if attr not in command: + command[attr] = default_value + if control == ControlMode.SEND_IR: + for attr, default_value in ( + (ATTR_TIMES, "1"), + (ATTR_DELAY, "0"), + (ATTR_INTERVALS, "0"), + (ATTR_FEQ, "0"), + ): + command[NSDP_KEY1] = {} + if attr not in command[NSDP_KEY1]: + command[NSDP_KEY1][attr] = default_value + if control in (ControlMode.STUDY, ControlMode.STUDY_EXIT): + if ATTR_STUDY_FREQ not in command: + command[ATTR_STUDY_FREQ] = "0" + else: + if control == ControlMode.SEND_IR: + command[NSDP_TYPE] = 0 + command[NSDP_HEAD] = "" # also known as ir_code + command[NSDP_KEY1] = base64_code # also code: key_code + command = {self._dp_id: json.dumps(command)} - self.debug(f"Sending IR Command: {command}") + self.debug(f"Sending Command: {command}") + if rf and base64_code: + decoded_code = self.rf_decode_button(base64_code) + self.debug(f"Decoded RF Button: {decoded_code}") + await self._device.set_dps(command) async def _delete_command(self, device, command) -> None: @@ -276,6 +329,7 @@ async def _delete_command(self, device, command) -> None: commands = devices_data[device] if command not in commands: + commands.pop("rf", False) raise ServiceValidationError( f"Couldn't find the command {command} for in {device} device. the available commands for this device is: {list(commands)}" ) @@ -339,13 +393,15 @@ def _get_code(self, device, command): commands = devices_data[device] if command not in commands: + commands.pop("rf", False) raise ServiceValidationError( f"Couldn't find the command {command} for in {device} device. the available commands for this device is: {list(commands)}" ) command = devices_data[device][command] + is_rf = devices_data[device].get("rf") - return command + return command, is_rf async def _async_migrate_func(self, old_major_version, old_minor_version, old_data): """Migrate to the new version.""" From 1f7a7c798673d445d45b83d9c2198009350d6420 Mon Sep 17 00:00:00 2001 From: Bander <46300268+xZetsubou@users.noreply.github.com> Date: Sat, 21 Dec 2024 14:30:19 +0300 Subject: [PATCH 2/7] fix learn/send for rf --- custom_components/localtuya/remote.py | 48 +++++++++++++++------------ 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/custom_components/localtuya/remote.py b/custom_components/localtuya/remote.py index 7bd458ae6..da0b1c1ad 100644 --- a/custom_components/localtuya/remote.py +++ b/custom_components/localtuya/remote.py @@ -133,14 +133,13 @@ def _ir_control_type(self): else: return ControlType.JSON - @staticmethod def rf_decode_button(base64_code): try: - jstr = base64.b64decode + jstr = base64.b64decode(base64_code) jdata = json.loads(jstr) return jdata except: - return None + return {} async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the remote.""" @@ -178,13 +177,13 @@ async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> Non # pulses = self.pronto_to_pulses(option_value) # base64_code = "1" + self.pulses_to_base64(pulses) for command in commands: - code, is_rf = self._get_code(device, command) + code = self._get_code(device, command) - base64_code = "1" + code + base64_code = code if repeats: current_repeat = 0 while current_repeat < repeats: - await self.send_signal(ControlMode.SEND_IR, base64_code, rf=is_rf) + await self.send_signal(ControlMode.SEND_IR, base64_code) if repeats_delay: await asyncio.sleep(repeats_delay) current_repeat += 1 @@ -270,6 +269,8 @@ async def async_delete_command(self, **kwargs: Any) -> None: await self._delete_command(device, command) async def send_signal(self, control, base64_code=None, rf=False): + rf_data = self.rf_decode_button(base64_code) + if self._ir_control_type == ControlType.ENUM: command = {self._dp_id: control} if control == ControlMode.SEND_IR: @@ -277,39 +278,43 @@ async def send_signal(self, control, base64_code=None, rf=False): command[self._dp_key_study] = base64_code command["13"] = 0 else: - command = {NSDP_CONTROL: control if not rf else MODE_IR_TO_RF.get(control)} - if rf: + command = { + NSDP_CONTROL: MODE_IR_TO_RF.get(control) if (rf_data or rf) else control + } + if rf_data or rf: + if freq := rf_data.get(ATTR_STUDY_FREQ): + command[ATTR_STUDY_FREQ] = freq + if ver := rf_data.get(ATTR_VER): + command[ATTR_VER] = ver + for attr, default_value in ( (ATTR_RF_TYPE, "sub_2g"), (ATTR_VER, "2"), + (ATTR_STUDY_FREQ, "433"), ): if attr not in command: command[attr] = default_value + if control == ControlMode.SEND_IR: + command[NSDP_KEY1] = {"code": base64_code} for attr, default_value in ( - (ATTR_TIMES, "1"), + (ATTR_TIMES, "6"), (ATTR_DELAY, "0"), (ATTR_INTERVALS, "0"), - (ATTR_FEQ, "0"), ): - command[NSDP_KEY1] = {} if attr not in command[NSDP_KEY1]: command[NSDP_KEY1][attr] = default_value - if control in (ControlMode.STUDY, ControlMode.STUDY_EXIT): - if ATTR_STUDY_FREQ not in command: - command[ATTR_STUDY_FREQ] = "0" else: if control == ControlMode.SEND_IR: command[NSDP_TYPE] = 0 command[NSDP_HEAD] = "" # also known as ir_code - command[NSDP_KEY1] = base64_code # also code: key_code + command[NSDP_KEY1] = "1" + base64_code # also code: key_code command = {self._dp_id: json.dumps(command)} self.debug(f"Sending Command: {command}") - if rf and base64_code: - decoded_code = self.rf_decode_button(base64_code) - self.debug(f"Decoded RF Button: {decoded_code}") + if rf_data: + self.debug(f"Decoded RF Button: {rf_data}") await self._device.set_dps(command) @@ -319,7 +324,7 @@ async def _delete_command(self, device, command) -> None: ir_controller = self._device_id devices_data = self._global_codes - if ir_controller in codes_data: + if ir_controller in codes_data and device in codes_data[ir_controller]: devices_data = codes_data[ir_controller] if device not in devices_data: @@ -383,7 +388,7 @@ def _get_code(self, device, command): ir_controller = self._device_id devices_data = self._global_codes - if ir_controller in codes_data: + if ir_controller in codes_data and device in codes_data[ir_controller]: devices_data = codes_data[ir_controller] if device not in devices_data: @@ -399,9 +404,8 @@ def _get_code(self, device, command): ) command = devices_data[device][command] - is_rf = devices_data[device].get("rf") - return command, is_rf + return command async def _async_migrate_func(self, old_major_version, old_minor_version, old_data): """Migrate to the new version.""" From 056b9efcd7e01aa577061bf94de9cb6ffade1718 Mon Sep 17 00:00:00 2001 From: Bander <46300268+xZetsubou@users.noreply.github.com> Date: Sat, 21 Dec 2024 15:13:20 +0300 Subject: [PATCH 3/7] fix decode func --- custom_components/localtuya/remote.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/custom_components/localtuya/remote.py b/custom_components/localtuya/remote.py index da0b1c1ad..7d0aafdf2 100644 --- a/custom_components/localtuya/remote.py +++ b/custom_components/localtuya/remote.py @@ -90,6 +90,15 @@ def flow_schema(dps): } +def rf_decode_button(base64_code): + try: + jstr = base64.b64decode(base64_code) + jdata = json.loads(jstr) + return jdata + except: + return {} + + class LocalTuyaRemote(LocalTuyaEntity, RemoteEntity): """Representation of a Tuya remote.""" @@ -133,14 +142,6 @@ def _ir_control_type(self): else: return ControlType.JSON - def rf_decode_button(base64_code): - try: - jstr = base64.b64decode(base64_code) - jdata = json.loads(jstr) - return jdata - except: - return {} - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the remote.""" self._attr_is_on = True From 4633c4437f23fe894fb73c3f1e6a10a58af5b949 Mon Sep 17 00:00:00 2001 From: Bander <46300268+xZetsubou@users.noreply.github.com> Date: Sat, 21 Dec 2024 15:15:58 +0300 Subject: [PATCH 4/7] pub func rf decode --- custom_components/localtuya/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/remote.py b/custom_components/localtuya/remote.py index 7d0aafdf2..e1c5d9b40 100644 --- a/custom_components/localtuya/remote.py +++ b/custom_components/localtuya/remote.py @@ -270,7 +270,7 @@ async def async_delete_command(self, **kwargs: Any) -> None: await self._delete_command(device, command) async def send_signal(self, control, base64_code=None, rf=False): - rf_data = self.rf_decode_button(base64_code) + rf_data = rf_decode_button(base64_code) if self._ir_control_type == ControlType.ENUM: command = {self._dp_id: control} From 04dc600f37f0064ec69b9c9b064c139f88a272c6 Mon Sep 17 00:00:00 2001 From: Bander <46300268+xZetsubou@users.noreply.github.com> Date: Sun, 22 Dec 2024 17:57:40 +0300 Subject: [PATCH 5/7] set default freq 433.92 instead and use event for wait --- custom_components/localtuya/remote.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/custom_components/localtuya/remote.py b/custom_components/localtuya/remote.py index e1c5d9b40..380689463 100644 --- a/custom_components/localtuya/remote.py +++ b/custom_components/localtuya/remote.py @@ -5,7 +5,6 @@ import base64 import logging from functools import partial -import struct from enum import StrEnum from typing import Any, Iterable from .config_flow import col_to_select @@ -118,6 +117,7 @@ def __init__( self._device_id = self._device_config.id self._lock = asyncio.Lock() + self._event = asyncio.Event() # self._attr_activity_list: list = [] # self._attr_current_activity: str | None = None @@ -217,7 +217,6 @@ async def async_learn_command(self, **kwargs: Any) -> None: async with self._lock: for command in commands: - last_code = self._last_code await self.send_signal(ControlMode.STUDY, rf=is_rf) persistent_notification.async_create( self.hass, @@ -228,20 +227,12 @@ async def async_learn_command(self, **kwargs: Any) -> None: try: self.debug(f"Waiting for code from DP: {self._dp_recieve}") - while now < timeout: - if last_code != (dp_code := self.dp_value(self._dp_recieve)): - self._last_code = dp_code - sucess = True - await self.send_signal(ControlMode.STUDY_EXIT, rf=is_rf) - break - - now += 1 - await asyncio.sleep(1) - - if not sucess: - raise ServiceValidationError(f"Failed to learn: {command}") - + await asyncio.wait_for(self._event.wait(), timeout) + await self._save_new_command(device, command, self._last_code) + except TimeoutError: + raise ServiceValidationError(f"Timeout: Failed to learn: {command}") finally: + self._event.clear() await self.send_signal(ControlMode.STUDY_EXIT, rf=is_rf) persistent_notification.async_dismiss( self.hass, notification_id="learn_command" @@ -249,7 +240,6 @@ async def async_learn_command(self, **kwargs: Any) -> None: # code retrive sucess and it's sotred in self._last_code # we will store the codes. - await self._save_new_command(device, command, self._last_code) if command != commands[-1]: await asyncio.sleep(1) @@ -290,8 +280,8 @@ async def send_signal(self, control, base64_code=None, rf=False): for attr, default_value in ( (ATTR_RF_TYPE, "sub_2g"), + (ATTR_STUDY_FREQ, "433.92"), (ATTR_VER, "2"), - (ATTR_STUDY_FREQ, "433"), ): if attr not in command: command[attr] = default_value @@ -415,6 +405,9 @@ async def _async_migrate_func(self, old_major_version, old_minor_version, old_da def status_updated(self): """Device status was updated.""" state = self.dp_value(self._dp_id) + if (dp_recv := self.dp_value(self._dp_recieve)) != self._last_code: + self._last_code = dp_recv + self._event.set() def status_restored(self, stored_state: State) -> None: """Device status was restored..""" From f58aeec4bc2ab36d3364700fc982ad4aa97fdaa1 Mon Sep 17 00:00:00 2001 From: Bander <46300268+xZetsubou@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:21:14 +0300 Subject: [PATCH 6/7] added "mode, rate and feq" to payload. --- custom_components/localtuya/remote.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/localtuya/remote.py b/custom_components/localtuya/remote.py index 380689463..50d009072 100644 --- a/custom_components/localtuya/remote.py +++ b/custom_components/localtuya/remote.py @@ -270,7 +270,7 @@ async def send_signal(self, control, base64_code=None, rf=False): command["13"] = 0 else: command = { - NSDP_CONTROL: MODE_IR_TO_RF.get(control) if (rf_data or rf) else control + NSDP_CONTROL: MODE_IR_TO_RF[control] if (rf_data or rf) else control } if rf_data or rf: if freq := rf_data.get(ATTR_STUDY_FREQ): @@ -282,6 +282,9 @@ async def send_signal(self, control, base64_code=None, rf=False): (ATTR_RF_TYPE, "sub_2g"), (ATTR_STUDY_FREQ, "433.92"), (ATTR_VER, "2"), + ("feq", "0"), + ("rate", "0"), + ("mode", "0"), ): if attr not in command: command[attr] = default_value From 12ee9308dc22d52c6bd84c231c4f2e21ca65cbec Mon Sep 17 00:00:00 2001 From: Bander <46300268+xZetsubou@users.noreply.github.com> Date: Sun, 12 Jan 2025 03:12:38 +0300 Subject: [PATCH 7/7] pump ha version --- hacs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index 2c0369d1c..e51805707 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "Local Tuya", - "homeassistant": "2024.11.0", + "homeassistant": "2025.1.0", "render_readme": true, "persistent_directory": "templates" }