Friday, November 6, 2015

How to control Philips Hue with C++ and cURL library

I presented a Shell script that can control Philips Hue light bulbs with cURL before. This post is to do the same with C++ code. This post assumes that you already created a user account in the Hue bridge; you can find more detail on Hue web page.
This program will use cURL library for C++ and c++11 compiler, which is gcc-4.7 in this example.
pi@desktoppi ~ $ sudo apt-get install libcurl4-openssl-dev
pi@desktoppi ~ $ sudo apt-get install g++-4.7
I made four files:
pi@desktoppi 01:06:01 hue$ ls
hue.cpp  Makefile  my_curl.hpp  stdafx.h
One of them, "hue.cpp", has the main logic:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// hue.cpp written by JaeHyukKwak
#include "stdafx.h"
#include "my_curl.hpp"
static const char *MAC_HUE = "00:17:88:09:ef:18";
static string ip_hue = "192.168.0.50";

int main( int argc, const char **argv )
{
    if ( argc == 1 )
    {
        cerr << "Usage: " << argv[0] << " light#" << endl;
        cerr << "    or " << argv[0] << " light# key value" << endl;
        cerr << "Example#1: " << argv[0] << " 1 on true" << endl;
        cerr << "Example#2: " << argv[0] << " 2 bri 255" << endl;
        cerr << "Example#3: " << argv[0] << " 3 hue 65535" << endl;
        return 1;
    }
    bool readOnly = ( argc == 2 );

    for ( ifstream fin( "/proc/net/arp" ); !fin.eof(); )
    {   // ex: 192.168.0.53  0x1  0x0  f4:7b:5e:41:91:00  *  eth0
        string line;
        getline( fin, line );
        if ( line.find( MAC_HUE ) == string::npos ) continue;
        ip_hue = line.substr( 0, line.find( ' ' ) );
        break; // IP address for HUE is found.
    }

    const char *lightIndex = argv[1];
    string URL_base = "http://";
    URL_base += ip_hue + "/api/newdeveloper/lights/" + lightIndex;
    string URL = URL_base + ( readOnly ? "" : "/state" );

    my_curl_t curl;
    curl.SetOpt_URL( URL );
    if ( readOnly ) cout << curl.RequestGet() << endl;
    else
    {
        const char *key = argv[2];
        const char *value = argv[3];
        string cmd = string( "{\"" ) + key + "\":" + value + "}";
        curl.SetOpt_READDATA( cmd );
        cout << curl.RequestPut() << endl;
    }
}
It has the MAC address of the Hue Bridge device and it look for the IP of the MAC address. It is because Hue bridge device relies on DHCP and the IP might be changed; unless you dedicated a static IP address for the MAC on the router. But it also has a default IP address in case the MAC address is not found from ARP cache table; or whatever reason. This MAC address resolving part is totally optional if you are fine with just a fixed IP address.

The program check the command arguments. If it doesn't have any command arguments, it prints out how to use it. It expects to have a light bulb number. It will return the status of the light bulb. When additional two more arguments are given, they will be used to change the status of the light bulb; first one will be used as a key and the second will be used as a value. Unlike the script version I presented, this doesn't handle error cases like when you haven't created user name; BTW, this example assumes that the user name on the Hue Bridge is "newdeveloper".

The Makefile will look like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Makefile written by Jae Hyuk Kwak
CC=g++-4.7
CFLAGS=-Wall -g -std=c++11

hue: hue.cpp my_curl.hpp stdafx.h.gch
    $(CC) $(CFLAGS) -lcurl -o hue hue.cpp

stdafx.h.gch : stdafx.h
    $(CC) $(CFLAGS) -c stdafx.h

clean:
    rm stdafx.h.gch hue
A precompile header file, "stdafx.h", is like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// stdafx.h written by JaeHyukKwak
#include <string>
#include <iostream> // cout, cerr
#include <fstream> // ifstream
#include <stdexcept> // runtime_error
#include <memory>   // unique_ptr
#include <string.h> // memcpy
using namespace std;

#include <curl/curl.h>
The last file, "my_curl.hpp", looks like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// my_curl.hpp written by JaeHyukKwak
class my_curl_t
{
    unique_ptr< CURL, decltype( curl_easy_cleanup )* > curl_;
    string html_;   // temporary buffer to store HTML content
    string put_;   // temporary buffer to store PUT content

public:
    my_curl_t() : curl_( curl_easy_init(), curl_easy_cleanup )
    {
        if ( nullptr == curl_ )
            throw runtime_error( "curl_easy_init() failed." );
        SetOpt_( CURLOPT_WRITEDATA, &html_ );
        SetOpt_( CURLOPT_WRITEFUNCTION, WriteMemoryCallback );
        SetOpt_( CURLOPT_READDATA, &put_ );
        SetOpt_( CURLOPT_READFUNCTION, ReadDataCallback );
    }

    string RequestGet() { return Request_( 0 ); }
    string RequestPut() { return Request_( 1 ); }

    void SetOpt_READDATA( string data ) { put_ = data; }
    void SetOpt_URL( string url ) { SetOpt_( CURLOPT_URL, url.c_str() ); }

private:
    string Request_( int post )
    {
        SetOpt_( CURLOPT_UPLOAD, post );
        const CURLcode res = curl_easy_perform( curl_.get() );
        if ( CURLE_OK != res )
            throw runtime_error( string( "curl_easy_perform() failed: " ) + curl_easy_strerror( res ) );
        const string htmlCopied = html_;
        html_.clear();
        return htmlCopied;
    }

    template< typename ParamType >
    void SetOpt_( CURLoption option, const ParamType &parameter )
    {
        CURLcode res = curl_easy_setopt( curl_.get(), option, parameter );
        if ( CURLE_OK != res )
            throw runtime_error( string( "curl_easy_setopt failed: " ) + curl_easy_strerror( res ) );
    }

    static size_t WriteMemoryCallback( void *contents, size_t size, size_t numberOfMemoryBlock, void *userChunk )
    {
        const size_t realSize = size * numberOfMemoryBlock;
        string &html = *static_cast< string * >( userChunk );
        html += string( static_cast< char* >( contents ), realSize );
        return realSize;
    }

    static size_t ReadDataCallback( char *buf, size_t size, size_t nitems, void *instream )
    {
        string &put = *static_cast< string * >( instream );
        const size_t putSize = put.size();
        const size_t minSize = min( putSize, size * nitems );
        memcpy( buf, put.c_str(), minSize );
        if ( minSize == putSize ) put.clear();
        else put = put.substr( minSize );
        return minSize;
    }
};
This big class is little different from the previously presented version. It is because now it has to handle HTTP PUT method; that's what Hue Bridge requires.

Once you compile it and run it, you will see something like this result.
pi@desktoppi 01:16:12 hue$ make
g++-4.7 -Wall -g -std=c++11 -c stdafx.h
g++-4.7 -Wall -g -std=c++11 -lcurl -o hue hue.cpp
pi@desktoppi 01:16:35 hue$ ./hue
Usage: ./hue light#
    or ./hue light# key value
Example#1: ./hue 1 on true
Example#2: ./hue 2 bri 255
Example#3: ./hue 3 hue 65535
pi@desktoppi 01:16:37 hue$ ./hue 2
{"state": {"on":false,"bri":254,"hue":14910,"sat":144,"effect":"none","xy":[0.4596,0.4105],"ct":369,"alert":"none","colormode":"ct","reachable":true}, "type": "Extended color light", "name": "Hue Lamp 2", "modelid": "LCT001", "manufacturername": "Philips","uniqueid":"00:17:88:01:00:b1:6b:b9-0b", "swversion": "66013452", "pointsymbol": { "1":"none", "2":"none", "3":"none", "4":"none", "5":"none", "6":"none", "7":"none", "8":"none" }}
pi@desktoppi 01:16:43 hue$ ./hue 2 on true
[{"success":{"/lights/2/state/on":true}}]
pi@desktoppi 01:16:51 hue$ ./hue 2 on false
[{"success":{"/lights/2/state/on":false}}]
pi@desktoppi 01:17:01 hue$
Enjoy the C++ version of Hue controller.

No comments:

Post a Comment

About Me

My photo
Tomorrow may not come, so I want to do my best now.