Providing a web interface for the TP-Link HS range of smart plugs. Now you can control your devices from tablets, kindles, desktops, and sundry machines where KASA doesn't exist.
A Pen by Andy Allsopp on CodePen.
<html ng-app="myApp" ng-cloak> | |
<head> | |
<meta charset="utf-8"> | |
<title>TP-Link Web UI</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.css"> | |
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | |
</head> | |
<body layout="column" style="overflow-y:hidden"> | |
<div ng-controller="dash" style="overflow-y:auto" md-theme="custom"> | |
<md-toolbar> | |
<div class="md-toolbar-tools"> | |
<h2 flex md-truncate>TP-Link Web UI</h2> | |
</div> | |
</md-toolbar> | |
<md-tabs md-dynamic-height md-border-bottom md-selected="selected_tab_index"> | |
<md-tab label="Devices"> | |
<md-content ng-if="tpl.devices.length"> | |
<md-list class="md-dense" flex> | |
<md-subheader class="md-no-sticky">TP-Link Devices</md-subheader> | |
<md-list-item class="md-2-line" ng-repeat="device in tpl.devices"> | |
<div class="md-list-item-text"> | |
<h3>{{device.alias}}</h3> | |
<p>{{device.deviceModel}} - {{device.deviceName}}</p> | |
</div> | |
<md-switch ng-change="tpl_setState($index,device.is_powered)" | |
ng-model="device.is_powered"></md-switch> | |
<md-divider/> | |
</md-list-item> | |
</md-list> | |
<md-button class="md-raised md-primary" ng-click="tpl_refreshDevices()">Refresh Devices</md-button> | |
</md-content> | |
<md-content ng-if="!tpl.devices.length" class="md-padding"> | |
<h3>Warning</h3> | |
No devices were found. Please visit the 'Settings' tab to verify your credentials or request a new | |
token.</h3> | |
</md-content> | |
</md-tab> | |
<md-tab label="Settings"> | |
<md-content class="md-padding"> | |
<h3>TP-Link Authentication</h3> | |
<p class="md-caption"> | |
Please provide the credentials used to connect to your TP-Link account. | |
</p> | |
<div layout="column" layout-gt-xs="row"> | |
<md-input-container flex="50"> | |
<label>Username/Email</label> | |
<input required type="email" ng-model="tpl.username" | |
minlength="10" maxlength="100" ng-pattern="/^.+@.+\..+$/"/> | |
<div ng-messages="tpl.username.$error" role="alert"> | |
<div ng-message-exp="['required', 'minlength', 'maxlength', 'pattern']"> | |
Your email must be between 10 and 100 characters long and look like an e-mail address. | |
</div> | |
</div> | |
</md-input-container> | |
<md-input-container flex="50"> | |
<label>Password</label> | |
<input required ng-model="tpl.password"/> | |
<div ng-messages="tpl.password.$error" role="alert"> | |
<div ng-message-exp="['required']"> | |
Password must be provided. | |
</div> | |
</div> | |
</md-input-container> | |
</div> | |
<div layout="column" layout-gt-xs="row"> | |
<md-input-container flex="50"> | |
<md-checkbox ng-model="tpl.store_credentials" aria-label="store credentials"> | |
Allow this browser to store credentials. | |
</md-checkbox> | |
</md-input-container> | |
<md-input-container flex="50"> | |
<md-checkbox ng-model="tpl.store_token" aria-label="store token"> | |
Allow this browser to store the authentication token. | |
</md-checkbox> | |
</md-input-container> | |
</div> | |
<md-button class="md-button md-raised md-primary" ng-click="tpl_authenticate()">Authenticate and Refresh | |
Devices | |
</md-button> | |
<div ng-if="tpl.token != ''"> | |
<br/> | |
<md-divider/> | |
<h3>Current Status</h3> | |
<p class="md-caption">Found {{tpl.devices.length}} devices using token: <code>{{tpl.token}}</code></p> | |
</div> | |
</md-content> | |
</md-tab> | |
<md-tab label="About"> | |
<md-content flex layout-padding> | |
<h3>About this application</h3> | |
<p>This webpage uses angular services to build a web control surface for the TP-Link series of smart | |
plugs. TP-Link's own official app (Kasa) only runs on iOS and Android devices, so those of you | |
with other tablets, mobiles, or desktops get left in the cold. This page attempts to address that.</p> | |
<md-divider/> | |
<div class="md-caption"> | |
<p>In use, the Kasa app generates a unique ID, then combines this with the registration information you | |
supply at sign-up to retrieve a secure token from a TP-Link hosted API service. Requests for smart plug | |
availability, status and relay states are sent with this token to protect your privacy.</p> | |
<p>This application is able to generate a random UUID of its own, capture your credentials, and pass these | |
to the TP-Link hosted API service to obtain a genuine token. That token can then be used to list the devices | |
which are present, indicate their status, and allow you to switch them on/off from anywhere you like.</p> | |
<p>Information is only sent to and from the TP-Link APIs, and is not exposed to any other server. If you | |
opt to retain your login credentials and/or authentication token, these are stored in cookies within your | |
browser. They are not released to me or anyone else.</p> | |
<p>To access your plugs, make sure they have remote control enabled, and are registered with the | |
official Kasa App from TP-Link (<a | |
href="http://www.tp-link.com/us/home-networking/smart-home/kasa.html" target="_blank">Get | |
Kasa here</a>). Unfortunately, I don't know a way to perform the registration without (at least) | |
temporary access to the official app, but once registered you can send requests from any modern browser.</p> | |
<p>The full source for this project is available for free, online at <a | |
href="https://github.com/arallsopp/tp-link-smart-switch-web-client-" target="_blank">GitHub</a>. | |
</p> | |
<p>I hope you are able to make use of it.</p> | |
</div> | |
<p>Andy Allsopp</p> | |
</md-content> | |
</md-tab> | |
</md-tabs> | |
</div> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular-animate.min.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular-cookies.min.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular-aria.min.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular-messages.min.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/angular_material/1.1.4/angular-material.min.js"></script> | |
<script src="script.js"></script> | |
</body> | |
</html> |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular-animate.min.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular-cookies.min.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular-aria.min.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular-messages.min.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/angular_material/1.1.4/angular-material.min.js"></script> |
var app = angular.module('myApp', ['ngMaterial','ngCookies']); | |
myDash.$inject = ['$scope', '$mdToast','$http','$interval','$cookies']; | |
angular.module('myApp').controller('dash', myDash) | |
.config(['$mdThemingProvider',function($mdThemingProvider) { | |
$mdThemingProvider.theme('custom').primaryPalette('blue-grey').accentPalette('deep-orange'); | |
}]); | |
function myDash($scope, $mdToast, $http, $interval, $cookies) { | |
$scope.getUUID = function () { | |
var d = new Date().getTime(); | |
if (window.performance && typeof window.performance.now === "function") { | |
d += performance.now(); | |
//use high-precision timer if available | |
} | |
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { | |
var r = (d + Math.random() * 16) % 16 | 0; | |
d = Math.floor(d / 16); | |
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16); | |
}); | |
return uuid; | |
}; | |
$scope.showToast = function (msg) { | |
$mdToast.show($mdToast.simple().textContent(msg).position('top right')) | |
}; | |
$scope.tpl_authenticate = function () { | |
$scope.tpl.UUID = $scope.getUUID(); | |
var auth_obj = { | |
"method": "login", "params": { | |
"appType": "Kasa_Android", | |
"cloudUserName": $scope.tpl.username, | |
"cloudPassword": $scope.tpl.password, | |
"terminalUUID": $scope.tpl.UUID | |
} | |
}; | |
$http.post("https://wap.tplinkcloud.com/", auth_obj).then(function mySuccess(response) { | |
var now = new Date(); | |
var exp = new Date(now.getFullYear()+1, now.getMonth(), now.getDate()); | |
$scope.tpl.token = response.data.result.token; | |
$cookies.put('tpl_uuid', $scope.tpl.UUID,{expires: exp}); | |
if($scope.tpl.store_credentials) { | |
$cookies.put('tpl_username', $scope.tpl.username,{ expires: exp}); | |
$cookies.put('tpl_password', $scope.tpl.password,{ expires: exp}); | |
$cookies.put('tpl_store_credentials',true,{ expires: exp}); | |
}else{ | |
$cookies.remove('tpl_username'); | |
$cookies.remove('tpl_password'); | |
$cookies.remove('tpl_store_credentials'); | |
} | |
if($scope.tpl.store_token) { | |
$cookies.put('tpl_token', $scope.tpl.token,{ expires: exp}); | |
$cookies.put('tpl_store_token',true,{ expires: exp}); | |
}else{ | |
$cookies.remove('tpl_token'); | |
$cookies.remove('tpl_store_token'); | |
} | |
$scope.tpl_refreshDevices(); | |
}, function myError(response) { | |
$scope.myWelcome = response.statusText; | |
}); | |
}; | |
$scope.tpl_refreshDevices = function () { | |
var request_obj = {"method": "getDeviceList"}; | |
$http.post("https://wap.tplinkcloud.com?token=" + $scope.tpl.token, request_obj).then(function mySuccess(response) { | |
$scope.tpl.devices = (response.data.result.deviceList); | |
console.log($scope.tpl.devices); | |
if ($scope.tpl.devices.length) { | |
for (var i = 0; i < $scope.tpl.devices.length; i++) { | |
$scope.tpl_getState(i); | |
} | |
$scope.selected_tab_index = 0; | |
} | |
}); | |
}; | |
$scope.tpl_getState = function (device_index) { | |
var url = $scope.tpl.devices[device_index].appServerUrl; | |
var deviceId = $scope.tpl.devices[device_index].deviceId; | |
var request_obj = { | |
"method": "passthrough", "params": { | |
"deviceId": deviceId, | |
"requestData": "{\"system\":{\"get_sysinfo\":null},\"emeter\":{\"get_realtime\":null}}" | |
} | |
}; | |
$http.post(url + "?token=" + $scope.tpl.token, request_obj).then(function mySuccess(response) { | |
window.response = response; | |
var testval = JSON.parse(response.data.result.responseData).system.get_sysinfo.relay_state; | |
console.log(device_index, testval, response); | |
$scope.tpl.devices[device_index].is_powered = (testval == true); | |
}); | |
}; | |
$scope.tpl_setState = function (device_index, device_state) { | |
var url = $scope.tpl.devices[device_index].appServerUrl; | |
var deviceId = $scope.tpl.devices[device_index].deviceId; | |
var request_obj = { | |
"method": "passthrough", "params": { | |
"deviceId": deviceId, | |
"requestData": "{\"system\":{\"set_relay_state\":{\"state\":" + (device_state ? 1 : 0 ) + "}}}" | |
} | |
}; | |
$http.post(url + "?token=" + $scope.tpl.token, request_obj).then(function mySuccess(response) { | |
window.response = response; | |
console.log(response); | |
}); | |
}; | |
$scope.tpl = {}; | |
$scope.tpl.refresh_rate = 5; //check every 5 seconds. Set this as you fancy. | |
$scope.tpl.devices = []; | |
$scope.tpl.store_token = $cookies.get('tpl_store_token') == "true"; | |
$scope.tpl.store_credentials = $cookies.get('tpl_store_credentials') == "true"; | |
$scope.tpl.UUID = $cookies.get('tpl_uuid'); | |
if($scope.tpl.store_credentials){ | |
$scope.tpl.username = $cookies.get('tpl_username'); | |
$scope.tpl.password = $cookies.get('tpl_password'); | |
} | |
if($scope.tpl.store_token) { | |
$scope.tpl.token = $cookies.get('tpl_token'); | |
} | |
$scope.selected_tab_index = 0; | |
if (typeof ($scope.tpl.token) === "undefined") { | |
$scope.tpl.token = ''; | |
} else { | |
$scope.tpl_refreshDevices(); | |
} | |
$interval(function () { | |
for (var i = 0; i < $scope.tpl.devices.length; i++) { | |
$scope.tpl_getState(i); | |
} | |
}, $scope.tpl.refresh_rate * 1000); | |
} |
Providing a web interface for the TP-Link HS range of smart plugs. Now you can control your devices from tablets, kindles, desktops, and sundry machines where KASA doesn't exist.
A Pen by Andy Allsopp on CodePen.