Air conditioner Automation

Introduction

I have a Toshiba RAS-B13N3KV2-E1 air conditioner system in my house that is not connected, meaning that I cannot turn it on or turn it off while I am outside. And many times, when I go out I forget to turn it on, or to program it to turn on and unfortunately when I come back, it is very warm inside. It would be perfect, if I could automate this air conditioner system in order to be able to switch on from outside, or even better to turn it on automagically if the temperature is too high.

In order to control my air conditioner system, I am using some remote controls using basic infra-red communication. Thus, the idea would be to have small system that I can reach from the network and that sends the good IR commands to my air conditioner system, in order to turn it on or off. I need two of them, because I have a module downstairs and another one upstairs. I shouldn’t be able to use only one, because IR needs to be in light of sight and there is no such place in my house, to be in light of sight of my two air conditioner modules.

remote

The different steps of this project are probably going to be:

  • Recover the correct IR commands sent by my both remote commands
  • Set up a system (raspberry pi or ESP8266) to be able to be reached from the network and to be able to send the previously retrieved IR commands
  • Implement some glue in homeassistant to be able to pilot, depending on the temperature and my presence at home, this air conditioner system

Of course, and obviously, everything needs to be in line with the WAF, otherwise things are going to go bad for myself.

Acting as my remote controller

IR commands detection

In order to get the IR commands sent by my official air conditioner remote, I use a raspberry pi and a simple IR receiver I got from an electronic starter kit.

IR_receiver

By default, the raspbian kernel does not load any drivers related to IR, thus it is necessary to update its configuration, in order for the IR receiver to be recognized by the kernel. The dtoverlay=gpio-ir,gpio_pin=17 line in the /boot/config.txt has to be uncommented for that.

The first pin of the IR receiver needs to be connected to the GPIO 17 of the RPI (according the configuration done previously), the middle pin needs to be connected to VCC and the last one to the ground. Depending on the IR receiver used, it may vary. If everything works well, we can read in the logs that the receiver has been detected by the kernel.

$ dmesg
...
[    6.843792] Registered IR keymap rc-rc6-mce
[    6.877002] IR RC6 protocol handler initialized
[    6.914625] rc rc0: gpio_ir_recv as /devices/platform/ir-receiver@11/rc/rc0
[    6.914899] rc rc0: lirc_dev: driver gpio_ir_recv registered at minor = 0, raw IR receiver, no transmitter
[    6.915179] input: gpio_ir_recv as /devices/platform/ir-receiver@11/rc/rc0/input0
...

It is also possible to ensure that the gpio modules are well loaded and the device exists.

$ lsmod | grep gpio
gpio_ir_recv           16384  0
$ cat /proc/bus/input/devices
I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="gpio_ir_recv"
P: Phys=gpio_ir_recv/input0
S: Sysfs=/devices/platform/ir-receiver@11/rc/rc0/input0
U: Uniq=
H: Handlers=kbd event0
B: PROP=20
B: EV=100017
B: KEY=fff 0 0 4200 108fc32e 2376051 0 0 0 7 158000 4192 4001 8e9680 0 0 10000000
B: REL=3
B: MSC=10

Then, it is necessary to install the IR userland tools (and kernel module) for Linux.

$ sudo apt install lirc

The lowlevel utility mode2 from the lirc package give information about the signals sent by the remote controller.

$ mode2
Using driver devinput on device auto
Trying device: /dev/input/event0
Using device: /dev/input/event0
Warning: Running as root.

In accordance to the rule of tartine de confiture, nothing happens when I am using my remote control :( However, the use of my TV controller in front of my IR receiver, show that it works as intended.

$ mode2
Using driver devinput on device auto
Trying device: /dev/input/event0
Using device: /dev/input/event0
Warning: Running as root.
code: 0x63ab4561380108000400040000000100
code: 0x63ab4561380108000000000000000000
code: 0x63ab456183c408000400040000000100
code: 0x63ab456183c40800000000000000000

The IR receiver does not seem to be compatible with my air conditioner remote. The use of a TSOP38238 IR receiver provide the same results.

The modification of the /etc/lirc/lirc_options.conf by adding driver = default and device = /dev/lirc0 allows to solve this issue.

$ mode2 -d /dev/lirc0
Using driver default on device /dev/lirc0
Trying device: /dev/lirc0
Using device: /dev/lirc0
Warning: Running as root.
pulse 4356
space 4357
pulse 532
...

IR commands replay

Now the IR information are well received by the RPI, it is necessary to record this information in order to replay it. The lirc tool irrecord is used to record and analyse a sequence of IR commands.

$ irrecord -d /dev/lirc0 --driver default --disable-namespace
Using driver default on device /dev/lirc0

irrecord -  application for recording IR-codes for usage with lirc
Copyright (C) 1998,1999 Christoph Bartelmus(lirc@bartelmus.de)

This program will record the signals from your remote control
and create a config file for lircd.
[...]
Press RETURN to continue.
Checking for ambient light  creating too much disturbances.
Please don't press any buttons, just wait a few seconds...

No significant noise (received 0 bytes)

Enter name of remote (only ascii, no spaces) :Enter name of remote (only ascii, no spaces) :aircooling
Using aircooling.lircd.conf as output filename

Now start pressing buttons on your remote control.

[...]
Press RETURN now to start recording.
................................................................................
Got gap (7530 us)}

Please keep on pressing buttons like described above.
...............................................................................

Please enter the name for the next button (press <ENTER> to finish recording)
KEY_POWER

Now hold down button "KEY_POWER".

Please enter the name for the next button (press <ENTER> to finish recording)

Checking for toggle bit mask.
Please press an arbitrary button repeatedly as fast as possible.
Make sure you keep pressing the SAME button and that you DON'T HOLD
the button down!.
If you can't see any dots appear, wait a bit between button presses.

Press RETURN to continue.
...........................


[...]
Toggle bit mask is 0x60006.

You have only recorded one button in a non-raw configuration file.
This file doesn't really make much sense, you should record at
least two or three buttons to get meaningful results. You can add
more buttons next time you run irrecord.


Successfully written config file aircooling.lircd.conf

You have to script your fingers in order to randomly push the buttons of your remote control. However in my case, I have only one button to push. The result is a lirc configuration file, that you can use to replay IR commands.

begin remote

  name  aircooling
  bits           72
  flags SPACE_ENC
  eps            30
  aeps          100

  header       4358  4363
  one           530  1624
  zero          530   546
  ptrail        530
  gap          7450
  min_repeat      1
#  suppress_repeat 1
#  uncomment to suppress unwanted repeats
  toggle_bit_mask 0x60006
  frequency    38000

      begin codes
          KEY_POWER                    0x000D03FC01608100E0
      end codes

end remote

In order to send an IR signal, we need an IR transmitter. I used the one of my electronic starter kit. Such as for the IR receiver, some new configurations need to be done in order for the transmitter to be recognized by the kernel. The add of the line dtoverlay=gpio-ir-tx,gpio_pin=18 in /boot/config.txt is sufficient. In my case, /dev/lirc0 becomes the transmitter and /dev/lirc1 the receiver.

IR_emitter

The final electronic schema is therefore the following.

circuit

In order to replay an IR command, we have to use the irsend binary. It reads the configuration file previously generated, and send the pattern specified on its command line. First, in order for lircd to be aware of this new configuration file, it is necessary to restart it (every time modifications will be applied, a restart will be necessary). The remote parameter is used to find the correct system, and the code parameter is used to send the correct command for that system. In my case, the remote is aircooling and the code is KEY_POWER, name I randomly choose in my configuration file.

$ mv aircooling.lircd.conf /etc/lirc/lircd.conf.d/
$ systemctl restart lircd
$ irsend SEND_ONCE aircooling KEY_POWER

Of course, and probably due to tartine_de_confiture law my air conditioner does not power on after sending this command :( Let’s dig into it.

IR communication deep dive

Warning: I have no knowledge in IR communication, therefore the following is only my interpretation

The IR messages sent by my remote command are a sequence of IR LED on and IR LED off. The duration of each state change. A couple of IR LED on and IR LED off is coding one information.

$ mode2 -d /dev/lirc1
[...]
pulse 4350
space 4361
pulse 531
space 1626
pulse 530
space 1624
pulse 530
space 1625
pulse 534
space 1621
pulse 534
space 544
pulse 532
space 545
pulse 533
space 1622
[...]

By reading the output of mode2, we can detect four kind of duration:

  • about 500
  • about 1500
  • about 4300
  • about 7000

According to the generated file (that is not working), a pulse (IR LED on) of 500 followed by a pause (IR LED off) of 500 is coding a 0, while a pulse of 500 followed by a pause of 1500 is coding a 1.

The following crappy script, parse the sequence of pulse, space and translate it into sequence of 0 and 1 and other values, if the duration is different.

def parse(s):
    def v(x):
        if x<1000:
            return 0
        elif x < 2000:
            return 1
        elif x < 5000:
            return 2
        else:
            return 3
    h = {(0,0):"0",(0,1):"1",(2,2):"A",(0,3):"B"}
    t = list(map(lambda x:int(x[6:]),s.split("\n")))[:-1]
    r = []
    for i in range(0,len(t),2):
        r.append(h.get((v(t[i]),v(t[i+1])),str((t[i],t[i+1]))))
    code = "".join(r)
	return code

Now let’s try to decode the remote command push power button.

$ mode2 -d /dev/lirc1 > power_button.txt
$ decode.py power_button.txt
A111100100000110100000011111111000000000101100000100000010000000011100000BA111100100000110100000011111111000000000101100000100000010000000011100000

That’s quite interesting, we can see a kind of starter (called A) then a kind of ender (called B) and between them some information. There are 72 bits sent, and then the same 72 bits. The remote air conditioner control is sending twice the same message. If I change a bit my python script to translate binaries in hexadecimal the result is the following:

$ decode.py power_button.txt
['A', 'F20D03FC01608100E0', 'B', 'A', 'F20D03FC01608100E0']

We can clearly see the same message sent.

What is quite interesting, is that when I send IR commands with my IR transmitter, I can also receive those commands with the IR receiver, and then compare if the result is what I was waiting for.

So when I execute the command irsend SEND_ONCE aircooling KEY_POWER, the dump is:

$ mode2 -d /dev/lirc1 | decode.py
['A', '000D03FC0160810000']

First, the irsend is not sending twice the information, but the main problem here is that the information sent is different. According to the configuration, the irsend command respects it, therefore the irrecord made a mistake during the generation.

$ cat /etc/lirc/lircd.conf.d/aircooling.lircd.conf
[...]
KEY_POWER                    0x000D03FC01608100D0
[...]

The value F20D03FC01608100E0 cannot be stored on a uint64_t number and lircd refuses to parse my aircooling configuration file.

Oct  2 14:34:49 raspberrypi lircd[981]: lircd-0.10.1[981]: Error: "0xF20D03FC01608100E0": must be a valid (uint64_t) number

The lirc configuration also allows a raw mode syntax which is a bit different. It is directly the sequence of space and pulse that can be retrieved using the -m option of mode2 (the final space has to be removed). In addition, we have to precise the gap value that is sum of each duration.

$ mode2 -d /dev/lirc1 -m
Using driver default on device /dev/lirc1
Trying device: /dev/lirc1
Using device: /dev/lirc1
Warning: Running as root.
 16777215

     4352     4361      530     1623      533     1624
      531     1624      531     1625      530      546
      532      544      531     1624      535      548
      531      542      555      522      533      545
      531      546      531     1625      532     1623
      556      521      530     1625      530      547
      530      547      531      546      531      546
      531      546      556      522      530     1625
      531     1624      532     1625      555     1600
      530     1625      536     1572      615     1589
      532     1625      530      546      531      547
      530      546      535      541      531      546
[...]
$ cat /etc/lirc/lircd.conf.d/aircoolingraw.lircd.conf
begin remote

  name  aircoolingraw
  flags RAW_CODES|CONST_LENGTH
  eps            30
  aeps          100
  gap           120150

      begin raw_codes
          name KEY_POWER
		    4352     4361      530     1623      533     1624
		      531     1624      531     1625      530      546
		      532      544      531     1624      535      548
		      531      542      555      522      533      545
		      531      546      531     1625      532     1623
		      556      521      530     1625      530      547
		      530      547      531      546      531      546
		      531      546      556      522      530     1625
		      531     1624      532     1625      555     1600
		      530     1625      536     1572      615     1589
		      532     1625      530      546      531      547
		      530      546      535      541      531      546
[...]
        end raw_codes
end remote
$ irsend SEND_ONCE aircoolingraw KEY_POWER

Now it works ! When configured in raw mode, lirc send the correct IR messages and my air conditioner is power on thanks to RPI, even without sending the signal twice. A brief analysis shows also that the signal sent to power on and power off my air conditioner are not the same.

POWER_ON =  0xF20D03FC01608100E0
POWER_OFF = 0xF20D03FC01608700E6

Air conditioner remote commands reverse

My air conditioner allows to

  • Be switched on/off
  • To configure the desired temperature from 17°C to 30°C
  • To change the fan speed
  • To be configured to switch ON/OFF at a specific time

In order to be fully automated, the same work as described previously has to be done for each command that I want to be available. By analyzing the commands sent, we can extract some patterns.

Every IR command contains:

  • A header that starts with 0xF20D
  • A length byte representing the full length added to 6 of the IR command
  • A byte that is 0xFF - the value of the previous byte (a kind of error correction)
  • The data of the packet
  • A checksum to ensure that the signal is correct
def crc(x):
   c = 0
   for i in range(16):
        c ^= (x>>(8*i))&0xFF
   return c

The data represents the action that the air conditioner needs to perform:

  • The first byte of the data is the type of action
    • 0x01 is to configure temperature/fan
    • 0x09 is to configure eco mode : HiPower or Eco
    • 0x21 is to configure the swing of the plastic part

If the command is 0x1, then:

  • The most significant 4 bits of the next byte are the temperature we want : 0x0 from 0xD means respectively from 17°C to 30°C
  • The most significant 4 bits of the byte after are the fan speed we want : 0x0 is auto then 0x4,0x6,0x8,0xA,0xC is the fan speed

We can also realize, that the different mode available are just some modes stored values of temperature/fan speed that we can configure as we want, such as the quiet mode that just change the fan speed.

Automation

Now I know exactly what to do, in order to switch ON and OFF my air conditioner, I need to make things more friendly than executing some dark Linux commands. In order to automate things in my house, I use homeassistant. It is very flexible and powerful and I love it. It allows to interface sensors with many things, it provides a nice interface and even a smartphone application. In addition, it does not depend on any dark private cloud that could decide to take the control of my house. Therefore the integration of my air conditioner with my homeassistant seems obvious.

Switch On/Off

MQTT to IR

I choose to pilot my air conditioner with the MQTT protocol which is widely used in the IOT world. In addition, it is very well integrated in homeassistant. For that, I need to develop a kind of proxy between the MQTT messages that are going to be sent by homeassistant, and the IR commands that will be sent by LIRC or directly to the gpio driver. This proxy is coded in go because I want to learn this language. It listens on a specific MQTT topic and as soon as it received a message, it sends:

  • Either a network message to lircd through its unix socket, to simulate a push on a lirc configured button
  • Or the correct sequence of pulses/spaces to the gpiodriver

The source code is available on my github.

In my case, I decided to directly communicate to the gpio driver and fully bypass LIRC. The reason behind is because LIRC forces to create a predefined button for each command we want to send. It is not possible (or at least I did not find how to do that), to dynamically create a button depending on parameters. For example, in my case in order to be able to use all the possible temperatures (14) with all the possible fan speed (6), I would need to define 6*14=84 buttons… Even if I probably do not need all the possible combinations, I do not like this approach and I prefer to dynamically send my commands. In order to do that, I need to directly talk to the gpio driver.

Air conditioner state

Even if I know how to switch off/on my air conditioner, it does not mean I know its state. Indeed, if someone use the remote controller, I have actually no way to know that. So I may think that my air conditioner is ON, because I just sent the correct MQTT message, while someone just switch off with the remote. If I was a good electrician, I would connect my raspberry pi directly on the mother board of my air conditioner in order to see if it is up or not. However, I am very bad in electronic and I do not want to plug my system on it, if I make a mistake I can break everything… So I need to find a safer way.

I could rely on my electrical consumption that I monitor closely, it would work to know if I correctly switch off or on my air conditioner (by looking for electrical gap), however it means I would need to change the state once just to get the state, and then change it again. After some days of breaking my head, I decided to use some door sensors. I already use many of them in my home, and they work smoothly (of course I get rid off the xiaomy chinese cloud). There are two parts, if they are closed enough the magnetic sensor inside detects it and the door is considered as closed, if they are too far, the door is considered as opened. I can use this exact same principle with my air conditioner. When it is on, there is a peace of plastic that moves. So if I put one part of the sensor on that peace of plastic, I can detect that it is not in place and therefore my air conditioner is on.

Xiaomi magnet

Homeassistant Configuration

The configuration in homeassistant, is quite easy:

  • It is composed of two scripts (ON and OFF)
  • A switch
switch_on_aircooling:
    alias: Switch on aircooling
    mode: single
    variables:
        temperature: 22
        fanspeed: 0
    fields:
        temperature:
            name: Temperature
            description: Temperature to set
            selector:
                number:
                    min: 17
                    max: 30
                    step: 1
                    mode: slider
        fanspeed:
            name: Fan speed
            description: Fan speed to set
            selector:
                number:
                    min: 0
                    max: 12
                    step: 2
                    mode: slider
    sequence:
        - service: mqtt.publish
          data:
              topic: "airconditioner/downstair/command"
              payload_template: "F20D03FC01{{ '%X' % (temperature - 17)  }}0{{ '%X' % fanspeed }}000{{ '%X' % (xor(temperature-17,fanspeed)) }}1"

switch_off_aircooling:
    alias: Switch off aircooling
    mode: single
    sequence:
        - service: mqtt.publish
          data:
              topic: "airconditioner/downstair/command"
              payload_template: "F20D03FC01608700E6"

This script uses a custom component providing the basic function xor that homeassistant does not provide (what a shame !).

And the associated switch:

switch:
    - platform: template
      switches:
          airconditioner_salon:
                value_template: "{{ states('binary_sensor.magnet_airconditioner_salon') }}"
                turn_on:
                    service: script.turn_on
                    target:
                        entity_id: script.switch_on_aircooling
                    data:
                        variables:
                            temperature: 23
                            fanspeed: 0
                turn_off:
                    service: script.turn_on
                    target:
                        entity_id: script.switch_off_aircooling
                icon_template: "mdi:air-conditioner"

HomeAssistant switch

Final assembly

Everything works, but it needs to be WAF. In my house, just under my air conditioner, there is my LED matrix I have built. It means that I could use the RPI Zero in it, to also control my air conditioner. No new system, no new battery, no need of argumentation to convince my family members that this new geek stuff is mandatory for our life.

I just need to connect the IR emitter to some RPI gpio, and then to create a hole in the LED Matrix for the LED.

My IR emitter (Yeah, I know I forget a resistance, meaning that my LED is probably going to die quite quickly…)

The final update of my LED Matrix is really minimal. Update of my LED Matrix

Conclusion

It was a very fun project to work on that allowed me to work on many different topics:

  • Receive IR signals
  • Decode and reverse those IR signals
  • Send IR Signals
  • Reverse the way of working of lircd to be able to bypass it
  • Program in golang

At the end, the goal is reached to be able to control this unconnected air conditioner. I added this new sensor, in some homeassistant automation such as: If it is 10pm, no one is in the house and the inside temperature is above 25°C, switch on the air conditioner.

In addition of the fun part of the project, the result is also a better comfort in my home.