Skip to content

Instantly share code, notes, and snippets.

@tripflex
Last active May 2, 2019 01:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tripflex/3eff9c425f8b0c037c40f5744e46c319 to your computer and use it in GitHub Desktop.
Save tripflex/3eff9c425f8b0c037c40f5744e46c319 to your computer and use it in GitHub Desktop.
Google Core IoT checkDeviceOnline/isDeviceOnline - Check if a device is online using Firebase Function, parsing timestamps or using iot commands
// Example code to call this function
// const checkDeviceOnline = functions.httpsCallable('checkDeviceOnline');
// Include 'current' key for 'current' online status to force update on db with delta
// const isOnline = await checkDeviceOnline({ deviceID: 'XXXX', current: true })
export const checkDeviceOnline = functions.https.onCall(async (data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError('failed-precondition', 'You must be logged in to call this function!');
}
// deviceID is passed in deviceID object key
const deviceID = data.deviceID
const dbUpdate = (isOnline) => {
if (('wasOnline' in data) && data.wasOnline !== isOnline) {
db.collection("devices").doc(deviceID).update({ online: isOnline })
}
return isOnline
}
const deviceLastSeen = () => {
// We only want to use these to determine "latest seen timestamp"
const stamps = ["lastHeartbeatTime", "lastEventTime", "lastStateTime", "lastConfigAckTime", "deviceAckTime"]
return stamps.map(key => moment(data[key], "YYYY-MM-DDTHH:mm:ssZ").unix()).filter(epoch => !isNaN(epoch) && epoch > 0).sort().reverse().shift()
}
await dm.setAuth()
const iotDevice: any = await dm.getDevice(deviceID)
if (!iotDevice) {
throw new functions.https.HttpsError('failed-get-device', 'Failed to get device!');
}
console.log('iotDevice', iotDevice)
// If there is no error status and there is last heartbeat time, assume device is online
if (!iotDevice.lastErrorStatus && iotDevice.lastHeartbeatTime) {
return dbUpdate(true)
}
// Add iotDevice.config.deviceAckTime to root of object
// For some reason in all my tests, I NEVER receive anything on lastConfigAckTime, so this is my workaround
if (iotDevice.config && iotDevice.config.deviceAckTime) iotDevice.deviceAckTime = iotDevice.config.deviceAckTime
// If there is a last error status, let's make sure it's not a stale (old) one
const lastSeenEpoch = deviceLastSeen()
const errorEpoch = iotDevice.lastErrorTime ? moment(iotDevice.lastErrorTime, "YYYY-MM-DDTHH:mm:ssZ").unix() : false
console.log('lastSeen:', lastSeenEpoch, 'errorEpoch:', errorEpoch)
// Device should be online, the error timestamp is older than latest timestamp for heartbeat, state, etc
if (lastSeenEpoch && errorEpoch && (lastSeenEpoch > errorEpoch)) {
return dbUpdate(true)
}
// error status code 4 matches
// lastErrorStatus.code = 4
// lastErrorStatus.message = mqtt: SERVER: The connection was closed because MQTT keep-alive check failed.
// will also be 4 for other mqtt errors like command not sent (qos 1 not acknowledged, etc)
if (iotDevice.lastErrorStatus && iotDevice.lastErrorStatus.code && iotDevice.lastErrorStatus.code === 4) {
return dbUpdate(false)
}
return dbUpdate(false)
})
const functions = require("firebase-functions")
const admin = require("firebase-admin")
const moment = require("moment")
import { DeviceManager } from "./DeviceManager"
// rename config_example.ts to config.ts and set the registry for use in these functions
import config from "./config"
// create a device manager instance with a registry id, optionally pass a region
const dm = new DeviceManager(config.registryId)
admin.initializeApp()
const db = admin.firestore()
// Set date settings for lastest version of fb
db.settings({ timestampsInSnapshots: true })
export {
functions,
admin,
moment,
DeviceManager,
config,
dm,
db
}
const { google } = require('googleapis')
interface Credential { };
interface Device {
id: string;
name?: string;
readonly numId?: string;
credentials: Credential[];
"config": any[];
"lastHeartbeatTime": string;
"lastEventTime": string;
"lastStateTime": string;
"lastConfigAckTime": string;
"lastConfigSendTime": string;
// "blocked": boolean,
"lastErrorTime": string;
"lastErrorStatus": {
object(Status)
}
// {
// object(DeviceConfig)
// },
// "state": {
// object(DeviceState)
// },
// "metadata": {
// string: string,
// ...
// },
}
async function getADC() {
const res = await google.auth.getApplicationDefault();
let auth = res.credential;
if (auth.createScopedRequired && auth.createScopedRequired()) {
const scopes = ['https://www.googleapis.com/auth/cloud-platform'];
auth = auth.createScoped(scopes);
}
const projectId = res.projectId as string;
console.log('GetADC ProjectID: ' + projectId);
return {
auth,
projectId
};
}
export class DeviceManager {
client: any;
ready: Boolean = false;
private parentName: string = '';
private registryName: string = '';
private project: string = '';
async setAuth() {
// console.log('set auth');
if (!this.ready) {
// console.log('client not set');
const adc = await getADC();
// console.log('setting client');
this.client = google.cloudiot({
version: 'v1',
auth: adc.auth
});
this.project = adc.projectId;
this.parentName = `projects/${this.project}/locations/${this.region}`;
this.registryName = `${this.parentName}/registries/${this.registryId}`
this.ready = true;
} else {
console.log('client already set');
}
}
constructor(private registryId: string, private region: string = 'us-central1') { }
createDevice(device: any) {
return new Promise((resolve, reject) => {
const request = {
parent: this.registryName,
resource: device
}
this.client.projects.locations.registries.devices.create(request, (err: any, data: any) => {
if (err) {
console.error(err);
reject(err);
} else {
// console.log('device created');
resolve(data);
}
});
});
}
updateDevice(deviceId: string, device: any, updateMask?: any) {
return new Promise((resolve, reject) => {
const request = {
name: `${this.registryName}/devices/${deviceId}`,
resource: device,
}
if (updateMask) {
// tslint:disable-next-line: no-any
(request as any)['updateMask'] = updateMask;
}
this.client.projects.locations.registries.devices.patch(request, (err: any, resp: any) => {
if (err) {
console.error(err);
reject(err);
} else {
resolve(resp.data);
};
});
});
}
deleteDevice(deviceId: string) {
return new Promise((resolve, reject) => {
console.log("delete");
this.client.projects.locations.registries.devices.delete({ name: `${this.registryName}/devices/${deviceId}` }, (err: any, resp: any) => {
if (err) {
console.error(err);
reject(err);
} else {
resolve(resp.data);
};
});
});
}
// sendConfig(deviceId:string, config) {}
getState(deviceId:string) {
return new Promise((resolve, reject) => {
this.client.projects.locations.registries.devices.states.list({ name: `${this.registryName}/devices/${deviceId}`, numStates: 1 }, (err: any, resp: any) => {
if (err) {
console.error(err);
reject(err);
} else {
resolve(resp.data);
};
});
});
}
getDevice(deviceId: string, fields?: string ) {
return new Promise((resolve, reject) => {
this.client.projects.locations.registries.devices.get({ name: `${this.registryName}/devices/${deviceId}`, fields: fields }, (err: any, resp: any) => {
if (err) {
console.error(err);
reject(err);
} else {
resolve(resp.data);
};
});
});
}
sendCommand(deviceId: string, commandMessage?: any, subfolder: string = 'firebase' ) {
return new Promise((resolve, reject) => {
const request = {
name: `${this.registryName}/devices/${deviceId}`,
binaryData: Buffer.from(JSON.stringify(commandMessage)).toString("base64"),
subfolder: subfolder
}
this.client.projects.locations.registries.devices.sendCommandToDevice( request, (err: any, resp: any) => {
if (err) {
console.log('Could not send command:', request);
console.log('Error: ', err);
reject(err);
} else {
resolve(resp);
};
});
});
}
listDevices(pageToken?: string) {
return new Promise((resolve, reject) => {
const request: any = {
parent: this.registryName,
//resource: body
pageSize: 50,
};
if (pageToken) {
request['pageToken'] = pageToken;
}
// console.log(request);
// console.log(this.client);
this.client.projects.locations.registries.devices.list(request, (err: any, resp: any) => {
if (err) {
console.error(err);
reject(err);
} else {
resolve(resp.data);
};
});
});
}
updateConfig(deviceId: string, config: any) {
return new Promise((resolve, reject) => {
const request = {
name: `${this.registryName}/devices/${deviceId}`,
binaryData: Buffer.from(JSON.stringify(config)).toString("base64"),
}
this.client.projects.locations.registries.devices.modifyCloudToDeviceConfig(request, (err: any, resp: any) => {
if (err) {
console.error(err);
reject(err);
} else {
resolve(resp.data);
};
});
});
}
updateConfigBinary(deviceId: string, config: Buffer) {
return new Promise((resolve, reject) => {
const request = {
name: `${this.registryName}/devices/${deviceId}`,
binaryData: config.toString("base64"),
}
this.client.projects.locations.registries.devices.modifyCloudToDeviceConfig(request, (err: any, resp: any) => {
if (err) {
console.error(err);
reject(err);
} else {
resolve(resp.data);
};
});
});
}
}
export const isDeviceOnline = functions.https.onCall(async (data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError('failed-precondition', 'You must be logged in to call this function!');
}
// deviceID is passed in deviceID object key
const deviceID = data.deviceID
await dm.setAuth()
const dbUpdate = (isOnline) => {
if (('wasOnline' in data) && data.wasOnline !== isOnline) {
console.log( 'updating db', deviceID, isOnline )
db.collection("devices").doc(deviceID).update({ online: isOnline })
} else {
console.log('NOT updating db', deviceID, isOnline)
}
return isOnline
}
try {
await dm.sendCommand(deviceID, 'alive?', 'alive')
console.log('Assuming device is online after succesful alive? command')
return dbUpdate(true)
} catch (error) {
console.log("Unable to send alive? command", error)
return dbUpdate(false)
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment