Saturday, January 7, 2017

ESP8266 Based Universal Remote

Idea:

I really wanted to be able to control my TV/Audio/Etc from Home Assistant and wasn't happy with any of the solutions I found online. The Harmony Hub is the closest I could find, but I knew that I could do equally as good if not better for far less. 


The Build:

I started looking around online to see if anyone had done this already because it's always better to just copy someone else, right? I had a little bit of success, but I wasn't able to find any blogs outlining the whole process. Sigh. 

I stumbled across a few different projects on Github, and ended up playing with a few of them on my D1 Mini but I wasn't satisfied until I landed on IRremoteESP8266. After playing around with different examples of Mark's I realized that IRGCTCPServer was exactly what I as looking for, as I didn't necessarily want to rely on MQTT to queue IR commands for the D1 to blast.

After loading the IRGCTCPServer with one tiny modification (more on that below) I was up in running in no time.

There was one small hangup, my WiFi connection decided to die right around when I powered on my D1 after loading the code. Hmm. After a while I realized that my D1 was broadcasting a new network, in addition to connecting to my home network. It's network was on the same channel as my home's 2.4Ghz, and one "WiFi.mode(WIFI_STA);" line later, my WiFi was back up.  


The Actual Build..

I ended up using 1x 2N2222 transistor, 1x 1k Ohm resistor, 1x 100 Ohm resistor, and 1x IR LED for now (all picked up at my local RadioShack). The circuit diagram is below (diagram shows multiple LEDs, which I plan on attaching later):


Mark's IRGCTCPServer code specifies GPIO 4 for the LED, but you can change that, and find the D1 mini pin mapping here

IR codes for the IRGCTCPServer are send via nc on my Raspberry Pi from Home Assistant. I will include examples below.

IRGCTCPServer expects "Global Cache-formatted" codes. This was super confusing to me until I realized that Global Cache is just a database of IR codes. Cool. After creating an account, I was able to easily search for the codes I needed and set up a shell command in home assistant to send them. almost there! In case you missed the link: https://irdb.globalcache.com/Home/Database.


Configuration:

The configuration on the Home Assistant side of things is simple. I have a shell_commands.yaml file containing all the commands I current need to use. Example:

tv_on: echo 38000,1,1,172,172,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,21,22,21,22,64,22,64,22,21,22,21,22,64,22,21,22,64,22,64,22,21,22,21,22,64,22,64,22,21,22,1820 | nc 10.0.1.44 4998 
tv_off: echo 38000,1,1,173,173,21,65,21,65,21,65,21,21,21,21,21,21,21,21,21,21,21,65,21,65,21,65,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,65,21,65,21,21,21,21,21,65,21,65,21,65,21,65,21,21,21,21,21,65,21,65,21,21,21,1832 | nc 10.0.1.44 4998 
tv_mute: echo 38000,1,1,172,172,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,21,22,21,22,21,22,64,22,64,22,64,22,64,22,1820 | nc 10.0.1.44 4998 
tv_chromecast: echo 38000,1,1,173,173,21,65,21,65,21,65,21,21,21,21,21,21,21,21,21,21,21,65,21,65,21,65,21,21,21,21,21,21,21,21,21,21,21,21,21,65,21,65,21,65,21,65,21,65,21,21,21,65,21,65,21,21,21,21,21,21,21,21,21,21,21,65,21,21,21,1832 | nc 10.0.1.44 4998 
tv_cable: echo 38000,1,1,172,172,21,64,21,64,21,64,21,21,21,21,21,21,21,21,21,21,21,64,21,64,21,64,21,21,21,21,21,21,21,21,21,21,21,64,21,64,21,21,21,64,21,64,21,21,21,21,21,21,21,21,21,21,21,64,21,21,21,21,21,64,21,64,21,64,21,1673 | nc 10.0.1.44 4998
tv_kyle: echo 38000,1,1,172,172,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,21,22,64,22,21,22,21,22,21,22,21,22,21,22,21,22,64,22,21,22,64,22,64,22,64,22,64,22,64,22,1820\r38000,1,1,172,172,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,21,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,21,22,21,22,64,22,64,22,64,22,64,22,1820\r38000,1,1,170,170,20,63,20,63,20,63,20,20,20,20,20,20,20,20,20,20,20,63,20,63,20,63,20,20,20,20,20,20,20,20,20,20,20,63,20,63,20,20,20,20,20,20,20,63,20,20,20,20,20,20,20,20,20,63,20,63,20,63,20,20,20,63,20,63,20,1798\r38000,1,1,172,172,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,21,22,21,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,64,22,21,22,64,22,64,22,64,22,64,22,64,22,1820 | nc 10.0.1.44 4998 

As you can see above, the commands instructions are to just echo a string (this is the encoded IR data) and pipe them to my D1 Mini, which listens for commands to blast on tcp port 4998. In the last example, you'll notice \r in the middle of two similar commands. This is an example of chaining IR commands, these will be blasted just as if you were pressing successive buttons on a remote. During testing I was able to use "telnet 10.0.1.44 4998" to open up a persistent connection to the service so I could send multiple commands, but beware, you'll cause the D1 to reboot if you enter an invalid one (ie: a blank line and press enter).

No that I had the basis of the hardware and software configured, I was able to build a custom Alexa skill to trigger these events. "Alexa, ask the TV to mute" is pretty lame I suppose, but at the same time, kind of cool!

I'd be happy to write up some info on that as well, but there is plenty of great documentation out there about setting up custom skills for the Alexa API. You'll find the Home Assistant part of that skill below. It is very simple. 

  TVControlIntent:
    action:
      - service_template: shell_command.tv_{{ Action }}

The D1 mini is running the following code (you'll need to clone the IRremoteESP8266 code library):
/*
 * IRremoteESP8266: IRGCTCPServer - send Global Cache-formatted codes via TCP.
 * An IR emitter must be connected to D1 Mini pin D3.
 * Version 0.1 1 April, 2016
 * Hisham Khalifa, http://www.hishamkhalifa.com
 *  
 * Example command - Samsung TV power toggle: 38000,1,1,170,170,20,63,20,63,20,63,20,20,20,20,20,20,20,20,20,20,20,63,20,63,20,63,20,20,20,20,20,20,20,20,20,20,20,20,20,63,20,20,20,20,20,20,20,20,20,20,20,20,20,63,20,20,20,63,20,63,20,63,20,63,20,63,20,63,20,1798\r\n
 */

#include <IRremoteESP8266.h>
#include <IRremoteInt.h>
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <WiFiServer.h>
const char* ssid = "SSID";
const char* password = "PASS";
WiFiServer server(4998); // Uses port 4998.
WiFiClient client;
unsigned int *codeArray;
IRsend irsend(0); //an IR emitter led is connected D1 Mini pin D3
void parseString(String str) {
  int nextIndex;
  int codeLength = 1;
  int currentIndex = 0;
  nextIndex = str.indexOf(',');
  // change to do/until and remove superfluous repetition below...
  while (nextIndex != -1) {
    if (codeLength > 1) {
        codeArray = (unsigned int*) realloc(codeArray, codeLength * sizeof(unsigned int));
    } else {
        codeArray = (unsigned int*) malloc(codeLength * sizeof(unsigned int));
    }
    codeArray[codeLength-1] = (unsigned int) (str.substring(currentIndex, nextIndex).toInt());

    codeLength++;
    currentIndex = nextIndex + 1;
    nextIndex = str.indexOf(',', currentIndex);    
  }
  codeArray = (unsigned int*) realloc(codeArray, codeLength * sizeof(unsigned int));
  codeArray[codeLength-1] = (unsigned int) (str.substring(currentIndex, nextIndex).toInt());

  irsend.sendGC(codeArray,codeLength);
}
void setup() {
  // initialize serial:
  Serial.begin(115200);
  Serial.println(" ");
  Serial.println("IR TCP Server");
  WiFi.mode(WIFI_STA); // Stop us from broadcasting
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(900);
    Serial.print(".");
  }
    server.begin();
    IPAddress myAddress = WiFi.localIP();
    Serial.println(myAddress);
    irsend.begin();
}
void loop() {
  while(!client) {
    client = server.available();
  }
  while(!client.connected()){
    delay(900);
    client = server.available();
  }

  if(client.available()){
    String irCode = client.readStringUntil('\r'); // Exclusive of \r
    client.readStringUntil('\n'); // Skip new line as well
    client.flush();
    parseString(irCode);
  }
}

Finishing Touches:

I bought some 2.5mm Audio Male and Female ends to make this project clean. You'll see in the pictures below that it will be helpful down the road to have these quick connectors. I'm using 22AWG doorbell chime wire between the IR led and the connector. 

I am still waiting for my D1 mini perf board shield to arrive so I can mount the connectors, and solder everything once and for all. You'll see in the photos below that I am also using the D1's li-po shield, which is great, by the way!

Thanks for reading! If you have any questions, feel free to comment below or email me at andrew@aneis.ch.

Finished version first:






















6 comments:

  1. Great project, and thanks for the details!

    I've got my Wemos up and running, and I can send the telnet commands to make the TV respond. I just can't figure out the Home Assistant side of things since I'm pretty new to that environment. Any chance you could share the IR code setup within HA's configuration.yaml?

    ReplyDelete
    Replies
    1. The config is pasted above, EG:

      tv_on: echo 38000,1,1,172,172,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,64,22,64,22,21,22,21,22,21,22,21,22,21,22,64,22,21,22,21,22,64,22,64,22,21,22,21,22,64,22,21,22,64,22,64,22,21,22,21,22,64,22,64,22,21,22,1820 | nc 10.0.1.44 4998

      Delete
    2. But FWIW, I have my configs separated into different files. The above is from my shell_commands.yaml

      Delete
  2. Yeah, I saw your code examples, and I have them in my configuration.yaml for now.

    Again, I'm pretty new to Home Assistant. I'm testing by going to the Developer tools section and selecting shell_command and tv_on then pressing Call Service to run the command. I'm assuming this would work. I just thought maybe you had a bit more there that I was missing. I'll get it figured out. Thanks!

    ReplyDelete
    Replies
    1. Brandon,

      If it's all in one file you'll probably have to make your configuration like this:

      shell_command:
      tv_on: echo 38000,1,1,172,172,22,64.... etc.

      You'll need to indent each shell command you specify below "shell_command:". For more info check out https://home-assistant.io/components/shell_command/.

      Also, https://community.home-assistant.io/ is very active and helpful if you need any more assistance!

      Delete
  3. I think I got it. I must have had a typo or something, and I think it was crashing the Wemos. Thanks again!

    ReplyDelete