Skip to content

Commit

Permalink
Add support for chuangmi.remote.h102a03 and v2
Browse files Browse the repository at this point in the history
All kudos to original work by @yawor's on PR #501.

Fixes #495, fixes #619, fixes #811.
Closes #501.
Partially covers #1020.
  • Loading branch information
oblitum committed Apr 22, 2021
1 parent 3053562 commit 487fac1
Show file tree
Hide file tree
Showing 7 changed files with 390 additions and 54 deletions.
2 changes: 1 addition & 1 deletion miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from miio.aqaracamera import AqaraCamera
from miio.ceil import Ceil
from miio.chuangmi_camera import ChuangmiCamera
from miio.chuangmi_ir import ChuangmiIr
from miio.chuangmi_ir import ChuangmiIr, ChuangmiRemote, ChuangmiRemoteV2
from miio.chuangmi_plug import ChuangmiPlug, Plug, PlugV1, PlugV3
from miio.cooker import Cooker
from miio.curtain_youpin import CurtainMiot
Expand Down
114 changes: 103 additions & 11 deletions miio/chuangmi_ir.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import base64
import re
from typing import List, Tuple

import click

try:
import heatshrink2
except:
heatshrink2 = None

from construct import (
Adapter,
Array,
Expand Down Expand Up @@ -87,33 +94,45 @@ def play_pronto(self, pronto: str, repeats: int = 1):
return self.play_raw(*self.pronto_to_raw(pronto, repeats))

@classmethod
def pronto_to_raw(cls, pronto: str, repeats: int = 1):
"""Play a Pronto Hex encoded IR command. Supports only raw Pronto format,
starting with 0000.
def _parse_pronto(
cls, pronto: str
) -> Tuple[List["ProntoBurstPair"], List["ProntoBurstPair"], int]:
"""Parses Pronto Hex encoded IR command and returns a tuple containing a list of
intro pairs, a list of repeat pairs and a signal carrier frequency."""
try:
pronto_data = Pronto.parse(bytearray.fromhex(pronto))
except Exception as ex:
raise ChuangmiIrException("Invalid Pronto command") from ex

return pronto_data.intro, pronto_data.repeat, int(round(pronto_data.frequency))

@classmethod
def pronto_to_raw(cls, pronto: str, repeats: int = 1) -> Tuple[str, int]:
"""Takes a Pronto Hex encoded IR command and number of repeats and returns a
tuple containing a string encoded IR signal accepted by controller and
frequency. Supports only raw Pronto format, starting with 0000.
:param str pronto: Pronto Hex string.
:param int repeats: Number of extra signal repeats.
"""

if repeats < 0:
raise ChuangmiIrException("Invalid repeats value")

try:
pronto_data = Pronto.parse(bytearray.fromhex(pronto))
except Exception as ex:
raise ChuangmiIrException("Invalid Pronto command") from ex
intro_pairs, repeat_pairs, frequency = cls._parse_pronto(pronto)

if len(pronto_data.intro) == 0:
if len(intro_pairs) == 0:
repeats += 1

times = set()
for pair in pronto_data.intro + pronto_data.repeat * (1 if repeats else 0):
for pair in intro_pairs + repeat_pairs * (1 if repeats else 0):
times.add(pair.pulse)
times.add(pair.gap)

times = sorted(times)
times_map = {t: idx for idx, t in enumerate(times)}
edge_pairs = []
for pair in pronto_data.intro + pronto_data.repeat * repeats:
for pair in intro_pairs + repeat_pairs * repeats:
edge_pairs.append(
{"pulse": times_map[pair.pulse], "gap": times_map[pair.gap]}
)
Expand All @@ -127,7 +146,7 @@ def pronto_to_raw(cls, pronto: str, repeats: int = 1):
)
).decode()

return signal_code, int(round(pronto_data.frequency))
return signal_code, frequency

@command(
click.argument("command", type=str),
Expand Down Expand Up @@ -185,6 +204,79 @@ def get_indicator_led(self):
return self.send("get_indicatorLamp")


class ChuangmiRemote(ChuangmiIr):
"""Class representing new type of Chuangmi IR Remote Controller identified by model
"chuangmi-remote-h102a03_".
The new controller uses different format for learned IR commands, which actually is
the old format but with additional layer of compression.
"""

@classmethod
def pronto_to_raw(cls, pronto: str, repeats: int = 1) -> Tuple[str, int]:
"""Takes a Pronto Hex encoded IR command and number of repeats and returns a
tuple containing a string encoded IR signal accepted by controller and
frequency. Supports only raw Pronto format, starting with 0000.
:raises ChuangmiIrException if heatshrink2 package is not installed.
:param str pronto: Pronto Hex string.
:param int repeats: Number of extra signal repeats.
"""

if heatshrink2 is None:
raise ChuangmiIrException("heatshrink2 library is missing")
raw, frequency = super().pronto_to_raw(pronto, repeats)
return (
base64.b64encode(
heatshrink2.encode("learn{}".format(raw).encode())
).decode(),
frequency,
)


class ChuangmiRemoteV2(ChuangmiIr):
"""Class representing new type of Chuangmi IR Remote Controller identified by model
"chuangmi-remote-v2".
The new controller uses different format for learned IR commands, which compresses
an ASCII list of comma separated edge timings.
"""

@classmethod
def pronto_to_raw(cls, pronto: str, repeats: int = 1) -> Tuple[str, int]:
"""Takes a Pronto Hex encoded IR command and number of repeats and returns a
tuple containing a string encoded IR signal accepted by controller and
frequency. Supports only raw Pronto format, starting with 0000.
:raises ChuangmiIrException if heatshrink package is not installed.
:param str pronto: Pronto Hex string.
:param int repeats: Number of extra signal repeats.
"""

if heatshrink2 is None:
raise ChuangmiIrException("heatshrink2 library is missing")

if repeats < 0:
raise ChuangmiIrException("Invalid repeats value")

intro_pairs, repeat_pairs, frequency = cls._parse_pronto(pronto)

if len(intro_pairs) == 0:
repeats += 1

timings = []
for pair in intro_pairs + repeat_pairs * repeats:
timings.append(pair.pulse)
timings.append(pair.gap)
timings[-1] = 0

timings = "{}\0".format(",".join(map(str, timings))).encode()

return base64.b64encode(heatshrink2.encode(timings)).decode(), frequency


class ProntoPulseAdapter(Adapter):
def _decode(self, obj, context, path):
return int(obj * context._.modulation_period)
Expand Down
5 changes: 4 additions & 1 deletion miio/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
ChuangmiCamera,
ChuangmiIr,
ChuangmiPlug,
ChuangmiRemote,
ChuangmiRemoteV2,
Cooker,
Device,
Fan,
Expand Down Expand Up @@ -134,7 +136,8 @@
"chuangmi-camera-ipc009": ChuangmiCamera,
"chuangmi-camera-ipc019": ChuangmiCamera,
"chuangmi-ir-v2": ChuangmiIr,
"chuangmi-remote-h102a03_": ChuangmiIr,
"chuangmi-remote-h102a03_": ChuangmiRemote,
"chuangmi-remote-v2": ChuangmiRemoteV2,
"zhimi-humidifier-v1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_V1),
"zhimi-humidifier-ca1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_CA1),
"zhimi-humidifier-cb1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_CB1),
Expand Down
152 changes: 152 additions & 0 deletions miio/tests/test_chuangmi_ir.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,157 @@
0
]
}
],
"test_pronto_ok_chuangmi_remote": [
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - with spaces",
"in": [
"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"
],
"out": [
"tllsNyt1am1WpFBokwodBoNDslCs9BoMys4AhUq51Kg0GhVGk3eg0G81qcUGg02jTOg1GggAeAB4AHzAAuqjRaEAGoBZ0GhAKLRQC1AXKhVGlUegz0A=",
38381
]
},
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - without spaces",
"in": [
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C"
],
"out": [
"tllsNyt1am1WpFBokwodBoNDslCs9BoMys4AhUq51Kg0GhVGk3eg0G81qcUGg02jTOg1GggAeAB4AHzAAuqjRaEAGoBZ0GhAKLRQC1AXKhVGlUegz0A=",
38381
]
},
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - 0 repeat frames",
"in": [
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C",
0
],
"out": [
"tllsNyt1am1WolBokwodBoNDslCs9BoNtvFFoNBo1BtVBoNEpVuu9BAA8ADwAPAAsEMgAIqNFoQAagFnQaEAooNJAFmAuVCoIA==",
38381
]
},
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - 2 repeat frames",
"in": [
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C",
2
],
"out": [
"tllsNyt1am1WplBokwodBoNDslCs9BoMys4AhUq51Kg0GhVGk3eg0G81qcUGg02jTOg1GggAeAB4AHzAAuqjRaEAGoBZ0GhAKLRQC1AXKhVGlUeg2us0Gez0",
38381
]
},
{
"desc": "Sony20, Dev 0, Subdev 0, Function 1",
"in": [
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0"
],
"out": [
"tllsNyt1am1WqlBo1vodBoNDmFCoNBoNhoNroNBpVCo4BRAAeAB4AHgAfQ6DUQEvAAaiAN+A3k9A",
39857
]
},
{
"desc": "Sony20, Dev 0, Subdev 0, Function 1 - 0 repeats",
"in": [
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0",
0
],
"out": [
"tllsNyt1am1VuFBo1vodBoNDmFCoNBoNhoNroNBpVCo4BRAAeAB4AHgAfQ6DUQEvAAaiUGeg",
39857
]
},
{
"desc": "Sony20, Dev 0, Subdev 0, Function 1 - 2 repeats",
"in": [
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0",
2
],
"out": [
"tllsNyt1am1WnNBo1vodBoNDmFCoNBoNhoNroNBpVCo4BRAAeAB4AHgAfQ6DUQEvAAaiAN+A34CngNxPQA==",
39857
]
}
],
"test_pronto_ok_chuangmi_remote_v2": [
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - with spaces",
"in": [
"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"
],
"out": [
"nMwmkwlk0mswm8sms3mYAfgB+AH4AfyyYzacgEeAR4BHgEWAH4DHgIeAH4FHgIeAh4BHgEfNJhOZhNZYJEkymU2mwCaTCAA=",
38381
]
},
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - without spaces",
"in": [
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C"
],
"out": [
"nMwmkwlk0mswm8sms3mYAfgB+AH4AfyyYzacgEeAR4BHgEWAH4DHgIeAH4FHgIeAh4BHgEfNJhOZhNZYJEkymU2mwCaTCAA=",
38381
]
},
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - 0 repeat frames",
"in": [
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C",
0
],
"out": [
"nMwmkwlk0mswm8sms3mYAfgB+AH4AfyyYzacgEeAR4BHgEWAH4DHgIeAH4FHgIeAh4BHgEfMIAA=",
38381
]
},
{
"desc": "NEC1, Dev 0, Subdev 127, Function 10 - 2 repeat frames",
"in": [
"0000006C00220002015B00AD001600160016001600160016001600160016001600160016001600160016001600160041001600410016004100160041001600410016004100160041001600160016001600160041001600160016004100160016001600160016001600160016001600410016001600160041001600160016004100160041001600410016004100160622015B005700160E6C",
2
],
"out": [
"nMwmkwlk0mswm8sms3mYAfgB+AH4AfyyYzacgEeAR4BHgEWAH4DHgIeAH4FHgIeAh4BHgEfNJhOZhNZYJEkymU2mwCaTmbTEB0gE9mEA",
38381
]
},
{
"desc": "Sony20, Dev 0, Subdev 0, Function 1",
"in": [
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0"
],
"out": [
"mU0mE4lk2mEylkxmUwmgBCAB+AH4AfgB+AH4AfgB+AH4AfhOJOJhNppLAq/An8APwA/AD8APwA/AD8APwA/ADWYQAA==",
39857
]
},
{
"desc": "Sony20, Dev 0, Subdev 0, Function 1 - 0 repeats",
"in": [
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0",
0
],
"out": [
"mU0mE4lk2mEylkxmUwmgBCAB+AH4AfgB+AH4AfgB+AH4AfgBnMIA",
39857
]
},
{
"desc": "Sony20, Dev 0, Subdev 0, Function 1 - 2 repeats",
"in": [
"00000068000000150060001800300018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001800180018001802D0",
2
],
"out": [
"mU0mE4lk2mEylkxmUwmgBCAB+AH4AfgB+AH4AfgB+AH4AfhOJOJhNppLAq/An8APwA/AD8APwA/AD8APwA/Cr8KvwA/AD8APwA/AD8APwA/AD8AP5lLJhAA=",
39857
]
}
]
}
Loading

0 comments on commit 487fac1

Please sign in to comment.