-
-
Notifications
You must be signed in to change notification settings - Fork 573
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for chuangmi.remote.h102a03 and chuangmi.remote.v2 #1021
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,4 +5,4 @@ python: | |
version: 3.7 | ||
pip_install: true | ||
extra_requirements: | ||
- docs | ||
- dev |
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 Exception: | ||||||||||||||||||
heatshrink2 = None | ||||||||||||||||||
|
||||||||||||||||||
from construct import ( | ||||||||||||||||||
Adapter, | ||||||||||||||||||
Array, | ||||||||||||||||||
|
@@ -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]} | ||||||||||||||||||
) | ||||||||||||||||||
|
@@ -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), | ||||||||||||||||||
|
@@ -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. | ||||||||||||||||||
Comment on lines
+217
to
+219
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
|
||||||||||||||||||
: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) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The docstring above mentions that only prontos starting with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, the another class expects that repeats >= 0, that should also be the case here? |
||||||||||||||||||
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". | ||||||||||||||||||
Comment on lines
+239
to
+240
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my comment above on simplifying the docstring & adding the model info. |
||||||||||||||||||
|
||||||||||||||||||
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) | ||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is
info()
working fine for this device? If yes, could you also add that here. The long term goal is to separate the meta information to separate files to allow creating instances based on the model information.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can only confirm that it works for chuangmi-remote-v2, and it does. What should be added? I didn't get. Do you wish to add that
info()
works for the device in the class description?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The info will deliver the model of the device, could you check with
miiocli device info
what is it for your device & add that to the corresponding class?The reason why I'm pushing towards a single class is that it makes it simpler for downstreams like homeassistant to support all supported remotes as long as 1) info is working or 2) the user provides the wanted model as a parameter so that the class can adjust itself accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get:
The
ChuangmiRemoteV2
child class already states to cover "chuangmi-remote-v2". Do you wish it changed to "chuangmi.remote.v2" according to the model output above? (The default hostname on the network doesn't use periods though, but the actual model name is with periods). I can't confirm anything for the h102a03 model.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yes, it's also in the ttile of the PR, sorry... The one reported by the info is the one to use for identification (as not all devices support the mDNS, which is used for the hostname).