Skip to content

Commit 683de86

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 683de86

File tree

7 files changed

+389
-54
lines changed

7 files changed

+389
-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: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import base64
22
import re
3+
from typing import List, Tuple
34

45
import click
6+
try:
7+
import heatshrink2
8+
except:
9+
heatshrink2 = None
10+
511
from construct import (
612
Adapter,
713
Array,
@@ -87,33 +93,45 @@ def play_pronto(self, pronto: str, repeats: int = 1):
8793
return self.play_raw(*self.pronto_to_raw(pronto, repeats))
8894

8995
@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.
96+
def _parse_pronto(
97+
cls, pronto: str
98+
) -> Tuple[List["ProntoBurstPair"], List["ProntoBurstPair"], int]:
99+
"""Parses Pronto Hex encoded IR command and returns a tuple containing a list of
100+
intro pairs, a list of repeat pairs and a signal carrier frequency."""
101+
try:
102+
pronto_data = Pronto.parse(bytearray.fromhex(pronto))
103+
except Exception as ex:
104+
raise ChuangmiIrException("Invalid Pronto command") from ex
105+
106+
return pronto_data.intro, pronto_data.repeat, int(round(pronto_data.frequency))
107+
108+
@classmethod
109+
def pronto_to_raw(cls, pronto: str, repeats: int = 1) -> Tuple[str, int]:
110+
"""Takes a Pronto Hex encoded IR command and number of repeats and returns a
111+
tuple containing a string encoded IR signal accepted by controller and
112+
frequency. Supports only raw Pronto format, starting with 0000.
93113
94114
:param str pronto: Pronto Hex string.
95115
:param int repeats: Number of extra signal repeats.
96116
"""
117+
97118
if repeats < 0:
98119
raise ChuangmiIrException("Invalid repeats value")
99120

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

105-
if len(pronto_data.intro) == 0:
123+
if len(intro_pairs) == 0:
106124
repeats += 1
107125

108126
times = set()
109-
for pair in pronto_data.intro + pronto_data.repeat * (1 if repeats else 0):
127+
for pair in intro_pairs + repeat_pairs * (1 if repeats else 0):
110128
times.add(pair.pulse)
111129
times.add(pair.gap)
112130

113131
times = sorted(times)
114132
times_map = {t: idx for idx, t in enumerate(times)}
115133
edge_pairs = []
116-
for pair in pronto_data.intro + pronto_data.repeat * repeats:
134+
for pair in intro_pairs + repeat_pairs * repeats:
117135
edge_pairs.append(
118136
{"pulse": times_map[pair.pulse], "gap": times_map[pair.gap]}
119137
)
@@ -127,7 +145,7 @@ def pronto_to_raw(cls, pronto: str, repeats: int = 1):
127145
)
128146
).decode()
129147

130-
return signal_code, int(round(pronto_data.frequency))
148+
return signal_code, frequency
131149

132150
@command(
133151
click.argument("command", type=str),
@@ -185,6 +203,79 @@ def get_indicator_led(self):
185203
return self.send("get_indicatorLamp")
186204

187205

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