rytilahti / python-miio

Python library & console tool for controlling Xiaomi smart appliances
https://python-miio.readthedocs.io
GNU General Public License v3.0
3.63k stars 549 forks source link

Add tests for the Chuangmi IR controller #175

Closed syssi closed 6 years ago

syssi commented 6 years ago

@cnrd Could you do me a favour and provide a example response of the learn_command call?

I would like to fix this comment: https://github.com/rytilahti/python-miio/blob/master/miio/chuangmi_ir.py#L19

The ouput of these calls would be perfect:

mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba info
mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command miIO.ir_learn '{"key": "1"}'
mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command miIO.ir_read '{"key": "1"}'
mirobo --ip 192.168.0.25 --token 689c4056fe28ebb3a2e8c2fe350e51ba raw_command miIO.ir_play '{"freq": 38400, "code": "base64..."}'
cnrd commented 6 years ago

I censored the ip, mac, and token, hope that is okay (Also added a few more commands):

$ mirobo discover
INFO:miio.discovery:Discovering devices with mDNS, press any key to quit...
INFO:miio.discovery:Found a supported 'ChuangmiIr' at [ip] - token: [token]
$ mirobo --ip [ip] --token [token] info
chuangmi.ir.v2 v1.2.4_38 ([MAC]) @ [ip] - token: [token]

Valid learn slot:

$ mirobo --ip [ip] --token [token] raw_command miIO.ir_learn '{"key": "1"}'
Sending cmd miIO.ir_learn with params {'key': '1'}
0

Invalid learn slot:

$ mirobo --ip [ip] --token [token] raw_command miIO.ir_learn '{"key": "-1"}'
Sending cmd miIO.ir_learn with params {'key': '-1'}
0

No command learned on the slot (It seems to forget old commands after some period of time):

$ mirobo --ip [ip] --token [token] raw_command miIO.ir_read '{"key": "1"}'
Sending cmd miIO.ir_read with params {'key': '1'}
{'error': {'code': -5002, 'message': 'no code for this key'}, 'id': 5}

Learned some random remote command:

$ mirobo --ip [ip] --token [token] raw_command miIO.ir_read '{"key": "1"}'
{'key': '1', 'code': 'Z6WPAasBAAA3BQAA4QYAAJMNAAAwJAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjABAAAAAAAAAAAAAAABAAAAAAAAAAEAAQAAAAAAAAAAAQABAQEBAAABAAEBAQAAAQQCMAEAAAAAAAAAAAAAAAEAAAAAAAAAAQABAAAAAAAAAAABAAEBAQEAAAEAAQEBAAABBAIwAQAAAAAAAAAAAAAAAQAAAAAAAAABAAEAAAAAAAAAAAEAAQEBAQAAAQABAQEAAAEEAjABAAAAAAAAAAAAAAABAAAAAAAAAAEAAQAAAAAAAAAAAQABAQEBAAABAAEBAQAAAQAA=='}
$ mirobo --ip [ip] --token [token] raw_command miIO.ir_play '{"freq": 38400, "code": "Z6WPAasBAAA3BQAA4QYAAJMNAAAwJAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjABAAAAAAAAAAAAAAABAAAAAAAAAAEAAQAAAAAAAAAAAQABAQEBAAABAAEBAQAAAQQCMAEAAAAAAAAAAAAAAAEAAAAAAAAAAQABAAAAAAAAAAABAAEBAQEAAAEAAQEBAAABBAIwAQAAAAAAAAAAAAAAAQAAAAAAAAABAAEAAAAAAAAAAAEAAQEBAQAAAQABAQEAAAEEAjABAAAAAAAAAAAAAAABAAAAAAAAAAEAAQAAAAAAAAAAAQABAQEBAAABAAEBAQAAAQAA=="}'
Sending cmd miIO.ir_play with params {'freq': 38400, 'code': 'Z6WPAasBAAA3BQAA4QYAAJMNAAAwJAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjABAAAAAAAAAAAAAAABAAAAAAAAAAEAAQAAAAAAAAAAAQABAQEBAAABAAEBAQAAAQQCMAEAAAAAAAAAAAAAAAEAAAAAAAAAAQABAAAAAAAAAAABAAEBAQEAAAEAAQEBAAABBAIwAQAAAAAAAAAAAAAAAQAAAAAAAAABAAEAAAAAAAAAAAEAAQEBAQAAAQABAQEAAAEEAjABAAAAAAAAAAAAAAABAAAAAAAAAAEAAQAAAAAAAAAAAQABAQEBAAABAAEBAQAAAQAA=='}
0

Sending learn and allow timeout:

$ mirobo --ip [ip] --token [token] raw_command miIO.ir_read '{"key": "1"}'
Sending cmd miIO.ir_read with params {'key': '1'}
{'error': {'code': -5003, 'message': 'learn timeout'}, 'id': 17}

[ip] is the ip of the device, [token] is a valid token for the device, [MAC] is the mac of the device.

Misc things:

  1. The 'id': int seem to increase for each request but is only shown in 'error' responses.
  2. The device seems to reset by itself after some period of time, resetting the id and forgetting any learned commands.

If there is anything else you need just ask :-)

syssi commented 6 years ago

Thanks for your input! It should be enough to implement some tests for the device.

cnrd commented 6 years ago

Also small thing (That is also in my PR for hass):

$ mirobo --ip [ip] --token [wrong token correct length] raw_command miIO.ir_learn '{"key": "1"}'
Sending cmd miIO.ir_learn with params {'key': '1'}
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/construct/core.py", line 1222, in _parse
    return self.subcon._parse(stream, context, path)
  File "/usr/local/lib/python3.6/site-packages/construct/core.py", line 2038, in _parse
    obj = self.cases.get(key, self.default)._parse(stream, context, path)
  File "/usr/local/lib/python3.6/site-packages/construct/core.py", line 2541, in _parse
    hash2 if not isinstance(hash2,bytes) else hexlify(hash2), ))
construct.core.ChecksumError: wrong checksum, read b'ffffffffffffffffffffffffffffffff', computed b'abd26cc135d1e2faee21b65b35167929'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/bin/mirobo", line 11, in <module>
    sys.exit(cli())
  File "/usr/local/lib/python3.6/site-packages/click/core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/click/core.py", line 697, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python3.6/site-packages/click/core.py", line 1066, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/local/lib/python3.6/site-packages/click/core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib/python3.6/site-packages/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/click/decorators.py", line 64, in new_func
    return ctx.invoke(f, obj, *args[1:], **kwargs)
  File "/usr/local/lib/python3.6/site-packages/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/miio/vacuum_cli.py", line 517, in raw_command
    click.echo(vac.raw_command(cmd, params))
  File "/usr/local/lib/python3.6/site-packages/miio/vacuum.py", line 270, in raw_command
    return self.send(cmd, params)
  File "/usr/local/lib/python3.6/site-packages/miio/device.py", line 233, in send
    m = Message.parse(data, ctx)
  File "/usr/local/lib/python3.6/site-packages/construct/core.py", line 156, in parse
    return self.parse_stream(BytesIO(data), context, **kw)
  File "/usr/local/lib/python3.6/site-packages/construct/core.py", line 169, in parse_stream
    return self._parse(stream, context2, "(parsing)")
  File "/usr/local/lib/python3.6/site-packages/construct/core.py", line 874, in _parse
    subobj = sc._parse(stream, context, path)
  File "/usr/local/lib/python3.6/site-packages/construct/core.py", line 1226, in _parse
    raise e.__class__("%s\n    %s" % (e, path))
construct.core.ChecksumError: wrong checksum, read b'ffffffffffffffffffffffffffffffff', computed b'abd26cc135d1e2faee21b65b35167929'
    (parsing) -> checksum

This is the response I get from using an invalid token, where I replaced a single int with another int. Not sure if this is something you can look into?

Also, the 'code' length can differ, these are all the codes that I have captured until now as you can see, the "prosonic_power" is 144 chars while the others are 140:

      prosonic_power:
        command:
          - "Z6VPAAUCAABgAgAAxQYAAOUIAACUEQAAqyIAADSeAABwdQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFEBAQEBAQEBAgICAgICAgEBAgECAQEBAQIBAgECAgICBgNXA1cDUA"
      prosonic_sleep:
        command:
          - "Z6VHACMCAABCAgAAqAYAAMkIAAB3EQAAyyIAANCdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFEBAQEBAQEBAgICAgICAgECAQICAQECAQECAQECAgECBgNQA="
      prosonic_vol_up:
        command:
          - 'Z6VHAAUCAABhAgAAxQYAAOQIAACUEQAArCIAADSeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFEBAQEBAQEBAgICAgICAgEBAgICAgEBAQIBAQEBAgICBgNQA='
      prosonic_vol_down:
        command:
          - 'Z6VHAAQCAABhAgAAxQYAAOYIAACTEQAArCIAADSeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFEBAQEBAQEBAgICAgICAgECAgICAgECAQEBAQEBAgECBgNQA='
      prosonic_vol_mute:
        command:
          - 'Z6VHAB8CAABGAgAAqwYAAMsIAAB5EQAAxiIAANCdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFEBAQEBAQEBAgICAgICAgEBAQEBAgECAQICAgIBAgECBgNQA='
      prosonic_channel_up:
        command:
          - 'Z6VHAAQCAABiAgAAxgYAAOUIAACWEQAAqiIAADSeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFEBAQEBAQEBAgICAgICAgECAgICAgEBAQEBAQEBAgICBgNQA='
      prosonic_channel_down:
        command:
          - 'Z6VHAAQCAABhAgAAxgYAAOUIAACVEQAArCIAADSeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFEBAQEBAQEBAgICAgICAgEBAQICAgECAQICAQEBAgECBgNQA='
cnrd commented 6 years ago

Also I just captured the 'prosonic_power' command again: Z6VPACwCAACpBgAAyAgAAHYRAADHIgAA0J0AAHB1AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAQEBAQEBAQAAAQABAAAAAAEAAQABAQEBBQJGAkYCQA which differes from the one in my config, both works so it would seem like some extra information is encoded in those.

rytilahti commented 6 years ago

Just a couple of comments:

The 'id': int seem to increase for each request but is only shown in 'error' responses.

This is just because we return just the "result" if it is available, otherwise the whole message.

The device seems to reset by itself after some period of time, resetting the id and forgetting any learned commands.

This is a bit worrying, doesn't that render the device completely useless? Should those be cached and refreshed then periodically?

  File "/usr/local/lib/python3.6/site-packages/miio/device.py", line 233, in send
    m = Message.parse(data, ctx)
<snip>
construct.core.ChecksumError: wrong checksum, read b'ffffffffffffffffffffffffffffffff', computed > b'abd26cc135d1e2faee21b65b35167929'
    (parsing) -> checksum

I think we should catch ChecksumErrors here and wrap them around ChecksumError(DeviceException). Something like:

try:
    m = Message.parse(data, ctx)
except construct.core.ChecksumError as ex:
    raise ChecksumError("Invalid checksum") from ex

while at it I think we should wrap returned messages containing an error to DeviceError or similar (also based on DeviceException). The homeassistant platform can then easily just catch DeviceExceptions in general and handle them accordingly.

cnrd commented 6 years ago

This is a bit worrying, doesn't that render the device completely useless? Should those be cached and refreshed then periodically?

No it should not be a problem, any command captured by read can be replayed at any time (Even if there is something else in the slot), the ones in my config are 6+ months old. I think that commands in the Xiaomi Home app are also cached in the app and not stored on device.

About the ChecksumError, yes that would be great, atm i'm catching the ChecksumError directly, but would rather catch a DeviceException, but I think that we should make it clear that the token is wrong in that exception.

rytilahti commented 6 years ago

I created #180 to handle the checksum error (and also for device-delivered errors to allow making a distinction when a device reports an error-case).

I'm not sure I understand what you mean by the commands being able to to be replayed at any time. Do you mean that even when you cannot read them back with ir_read, invoking them still works?

cnrd commented 6 years ago

I'm on mobile so sorry for the formatting.

Yes invoking ir_play with any of the base64 encoded messages will send the correct IR "command" so it would seem like the actual IR "command" is encoded as part of the base64 string and slots are only used for learning as temp. storage.

But I'm pretty sure that the base64 string includes more than just the IR command, as I got slightly different strings after learning the same command twice.

Also another thing: the frequency parameter does not seem to matter, I've tried the default, but I've also tried setting it to 1 just for fun, and the TV turned on in both cases.

syssi commented 6 years ago

Could you execute the commands ir_lean and ir_play again with debug mode enabled and provide the responses?

$ mirobo --ip [ip] --token [token] raw_command miIO.ir_learn '{"key": "1"}'
['ok', 'may be something else?']

I will make sure the response contains only "['ok']".

cnrd commented 6 years ago
$ mirobo -d --ip [ip] --token [token] raw_command miIO.ir_learn '{"key": "1"}'
INFO:miio.vacuum_cli:Debug mode active
DEBUG:miio.vacuum_cli:Read stored sequence ids: {'seq': 27, 'manual_seq': 0}
DEBUG:miio.vacuum_cli:Connecting to [ip] with token [token]
Sending cmd miIO.ir_learn with params {'key': '1'}
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b''
DEBUG:miio.device:Got a response: Container:
    data = Container:
        data =  (total 0)
        value =  (total 0)
        offset1 = 32
        offset2 = 32
        length = 0
    header = Container:
        data = !1\x00 \x00\x00\x00\x00\x03\xef\x8b\xcb\x00\x00\x05\xc8 (total 16)
        value = Container:
            length = 32
            unknown = 0
            device_id = 03ef8bcb (total 8)
            ts = 1970-01-01 00:24:40
        offset1 = 0
        offset2 = 16
        length = 16
    checksum = VC\xcb\x1f\x15&\xa7\xd7t'\xdemo|P\xd2 (total 16)
DEBUG:miio.device:Discovered b'03ef8bcb' with ts: 1970-01-01 00:24:40, token: b'[token]'
DEBUG:miio.device:[ip]:54321 >>: {'id': 28, 'method': 'miIO.ir_learn', 'params': {'key': '1'}}
DEBUG:miio.device:[ip]:54321 (ts: 1970-01-01 00:24:40, id: 28) << {'result': 0, 'id': 28}
0
DEBUG:miio.vacuum_cli:Writing {'seq': 28, 'manual_seq': 0} to /Users/cnrd/Library/Caches/python-miio/python-mirobo.seq

Learn timed out:

$ mirobo -d --ip [ip] --token [token] raw_command miIO.ir_read '{"key": "1"}'
INFO:miio.vacuum_cli:Debug mode active
DEBUG:miio.vacuum_cli:Read stored sequence ids: {'seq': 28, 'manual_seq': 0}
DEBUG:miio.vacuum_cli:Connecting to [ip] with token [token]
Sending cmd miIO.ir_read with params {'key': '1'}
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b''
DEBUG:miio.device:Got a response: Container:
    data = Container:
        data =  (total 0)
        value =  (total 0)
        offset1 = 32
        offset2 = 32
        length = 0
    header = Container:
        data = !1\x00 \x00\x00\x00\x00\x03\xef\x8b\xcb\x00\x00\x06Y (total 16)
        value = Container:
            length = 32
            unknown = 0
            device_id = 03ef8bcb (total 8)
            ts = 1970-01-01 00:27:05
        offset1 = 0
        offset2 = 16
        length = 16
    checksum = VC\xcb\x1f\x15&\xa7\xd7t'\xdemo|P\xd2 (total 16)
DEBUG:miio.device:Discovered b'03ef8bcb' with ts: 1970-01-01 00:27:05, token: b'[token]'
DEBUG:miio.device:[ip]:54321 >>: {'id': 29, 'method': 'miIO.ir_read', 'params': {'key': '1'}}
DEBUG:miio.device:[ip]:54321 (ts: 1970-01-01 00:27:05, id: 29) << {'error': {'code': -5003, 'message': 'learn timeout'}, 'id': 29}
{'error': {'code': -5003, 'message': 'learn timeout'}, 'id': 29}
DEBUG:miio.vacuum_cli:Writing {'seq': 29, 'manual_seq': 0} to /Users/cnrd/Library/Caches/python-miio/python-mirobo.seq

No learned command in slot:

$ mirobo -d --ip [ip] --token [token] raw_command miIO.ir_read '{"key": "2"}'
INFO:miio.vacuum_cli:Debug mode active
DEBUG:miio.vacuum_cli:Read stored sequence ids: {'seq': 29, 'manual_seq': 0}
DEBUG:miio.vacuum_cli:Connecting to [ip] with token [token]
Sending cmd miIO.ir_read with params {'key': '2'}
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b''
DEBUG:miio.device:Got a response: Container:
    data = Container:
        data =  (total 0)
        value =  (total 0)
        offset1 = 32
        offset2 = 32
        length = 0
    header = Container:
        data = !1\x00 \x00\x00\x00\x00\x03\xef\x8b\xcb\x00\x00\x06\xed (total 16)
        value = Container:
            length = 32
            unknown = 0
            device_id = 03ef8bcb (total 8)
            ts = 1970-01-01 00:29:33
        offset1 = 0
        offset2 = 16
        length = 16
    checksum = VC\xcb\x1f\x15&\xa7\xd7t'\xdemo|P\xd2 (total 16)
DEBUG:miio.device:Discovered b'03ef8bcb' with ts: 1970-01-01 00:29:33, token: b'[token]'
DEBUG:miio.device:[ip]:54321 >>: {'id': 30, 'method': 'miIO.ir_read', 'params': {'key': '2'}}
DEBUG:miio.device:[ip]:54321 (ts: 1970-01-01 00:29:33, id: 30) << {'error': {'code': -5002, 'message': 'no code for this key'}, 'id': 30}
{'error': {'code': -5002, 'message': 'no code for this key'}, 'id': 30}
DEBUG:miio.vacuum_cli:Writing {'seq': 30, 'manual_seq': 0} to /Users/cnrd/Library/Caches/python-miio/python-mirobo.seq

Learned command in slot:

$ mirobo -d --ip [ip] --token [token] raw_command miIO.ir_read '{"key": "1"}'
INFO:miio.vacuum_cli:Debug mode active
DEBUG:miio.vacuum_cli:Read stored sequence ids: {'seq': 31, 'manual_seq': 0}
DEBUG:miio.vacuum_cli:Connecting to [ip] with token [token]
Sending cmd miIO.ir_read with params {'key': '1'}
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b''
DEBUG:miio.device:Got a response: Container:
    data = Container:
        data =  (total 0)
        value =  (total 0)
        offset1 = 32
        offset2 = 32
        length = 0
    header = Container:
        data = !1\x00 \x00\x00\x00\x00\x03\xef\x8b\xcb\x00\x00\x07R (total 16)
        value = Container:
            length = 32
            unknown = 0
            device_id = 03ef8bcb (total 8)
            ts = 1970-01-01 00:31:14
        offset1 = 0
        offset2 = 16
        length = 16
    checksum = VC\xcb\x1f\x15&\xa7\xd7t'\xdemo|P\xd2 (total 16)
DEBUG:miio.device:Discovered b'03ef8bcb' with ts: 1970-01-01 00:31:14, token: b'[token]'
DEBUG:miio.device:[ip]:54321 >>: {'id': 32, 'method': 'miIO.ir_read', 'params': {'key': '1'}}
DEBUG:miio.device:[ip]:54321 (ts: 1970-01-01 00:31:14, id: 32) << {'result': {'key': '1', 'code': 'Z6WPAasBAAA5BQAA6gYAAIoNAAAwJAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjABAAAAAAAAAAAAAAABAAAAAAAAAAEAAQAAAAAAAAAAAQABAQEBAAABAAEBAQAAAQQCMAEAAAAAAAAAAAAAAAEAAAAAAAAAAQABAAAAAAAAAAABAAEBAQEAAAEAAQEBAAABBAIwAQAAAAAAAAAAAAAAAQAAAAAAAAABAAEAAAAAAAAAAAEAAQEBAQAAAQABAQEAAAEEAjABAAAAAAAAAAAAAAABAAAAAAAAAAEAAQAAAAAAAAAAAQABAQEBAAABAAEBAQAAAQAA=='}, 'id': 32}
{'key': '1', 'code': 'Z6WPAasBAAA5BQAA6gYAAIoNAAAwJAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjABAAAAAAAAAAAAAAABAAAAAAAAAAEAAQAAAAAAAAAAAQABAQEBAAABAAEBAQAAAQQCMAEAAAAAAAAAAAAAAAEAAAAAAAAAAQABAAAAAAAAAAABAAEBAQEAAAEAAQEBAAABBAIwAQAAAAAAAAAAAAAAAQAAAAAAAAABAAEAAAAAAAAAAAEAAQEBAQAAAQABAQEAAAEEAjABAAAAAAAAAAAAAAABAAAAAAAAAAEAAQAAAAAAAAAAAQABAQEBAAABAAEBAQAAAQAA=='}
DEBUG:miio.vacuum_cli:Writing {'seq': 32, 'manual_seq': 0} to /Users/cnrd/Library/Caches/python-miio/python-mirobo.seq
yawor commented 6 years ago

I've analysed the binary data encoded in the base64 strings provided by @cnrd and I've reverse-engineered the format of the signal. It's really simple and won't be able to properly learn some IR protocols. I won't go into details here as the subject of IR control is really big and ugly. I was able to extract the signal from the base64 for prosonic power button (it's NEC1, Dev=0, Func=10). The format stores only a single stream of pulses and gaps. It uses little endian. $00 - 2 bytes - modulation frequency given as off and on time (one byte each), the unit is [100 nanoseconds], for signals above its: 0xA5=16.5 ms, 0x67=10.3 ms and the frequency can be calculated by freq = 1/(16.5 ms + 10.3 ms) = 1/(26.8 ms) =~ 37.3 kHz which is consistent with NEC1 signal. $02 - 2 bytes - number of rising/falling edges + 1 $04 - 16 x 4 bytes - index of 16 4-byte integers representing possible time values in milliseconds, sorted by ascending value - index 0 is at $04, index 1 is at $08 etc. $44 - start of IR stream - its length is the value from $04 * 4bits, rounded up to a whole byte

Each byte in IR stream represents a pair of a pulse and gap. The least significant 4 bits contain an index position of time of the pulse, followed by a gap which time is contained under index value represented by most significant 4 bits.

Having that information, it should not be hard to write method in the ChuangmiIr class, which would take other signal formats (like the most popular Pronto Hex), convert them on the fly and send them out.

yawor commented 6 years ago

Here's the working Construct for the signal format:

ChuangmiIrSignal = Struct(
    'frequency' / Struct(
        'off_time' / Byte,
        'on_time' / Byte,
    ),
    'edge_count' / Rebuild(Int16ul, len_(this.edge_pairs) * 2 - 1),
    'index' / Array(16, Int32ul),
    'edge_pairs' / Array((this.edge_count + 1) / 2, BitStruct(
        'gap' / BitsInteger(4),
        'pulse' / BitsInteger(4),
    ))
)
yawor commented 6 years ago

@cnrd can you try sending this one? omJDAD0CAACdBgAAmxEAAFAjAABpmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAQEBAQEBAQEAAQABAAAAAAEAAQABAQEBBA It should send the prosonic power button, but it's freshly generated from a Pronto Hex code. If this works then I have the prototype code working, which I would just need to cleanup and put into the library. @syssi where should I put the Construct definitions? Should I create a new module for them or put them directly into the chuangmi_ir? I have two Structs, one for the Chuangmi IR signal format and second one for parsing Pronto.

syssi commented 6 years ago

I would prefer a utility class at the chuangmi_ir.py. @rytilahti has more experience with such decisions. :-)

cnrd commented 6 years ago

@yawor This is the response I got from sending that command:

$ mirobo -d --ip [ip] --token [token] raw_command miIO.ir_play '{"freq": 38400, "code": "omJDAD0CAACdBgAAmxEAAFAjAABpmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAQEBAQEBAQEAAQABAAAAAAEAAQABAQEBBA"}'
INFO:miio.vacuum_cli:Debug mode active
DEBUG:miio.vacuum_cli:Read stored sequence ids: {'seq': 32, 'manual_seq': 0}
DEBUG:miio.vacuum_cli:Connecting to [ip] with token [token]
Sending cmd miIO.ir_play with params {'freq': 38400, 'code': 'omJDAD0CAACdBgAAmxEAAFAjAABpmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAQEBAQEBAQEAAQABAAAAAAEAAQABAQEBBA'}
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b''
DEBUG:miio.device:Got a response: Container:
    data = Container:
        data =  (total 0)
        value =  (total 0)
        offset1 = 32
        offset2 = 32
        length = 0
    header = Container:
        data = !1\x00 \x00\x00\x00\x00\x03\xef\x8b\xcb\x00\x01\xb8\xc4 (total 16)
        value = Container:
            length = 32
            unknown = 0
            device_id = 03ef8bcb (total 8)
            ts = 1970-01-02 07:20:36
        offset1 = 0
        offset2 = 16
        length = 16
    checksum = VC\xcb\x1f\x15&\xa7\xd7t'\xdemo|P\xd2 (total 16)
DEBUG:miio.device:Discovered b'03ef8bcb' with ts: 1970-01-02 07:20:36, token: b'[token]'
DEBUG:miio.device:[ip]:54321 >>: {'id': 33, 'method': 'miIO.ir_play', 'params': {'freq': 38400, 'code': 'omJDAD0CAACdBgAAmxEAAFAjAABpmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAQEBAQEBAQEAAQABAAAAAAEAAQABAQEBBA'}}
DEBUG:miio.device:[ip]:54321 (ts: 1970-01-02 07:20:37, id: 33) << {'error': {'code': -5008, 'message': 'magic error'}, 'id': 33}
{'error': {'code': -5008, 'message': 'magic error'}, 'id': 33}
DEBUG:miio.vacuum_cli:Writing {'seq': 33, 'manual_seq': 0} to /Users/cnrd/Library/Caches/python-miio/python-mirobo.seq

The TV however did not turn on, looks like there was some 'magic error', whatever that is :P

Not sure if you saw this, these are both prosonic on/off commands, but they are captured with a few months between them (Using the same remote): The newest:

Z6VPACwCAACpBgAAyAgAAHYRAADHIgAA0J0AAHB1AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAQEBAQEBAQAAAQABAAAAAAEAAQABAQEBBQJGAkYCQA

The older:

Z6VPAAUCAABgAgAAxQYAAOUIAACUEQAAqyIAADSeAABwdQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFEBAQEBAQEBAgICAgICAgEBAgECAQEBAQIBAgECAgICBgNXA1cDUA

As you can see, there is quite a bit of difference between them, but they both turn on/off the TV correctly.

Also here is the command captured four times in a row, both to 'slot: 1' (Captured these just now):

Z6UFANEAAAAjAQAAAwkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQIAE=
Z6VLADcCAACKBgAApQgAAFcRAADpIgAA0J0AAAx1AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAQEBAQEBAQAAAQABAAAAAAEAAQABAQEBBQJGAkAA==

I did hold the remote towards the device for a bit longer for the third and fourth capture.

Z6VLACICAABDAgAAhwYAAKQIAABXEQAA6SIAANCdAAAMdQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAQEBAQEBAQEhISEhISEhAQEhASEBAQEBIQEhASEhISFhNXE1AQ==
Z6VPADgCAACJBgAApggAAMIIAABXEQAA4CIAANCdAAAMdQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAQEBAQEBAQAAAQABAAAAAAEAAQABAQEBBgNXAlcCUA

Just to make it completely clear, all six command examples are the same on/off (Red remote button) from the remote that came with the Prosonic TV.

yawor commented 6 years ago

The difference between these two is not an issue. The IR capture is never ideal and you'll almost certainly always get a different result. You can check that by trying to record the same button multiple times. Also because of the way this device records the signal you'll probably get different results because of how long you hold the button during the recording. There is a lot of variables:

The "magic error" is probably because I've misinterpreted first two bytes of the signal format. But it fit so well to the frequency calculation that I thought that it must be it. If these first two bytes are magic bytes, then there's no way to set the carrier frequency and it's locked to ~38 kHz. This means there's no way to control some devices which work at much higher carrier frequencies (like some MCE receivers, which us ~56 kHz).

OK try this one Z6VDAD0CAACdBgAAmxEAAFAjAABpmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAQEBAQEBAQEAAQABAAAAAAEAAQABAQEBBA

I've set first two bytes to a constant value from your original captured signals. This is again the code for power button.

cnrd commented 6 years ago

Actually we set the freq as part of the command: raw_command miIO.ir_play '{"freq": 38400, "code": "om... but I'm unsure if the device is actually using it, as setting it to 1 works the same. Not really sure that I can test the frequency theory in any way, as I don't know what frequency any of my remotes use and I have no equipment to test it.

yawor commented 6 years ago

Ah, this explains a lot :). Sorry, I've missed that. That's awesome. Try setting the frequency to 38381. I know it's not a big difference but at least we'll see if if can be set to some arbitrary values.

cnrd commented 6 years ago

No errors, but TV does not react to the last command you posted:

$ mirobo -d --ip [ip] --token [token] raw_command miIO.ir_play '{"freq": 38400, "code": "Z6VDAD0CAACdBgAAmxEAAFAjAABpmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAQEBAQEBAQEAAQABAAAAAAEAAQABAQEBBA"}'
INFO:miio.vacuum_cli:Debug mode active
DEBUG:miio.vacuum_cli:Read stored sequence ids: {'seq': 47, 'manual_seq': 0}
DEBUG:miio.vacuum_cli:Connecting to [ip] with token [token]
Sending cmd miIO.ir_play with params {'freq': 38400, 'code': 'Z6VDAD0CAACdBgAAmxEAAFAjAABpmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAQEBAQEBAQEAAQABAAAAAAEAAQABAQEBBA'}
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b''
DEBUG:miio.device:Got a response: Container:
    data = Container:
        data =  (total 0)
        value =  (total 0)
        offset1 = 32
        offset2 = 32
        length = 0
    header = Container:
        data = !1\x00 \x00\x00\x00\x00\x03\xef\x8b\xcb\x00\x01\xc2; (total 16)
        value = Container:
            length = 32
            unknown = 0
            device_id = 03ef8bcb (total 8)
            ts = 1970-01-02 08:00:59
        offset1 = 0
        offset2 = 16
        length = 16
    checksum = VC\xcb\x1f\x15&\xa7\xd7t'\xdemo|P\xd2 (total 16)
DEBUG:miio.device:Discovered b'03ef8bcb' with ts: 1970-01-02 08:00:59, token: b'[token]'
DEBUG:miio.device:[ip]:54321 >>: {'id': 48, 'method': 'miIO.ir_play', 'params': {'freq': 38400, 'code': 'Z6VDAD0CAACdBgAAmxEAAFAjAABpmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAQEBAQEBAQEAAQABAAAAAAEAAQABAQEBBA'}}
DEBUG:miio.device:[ip]:54321 (ts: 1970-01-02 08:00:59, id: 48) << {'result': 0, 'id': 48}
0
DEBUG:miio.vacuum_cli:Writing {'seq': 48, 'manual_seq': 0} to /Users/cnrd/Library/Caches/python-miio/python-mirobo.seq

Also not sure if you saw that part, I tried setting the freq to 1, the command still worked, so the device may ignore the freq part?

Also tried setting the freq to 38381 (With a known working code) this is the response (TV did turn on):

mirobo -d --ip [ip] --token [token] raw_command miIO.ir_play '{"freq": 38381, "code": "Z6VPADgCAACJBgAApggAAMIIAABXEQAA4CIAANCdAAAMdQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAQEBAQEBAQAAAQABAAAAAAEAAQABAQEBBgNXAlcCUA"}'
INFO:miio.vacuum_cli:Debug mode active
DEBUG:miio.vacuum_cli:Read stored sequence ids: {'seq': 48, 'manual_seq': 0}
DEBUG:miio.vacuum_cli:Connecting to [ip] with token [token]
Sending cmd miIO.ir_play with params {'freq': 38381, 'code': 'Z6VPADgCAACJBgAApggAAMIIAABXEQAA4CIAANCdAAAMdQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAQEBAQEBAQAAAQABAAAAAAEAAQABAQEBBgNXAlcCUA'}
DEBUG:miio.protocol:Unable to decrypt, returning raw bytes: b''
DEBUG:miio.device:Got a response: Container:
    data = Container:
        data =  (total 0)
        value =  (total 0)
        offset1 = 32
        offset2 = 32
        length = 0
    header = Container:
        data = !1\x00 \x00\x00\x00\x00\x03\xef\x8b\xcb\x00\x01\xc3\x01 (total 16)
        value = Container:
            length = 32
            unknown = 0
            device_id = 03ef8bcb (total 8)
            ts = 1970-01-02 08:04:17
        offset1 = 0
        offset2 = 16
        length = 16
    checksum = VC\xcb\x1f\x15&\xa7\xd7t'\xdemo|P\xd2 (total 16)
DEBUG:miio.device:Discovered b'03ef8bcb' with ts: 1970-01-02 08:04:17, token: b'[token]'
DEBUG:miio.device:[ip]:54321 >>: {'id': 49, 'method': 'miIO.ir_play', 'params': {'freq': 38381, 'code': 'Z6VPADgCAACJBgAApggAAMIIAABXEQAA4CIAANCdAAAMdQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFAAAAAAAAAAAQEBAQEBAQAAAQABAAAAAAEAAQABAQEBBgNXAlcCUA'}}
DEBUG:miio.device:[ip]:54321 (ts: 1970-01-02 08:04:17, id: 49) << {'result': 0, 'id': 49}
0
DEBUG:miio.vacuum_cli:Writing {'seq': 49, 'manual_seq': 0} to /Users/cnrd/Library/Caches/python-miio/python-mirobo.seq

If you need more debugging code, just send me a modified version of mirobo and I'll use that instead :-)

yawor commented 6 years ago

Try this Z6VPAD0CAACdBgAA2ggAAJsRAABQIwAAaZsAAMF3AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAQEBAQEBAQEAAQABAAAAAAEAAQABAQEBBQJGAkYCQA

I've added 3 repeat frames at the end like in your original signal. Maybe your TV needs at least 2 or 3 repeat frames for the power button and won't accept the signal otherwise.

cnrd commented 6 years ago

Nope :-/ Are you sure that it is the correct Proton code you are using, the specific TV model is: Prosonic PBT-23000

yawor commented 6 years ago

Mea culpa. I've messed up the params for the NEC1 protocol. I'm not using any database. I've decoded your signal to check what it is and then generated new one from a program I'm using (IrScrutinizer, it can parse signal, detect and decode the protocol data, generate signal with any protocol it knows with any data etc). NEC1 has a Device ID and Subdevice ID fields. I've set the Device ID to 0 correctly, but I've missed that your signal has Subdevice ID = 127. I've left a default value which is 255 for dev id 0.

I hope that this one will finally work: Z6VPAD0CAACdBgAA2ggAAJsRAABQIwAAyZ8AAMF3AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAQEBAQEBAQAAAQABAAAAAAEAAQABAQEBBQJGAkYCRg

cnrd commented 6 years ago

Yes!

yawor commented 6 years ago

I've created #183 which implements ability to play Pronto codes.

syssi commented 6 years ago

I think the scope of this issue is solved here. Thanks to all!