
Wake-on-LAN is a feature built into many motherboards that allows a computer to be started remotely by a special message received from the local network. This message called a magic packet is about a 100 bytes or so and contains the MAC address of the computer to be woken up or started. The magic packet is sent by another device on the same local network such as another computer or a mobile phone. Apparently, when the network card of the destination computer receives the magic packet it boots up the machine just as pressing its power button would.
Wake-on-LAN, I am told, works best for wired LAN connections. Obviously, the router should be on, and the power supply to the desktop should also be on. Also, the wake-on-LAN feature should be enabled in the BIOS. My desktop has a wired LAN connection to the router. It has an ASUS motherboard. For this motherboard wake-on-lan is enabled by enabling Power On By PCI-E
under the Advanced Power Management(APM) configuration. Motherboards of other manufacturers may have some other setting that you have to enable for this purpose.
For a while I was using an Android application to send the magic packet to the desktop from my phone to wake it up. On a whim, I wondered whether the magic packet could be sent by a voice command to the Amazon Echo Dot that I had with me. When I looked it up, I found out that recently the Alexa Smart Home API had introduced something called the Wake-on-LAN Controller
that could help do just that.
Someone by then had already come up with an Alexa Skill using this new controller. Though the skill was free to use one had to register with one's Amazon account at the skill's web site. I was reluctant to do that and moreover I wanted to try make the skill on my own.
Here is how I went about making the Alexa skill to wake up my desktop using my Echo Dot. This is the only Alexa skill(other than a trivial tutorial skill) I have made so far.
As I have said already, the Wake-on-LAN
controller is part of the Smart Home API and therefore I had to make an Alexa Smart Home Skill. This page gives step by step instructions to bootstrap the skill.
The first thing I needed was an Alexa developer account. I could register for one for free using my Amazon credentials. Next, I had to register for a Amazon Web Services(AWS) account. An AWS account offers several services for so-called cloud computing. I was going to use only two of those services -- one, AWS Lambda, that allows you to write custom functions that run on Amazon servers, and two, DynamoDB, a document database.
Before creating a Lambda function I had to create an Identity and Access Management(IAM) role that the Lambda function would be running under. An IAM role makes sure that the Lambda function is run with just the right permissions and no more.
I used the nodejs sample function given on the instructions page as my starting point.
Once you develop and enable a skill for a Smart Home device, Alexa tries to discover its capabilities by sending a Discovery
request to your Lambda function which should then return an appropriate response.
After quite a bit of back and forth with the documentation and quite a bit of trial and error this is the function I finally used for handling Discovery
.
function handleDiscovery(request, context) {
const payload = {
endpoints: [
{
endpointId: 'MyDesktop',
manufacturerName: 'Asus',
friendlyName: 'Desktop',
description: 'Desktop',
displayCategories: ['COMPUTER'],
additionalAttributes: {
manufacturer: 'Asus',
model: '158',
serialNumber: '191161258905903',
firmwareVersion: 'f1229c44-87f3-012b-12cb-a85e456c0bfd',
softwareVersion: 'BIOS 5.12',
customIdentifier: 'My_computer',
},
cookie: {},
capabilities: [
{
type: 'AlexaInterface',
interface: 'Alexa',
version: '3',
},
{
type: 'AlexaInterface',
interface: 'Alexa.WakeOnLANController',
version: '3',
properties: {},
configuration: {
MACAddresses: ['a8:5e:45:6c:0b:fd'],
},
},
{
type: 'AlexaInterface',
interface: 'Alexa.PowerController',
version: '3',
properties: {
supported: [
{
name: 'powerState',
},
],
proactivelyReported: false,
retrievable: false,
},
},
],
},
],
};
const req_msg_id = request.directive.header.messageId;
const header = {
namespace: 'Alexa.Discovery',
name: 'Discover.Response',
payloadVersion: '3',
messageId: req_msg_id + '-R',
};
log(
'DEBUG',
'Discovery Response:',
JSON.stringify({ header: header, payload: payload })
);
context.succeed({ event: { header: header, payload: payload } });
}
This page lists the required fields in the response object. The capabilities
field is the most important. For Wake-on-LAN, this array should include the Alexa.WakeOnLANController
interface with the MAC address of the machine you want to wake up and also the Alexa.PowerController
interface with powerState
as a supported property.
The voice commands to Smart Home Skills are limited. The WakeOnLANController
has no voice command or directive to the code associated directly with it. Instead it uses the PowerController.TurnOn
directive sent to the Lambda function when I give the voice command Alexa Turn On Desktop
. Responding correctly to the PowerController.TurnOn
directive will turn on the computer.
According to the documentation, this is how you respond to the PowerController.TurnOn
directive. First, instead of sending a normal response to the directive as we did for the Discovery
directive we have to send a so-called DeferredResponse
. Next, we have to send a WakeOnLANController.WakeUp
POST event to the Alexa event gateway, which, for India and Europe, is /api.eu.amazonalexa.com
. On receiving a successful return(status code of 202) from the event gateway, we send a final response. Apparently, when the WakeUp
event is successfully processed, Alexa makes the Echo Dot broadcast a Wake-on-LAN message(the magic packet) to the network the Echo Dot and my computer are both a part of.
Unfortunately it did not quite work out the way it was given in the documentation. I had to get rid of the DeferredResponse
totally before it finally worked.
The other hiccup was that the api call to the event gateway had to be authenticated using an access token both in a header and in the body of the call. To obtain the access token I had to make a further call to api.amazon.com/auth/o2/token
with my Alexa client Id and Alexa Client Secret as a part of the body. The access token that I received came along with a refresh token that had to be used to get a fresh access token, refresh token pair after the expiry duration that the access token had. The expiry duration was also included in the response that contained the access token, refresh token pair. I stored the access token, refresh token and the expiry duration in a DynamoDB document database so that I could avoid a call to the authentication api if I was within the expiry duration. I had to add a permission to access a DynamoDB database to the IAM role I had created earlier.
Here is the entire Lambda function.
const https = require('https');
const DynamoDB = require('aws-sdk/clients/dynamodb');
exports.handler = function (request, context, callback) {
if (
request.directive.header.namespace === 'Alexa.Discovery' &&
request.directive.header.name === 'Discover'
) {
log('DEBUG:', 'Discover request', JSON.stringify(request));
handleDiscovery(request, context);
} else if (request.directive.header.namespace === 'Alexa.PowerController') {
if (
request.directive.header.name === 'TurnOn' ||
request.directive.header.name === 'TurnOff'
) {
log('DEBUG:', 'TurnOn or TurnOff request', JSON.stringify(request));
handlePowerControl(request, context, callback);
}
}
function log(message1, message2, message3) {
console.log(`${message1} ${message2} ${message3}`);
}
function getFromDb(user_token) {
return new Promise((resolve, reject) => {
const doc_client = new DynamoDB.DocumentClient();
const db_params = {
TableName: 'Access_tokens',
Key: {
user: 'my user name',
},
};
doc_client.get(db_params, (err, data) => {
if (err) {
reject(err);
} else {
log('DEBUG:', 'Get from Db success', JSON.stringify(data));
resolve(data.Item);
}
});
});
}
function updateDb(user_token, atr_obj) {
return new Promise((resolve, reject) => {
const doc_client = new DynamoDB.DocumentClient();
const db_params = {
TableName: 'Access_tokens',
Key: {
user: 'my user name',
},
UpdateExpression:
'set access_token = :a, refresh_token = :r, expires_in = :e, token_time_stamp = :t',
ExpressionAttributeValues: {
':a': atr_obj.access_token,
':r': atr_obj.refresh_token,
':e': Number(atr_obj.expires_in),
':t': Date.now(),
},
ReturnValues: 'UPDATED_NEW',
};
doc_client.update(db_params, (err, data) => {
if (err) {
reject(JSON.stringify(err));
} else {
log('DEBUG:', 'Update Db success', JSON.stringify(data));
resolve(data.Attributes);
}
});
});
}
function makeAuthRequest(code, refresh_token) {
log(
'DEBUG:',
'Making Authentication Request',
`Code: ${code}, Refresh token: ${refresh_token}`
);
return new Promise((resolve, reject) => {
const auth_options = {
hostname: 'api.amazon.com',
path: '/auth/o2/token',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
};
const auth_body = code
? { grant_type: 'authorization_code', code }
: { grant_type: 'refresh_token', refresh_token };
auth_body.client_id = 'xxxxxxxx';
auth_body.client_secret = 'xxxxxxxx';
const auth_req = https.request(auth_options, (res) => {
res.setEncoding('utf8');
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => {
if (res.statusCode == 200) {
log('DEBUG:', 'Authentication request success', body);
resolve(body);
} else {
log('DEBUG:', 'Authentication response failure', res.statuscode);
reject('Auth response error:' + res.statusCode);
}
});
});
auth_req.on('error', (e) => reject('Auth request error: ' + e));
auth_req.write(JSON.stringify(auth_body));
auth_req.end();
});
}
function getAccessToken(user_token) {
log('DEBUG:', 'Getting access token for user:', user_token);
return new Promise((resolve, reject) => {
getFromDb(user_token)
.then((data) => {
const current_timestamp = Date.now();
const time_elapsed =
(current_timestamp - data.token_time_stamp) / 1000;
if (time_elapsed < data.expires_in) {
resolve(data.access_token);
} else {
log('DEBUG:', 'Access token expired', 'Refreshing access token');
makeAuthRequest('', data.refresh_token)
.then((atr) => {
const atr_obj = JSON.parse(atr);
updateDb(user_token, atr_obj)
.then((updated_data) => {
resolve(updated_data.access_token);
})
.catch((e) => reject(e));
})
.catch((e) => reject(e));
}
})
.catch((e) => reject(e));
});
}
function sendWakeUpEvent(req_msg_id, access_token, cor_token, endpoint_id) {
log('DEBUG:', 'Sending WakeUp event', '');
return new Promise((resolve, reject) => {
const wakeup_event = {
event: {
header: {
namespace: 'Alexa.WakeOnLANController',
name: 'WakeUp',
messageId: req_msg_id + '-WAKEUP',
correlationToken: cor_token,
payloadVersion: '3',
},
endpoint: {
scope: {
type: 'BearerToken',
token: access_token,
},
endpointId: endpoint_id,
},
payload: {},
},
context: {
properties: [
{
namespace: 'Alexa.PowerController',
name: 'powerState',
value: 'OFF',
timeOfSample: new Date(),
uncertaintyInMilliseconds: 500,
},
],
},
};
const gateway_options = {
hostname: 'api.eu.amazonalexa.com',
path: '/v3/events',
method: 'POST',
headers: {
Authorization: 'Bearer ' + access_token,
'Content-Type': 'application/json',
},
};
const req = https.request(gateway_options, (res) => {
const status_code = res.statusCode;
if (status_code == 202) {
resolve();
} else {
reject('Gateway response error for wakeup event ' + status_code);
}
});
req.on('error', (e) =>
reject('Gateway request error for wakeup event:' + e)
);
req.write(JSON.stringify(wakeup_event));
req.end();
});
}
function handleDiscovery(request, context) {
const payload = {
endpoints: [
{
endpointId: 'MyDesktop',
manufacturerName: 'Asus',
friendlyName: 'Desktop',
description: 'Desktop',
displayCategories: ['COMPUTER'],
additionalAttributes: {
manufacturer: 'Asus',
model: '158',
serialNumber: '191161258905903',
firmwareVersion: 'f1229c44-87f3-012b-12cb-a85e456c0bfd',
softwareVersion: 'BIOS 5.12',
customIdentifier: 'My_computer',
},
cookie: {},
capabilities: [
{
type: 'AlexaInterface',
interface: 'Alexa',
version: '3',
},
{
type: 'AlexaInterface',
interface: 'Alexa.WakeOnLANController',
version: '3',
properties: {},
configuration: {
MACAddresses: ['a8:5e:45:6c:0b:fd'],
},
},
{
type: 'AlexaInterface',
interface: 'Alexa.PowerController',
version: '3',
properties: {
supported: [
{
name: 'powerState',
},
],
proactivelyReported: false,
retrievable: false,
},
},
],
},
],
};
const req_msg_id = request.directive.header.messageId;
const header = {
namespace: 'Alexa.Discovery',
name: 'Discover.Response',
payloadVersion: '3',
messageId: req_msg_id + '-R',
};
log(
'DEBUG',
'Discovery Response:',
JSON.stringify({ header: header, payload: payload })
);
context.succeed({ event: { header: header, payload: payload } });
}
function handlePowerControl(request, context, callback) {
//context.callbackWaitsForEmptyEventLoop = false;
const req_msg_id = request.directive.header.messageId;
const cor_token = request.directive.header.correlationToken;
const user_token = request.directive.endpoint.scope.token;
const endpoint_id = request.directive.endpoint.endpointId;
getAccessToken(user_token)
.then((access_token) => {
sendWakeUpEvent(req_msg_id, access_token, cor_token, endpoint_id)
.then(() => {
log('DEBUG:', 'WakeUp event accepted', '');
const wakeup_response = {
event: {
header: {
namespace: 'Alexa',
name: 'Response',
messageId: req_msg_id + '-WR',
correlationToken: cor_token,
payloadVersion: '3',
},
endpoint: {
scope: {
type: 'BearerToken',
token: user_token,
},
endpointId: endpoint_id,
},
payload: {},
},
context: {
/*
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": "ON",
"timeOfSample": new Date(),
"uncertaintyInMilliseconds": 500
}
]
*/
properties: [],
},
};
log(
'DEBUG:',
'Sending WakeUp Response',
JSON.stringify(wakeup_response)
);
//callback(null, JSON.stringify(wakeup_response));
callback(null, wakeup_response);
})
.catch((err) => {
log('DEBUG: ', 'Event gateway error', JSON.stringify(err));
const errorResponse = {
event: {
header: {
namespace: 'Alexa',
name: 'ErrorResponse',
messageId: req_msg_id + '-ERR',
payloadVersion: '3',
},
endpoint: {
scope: {
type: 'BearerToken',
token: user_token,
},
endpointId: endpoint_id,
},
payload: {
type: 'INTERNAL_ERROR',
message: 'Internal Error',
},
},
};
callback(new Error(errorResponse));
});
/*
const deferred_response = {
event: {
header: {
"namespace": "Alexa",
"name": "DeferredResponse",
"messageId": req_msg_id + "-DR",
"correlationToken": cor_token,
"payloadVersion": "3"
},
"payload": {
"estimatedDeferralInSeconds": 2
}
}
};
log("DEBUG:", "Sending deferredResponse",JSON.stringify(deferred_response));
//callback(null, JSON.stringify(deferred_response));
callback(null, deferred_response);
*/
})
.catch((err) => {
log('DEBUG:', 'Error getting access token', JSON.stringify(err));
});
}
};