-
Notifications
You must be signed in to change notification settings - Fork 0
/
ir_decode.py
189 lines (150 loc) · 5.75 KB
/
ir_decode.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import tuya
import consts
import argparse
# message format:
# 100 96 92 88 84 80 76 72 68 64 60 56 52 48 44 40 36 32 28 24 20 16 12 8 4 0
# 0010_1010_0000_0000_0000_0000_0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_1010_0000_0000_0000_1110_0000_1100_0111_1100_0011
# 1101_1001_0001_0111_0000_0000_0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_1010_0000_0000_0000_1110_0000_0101_1111_1100_0011
# 0111_1011_0001_0111_1001_1000_0010_0000_0000_0000_0000_0000_0000_0010_0000_0000_1010_0000_0000_0000_1110_0000_0110_0111_1100_0011
# 1000_0111_0001_0111_1001_1100_0010_0000_0000_0000_0000_0000_0000_0010_0000_0000_1010_0000_0000_0000_1110_0000_0110_1111_1100_0011
# --------- ? ---- ---- --- - --- -_ --- ??? ---- -___ ---------
# checksum button temp f on/off mode sleep speed temp preamble
# c/f swing
def main():
parser = argparse.ArgumentParser()
parser.add_argument("code_file", type=argparse.FileType())
args = parser.parse_args()
print(
" 100 96 92 88 84 80 76 72 68 64 60 56 52 48 44 40 36 32 28 24 20 16 12 8 4 0"
)
all_settings = []
diff = 0
previous = 0
for i, tuya_code in enumerate(args.code_file.readlines()):
try:
payload = decode_tuya_code(tuya_code)
settings = decode_settings(payload)
except AssertionError as a:
print(a)
continue
if i > 0:
diff |= payload ^ previous
previous = payload
print(f"{i:>3}: {payload:0>129_b}")
all_settings.append((i, settings))
# print(f" {tuya_code}")
# highlight bit differences across all codes
diff_str = f"{diff:0>129_b}".replace("0", " ").replace("_", " ").replace("1", "^")
print(f" {diff_str}")
print(
" 100 96 92 88 84 80 76 72 68 64 60 56 52 48 44 40 36 32 28 24 20 16 12 8 4 0"
)
print()
for i, s in all_settings:
print(f"{i:>3}: {s}")
def decode_tuya_code(line):
ir_code = tuya.decode_ir(line)
# print(ir_code)
# skip the first two entries, they're a preamble that doesn't contain any data
ir_code = ir_code[2:]
encoded_bits = (len(ir_code) - 1) // 2
assert (
encoded_bits == consts.MESSAGE_LENGTH_BITS
), f"IR code doesn't contain exact bits for a full message: {encoded_bits}/{consts.MESSAGE_LENGTH_BITS}\n{line.strip()}\n{ir_code}"
payload = 0
# zipping something with itself will iterate through adjacent pairs. the message is of odd length because of the
# SHORT-length footer at the end. this method of zipping will deliberately ignore it
code_iter = iter(ir_code)
for i, (v1, v2) in enumerate(zip(code_iter, code_iter)):
bit = decode_pair(v1, v2)
assert (
bit is not None
), f"Bit failed to decode: {v1}, {v2}\n{line.strip()}\n{ir_code}"
payload |= bit << i
return payload
def decode_pair(i1, i2):
if i1 <= consts.LENGTH_CUTOFF and i2 <= consts.LENGTH_CUTOFF:
return 0
if i1 <= consts.LENGTH_CUTOFF and i2 > consts.LENGTH_CUTOFF:
return 1
def decode_settings(payload):
# the temperature is offset by 8 probably to save one bit in the message
temp_c = ((payload >> 11) & 0x1F) + 8
# no idea why F is off by 8 but in the other direction, it doesn't save any space?
temp_f = ((payload >> 81) & 0x7F) - 8
# other models have multiple swing values, this one's just on/off
swing = not bool((payload >> 8) & 0x7)
speed = decode_speed_value((payload >> 37) & 0x7)
unit = "F" if ((payload >> 49) & 1) else "C"
sleep = bool((payload >> 50) & 1)
mode = decode_mode_value((payload >> 53) & 0x7)
on = bool((payload >> 77) & 1)
button = decode_button_value((payload >> 88) & 0xF)
actual_csum = (payload >> 96) & 0xFF
our_csum = calc_checksum(payload)
assert (
actual_csum == our_csum
), f"Checksum mismatch: {actual_csum:0>9_b} vs {our_csum:0>9_b}"
return (
f"on:{on:>1} temp:{temp_c if unit == 'C' else temp_f:>2}{unit} mode:{mode:>4} speed:{speed:>4} "
f"swing:{swing:>1} sleep:{sleep:>1} button:{button:>5} csum:{actual_csum:0>9_b}"
)
def decode_mode_value(v):
assert v in [0, 1, 2, 6], f"Unknown mode value: {v} ({v:03b})"
if v == 0:
return "AUTO"
elif v == 1:
return "COOL"
elif v == 2:
return "DRY"
elif v == 6:
return "FAN"
def decode_speed_value(v):
assert v in [1, 2, 3, 5], f"Unknown fan speed value: {v} ({v:03b})"
if v == 1:
return "HIGH"
elif v == 2:
return "MID"
elif v == 3:
return "LOW"
elif v == 5:
return "AUTO"
def decode_button_value(v):
assert v in [
0,
1,
2,
4,
5,
6,
7,
11,
13,
], f"Unknown button value: {v} ({v:04b})"
if v == 0:
return "PLUS"
elif v == 1:
return "MINUS"
elif v == 2:
return "SWING"
elif v == 4:
return "SPEED"
elif v == 5:
return "ONOFF"
elif v == 6:
return "MODE"
elif v == 7:
return "UNIT"
elif v == 11:
return "SLEEP"
elif v == 13:
return "TIMER"
def calc_checksum(payload):
payload &= ~(0xFF << 96) # zero the included checksum from the payload
sum = 0
while payload > 0:
sum += payload & 0xFF
payload >>= 8
return sum & 0xFF
if __name__ == "__main__":
main()