How to get Zipabox to detect its own zombification and self-reboot

David Pritchard shared this idea 6 months ago
Under Consideration

Quite often now (perhaps because I have a lot of rules), I notice that, after synchronising a new set of changes to rules, the Zipabox becomes partially zombified. It will continue to receive data from outside (from sensors, or from Google App Scripts that I have running to send information to it), but the rules that I have running on a constant cycle of ten minutes just stop working.


So I wondered if I could detect this state, and also whether I could get the Zipabox to reboot itself. The answer: yes, and yes :-)


For the reboot you'll need some sort of wireless socket. I deliberately chose one that is not intended for integration into home automation so that it will work completely independently of the Zipabox. I chose the TP-Link HS100/110. At first I just used it to reboot the Zipabox from my smartphone when I saw that it wasn't responding any more, but then I got a little more ambitious.


The TP-Link has the additional advantage of being able to accept commands in the form of HTTP requests, and being able to reboot itself. This is where I found the reboot command:


https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/


In order to be able to send it commands, you need to obtain the device ID and the authentication token, and note the URL (which is probably https://eu-wap.tplinkcloud.com for most of us), all of which is explained here:


http://itnerd.space/2017/01/22/how-to-control-your-tp-link-hs100-smartplug-from-internet/


You just need to get this info once by sending a couple of HTTP commands, and you're ready to send HTTP requests. The reboot command is as follows:


POST


URL: https://eu-wap.tplinkcloud.com/?token=<YOUR AUTHENTICATION TOKEN>

Headers: Content-Type: application/json

Body:

{

"method":"passthrough",

"params":

{

"deviceId":"<YOUR DEVICE ID>",

"requestData":"{\"system\":{\"reboot\":{\"delay\":2}}}"

}

}


That's the self-reboot part. Just put that in a rule.


Now for the detection of zombification. Since I want to detect when my variable-setting rules are not working, I save a time stamp each time those rules run (see my date-time meter post on creating time stamps). Then I create another rule that detects when that time stamp is out of date. Since my external data sources are still updating even when the box is not responding properly, I can create a rule that reacts to changes in those data sources, have the rule check the time stamp, and if the time stamp is old, reboot the box. See attachment.


The rule checks a selection of external data sources to ensure that it gets called pretty frequently. I disable the check immediately after a reboot because the system takes a while to get itself together.

Comments (7)

photo
2

How to remotely de-zombify a Zipabox guide... awesome.

photo
2

Well Dave, you just showed again how greatly you know the system and how to get around it...

photo
1

Forget to format the code. Sorry!

photo
1

David, this date-time meter is a virtual meter and current date-time acts as counter?

/gyG5IUP+foTkwIMGZlhIqftNWGkIy3NCcmNYrW9VakNqMKQEW+VgSBkyZEiwVfY9aGCGPZhs85uQUt0iz4rolWF9VtSojmj+kOKPaNVDhgxp039tYP5f4L0LbHrIFW0AAAAASUVORK5CYIIA

Or, are you able to store date time in virtual meter?

thanx, Martin

photo
1

This is my post on the date-time meter:


https://community.zipato.com/topic/a-method-for-creating-manipulable-time-and-date-values


It works very well and I use it all over the place.


Yesterday I was puzzled in by a claim in this post:


https://community.zipato.com/topic/creating-a-rule-using-sunsetsunrise-for-outdoor-lights


that you can store time values in variables. In my experience, that doesn't work. I'll check it out in case Zipato have fixed something.

photo
1

For anyone thinking of implementing my scheme, it ran into a bit of a snag: the TP-Link token expires fairly frequently. I'd already set up a Google App Script that monitors the updates to a Google Sheet where the Zipabox logs important data every ten minutes. If it sees that there hasn't been an update for a while, it sends me a message. I decided to extend that to make the script reboot the Zipabox directly. From a script, getting a new token is not a problem. I force the renewal of the token each week, and if I get the expired token error, I also renew it and try again. I store the token in another Google Sheet which I use to note down when my calls to weather services have failed recently.


To complete this solution, I invoke the reboot part of the Google App Script from the RebootSelf rule in the Zipabox. And it apparently works!


I will just post the reboot part here for now. If you also want the part that monitors the log, let me know.


var sTPLinkLoginAccessToken = "";

// Allows reboot to be executed from Zipabox
function doPost(e) 
{
  Logger.log("doPost");
  return RebootZipabox();
};

function RebootZipabox()
{
    var nErrorCodeToken = 0;
    var bRenewAccessToken = false;
    
    // Should we force the renewal of the access token? Only on Saturdays between 12:00 and 12:10
    var dateNow = new Date();
    var nHour = dateNow.getHours();
    var nMinute = dateNow.getMinutes();
    
    if (dateNow.getDay() == 6 && nHour == 12 && nMinute >= 0 && nMinute <= 10)
    {
      bRenewAccessToken = true;
    }        
    
    // Open API call failure spreadsheet to get/renew token
    var docCallFailures = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/<SPREADSHEETID>/edit');
    var sheetCallFailures = docCallFailures.getSheets()[0];  
    
    // Check sheet for login auth
    // getRange(row, column, NumRows (number of rows in the range)). Numbers are 1-based
    sTPLinkLoginAccessToken = sheetCallFailures.getRange(9,5,1).getCell(1,1).getValue();
    
    // Need to get access token if we don't have it
    if (sTPLinkLoginAccessToken == '')
    {
      bRenewAccessToken = true;
    }        
    
    // Get new token
    if (bRenewAccessToken)
    {
      nErrorCodeToken = GetNewTPLinkToken(sheetCallFailures);
    }
    
    if (nErrorCodeToken == 0)
    {
      // Try rebooting TPLink
      var nErrorCodeReboot = TryToRebootTPLink();
      
      // Did reboot fail due to an old access token?
      if (nErrorCodeReboot = -20651)
      {
        // Get new access token
        nErrorCodeToken = GetNewTPLinkToken(sheetCallFailures);
        
        if (nErrorCodeToken == 0)
        {
          // Try again
          nErrorCodeReboot = TryToRebootTPLink();
          
          if (nErrorCodeReboot != 0)
          {
            return ContentService
            .createTextOutput(JSON.stringify({"result":"error", "error": nErrorCodeReboot}))
            .setMimeType(ContentService.MimeType.JSON);              
          }
        }
      }
    }
    
    // Prepare standard HTTP POST stuff for Newtifry
    var options =
        {
          "contentType" : "text/plain",
          "method" : "post"
        };
    
    // Send Newtifry message
    UrlFetchApp.fetch("http://api.pushingbox.com/pushingbox?devid=<YOURPUSHINGBOXSCENARIOID>";, options);        
    
    // Notify myself via email
    MailApp.sendEmail({to:'<YOUREMAIL>',‌subject: "Zipabox is zombified",body: "Rebooting Zipabox from Google App Script"});    
        
    // return json success results
    return ContentService
          .createTextOutput(JSON.stringify({"result":"success"}))
          .setMimeType(ContentService.MimeType.JSON);        
}

function TryToRebootTPLink()
{
    // Prepare params for body
    var bodyParams = 
        {
          "deviceId":"<YOURTPLINKDEVICEID>",
          "requestData":"{\"system\":{\"reboot\":{\"delay\":2}}}"
        };
    
    // Prepare body of POST (payload)
    var postBody = 
        {
          "method":"passthrough",
          "params": bodyParams
        };
    
    var payload = JSON.stringify(postBody);
    
    var optionsTPLink = 
        {
          "method" : "POST",
          "contentType" : "application/json",
          "muteHttpExceptions": true,
          "payload" : payload
        };
        
    var sURL = "https://eu-wap.tplinkcloud.com/?token="; + sTPLinkLoginAccessToken;
    
    var requestTest = UrlFetchApp.getRequest(sURL, optionsTPLink);
    
    // Send the command to the TP-Link
    var requestTPLink = UrlFetchApp.fetch(sURL, optionsTPLink);
    
    if (requestTPLink.getResponseCode() == 200)
    {
      var resultTPLink = JSON.parse(requestTPLink.getContentText());
      
      if (resultTPLink != null)
      {
        return resultTPLink.error_code;
      }
    }
    
    return 0;
}

function GetNewTPLinkToken(sheetCallFailures)
{
    // Prepare params for body
    var bodyParams = 
        {
           "appType": "Kasa_Android",
           "cloudUserName": "<YOURKASAUSERNAME>",
           "cloudPassword": "<YOURKASAPASSWORD>",
           "terminalUUID": "MY_UUID_v4"
        };
    
    // Prepare body of POST (payload)
    var postBody = 
        {
          "method":"login",
          "params": bodyParams
        };
    
    var payload = JSON.stringify(postBody);
     
    var optionsTPLink = 
        {
          "method" : "POST",
          "contentType" : "application/json",
          "payload" : payload
        };
  
    // Send the command to the TP-Link
    var requestTPLink = UrlFetchApp.fetch("https://wap.tplinkcloud.com";, optionsTPLink);
    
    if (requestTPLink.getResponseCode() == 200)
    {
      var resultTPLink = JSON.parse(requestTPLink.getContentText());
      
      if (resultTPLink != null)
      {
        if (resultTPLink.error_code == 0)
        {
          sTPLinkLoginAccessToken = resultTPLink.result.token;

          // Store new token in sheet
          // getRange(row, column, NumRows (number of rows in the range)). Numbers are 1-based
          sheetCallFailures.getRange(9,5,1).getCell(1,1).setValue(sTPLinkLoginAccessToken);
        }
        
        return resultTPLink.error_code;
      }
    }
    
    return 0;
}

photo
1

d4998a9ea2a44df95c216a678a9a0d3f


This is the rule that calls the app script. The GaS must be made accessible to anyone, even anonymous (I think). Otherwise you can't invoke it from outside.