Generating IR codes programmatically
I have a few minisplit units that I'd like to add support for (each a different brand/remote). I want to support as many features as possible, but manually capturing codes for every possible combination is tedious and error-prone. It also limits support to specific IR blaster models which are compatible with the codes.
Most IR codes are relatively easy to reverse engineer. Some time ago I wrote a library to automatically parse and guess IR encodings. I have published it here (I plan to publish it on PyPI soon if this is useful): https://github.com/marcan/circa
Would SmartIR be interested in adding this kind of approach for code generation? This would require extending the code format with a couple things:
- A bit more flexibility in describing device capabilities (for things like different temperature ranges for different modes, I've seen some devices handle this and am asking on the Discord about it)
- Some way to describe the algorithm to create an IR packet out of the current device state. Ideally this would be a Python snippet. This could be in actual py files, or we could allow the json code files to include an in-line Python snippet for this.
Is this something that would be welcome in this integration? I think it could allow many of the existing code files to become simpler and fix any errors over time (we could use heuristics to automatically reverse engineer and reduce many code files to a piece of code, of course with manual user validation to ensure it still works) and it would allow supporting a much larger feature matrix than is possible with an exhaustive code list.
I would be happy to prototype this, I just want to get a feel for the interest/direction. If SmartIR is not interested in this approach I could write my own integration or fork SmartIR, but I think a lot of the general scaffolding/logic could be shared with SmartIR (it's just the code specification/generation that would be replaced), so I feel like this makes more sense as a contribution to SmartIR rather than a completely separate fork/project.
Just to show what I mean, this is what circa decodes from my remote in auto mode increasing the temperature offset from 0 to +5 (in .5 increments):
80.5% nec:tp=448,ph=3744,a=-1,pg=37304,b=6:11,da,27,00,02,00,00,00,00,03,00,00,10,00,40,00,00,00,00,67;11,da,27,00,00,09,c0,80,a0,00,00,06,60,00,00,c3,00,00,24
81.1% nec:tp=451,ph=3711,a=-1,pg=37402,b=6:11,da,27,00,02,00,00,00,00,03,00,00,10,00,40,00,00,00,00,67;11,da,27,00,00,09,c1,80,a0,00,00,06,60,00,00,c3,00,00,25
75.9% nec:tp=448,ph=3728,a=-1,pg=37402,b=6:11,da,27,00,02,00,00,00,00,03,00,00,10,00,40,00,00,00,00,67;11,da,27,00,00,09,c3,80,a0,00,00,06,60,00,00,c3,00,00,27
75.3% nec:tp=450,ph=3694,a=-1,pg=37402,b=6:11,da,27,00,02,00,00,00,00,03,00,00,10,00,40,00,00,00,00,67;11,da,27,00,00,09,c4,80,a0,00,00,06,60,00,00,c3,00,00,28
80.7% nec:tp=448,ph=3711,a=-1,pg=37402,b=6:11,da,27,00,02,00,00,00,00,03,00,00,10,00,40,00,00,00,00,67;11,da,27,00,00,09,c5,80,a0,00,00,06,60,00,00,c3,00,00,29
81.4% nec:tp=452,ph=3678,a=-1,pg=37402,b=6:11,da,27,00,02,00,00,00,00,03,00,00,10,00,40,00,00,00,00,67;11,da,27,00,00,09,c6,80,a0,00,00,06,60,00,00,c3,00,00,2a
63.2% nec:tp=450,ph=3728,a=-1,pg=37369,b=6:11,da,27,00,02,00,00,00,00,03,00,00,10,00,40,00,00,00,00,67;11,da,27,00,00,09,c7,80,a0,00,00,06,60,00,00,c3,00,00,2b
81.4% nec:tp=452,ph=3678,a=-1,pg=37402,b=6:11,da,27,00,02,00,00,00,00,03,00,00,10,00,40,00,00,00,00,67;11,da,27,00,00,09,c8,80,a0,00,00,06,60,00,00,c3,00,00,2c
87.8% nec:tp=461,ph=3728,a=-1,pg=37369,b=6:11,da,27,00,02,00,00,00,00,03,00,00,10,00,40,00,00,00,00,67;11,da,27,00,00,09,c9,80,a0,00,00,06,60,00,00,c3,00,00,2d
81.2% nec:tp=451,ph=3694,a=-1,pg=37402,b=6:11,da,27,00,02,00,00,00,00,03,00,00,10,00,40,00,00,00,00,67;11,da,27,00,00,09,ca,80,a0,00,00,06,60,00,00,c3,00,00,2e
The IR protocol was identified as nec with certain signal parameters (that are quite consistent). The temperature is obviously the changing byte near the middle (c0..ca), and likely the last byte is a checksum (the whole code seems to consist of two back to back packets, probably each with its own checksum). This format makes it very easy to programmatically generate the codes.
For referece, this is roughly the logic required for generating the codes for one of my units (not all features added, but most):
def code(hvac_mode, swing_mode, fan_mode, temp, cleaning_enabled):
if swing_mode == "nice":
fan_mode = "auto"
if hvac_mode == "auto" and fan_mode not in ("auto", "quiet"):
fan_mode = "auto"
c_fan_mode = {
"auto": 0xa0,
"quiet": 0xb0,
"low": 0x30,
"low_medium": 0x40,
"medium": 0x50,
"medium_high": 0x60,
"high": 0x70,
}[fan_mode]
if swing_mode == "on":
c_fan_mode |= 0xf
c_fan_direction = {
"on": 0,
"top": 1,
"upper": 2,
"middle": 3,
"lower": 4,
"bottom": 5,
"nice": 0,
}[swing_mode]
c_mode = {
"off": 0x38,
"auto": 0x09,
"dry": 0x29,
"cool": 0x39,
"heat": 0x49,
"fan_only": 0x69,
}[hvac_mode]
c_temp_relative = hvac_mode in ("dry", "auto")
if c_temp_relative:
c_temp = 0xc0 | (int(temp * 2) & 0x1f)
else:
c_temp = int(temp * 2)
return [
[
0x11, 0xda, 0x27, 0x00, 0x02, 0, 0, 0, 0,
0x03, # Button pressed, probably unimportant
0,
0x80 if hvac_mode == 'off' else 0,
c_fan_direction,
0,
0x40 if cleaning_enabled else 0,
0,0,0,0;
],
[
0x11,0xda,0x27,0x00,0x00,
c_mode,
c_temp,
0x80 if c_temp_relative else 0,
c_fan_mode,
0, 0, 0x06, 0x60, 0, 0, 0xc3,
1 if swing_mode == nice else 0,
0
],
]
This generates almost 8000 possible code combinations programmativally, which would be practically impossible to capture manually without error. My feeling right now is that, to implement this, it would be nice to have a Python script next to or replacing the json code file, which holds this logic. The main component can then load this script to call the code generation function.
My opinion is that before you think to integrate it in SmartIR you should prove your theory that these generated IR codes works.
For now make a python script with your logic that ends generating a .json file that we can manually add to codes/climate folder.
Then if users report that all these generated IR codes effectively work you can start thinking to integrate all this in SmartIR.
The codes definitely work, I've done this kind of thing before and other projects exist that generate aircon codes programmatically like this.
What I'm trying to do is add support for a matrix of features that is not feasible to do with a giant json file of codes. Also things like different temperature ranges for different modes (which is common). So it's not just the codes. The existing code infrastructure is not sufficient for what I'm trying to do, so it's not just a matter of generating a json. And it doesn't make sense to add the missing infrastructure to SmartIR before support for the code generation approach, because the current code dump approach doesn't really scale to that level of complexity with any ease.
I've started working on this here: https://github.com/marcan/SmartIR
This branch also adds support for cross-controller compatibility, so you can use code files with different controllers (as long as circa knows how to convert the formats).
I have now updated the above branch with a full PoC that works on one of my aircons (Daikin ARC478A30 remote, which works for over 100 aircon models). The device info file is replaced with a Python script which contains the same info structure (as a dictionary) alongside a function to programmatically generate the codes. Python device files are specified as 5-digit codes, to differentiate them from the JSON ones. This is also intended to allow both codes to coexist for testing, so for example, NNNN and 1NNNN could be for the same device, but one uses the code dump and the other programmatic generation. Then as users verify that it works, the old JSON file could be deprecated or redirected.
Other improvements:
- Switch from distutils to packaging (to support newer Python versions)
- Reduce some code duplication
- Rename the Xiaomi raw format to "Xiaomi" (since this is not a documented raw format and it should be treated differently to other raw formats)
- Allow specifying the controller type in the config, so codes written using one format can be used on other controllers as long as circa knows how to convert them
- Add generic format conversion. This allows converting across all formats and controllers except Xiaomi Raw.
- Support per-mode temperatures in the Climate integration. When temperature min/max are specified per mode, the limits are enforced and also the temperature is remembered per mode, so switching modes goes back to the old set temperature for that mode (this is also how the real remote control operates). Modes without valid temperature (off and fan) will just show a dummy 0-0 range.
If the SmartIR maintainers are interested in these changes I'd be happy to open a PR.
I added support for a Toshiba aircon that I have, which uses essentially the same codes as 1260 (with minor differences in interpretation). this is the 1260 code file and this is the "11260" code file for my Python implementation of the format. Note how it becomes much shorter and less error-prone. And yes, I tested it, both work :)
I'd just like to say I have also used this approach before with a Fujitsu heat pump in a previous home, and I think it's superior and highly worth pursuing.
Having both approaches available is the best of both worlds.
I have a Fujitsu model (a different one) in my current home which is partially compatible with an existing one in this project, but has horizontal and vertical swing options. To add these in I'd have to capture a huge number of additional combinations which I don't really want to do, but I'd be quite willing to reverse engineer the protocol and add it via this programmatic approach.
How to make a file like this https://github.com/marcan/SmartIR/blob/master/codes/climate/11260.py
Could you not just make a separate tool that generates the file we need for the current smartIR version too?
You could generate the codes for the current version, but it would be harder to maintain (Where do you keep the generator code, a separate repo? Who makes sure the files are synchronized?) and it would not solve the problem of explosive complexity as you add features (you could end up with code files in the megabytes if you add more dimensions). You would also lose the automatic conversion feature which allows codes to work across different remote types.
It's kind of a worst of both worlds approach. If the code format is known, it doesn't make sense to store all code combinations in raw form any more.
FWIW, my current fork works quite well for me and does what I want, but I have limited time to explore other approaches. If the project maintainers want to move forward in this direction I'm happy to work with them, but if they don't I'll keep the changes in my branch and anyone is free to use it if it's useful to them.
How to make a file like this https://github.com/marcan/SmartIR/blob/master/codes/climate/11260.py
You use circa to parse codes like this:
python -m circa decode broadlink:<code here>
For example, for the first code in 1260.json:
$ python -m circa decode broadlink:JgAqAY6QETYRNRE2EDcREhAUETURExATEBMSEhATETYQNhAUEDYQFRASEBMQFBATERIRNhI1EDYSNRE2ETYRNRE2ERIRFBASEBMQFBETEBMQFBASETYREhETEBMREhEUDxMQExITEBIQFA8UERIQExETEDYRNhESERMREhETEhERExATERMQExATERIQFA8UERIQNxATEPaPkBA2ETURNhE2EBQPFBE1EBQQExAUEBQQEhA3EDYQFBE1ERMREhESERMQExAUEDYRNhE2EDYRNhA3EDYQNxESEBUQEhETERMQExATERMPExA3ERIQFRASEBMRFBASERMRExASERIQFBESERMQExE1EjURExESEBMQFBATEBMSEhATERMPFRATERIQExESETYQExEADQUAAAAAAAAAAAAAAAAAAA==
100.0% raw:4333,4395,519,1648,519,1617,519,1648,488,1679,518,550,488,610,519,1617,519,580,488,580,489,579,550,549,488,580,519,1648,488,1648,488,611,488,1648,488,641,488,550,488,580,488,610,489,580,518,550,518,1648,550,1617,488,1648,550,1617,519,1648,519,1648,518,1618,519,1648,518,550,519,610,488,549,489,580,488,610,519,580,488,580,488,611,488,549,519,1648,519,549,519,580,488,580,519,549,519,610,458,580,488,580,549,580,488,550,488,610,458,610,519,549,489,580,518,580,488,1648,519,1648,519,549,519,580,519,549,519,580,549,519,519,579,489,579,519,580,488,580,489,579,519,549,489,610,458,610,519,549,489,1678,488,580,488,7508,4364,4394,489,1648,518,1618,519,1648,518,1648,489,610,458,610,519,1617,489,610,488,580,488,611,488,610,489,549,488,1679,488,1648,488,611,518,1618,519,579,519,549,519,550,518,580,489,579,489,610,488,1648,519,1648,519,1648,488,1648,519,1648,488,1678,489,1648,488,1678,519,550,488,641,488,549,519,580,519,580,488,580,488,580,519,579,458,580,488,1679,519,549,488,641,488,550,488,580,519,610,488,549,519,580,519,580,488,549,519,549,489,610,519,549,519,580,488,580,519,1617,549,1618,519,580,518,550,488,580,488,610,489,580,488,580,549,549,489,579,519,580,458,641,488,580,519,549,488,580,519,549,519,1648,488,580,519,101715
100.0% rawpm:4333,-4395,519,-1648,519,-1617,519,-1648,488,-1679,518,-550,488,-610,519,-1617,519,-580,488,-580,489,-579,550,-549,488,-580,519,-1648,488,-1648,488,-611,488,-1648,488,-641,488,-550,488,-580,488,-610,489,-580,518,-550,518,-1648,550,-1617,488,-1648,550,-1617,519,-1648,519,-1648,518,-1618,519,-1648,518,-550,519,-610,488,-549,489,-580,488,-610,519,-580,488,-580,488,-611,488,-549,519,-1648,519,-549,519,-580,488,-580,519,-549,519,-610,458,-580,488,-580,549,-580,488,-550,488,-610,458,-610,519,-549,489,-580,518,-580,488,-1648,519,-1648,519,-549,519,-580,519,-549,519,-580,549,-519,519,-579,489,-579,519,-580,488,-580,489,-579,519,-549,489,-610,458,-610,519,-549,489,-1678,488,-580,488,-7508,4364,-4394,489,-1648,518,-1618,519,-1648,518,-1648,489,-610,458,-610,519,-1617,489,-610,488,-580,488,-611,488,-610,489,-549,488,-1679,488,-1648,488,-611,518,-1618,519,-579,519,-549,519,-550,518,-580,489,-579,489,-610,488,-1648,519,-1648,519,-1648,488,-1648,519,-1648,488,-1678,489,-1648,488,-1678,519,-550,488,-641,488,-549,519,-580,519,-580,488,-580,488,-580,519,-579,458,-580,488,-1679,519,-549,488,-641,488,-550,488,-580,519,-610,488,-549,519,-580,519,-580,488,-549,519,-549,489,-610,519,-549,519,-580,488,-580,519,-1617,549,-1618,519,-580,518,-550,488,-580,488,-610,489,-580,488,-580,549,-549,489,-579,519,-580,458,-641,488,-580,519,-549,488,-580,519,-549,519,-1648,488,-580,519,-101715
100.0% broadlink:JgAqAY6QETYRNRE2EDcREhAUETURExATEBMSEhATETYQNhAUEDYQFRASEBMQFBATERIRNhI1EDYSNRE2ETYRNRE2ERIRFBASEBMQFBETEBMQFBASETYREhETEBMREhEUDxMQExITEBIQFA8UERIQExETEDYRNhESERMREhETEhERExATERMQExATERIQFA8UERIQNxATEPaPkBA2ETURNhE2EBQPFBE1EBQQExAUEBQQEhA3EDYQFBE1ERMREhESERMQExAUEDYRNhE2EDYRNhA3EDYQNxESEBUQEhETERMQExATERMPExA3ERIQFRASEBMRFBASERMRExASERIQFBESERMQExE1EjURExESEBMQFBATEBMSEhATERMPFRATERIQExESETYQExEADQUAAAAAAAAAAAAAAAAAAA==
100.0% broadlink-hex:26002a018e901136113511361037111210141135111310131013121210131136103610141036101510121013101410131112113612351036123511361136113511361112111410121013101411131013101410121136111211131013111211140f1310131213101210140f14111210131113103611361112111311121113121111131013111310131013111210140f1411121037101310f68f90103611351136113610140f14113510141013101410141012103710361014113511131112111211131013101410361136113610361136103710361037111210151012111311131013101311130f13103711121015101210131114101211131113101211121014111211131013113512351113111210131014101310131212101311130f1510131112101311121136101311000d050000
95.6% pronto:0000 006D 0094 0000 00A5 00A7 0014 003F 0013 003E 0014 003E 0013 0040 0013 0015 0013 0017 0014 003D 0014 0016 0013 0016 0012 0016 0015 0015 0013 0016 0014 003E 0013 003F 0012 0017 0013 003F 0012 0019 0012 0015 0013 0016 0012 0018 0012 0016 0014 0015 0014 003E 0015 003E 0012 003F 0015 003D 0014 003F 0014 003E 0014 003E 0013 003F 0014 0015 0013 0018 0012 0015 0013 0016 0012 0018 0013 0016 0013 0016 0013 0017 0012 0015 0014 003F 0013 0015 0014 0016 0013 0016 0014 0014 0014 0017 0012 0016 0012 0017 0014 0017 0012 0015 0013 0017 0011 0017 0014 0015 0013 0016 0013 0016 0013 003F 0014 003E 0014 0015 0014 0016 0013 0015 0014 0016 0015 0014 0013 0017 0012 0016 0014 0016 0013 0016 0012 0016 0014 0015 0012 0018 0011 0017 0014 0015 0013 003F 0013 0016 0013 011D 00A6 00A8 0012 003F 0014 003D 0014 003F 0013 003F 0013 0017 0011 0017 0014 003E 0012 0017 0013 0016 0013 0017 0012 0018 0012 0015 0013 0040 0012 003F 0012 0018 0013 003E 0014 0016 0014 0014 0014 0015 0014 0016 0012 0016 0013 0017 0013 003F 0013 003F 0014 003E 0013 003F 0013 003F 0013 0040 0012 003F 0012 0040 0014 0015 0013 0018 0013 0014 0014 0016 0014 0016 0013 0016 0012 0016 0014 0016 0012 0016 0012 0040 0014 0015 0012 0019 0012 0015 0013 0016 0013 0018 0012 0015 0014 0016 0014 0016 0012 0015 0014 0015 0012 0018 0013 0015 0014 0016 0013 0016 0014 003D 0015 003D 0014 0016 0014 0015 0013 0016 0012 0017 0013 0016 0013 0016 0015 0014 0013 0016 0014 0016 0011 0019 0012 0016 0014 0015 0013 0016 0013 0015 0014 003F 0012 0016 0014 0F1E
88.4% nec:tp=503,t0=579,ph=4348,pl=4394,cm=2,pg=7508,ck=1:4f,c0,80,00,c0,00;4f,c0,80,00,c0,00
88.4% necb:tp=503,t0=579,ph=4348,pl=4394,cm=2,pg=7508,ck=2:f2,03,01,00,03,00;f2,03,01,00,03,00
77.1% nec:ph=4348,pl=4394,cm=2,pg=7508,ck=1:4f,c0,80,00,c0,00;4f,c0,80,00,c0,00
77.1% necb:ph=4348,pl=4394,cm=2,pg=7508,ck=2:f2,03,01,00,03,00;f2,03,01,00,03,00
You ignore all the stuff at the top and look at the simplified codes at the end. If you keep doing it for various codes you get stuff like:
# For heat-auto-17deg
88.4% nec:tp=503,t0=579,ph=4348,pl=4394,cm=2,pg=7508,ck=1:4f,c0,80,00,c0,00;4f,c0,80,00,c0,00
88.4% necb:tp=503,t0=579,ph=4348,pl=4394,cm=2,pg=7508,ck=2:f2,03,01,00,03,00;f2,03,01,00,03,00
77.1% nec:ph=4348,pl=4394,cm=2,pg=7508,ck=1:4f,c0,80,00,c0,00;4f,c0,80,00,c0,00
77.1% necb:ph=4348,pl=4394,cm=2,pg=7508,ck=2:f2,03,01,00,03,00;f2,03,01,00,03,00
# For heat-auto-18deg
88.8% nec:tp=507,t0=576,t1=1643,ph=4348,pl=4380,cm=2,pg=7508,ck=1:4f,c0,80,08,c0,00;4f,c0,80,08,c0,00
88.8% necb:tp=507,t0=576,t1=1643,ph=4348,pl=4380,cm=2,pg=7508,ck=2:f2,03,01,10,03,00;f2,03,01,10,03,00
76.8% nec:ph=4348,pl=4380,cm=2,pg=7508,ck=1:4f,c0,80,08,c0,00;4f,c0,80,08,c0,00
76.8% necb:ph=4348,pl=4380,cm=2,pg=7508,ck=2:f2,03,01,10,03,00;f2,03,01,10,03,00
# For heat-auto-30deg
83.6% nec:tp=497,t0=588,t1=1649,ph=4303,pl=4425,cm=2,pg=7508,ck=1:4f,c0,80,0b,c0,00;4f,c0,80,0b,c0,00
83.6% necb:tp=497,t0=588,t1=1649,ph=4303,pl=4425,cm=2,pg=7508,ck=2:f2,03,01,d0,03,00;f2,03,01,d0,03,00
The stuff at the beginning is the code format, then the data is after the :. The code format should be roughly consistent and you can usually pick the shortest/simplest one. In my case I chose necb:ph=4367,pl=4395,cm=2,pg=5249,ck=2 (pg is significantly different because I didn't use the codes from the json file, my remote is actually slightly different and I captured codes directly from it, but it doesn't matter).
Whether it's necb or nec depends on what makes sense for the data, and for some codes only one of those will show up, or the other will have longer data and a different ck (checksum) option, then you know which one it is.
If you remove the stuff at the front then you have:
# For heat-auto-17deg
nec 4f,c0,80,00,c0,00;4f,c0,80,00,c0,00
necb f2,03,01,00,03,00;f2,03,01,00,03,00
# For heat-auto-18deg
nec 4f,c0,80,08,c0,00;4f,c0,80,08,c0,00
necb f2,03,01,10,03,00;f2,03,01,10,03,00
# For heat-auto-19deg
nec 4f,c0,80,04,c0,00;4f,c0,80,04,c0,00
necb f2,03,01,20,03,00;f2,03,01,20,03,00
[...]
# For heat-auto-30deg
nec 4f,c0,80,0b,c0,00;4f,c0,80,0b,c0,00
necb f2,03,01,d0,03,00;f2,03,01,d0,03,00
You can tell the code is sent twice (separated by ;), always two copies. That's the [d, d] part of the Python script. Then for the actual data you can tell the temperature is in the third-to-last byte. necb goes 00-10-20 ... d0 which makes sense for an increasing number, nec goes 00-08-04...0b which doesn't. So now you know the temperature is in the top nybble of that byte, and 0 means 17 degrees, and d (13) means 30 degrees, so that byte should be (temperature - 17) << 4.
You keep doing that for a bunch of other codes covering all possible modes and features and in the end you can work out the logic for how the whole code is built, without having to literally enumerate every single combination (for example, above I just skipped from 17, 18, 19 degrees to 30, you can assume 20...29 all follow the pattern).