Thursday, 16 June 2016

Radpi #2: boiler

The first step of the Radpi project was to figure out how to control the boiler. I knew I could switch on/off the boiler with a wemo wifi socket (connected to a wifi repeater due to the thickness of the walls and the distance to the main AP; one of the joys of 150 year old houses). I assumed I could figure out how to control the wemo without using my phone and then use that to control the boiler from a Raspberry pi.

I looked around for information and reverse engineering the wemo app didn't pause any problem: I reversed engineer lots of programs in my youth (in the 80s that's how you learned programming and how things work, including MS-DOS) and decompiling Java apps rates among the easiest things to do and analyse.

I discovered that the communication between the phone on the local network and the wemo is done via XML over HTTP (aka SOAP) on 2 possible ports (49153 and 49154). To talk to it I just needed to send the right packet to the known IP of the wemo and voila.

To switch on the wemo, you just need to send:

<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent:1"><BinaryState>1</BinaryState></u:SetBinaryState></s:Body></s:Envelope>
For action Belkin:service:basicevent:1#SetBinaryState.

I used PHP to program the pis because it's the fastest for me when I want to have web interfaces, so to switch on the wemo I used the following code:

$server = "http://192.168.0.35";
$ports = Array(49153,49154);

$headers = array(
            "Accept: ",
            "Content-type: text/xml;charset=\"utf-8\"",
       "SOAPACTION: \"urn:Belkin:service:basicevent:1#SetBinaryState\""
);

$post = '<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent:1"><BinaryState>1</BinaryState></u:SetBinaryState></s:Body></s:Envelope>';

$url = $server.":".$ports[$_REQUEST["port"]]."/upnp/control/basicevent1";

$defaults = array(
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_0,
    CURLOPT_USERAGENT => '',
    CURLOPT_POSTFIELDS => $post,
    CURLOPT_HTTPHEADER => $headers,
    CURLOPT_POST => 1,
    CURLOPT_HEADER => 0,
    CURLOPT_URL => $url,
    CURLOPT_FRESH_CONNECT => 1,
    CURLOPT_RETURNTRANSFER => 1,
    CURLOPT_FORBID_REUSE => 1,
    CURLOPT_TIMEOUT => 4,
);

$ch = curl_init();
curl_setopt_array($ch, $defaults);
$result = curl_exec($ch);
// Deal with results here

curl_close($ch);

Dead easy heh? To switch it off, you just need to change the value set BinaryState value to 0:

$server = "http://192.168.0.35";
$ports = Array(49153,49154);

$headers = array(
            "Accept: ",
            "Content-type: text/xml;charset=\"utf-8\"",
       "SOAPACTION: \"urn:Belkin:service:basicevent:1#SetBinaryState\""
);

$post = '<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent:1"><BinaryState>0</BinaryState></u:SetBinaryState></s:Body></s:Envelope>';

$url = $server.":".$ports[$_REQUEST["port"]]."/upnp/control/basicevent1";

$defaults = array(
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_0,
    CURLOPT_USERAGENT => '',
    CURLOPT_POSTFIELDS => $post,
    CURLOPT_HTTPHEADER => $headers,
    CURLOPT_POST => 1,
    CURLOPT_HEADER => 0,
    CURLOPT_URL => $url,
    CURLOPT_FRESH_CONNECT => 1,
    CURLOPT_RETURNTRANSFER => 1,
    CURLOPT_FORBID_REUSE => 1,
    CURLOPT_TIMEOUT => 4,
);

$ch = curl_init();
curl_setopt_array($ch, $defaults);
$result = curl_exec($ch);
// Deal with results here

curl_close($ch);

The port to use comes from the HTTP request (not sanitised, you need to make sure you don't accept any old crap from the interwebs). It is selected after running a test to see which port is currently used by the wemo (it changes regularly). To do that, you can request the BinaryState of your wemo using action Belkin:service:basicevent:1#GetBinaryState and change the port in case of failure:

$server = "http://192.168.0.35";
$ports = Array(49153,49154);

$headers = array(
            "Accept: ",
            "Content-type: text/xml;charset=\"utf-8\"",
       "SOAPACTION: \"urn:Belkin:service:basicevent:1#GetBinaryState\""
);

$post = '<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:GetBinaryState xmlns:u="urn:Belkin:service:basicevent:1"><BinaryState>1</BinaryState></u:GetBinaryState></s:Body></s:Envelope>';

$powerstate = "";

$portindex = 0;
while (trim($powerstate)=="" && $portindex<count($ports)) {

        $url1 = $server.":".$ports[$portindex]."/upnp/control/basicevent1";

        $defaults = array(
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_0,
            CURLOPT_USERAGENT => '',
            CURLOPT_POSTFIELDS => $post,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_POST => 1,
            CURLOPT_HEADER => 0,
            CURLOPT_URL => $url1,
            CURLOPT_FRESH_CONNECT => 1,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_FORBID_REUSE => 1,
            CURLOPT_TIMEOUT => 4,
        );


        $ch = curl_init();
        curl_setopt_array($ch, $defaults);

        $powerstate = curl_exec($ch);
        if (trim($powerstate)=="") {
                $portindex++;
        }
}

// Now we've tested the ports until we found a working one or failed on all of them. We salso know that if $portindex is outside the $port array we failed to communicate. We can take action.


$power = substr($powerstate, stripos($powerstate,"BinaryState>") + strlen("BinaryState>"),1);

At this point, $power==0 of the wemo is off, 1 if it's on, empty if we failed to communicate with the wemo (but we already know that from the value of $portindex). It's up to you to take action and communicate to the user.

With that sorted, I could read the state, start, and stop my boiler as I wanted. Now I just needed to know when to take action.




No comments:

Post a Comment