Skip to content

Commit 25ad954

Browse files
committed
Add support for chuangmi.remote.h102a03 and v2
All kudos to original work by @yawor's on PR rytilahti#501. Fixes rytilahti#495, fixes rytilahti#619, fixes rytilahti#811. Closes rytilahti#501. Partially covers rytilahti#1020.
1 parent 3053562 commit 25ad954

File tree

7 files changed

+390
-54
lines changed

7 files changed

+390
-54
lines changed

miio/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from miio.aqaracamera import AqaraCamera
2828
from miio.ceil import Ceil
2929
from miio.chuangmi_camera import ChuangmiCamera
30-
from miio.chuangmi_ir import ChuangmiIr
30+
from miio.chuangmi_ir import ChuangmiIr, ChuangmiRemote, ChuangmiRemoteV2
3131
from miio.chuangmi_plug import ChuangmiPlug, Plug, PlugV1, PlugV3
3232
from miio.cooker import Cooker
3333
from miio.curtain_youpin import CurtainMiot

miio/chuangmi_ir.py

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import base64
22
import re
3+
from typing import List, Tuple
34

45
import click
6+
7+
try:
8+
import heatshrink2
9+
except Exception:
10+
heatshrink2 = None
11+
512
from construct import (
613
Adapter,
714
Array,
@@ -87,33 +94,45 @@ def play_pronto(self, pronto: str, repeats: int = 1):
8794
return self.play_raw(*self.pronto_to_raw(pronto, repeats))
8895

8996
@classmethod
90-
def pronto_to_raw(cls, pronto: str, repeats: int = 1):
91-
"""Play a Pronto Hex encoded IR command. Supports only raw Pronto format,
92-
starting with 0000.
97+
def _parse_pronto(
98+
cls, pronto: str
99+
) -> Tuple[List["ProntoBurstPair"], List["ProntoBurstPair"], int]:
100+
"""Parses Pronto Hex encoded IR command and returns a tuple containing a list of
101+
intro pairs, a list of repeat pairs and a signal carrier frequency."""
102+
try:
103+
pronto_data = Pronto.parse(bytearray.fromhex(pronto))
104+
except Exception as ex:
105+
raise ChuangmiIrException("Invalid Pronto command") from ex
106+
107+
return pronto_data.intro, pronto_data.repeat, int(round(pronto_data.frequency))
108+
109+
@classmethod
110+
def pronto_to_raw(cls, pronto: str, repeats: int = 1) -> Tuple[str, int]:
111+
"""Takes a Pronto Hex encoded IR command and number of repeats and returns a
112+
tuple containing a string encoded IR signal accepted by controller and
113+
frequency. Supports only raw Pronto format, starting with 0000.
93114
94115
:param str pronto: Pronto Hex string.
95116
:param int repeats: Number of extra signal repeats.
96117
"""
118+
97119
if repeats < 0:
98120
raise ChuangmiIrException("Invalid repeats value")
99121

100-
try:
101-
pronto_data = Pronto.parse(bytearray.fromhex(pronto))
102-
except Exception as ex:
103-
raise ChuangmiIrException("Invalid Pronto command") from ex
122+
intro_pairs, repeat_pairs, frequency = cls._parse_pronto(pronto)
104123

105-
if len(pronto_data.intro) == 0:
124+
if len(intro_pairs) == 0:
106125
repeats += 1
107126

108127
times = set()
109-
for pair in pronto_data.intro + pronto_data.repeat * (1 if repeats else 0):
128+
for pair in intro_pairs + repeat_pairs * (1 if repeats else 0):
110129
times.add(pair.pulse)
111130
times.add(pair.gap)
112131

113132
times = sorted(times)
114133
times_map = {t: idx for idx, t in enumerate(times)}
115134
edge_pairs = []
116-
for pair in pronto_data.intro + pronto_data.repeat * repeats:
135+
for pair in intro_pairs + repeat_pairs * repeats:
117136
edge_pairs.append(
118137
{"pulse": times_map[pair.pulse], "gap": times_map[pair.gap]}
119138
)
@@ -127,7 +146,7 @@ def pronto_to_raw(cls, pronto: str, repeats: int = 1):
127146
)
128147
).decode()
129148

130-
return signal_code, int(round(pronto_data.frequency))
149+
return signal_code, frequency
131150

132151
@command(
133152
click.argument("command", type=str),
@@ -185,6 +204,79 @@ def get_indicator_led(self):
185204
return self.send("get_indicatorLamp")
186205

187206

207+
class ChuangmiRemote(ChuangmiIr):
208+
"""Class representing new type of Chuangmi IR Remote Controller identified by model
209+
"chuangmi-remote-h102a03_".
210+
211+
The new controller uses different format for learned IR commands, which actually is
212+
the old format but with additional layer of compression.
213+
"""
214+
215+
@classmethod
216+
def pronto_to_raw(cls, pronto: str, repeats: int = 1) -> Tuple[str, int]:
217+
"""Takes a Pronto Hex encoded IR command and number of repeats and returns a
218+
tuple containing a string encoded IR signal accepted by controller and
219+
frequency. Supports only raw Pronto format, starting with 0000.
220+
221+
:raises ChuangmiIrException if heatshrink2 package is not installed.
222+
223+
:param str pronto: Pronto Hex string.
224+
:param int repeats: Number of extra signal repeats.
225+
"""
226+
227+
if heatshrink2 is None:
228+
raise ChuangmiIrException("heatshrink2 library is missing")
229+
raw, frequency = super().pronto_to_raw(pronto, repeats)
230+
return (
231+
base64.b64encode(
232+
heatshrink2.encode("learn{}".format(raw).encode())
233+
).decode(),
234+
frequency,
235+
)
236+
237+
238+
class ChuangmiRemoteV2(ChuangmiIr):
239+
"""Class representing new type of Chuangmi IR Remote Controller identified by model
240+
"chuangmi-remote-v2".
241+
242+
The new controller uses different format for learned IR commands, which compresses
243+
an ASCII list of comma separated edge timings.
244+
"""
245+
246+
@classmethod
247+
def pronto_to_raw(cls, pronto: str, repeats: int = 1) -> Tuple[str, int]:
248+
"""Takes a Pronto Hex encoded IR command and number of repeats and returns a
249+
tuple containing a string encoded IR signal accepted by controller and
250+
frequency. Supports only raw Pronto format, starting with 0000.
251+
252+
:raises ChuangmiIrException if heatshrink package is not installed.
253+
254+
:param str pronto: Pronto Hex string.
255+
:param int repeats: Number of extra signal repeats.
256+
"""
257+
258+
if heatshrink2 is None:
259+
raise ChuangmiIrException("heatshrink2 library is missing")
260+
261+
if repeats < 0:
262+
raise ChuangmiIrException("Invalid repeats value")
263+
264+
intro_pairs, repeat_pairs, frequency = cls._parse_pronto(pronto)
265+
266+
if len(intro_pairs) == 0:
267+
repeats += 1
268+
269+
timings = []
270+
for pair in intro_pairs + repeat_pairs * repeats:
271+
timings.append(pair.pulse)
272+
timings.append(pair.gap)
273+
timings[-1] = 0
274+
275+
timings = "{}\0".format(",".join(map(str, timings))).encode()
276+
277+
return base64.b64encode(heatshrink2.encode(timings)).decode(), frequency
278+
279+
188280
class ProntoPulseAdapter(Adapter):
189281
def _decode(self, obj, context, path):
190282
return int(obj * context._.modulation_period)

miio/discovery.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
ChuangmiCamera,
2929
ChuangmiIr,
3030
ChuangmiPlug,
31+
ChuangmiRemote,
32+
ChuangmiRemoteV2,
3133
Cooker,
3234
Device,
3335
Fan,
@@ -134,7 +136,8 @@
134136
"chuangmi-camera-ipc009": ChuangmiCamera,
135137
"chuangmi-camera-ipc019": ChuangmiCamera,
136138
"chuangmi-ir-v2": ChuangmiIr,
137-
"chuangmi-remote-h102a03_": ChuangmiIr,
139+
"chuangmi-remote-h102a03_": ChuangmiRemote,
140+
"chuangmi-remote-v2": ChuangmiRemoteV2,
138141
"zhimi-humidifier-v1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_V1),
139142
"zhimi-humidifier-ca1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_CA1),
140143
"zhimi-humidifier-cb1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_CB1),

miio/tests/test_chuangmi_ir.json

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,157 @@
111111
0
112112
]
113113
}
114+
],
115+
"test_pronto_ok_chuangmi_remote": [
116+
{
117+
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - with spaces",
118+
"in": [
119+
"0000 006C 0022 0002 015B 00AD 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0041 0016 0041 0016 0041 0016 0041 0016 0041 0016 0041 0016 0041 0016 0016 0016 0016 0016 0041 0016 0016 0016 0041 0016 0016 0016 0016 0016 0016 0016 0016 0016 0041 0016 0016 0016 0041 0016 0016 0016 0041 0016 0041 0016 0041 0016 0041 0016 0622 015B 0057 0016 0E6C"
120+
],
121+
"out": [
122+
"tllsNyt1am1WpFBokwodBoNDslCs9BoMys4AhUq51Kg0GhVGk3eg0G81qcUGg02jTOg1GggAeAB4AHzAAuqjRaEAGoBZ0GhAKLRQC1AXKhVGlUegz0A=",
123+
38381
124+
]
125+
},
126+
{
127+
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - without spaces",
128+
"in": [
129+
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C"
130+
],
131+
"out": [
132+
"tllsNyt1am1WpFBokwodBoNDslCs9BoMys4AhUq51Kg0GhVGk3eg0G81qcUGg02jTOg1GggAeAB4AHzAAuqjRaEAGoBZ0GhAKLRQC1AXKhVGlUegz0A=",
133+
38381
134+
]
135+
},
136+
{
137+
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - 0 repeat frames",
138+
"in": [
139+
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C",
140+
0
141+
],
142+
"out": [
143+
"tllsNyt1am1WolBokwodBoNDslCs9BoNtvFFoNBo1BtVBoNEpVuu9BAA8ADwAPAAsEMgAIqNFoQAagFnQaEAooNJAFmAuVCoIA==",
144+
38381
145+
]
146+
},
147+
{
148+
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - 2 repeat frames",
149+
"in": [
150+
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C",
151+
2
152+
],
153+
"out": [
154+
"tllsNyt1am1WplBokwodBoNDslCs9BoMys4AhUq51Kg0GhVGk3eg0G81qcUGg02jTOg1GggAeAB4AHzAAuqjRaEAGoBZ0GhAKLRQC1AXKhVGlUeg2us0Gez0",
155+
38381
156+
]
157+
},
158+
{
159+
"desc": "Sony20, Dev 0, Subdev 0, Function 1",
160+
"in": [
161+
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0"
162+
],
163+
"out": [
164+
"tllsNyt1am1WqlBo1vodBoNDmFCoNBoNhoNroNBpVCo4BRAAeAB4AHgAfQ6DUQEvAAaiAN+A3k9A",
165+
39857
166+
]
167+
},
168+
{
169+
"desc": "Sony20, Dev 0, Subdev 0, Function 1 - 0 repeats",
170+
"in": [
171+
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0",
172+
0
173+
],
174+
"out": [
175+
"tllsNyt1am1VuFBo1vodBoNDmFCoNBoNhoNroNBpVCo4BRAAeAB4AHgAfQ6DUQEvAAaiUGeg",
176+
39857
177+
]
178+
},
179+
{
180+
"desc": "Sony20, Dev 0, Subdev 0, Function 1 - 2 repeats",
181+
"in": [
182+
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0",
183+
2
184+
],
185+
"out": [
186+
"tllsNyt1am1WnNBo1vodBoNDmFCoNBoNhoNroNBpVCo4BRAAeAB4AHgAfQ6DUQEvAAaiAN+A34CngNxPQA==",
187+
39857
188+
]
189+
}
190+
],
191+
"test_pronto_ok_chuangmi_remote_v2": [
192+
{
193+
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - with spaces",
194+
"in": [
195+
"0000 006C 0022 0002 015B 00AD 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0041 0016 0041 0016 0041 0016 0041 0016 0041 0016 0041 0016 0041 0016 0016 0016 0016 0016 0041 0016 0016 0016 0041 0016 0016 0016 0016 0016 0016 0016 0016 0016 0041 0016 0016 0016 0041 0016 0016 0016 0041 0016 0041 0016 0041 0016 0041 0016 0622 015B 0057 0016 0E6C"
196+
],
197+
"out": [
198+
"nMwmkwlk0mswm8sms3mYAfgB+AH4AfyyYzacgEeAR4BHgEWAH4DHgIeAH4FHgIeAh4BHgEfNJhOZhNZYJEkymU2mwCaTCAA=",
199+
38381
200+
]
201+
},
202+
{
203+
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - without spaces",
204+
"in": [
205+
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C"
206+
],
207+
"out": [
208+
"nMwmkwlk0mswm8sms3mYAfgB+AH4AfyyYzacgEeAR4BHgEWAH4DHgIeAH4FHgIeAh4BHgEfNJhOZhNZYJEkymU2mwCaTCAA=",
209+
38381
210+
]
211+
},
212+
{
213+
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - 0 repeat frames",
214+
"in": [
215+
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C",
216+
0
217+
],
218+
"out": [
219+
"nMwmkwlk0mswm8sms3mYAfgB+AH4AfyyYzacgEeAR4BHgEWAH4DHgIeAH4FHgIeAh4BHgEfMIAA=",
220+
38381
221+
]
222+
},
223+
{
224+
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - 2 repeat frames",
225+
"in": [
226+
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C",
227+
2
228+
],
229+
"out": [
230+
"nMwmkwlk0mswm8sms3mYAfgB+AH4AfyyYzacgEeAR4BHgEWAH4DHgIeAH4FHgIeAh4BHgEfNJhOZhNZYJEkymU2mwCaTmbTEB0gE9mEA",
231+
38381
232+
]
233+
},
234+
{
235+
"desc": "Sony20, Dev 0, Subdev 0, Function 1",
236+
"in": [
237+
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0"
238+
],
239+
"out": [
240+
"mU0mE4lk2mEylkxmUwmgBCAB+AH4AfgB+AH4AfgB+AH4AfhOJOJhNppLAq/An8APwA/AD8APwA/AD8APwA/ADWYQAA==",
241+
39857
242+
]
243+
},
244+
{
245+
"desc": "Sony20, Dev 0, Subdev 0, Function 1 - 0 repeats",
246+
"in": [
247+
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0",
248+
0
249+
],
250+
"out": [
251+
"mU0mE4lk2mEylkxmUwmgBCAB+AH4AfgB+AH4AfgB+AH4AfgBnMIA",
252+
39857
253+
]
254+
},
255+
{
256+
"desc": "Sony20, Dev 0, Subdev 0, Function 1 - 2 repeats",
257+
"in": [
258+
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0",
259+
2
260+
],
261+
"out": [
262+
"mU0mE4lk2mEylkxmUwmgBCAB+AH4AfgB+AH4AfgB+AH4AfhOJOJhNppLAq/An8APwA/AD8APwA/AD8APwA/Cr8KvwA/AD8APwA/AD8APwA/AD8AP5lLJhAA=",
263+
39857
264+
]
265+
}
114266
]
115267
}

0 commit comments

Comments
 (0)