Sonoff switch complete hack without firmware upgrade

Ipsum Domus
Smart Home DIY
Published in
9 min readMar 21, 2017

--

Usually, when we are reading reviews about cost effective smart home power relays or switches, we are speaking about Sonoff (@Iteadstudio). 5USD wifi managed reliable one channel relay. What can be better to build real smart home infrastructure? The problem is, that this nice device is only cloud-dependent. Meaning, you cannot run it without internet connection on your local network. Solution is also looks simple, if you google it for a little — reflash it with one of open source firmwares and be happy.

But this is not our way of doing things. We do not want to touch hardware, we want to solve everything with server software only. So let’s do it!

Pairing and initialization of Sonoff WiFi Wireless Smart Switch

First of all, let’s do pairing. This is very straight forward, like any other smart device. Long click on manage button (let will start blinking fast). Look for wifi access point starting with “ITEAD-10000”. Connect to it with password “12345678”. Take into account, ITLead smart switch has time out in AP mode. If you will not configure it in given time, it closes AP and you should start all over again.

Now when you connected, get device info by sending simple HTTP get request to http://10.10.7.1/device

Another important point, gateway is not implemented in this hardware, so you should define route into 10.10.7.1. For example like that

route change 0.0.0.0 mask 0.0.0.0 10.10.7.1

As response you will get following JSON object

{
"deviceid":"10000xxxxx",
"apikey":"xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"accept":"post"
}

This is your API key and DeviceID. Store it, you will need it later

Next call will configure default server for the device. Send HTTP POST message to

http://10.10.7.1/ap

The payload is following:

{
"version": 4,
"ssid": [YOUR NETWORK SSID],
"password": [YOUR NETWORK PASSWORD],
"serverName": [IP OF YOUR SERVER],
"port": [PORT OF YOUR SERVER]
}

Obviously, you have to run your webserver on the address you have provided, so device will be able to connect to it.

Configuration of Sonoff WiFi Wireless Smart Switch on local network without internet access

Now we are coming to the exciting part. Your server you should be HTTPS server. Sonoff official firmware below version 1.5.2 does not know to work with HTTP. It not really validates certificate, you create your own self-signed and it would be good enough for this device to connect. How we like to say: “S in IOT is for security…

When wifi relay will close AP and connect to your network, it will send HTTPS request Websockets configuration request to your server. Just answer to it with your websocket server configuration

{ 
"error": 0,
"reason": "ok",
"IP": [YOUR WEBSOCKET SERVER IP],
"port": [YOUR WEBSOCKET SERVER PORT]
}

Also here, you need your websocket server to run. WebSocket server should also be “secured” by bogus certificate. And this would be enough to make device think that it works over real cloud, when the actual cloud is your local network.

Controlling of original wifi smart switch on local network without Itead cloud

There are 4 possible messages this relay is using for operation.

First is “register”. Payload of it: deviceID, apikey and some additional information.

Response would be just ACK, BUT with very secure new API Key (for example 111111111–1111–1111–1111–111111111111. Device does not really care about it. It will just send it back on each following request and will expect it back.

{
"error" : 0,
"deviceid" : [ACTUAL DEVICE ID],
"apikey" : "111111111-1111-1111-1111-111111111111"
}

Also here you can determinate the device type. First two digits of device id defines its type as following:

01 — “smart switch relay” (our device)
02 — “smart light” (another product of the same company, which behaviors the same, for example Slamper)
03 — “temperature and humidity sensor”. For example CS. This one has no timers.

Next is “date”. Response would be

{
"error" : 0,
"date" : [DATE IN ISO FORMAT],
"deviceid" : [ACTUAL DEVICE ID],
"apikey" : "111111111-1111-1111-1111-111111111111"
}

Then “query” for “timers” or any other parameters (sent in params property). Here you can response with timers to be setup on this device

Another request is “update” — this is when device wants to tell you something (for example its state [on or off])

So how to manage it?

Simple. To turn it on or off, send

{action: 'update', value: {switch : state?'on':'off'}

over websocket

To set or remove timers, response to “query” request or send “update” request with your times information.

{action: 'update', value: {timers : d.timers}

Format for timer setup is

{
enabled : true,
type : 'once' OR 'repeat',
at : time,
do : {
switch : 'on' OR 'off'
}
}

To setup timer once, you should send type=”once” and at=time in ISO format. To setup repeat timer, send type=”repeat” and at=time in CRON format (e.g. “* * * * * *”).

That all, we done, now you can use Sonoff wifi relay on your local network without any dependency on internet or ITLEAD cloud services without reflashing it or even opening your device. Just connect it to the system and use it.

Here is the full nodejs source for this article.

const os = require('os');
const http = require('http');
const https = require('https');
const url = require('url');
const fs = require('fs');
var ws = require("nodejs-websocket");
var exec = require('child_process').exec;
var wlan = require('./wlan')();
var emitter = require('events').EventEmitter;
var inherits = require('util').inherits;
module.exports = Sonoff;function Sonoff() {
if (!(this instanceof Sonoff))
return new Sonoff();
emitter.call(this);
}
inherits(Sonoff, emitter);Sonoff.prototype.init = function init() {
var self = this;
self._initialized = false;
self._connected = false;
self._knownDevices = [];

self._inithttps(self);
};
Sonoff.prototype.pair = function (force, ssid, pwd) {
var self = this;
var apSSID = "ITEAD-10000";
var find = setInterval(() => {
if (!self._initialized) {
console.log('Waiting for initialization.');
return; //wait for init
}
wlan.Discover().then(nets => {
var apNet = nets.find(n => n.ssid.startsWith(apSSID));
if (!apNet) {
console.log('ERR | Sonoff is not in pairing mode. Please, Long press until led start blinking fast.');
} else {
console.log('OK | Sonoff found in pairing mode.');
//apSSID = apNet.ssid;
clearInterval(find);
if (self._nic.ssid != apNet.ssid) {
wlan.Connect(apNet, '12345678').then(() => {
wlan.getNic().then(n => {
var nic = null;
if (n.length >= 1) {
//get only first
nic = n[0];
} else {
console.log('ERR | No WLAN interfaces found. Unable to process.');
return;
}
if (nic.ssid != apNet.ssid) {
console.log('ERR | Unable to connect to the configuration AP.');
return;
} else {
_initDevice(self, nic, self._nic.ssid, self._nic.key, force);
}
});
});
} else {
console.log('ERR | You should not be connected to Sonoff configuration AP to pair device.');
}
}
});
}, 3000);
};Sonoff.prototype.powerState = function (device, state) {
var self = this;
return new Promise(function(resolve, reject) {
var d = self._knownDevices.find(d=>d.id === device.id);
if(!d) {
reject('Sonoff device '+device.kind+' not found');
} else {
if(self._connected) {
var h = f => {
if(f.device.id == d.id){
self.removeListener('msg',h);
if(!f.err) resolve(f.device);
else reject(f.err);
}
};
self.on('msg', h);
self.emit('push', {action: 'update', value: {switch : state?'on':'off'}, target: d.id});
}
}
});
};
Sonoff.prototype.setTimer = function(device, time, state){
var self = this;
var d = self._knownDevices.find(d=>d.id === device.id);
if(!d) {
console.log('Sonoff device '+device.kind+' not found');
} else {
if(self._connected) {
d.timers = d.timers || [];
d.timers.push({
enabled : true,
type : time.includes('T')?'once':'repeat',
at : time,
do : {
switch : state?'on':'off'
}
});
self.emit('push', {action: 'update', value: {timers : d.timers}, target: d.id});
}
}
}
var _initDevice = (self, nic, ssid, pwd, force) => {
exec("route change 0.0.0.0 mask 0.0.0.0 10.10.7.1", function (err, res) {
if (err) {
console.log('ERR | unable to set AP network: ', err);
reject(err);
} else {
console.log('OK | '+res);
http.get('http://10.10.7.1/device', (res) => {
const sc = res.statusCode;
const ct = res.headers['content-type'];
if (sc !== 200) {
console.log('Unable to connect to the target device. Code: ' + sc);
res.resume();
return;
}
res.setEncoding('utf8');
var data = '';
res.on('data', (c) => data += c);
res.on('end', () => {
var response = JSON.parse(data);
var device = {
deviceid: response.deviceid,
apikey : response.apikey
};
self._httpPost('http://10.10.7.1/ap', {
"version": 4,
"ssid": self._nic.ssid,
"password": self._nic.key,
"serverName": self._ip,
"port": self._port
}, (re, err) => {
if (err) {
console.log('Unable to configure endpoint ' + err);
} else {
console.log(JSON.stringify(re));
}
});
});
}).on('error', (e) => {
console.log(`Unable to establish connection to the device: ${e.message}`);
});
}
});
};
Sonoff.prototype._initServer = (self) => {
wlan.getNic().then(n => {
self._nic = n[0];
var ifaces = os.networkInterfaces();
for (var i in ifaces) {
for (var k in ifaces[i]) {
var address = ifaces[i][k];
if (address.family === 'IPv4' && !address.internal && address.mac == self._nic.mac) {
self._ip = address.address;
self._port = 80;
//create server
http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end();
}).listen(self._port, self._ip, () => {
self._initialized = true;
});
break;
}
}
}
});
};
Sonoff.prototype._initws = (self, ip, port)=>{
var options = {
secure : true,
key: fs.readFileSync('./tools/ipsum-key.pem'),
cert: fs.readFileSync('./tools/ipsum-cert.pem'),
};
var server = ws.createServer(options,function (conn) {
console.log("WS | Server is up %s:%s to %s:%s",ip,port,conn.socket.remoteAddress,conn.socket.remotePort);
self._connected = true;
self.on('push',a=>{
var rq = {
"apikey" : "111111111-1111-1111-1111-111111111111",
"action" : a.action,
"deviceid" : a.target,
"params" : a.value
};
var r = JSON.stringify(rq);
console.log('REQ | WS | APP | ' + r);
conn.sendText(r);
});
conn.on("text", function (str) {
var data = JSON.parse(str);
console.log('REQ | WS | DEV | %s', JSON.stringify(data));
res = {
"error" : 0,
"deviceid" : data.deviceid,
"apikey" : "111111111-1111-1111-1111-111111111111"
};
if(data.action) {
switch(data.action){
case 'date':
res.date = new Date().toISOString();
break;
case 'query':
//device wants information
var device = self._knownDevices.find(d=>d.id == data.deviceid);
if(!device) {
console.log('ERR | WS | Unknown device ',data.deviceid);
} else {
/*if(data.params.includes('timers')){
console.log('INFO | WS | Device %s asks for timers',device.id);
if(device.timers){
res.params = [{timers : device.timers}];
}
}*/
res.params = {};
data.params.forEach(p=>{
res.params[p] = device[p];
});
}
break;
case 'update':
//device wants to update its state
var device = self._knownDevices.find(d=>d.id == data.deviceid);
if(!device) {
console.log('ERR | WS | Unknown device ',data.deviceid);
} else {
device.state = data.params.switch;
self._updateKnownDevice(self,device);
}
break;
case 'register':
var device = {
id : data.deviceid
};
var type = data.deviceid.substr(0, 2);
if(type == '01') device.kind = 'switch';
else if(type == '02') device.kind = 'light';
else if(type == '03') device.kind = 'sensor'; //temperature and humidity. No timers here;
device.version = data.romVersion;
device.model = data.model;
self._updateKnownDevice(self,device);
console.log('INFO | WS | Device %s registered', device.id);
break;
default: console.log('TODO | Unknown action "%s"',data.action); break;
}
} else {
console.log('TODO | WS | Not data action frame');
}
var r = JSON.stringify(res);
console.log('RES | WS | DEV | ' + r);
conn.sendText(r);
var td = self._knownDevices.find(d=>d.id == res.deviceid);
self.emit('msg',{device : td});
});
conn.on("close", function (code, reason) {
console.log("Connection closed");
});
}).listen(port,ip);
};
Sonoff.prototype._inithttps = (self)=>{
wlan.getNic().then(n => {
self._nic = n[0];
var ifaces = os.networkInterfaces();
for (var i in ifaces) {
for (var k in ifaces[i]) {
var address = ifaces[i][k];
if (address.family === 'IPv4' && !address.internal && address.mac == self._nic.mac) {
self._ip = address.address;
self._port = 80;
self._initws(self,self._ip,self._port + 1);
const options = {
key: fs.readFileSync('./tools/ipsum-key.pem'),
cert: fs.readFileSync('./tools/ipsum-cert.pem'),
};
var server = https.createServer(options, (req, res) => {
console.log('REQ | %s | %s ',req.method, req.url);
var body = [];
req.on('data', function(chunk) {
body.push(chunk);
}).on('end', function() {
body = JSON.parse(Buffer.concat(body).toString('utf-8'));
console.log('REQ | %s',JSON.stringify(body));
res.writeHead(200);
res.end(JSON.stringify({
"error": 0,
"reason": "ok",
"IP": self._ip,
"port": self._port + 1
}));
});

}).listen(self._port,self._ip);
server.on('connection', c=>{
console.log("Connection: %s:%s",c.remoteAddress, c.remotePort);
});
break;
}
}
}
});
};
Sonoff.prototype._httpPost = (target, data, callback) => {
var dta = JSON.stringify(data);
var u = url.parse(target);
var options = {
hostname: u.hostname,
port: u.port || 80,
path: u.path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(dta)
}
};
console.log('REQ | Sending %s to %s:%s%s',dta,options.hostname, options.port, options.path);
var req = http.request(options, (res) => {
var d = '';
res.on('data', (c) => d += c);
res.on('end', () => {
var response = JSON.parse(d);
callback(response);
});
}).on('error', (e) => {
console.log(`unable to post request: ${e.message}`);
callback(null, e);
});
req.write(dta);
req.end();
};
Sonoff.prototype._updateKnownDevice = (self, device) => {
var updated = false;
for (var i = 0; i < self._knownDevices.length; i++) {
if (self._knownDevices[i].id == device.id) {
self._knownDevices[i] = device;
updated = true;
self.emit('deviceUpdated',device);
}
}
if (!updated) {
self._knownDevices.push(device);
self.emit('deviceAdded',device);
}
};

--

--