JeanCarl's Adventures

Smart TV Watcher with SmartThings

July 30, 2015 |

Internet of Things has been a fascinating area of tech to watch. As more and more things become internet connected, the developer playground has expanded with countless possibilities. For example, when I open my front door, a door sensor triggers the system to turn on the lights of it is dark. When I walk out of a room, a motion sensor turns off the lights automatically, saving electricity and also the effort in flipping the light switch. I can also be notified via SMS when someone comes home.

It isn’t only about convenience, but can be functional. A water sensor can detect a water leak and turn off water valves, alert the homeowner about the problem, and prevent a huge disaster of a flooded basement.

I have a smart plug connected to my SmartThings hub. For the Internet of Things Devpost hackathon challenge, I built a web app that encourages couch potatoes to perform mental exercises in exchange for TV time. Each time the plug is switched on, it checks to see if there is TV time available and sets a timer for the specified time. When the time expires, the plug will turn off.

SmartThings

To monitor when the smart plug turns on, we need to create a SmartApp.

Photo

SmartThings has a webbased IDE to write apps and deploy them.

Photo

Open up the IDE and insert the following code:

/**
 /**
 *  SmartToWeb
 *
 *  Copyright 2015 JeanCarl Bisson
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 */
definition(
    name: "Smart TV Watcher",
    namespace: "com.jeancarlbisson.smarttvwatcher",
    author: "JeanCarl Bisson",
    description: "Solve math problems and earn TV time. ",
    category: "Fun & Social",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
    iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
    oauth: true)

preferences {

    section("Webhook URL"){
        input "url", "text", title: "Webhook URL", description: "Your webhook URL", required: true
    }

    section("Choose what events you want to trigger"){
        input "switchv", "capability.switch", title: "Switch", required: false, multiple: true
    }
    
    section("Send a text message to this number") {
        input "phone", "phone", required: true
    }    
}

def installed() {
    log.debug "Installed with settings: ${settings}"
    subscribeToEvents()
    initialize()
}

def updated() {
    log.debug "Updated with settings: ${settings}"
    unsubscribe()
    subscribeToEvents()
}

def subscribeToEvents() {
    subscribe(app, appEventHandler)
    subscribe(switchv, "switch", eventHandler)
}

def appEventHandler(evt) {
    if(state.expiration != "") {
        sendPushMessage("You have until "+state.expiration+" to watch TV.");
    }
}

def eventHandler(evt) {
    if(evt.value == "on") { 
        httpPostJson([
            uri: "${settings.url}/api/gettime",
            body: [
                id: evt.deviceId, 
                displayname: evt.displayName, 
            ] 
        ]) { resp ->
            if(resp.data.timeLeft == 0){
                turnOff()
                sendSmsMessage(phone, "Get more time at: ${settings.url}/#/play/${evt.deviceId}")
            }
            else
            {
                def d = new Date()
                def e = new Date(d.getTime()+(resp.data.timeLeft.toInteger()*1000))
                state.expiration = e.toString()

                httpPostJson([
                    uri: "${settings.url}/api/resettime/${evt.deviceId}",
                    body: []
                ]) { resps ->
                }   

                sendPush("You have until "+state.expiration+" to watch TV.")
            }

            runIn(resp.data.timeLeft, turnOff)
        }
    } else {
        state.expiration = ""
    }
}

def turnOff() {
    switchv.off()
}

Setup

There are five files for this project. app.js is the Node.js that keeps track of the TV time earned. The AngularJS app, smartthings.js and index.html, use two views, a placeholder main.html and the game play.html.

// Filename: app.js

var PORT = 3000;
var MONGODB_ADDRESS = 'mongodb://127.0.0.1:27017/test';

var express = require('express');
var bodyParser = require('body-parser');
var url = require('url');
var mongoose = require('mongoose');

var app = express();

app.use(bodyParser.json());

mongoose.connect(MONGODB_ADDRESS);

var DeviceModel = mongoose.model('Device', {
  deviceId: String,
  deviceName: String,
  timeLeft: Number,  
});

// Creates a device profile if one doesn't exist, and returns the rewarded time.
app.post('/api/gettime', function(req, res) {
  DeviceModel.findOne({deviceId: req.body.id}, function(err, device) {
    if(err || !device) {
      DeviceModel.create({
        deviceId: req.body.id,
        deviceName: req.body.displayname,
        timeLeft: 0
      }, function(err, device) {
        if(err) {
          console.log(err);
          return;
        }
        
        res.send({timeLeft: device.timeLeft});
      });
    } else {
      res.send({timeLeft: device.timeLeft});  
    }
  });
});

// Returns the time that has been rewarded to this device.
app.get('/api/gettime/:deviceId', function(req, res) {
  DeviceModel.findOne({deviceId: req.params.deviceId}, function(err, device) {
    if(err || !device) {
      console.log(err);
      return;
    } else {
      res.send({timeLeft: device.timeLeft});  
    }
  });
});

// Resets the time that has been rewarded to this device.
app.post('/api/resettime/:deviceId', function(req, res) {
  DeviceModel.findOne({deviceId: req.params.deviceId}, function(err, device) {
    DeviceModel.update({deviceId: req.params.deviceId}, {
      timeLeft: 0
    }, function(err, numAffected) {
      res.send({timeLeft: device.timeLeft});
    });
  });
});

// Adds rewarded time to this device.
app.post('/api/addtime/:deviceId/:seconds', function(req, res) {
  DeviceModel.findOne({deviceId: req.params.deviceId}, function(err, device) {
    if(err || !device) {
      res.send({error: 'Device not registered.'});
    } else {
      device.timeLeft = device.timeLeft + parseInt(req.params.seconds);

      DeviceModel.update({deviceId: device.deviceId}, {timeLeft:device.timeLeft}, function(err, numAffected) {
        if(err) {
          console.log(err);
        }
      });

      res.send({timeLeft: device.timeLeft});  
    }
  });
});

app.listen(PORT);
console.log('Application listing on port '+PORT);

app.use(express.static(__dirname + '/public'));
// Filename: public/smartthings.js

angular.module('SmartTVWatcherApp', ['ngRoute'])
.config(['$routeProvider', function($routeProvider) {
  $routeProvider.
    when('/', {
      templateUrl: 'main.html',
    }).
    when('/play/:deviceId', {
      templateUrl: 'play.html',
      controller: 'GameCtrl'
    }).
    otherwise({
      redirectTo: '/'
    });
}])
.constant('SECONDS_FOR_CORRECT_ANSWER', 50)
.filter('secondsToDateTime', [function() {
    return function(seconds) {
        return new Date(1970, 0, 1).setSeconds(seconds);
    };
}])
.controller('GameCtrl', ['$scope', '$http', '$routeParams', 'SECONDS_FOR_CORRECT_ANSWER', function($scope, $http, $routeParams, POINTS_FOR_CORRECT_ANSWER) {
  $scope.timeLeft = 0;
  $scope.deviceId = $routeParams.deviceId;
  $scope.deviceName = $routeParams.deviceName;

  $http.get('/api/gettime/'+$scope.deviceId).success(function(response) {
    console.log(response);
    $scope.timeLeft = response.timeLeft;
  });

  $scope.generateProblem = function() {
    var a = Math.floor(Math.random()*10);
    var b = Math.floor(Math.random()*10);

    $scope.answer = a+b;
    $scope.problem = a+'+'+b+' = ?';
    $scope.answers = [a+b, a+b-1, a+b+1];

    for(var j, x, i = $scope.answers.length; i; j = Math.floor(Math.random() * i), x = $scope.answers[--i], $scope.answers[i] = $scope.answers[j], $scope.answers[j] = x);
  }

  $scope.checkAnswer = function(answer) {
    if($scope.answer == answer) {
      $http.post('/api/addtime/'+$scope.deviceId+'/'+SECONDS_FOR_CORRECT_ANSWER).success(function(response) {
        $scope.timeLeft = response.timeLeft;
        $scope.generateProblem();
      });
    } else {
      alert('Not quite!');
      $scope.generateProblem();
    }
  }

  $scope.generateProblem();
}]);
<!-- Filename: public/index.html -->

<!DOCTYPE html> 
<html ng-app="SmartTVWatcherApp">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1"> 
  <title>Smart TV Watcher</title> 
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.16/angular.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-route.min.js"></script>
  <script src="smartthings.js"></script>
</head> 

<body style="font-family:Arial"> 

<div ng-view></div>
  

</body>
</html>
<!-- Filename: public/main.html -->

<h2>Smart TV Watcher</h2>

<p>Please access this app through the SmartThings SmartApp.</p>
<!-- Filename: public/play.html -->

<div style="text-align:right">TV Time Earned: {{timeLeft | secondsToDateTime | date:"HH:mm:ss"}}</div>

<div style="font-size:36pt; text-align:center">
{{problem}}
</div>

<div style="text-align:center">
  <div ng-repeat="answer in answers" style="padding-top:10px">
    <input type="button" ng-click="checkAnswer(answer)" value="{{answer}}" style="font-size:20pt; width:100%; height:50px" />
  </div>
</div>

<div style="color:#C0C0C0; padding:20px">
  When you have accrued enough time to watch a TV show, return to the SmartThings app and tap on the switch to turn it on. 
</div>
// Filename: package.json
{
  "name": "hear-it",
  "description": "Smart TV Watcher game for Node.js",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "express": "*",
    "url": "",    
    "body-parser": "*",
    "mongoose": "*"
  }
}

Install the Node.js dependencies by running this command:

npm install

And start the Node.js app by running this command:

nodejs app.js

Smart TV Watcher

This is the SmartThings iOS app. You can turn on and off switches, and receive notifications when certain triggers happen, such as when a door opens.

Photo

The third icon represents the switch that is connected to my television. If I tap on it, it will turn the switch on. However, since Smart TV Watcher is listening for this event, it will override the switch. The Smart TV Watcher app checks the Node.js application to see if there is any TV time earned.

A text message is sent since there is no time earned.

Photo

Photo

Following the link in the text message, I am taken to the Smart TV Watcher web app. This is a simple artimethic game of addition. For each problem I solve correctly, I earn 50 seconds of TV time. After solving 37 math problems (which took me less than a minute), the screen looks like this:

Photo

After I earn 30 minutes of TV time, I return back to the SmartThings app and turn the switch on. This time, since I have TV time available, the switch stays on. A notification shows at the top letting me know how long I have.

Photo

At any point, I can also access the SmartApps screen and tap on the Smart TV Watcher app to find out how long I have until the switch turns off.

Photo

Photo

I can go back in and solve more math problems to earn additional time. Otherwise, at the specified time the switch will turn off and my TV watching ends.

Source Code

You can find the repo on Github.

Post Mortem

This was a fun project to build. I have never used Groovy, so I got to learn the “Groovy” syntax. The SmartThings platform is pretty simple yet powerful with the event based actions that you can subscribe and respond to.

In the future, the amount of TV time that can be earned could be adjustable and the math problems could be more challenging the longer you watch TV. This application could also apply to limiting how long you play with game consoles, and other things that require electricity. Perhaps I have to solve math problems to heat up my food in the microwave.

Which reminds me, I have some TV to watch!