Skip to content

Commit c4d3bb9

Browse files
committed
Fix RFRemoteControlDevice rf_send_button silently sending malformed payloads
Three bugs that each independently caused rfstudy_send commands to be ignored by the device: - rf_decode_button: fix missing () on base64.b64decode call; always returned None - send_command: build correct rfstudy_send payload (feq as int, add mode/rate, inject ver into each key dict); study/exit commands are unaffected - rf_send_button: do not forward study_feq into feq; feq=0 tells the device to use the frequency embedded in the code, passing the actual value selects a different chip configuration path Adds regression tests for all three fixes.
1 parent e89b9fb commit c4d3bb9

File tree

2 files changed

+76
-4
lines changed

2 files changed

+76
-4
lines changed

tests.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
log.setLevel(level=logging.INFO)
1717
log.setLevel(level=logging.DEBUG) # Debug hack!
1818

19+
import base64
20+
1921
import tinytuya
22+
from tinytuya.Contrib.RFRemoteControlDevice import RFRemoteControlDevice
2023

2124
LOCAL_KEY = '0123456789abcdef'
2225

@@ -255,5 +258,59 @@ def test_not_a_bulb(self):
255258
self.assertDictEqual(result_payload, expected_payload)
256259

257260

261+
def build_mock_rf():
262+
d = RFRemoteControlDevice('DEVICE_ID_HERE', 'IP_ADDRESS_HERE', LOCAL_KEY, control_type=1)
263+
d.set_version(3.3)
264+
d.set_value = MagicMock()
265+
return d
266+
267+
268+
class TestRFRemoteControlDevice(unittest.TestCase):
269+
def test_rf_decode_button_returns_dict(self):
270+
# Bug 1: rf_decode_button was missing the () call on base64.b64decode,
271+
# causing it to always return None instead of the decoded JSON dict.
272+
sample = {"study_feq": "433", "ver": "2"}
273+
encoded = base64.b64encode(json.dumps(sample).encode()).decode()
274+
275+
result = RFRemoteControlDevice.rf_decode_button(encoded)
276+
277+
self.assertIsNotNone(result, "rf_decode_button returned None — function was not called")
278+
self.assertDictEqual(result, sample)
279+
280+
def test_rf_send_button_payload_structure(self):
281+
# Bug 2: send_command('rfstudy_send', ...) was building the wrong payload:
282+
# - used 'study_feq' (string) instead of 'feq' (int)
283+
# - omitted 'mode' and 'rate' fields
284+
# - omitted 'ver' inside each key dict
285+
# Bug 3: rf_send_button was forwarding study_feq from the decoded button into
286+
# feq, but feq must always be 0 so the device uses the frequency embedded in
287+
# the code. Passing the actual frequency value selects a different chip path.
288+
d = build_mock_rf()
289+
290+
# Use a button that has study_feq set to a non-zero value to confirm it is
291+
# NOT forwarded into the payload's feq field.
292+
button_data = {"study_feq": "433", "ver": "2"}
293+
base64_code = base64.b64encode(json.dumps(button_data).encode()).decode()
294+
295+
d.rf_send_button(base64_code)
296+
297+
call_args = d.set_value.call_args
298+
dp = call_args[0][0]
299+
payload = json.loads(call_args[0][1])
300+
301+
self.assertEqual(dp, RFRemoteControlDevice.DP_SEND_IR)
302+
self.assertEqual(payload['control'], 'rfstudy_send')
303+
304+
self.assertIn('feq', payload, "payload missing 'feq' (was 'study_feq')")
305+
self.assertNotIn('study_feq', payload, "payload must not contain 'study_feq' for rfstudy_send")
306+
self.assertIsInstance(payload['feq'], int, "'feq' must be int, not string")
307+
self.assertEqual(payload['feq'], 0, "feq must be 0 so the device uses the frequency embedded in the code")
308+
self.assertIn('mode', payload, "payload missing 'mode'")
309+
self.assertIn('rate', payload, "payload missing 'rate'")
310+
311+
self.assertIn('key1', payload)
312+
self.assertIn('ver', payload['key1'], "key1 missing 'ver'")
313+
314+
258315
if __name__ == '__main__':
259316
unittest.main()

tinytuya/Contrib/RFRemoteControlDevice.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,26 @@ def send_command( self, mode, data={} ):
7575
data['freq'] = '0'
7676
if 'ver' not in data or not data['ver']:
7777
data['ver'] = '2'
78-
command = { RFRemoteControlDevice.NSDP_CONTROL: mode, 'rf_type': data['rf_type'], 'study_feq': data['freq'], 'ver': data['ver'] }
7978
if mode == 'rfstudy_send':
79+
# rfstudy_send uses a different payload schema than the study/exit commands:
80+
# - frequency field is 'feq' (int), not 'study_feq' (string)
81+
# - requires 'mode' and 'rate' fields
82+
# - each key dict must include 'ver'
83+
if 'mode' not in data:
84+
data['mode'] = 0
85+
if 'rate' not in data:
86+
data['rate'] = 0
87+
ver = data['ver']
88+
command = { RFRemoteControlDevice.NSDP_CONTROL: mode, 'rf_type': data['rf_type'], 'feq': int(data['freq']), 'mode': data['mode'], 'rate': data['rate'], 'ver': ver }
8089
for i in range( 1, 10 ):
8190
k = 'key%d' % i
8291
if k in data:
83-
command[k] = data[k]
92+
key_data = dict(data[k])
93+
if 'ver' not in key_data:
94+
key_data['ver'] = ver
95+
command[k] = key_data
96+
else:
97+
command = { RFRemoteControlDevice.NSDP_CONTROL: mode, 'rf_type': data['rf_type'], 'study_feq': data['freq'], 'ver': data['ver'] }
8498
self.set_value( RFRemoteControlDevice.DP_SEND_IR, json.dumps(command), nowait=True )
8599
elif mode == 'send_cmd':
86100
data[RFRemoteControlDevice.NSDP_CONTROL] = mode
@@ -172,7 +186,8 @@ def rf_send_button( self, base64_code, times=6, delay=0, intervals=0 ):
172186
key1 = { 'code': base64_code, 'times': times, 'delay': delay, 'intervals': intervals }
173187
data = { 'key1': key1 }
174188
if bdata:
175-
if 'study_feq' in bdata: data['freq'] = bdata['study_feq']
189+
# study_feq is intentionally NOT forwarded to feq.
190+
# feq=0 tells the device to use the frequency embedded in the code itself.
176191
if 'ver' in bdata: data['ver'] = bdata['ver']
177192
return self.send_command( 'rfstudy_send', data )
178193

@@ -255,7 +270,7 @@ def rf_print_button( base64_code, use_log=None ):
255270
@staticmethod
256271
def rf_decode_button( base64_code ):
257272
try:
258-
jstr = base64.b64decode
273+
jstr = base64.b64decode( base64_code )
259274
jdata = json.loads( jstr )
260275
return jdata
261276
except:

0 commit comments

Comments
 (0)