hw-server
Version:
Home automation system
1,416 lines (1,356 loc) • 45 kB
JavaScript
/*global module,require,global,setInterval,clearInterval,setTimeout*/
;
const WebSocket = require("ws");
const WebSocketServer = WebSocket.Server;
const dateFormat = require("dateformat");
const Rule = require("./rule.js");
const Variable = require("./variable.js");
const Timer = require("./timer.js");
const WebServer = require("./webserver.js");
const Scene = require("./scene.js").Scene;
var wss = undefined;
var homework = undefined;
var connections = [];
var Console;
var sendCloud;
function findDevice(uuid)
{
var devs = homework.devices;
if (!(devs instanceof Array))
return undefined;
return devs.find((d) => { return d.uuid == uuid; });
}
function isempty(v)
{
if (v === undefined || v === null)
return true;
if (typeof v === "string" && !v.length)
return true;
return false;
}
function send(ws, msg, result)
{
ws.send(JSON.stringify({ id: msg.id, wsid: msg.wsid, result: result }));
}
function error(ws, msg, err)
{
ws.send(JSON.stringify({ id: msg.id, wsid: msg.wsid, error: err }));
}
function sendToAll(result)
{
const data = JSON.stringify(result);
for (var i = 0; i < connections.length; ++i) {
connections[i].send(data);
}
}
function construct(constructor, args) {
function F() {
return constructor.apply(this, args);
}
F.prototype = constructor.prototype;
return new F();
}
var requestState = { id: 0, pending: Object.create(null) };
function requestCloud(req)
{
var p = new Promise(function(resolve, reject) {
var id = ++requestState.id;
req.cid = id;
// this.log("sending req", JSON.stringify(req));
requestState.pending[id] = { resolve: resolve, reject: reject };
try {
sendCloud(req);
} catch (e) {
console.log(e);
}
});
return p;
}
function resolveCloud(resp)
{
if (resp.cid in requestState.pending) {
let pending = requestState.pending[resp.cid];
delete requestState.pending[resp.cid];
if (resp.error) {
pending.reject(resp.msg);
} else {
pending.resolve(resp.msg);
}
}
}
function rejectCloud()
{
for (var k in requestState.pending) {
requestState.pending[k].reject("Cloud gone");
}
requestState.pending = Object.create(null);
requestState.id = 0;
}
function saveFilesInCloud(files)
{
homework.save((fn, data) => {
if (!files || files.indexOf(fn) !== -1) {
requestCloud({ type: "saveFile", fileName: fn, data: data }).then((msg) => {
Console.log(msg);
}).catch((msg) => {
Console.error("cloud error:", msg);
});
} else {
Console.log("file skipped", fn);
}
});
}
const types = {
call: (ws, msg) => {
// find the thing
if (typeof msg.call !== "string") {
Console.log("no call:", msg);
error(ws, msg, "message has no call");
return;
}
var split = msg.call.split(".");
var callee = undefined;
var thing = homework;
for (var i = 0; i < split.length; ++i) {
if (!(split[i] in thing)) {
Console.log(`no ${split[i]} in ${thing}`);
error(ws, msg, "message has invalid call");
return;
}
callee = thing;
thing = thing[split[i]];
}
var ret;
if (typeof thing === "function") {
if (typeof callee !== "object")
callee = undefined;
if (msg.arguments instanceof Array) {
ret = thing.apply(callee ? callee : null, msg.arguments);
} else {
ret = thing.call(callee ? callee : null);
}
} else {
ret = thing;
}
var retmsg;
try {
retmsg = JSON.stringify({ id: msg.id, wsid: msg.wsid, result: ret });
} catch (e) {
Console.log("ws data exception", e);
retmsg = JSON.stringify({ id: msg.id, wsid: msg.wsid, error: "message data exception" });
}
ws.send(retmsg);
},
devices: (ws, msg) => {
Console.log("asked for devices");
const devs = homework.devices;
if (devs instanceof Array) {
const ret = devs.map((e) => { return { type: e.type, name: e.name, room: e.room, floor: e.floor, uuid: e.uuid, groups: e.groups }; });
send(ws, msg, ret);
} else {
error(ws, msg, "no devices");
}
},
device: (ws, msg) => {
var uuid = msg.uuid;
var devs = homework.devices;
if (devs instanceof Array) {
for (var i = 0; i < devs.length; ++i) {
if (devs[i].uuid == uuid) {
var d = devs[i];
send(ws, msg, { type: d.type, name: d.name, room: d.room, floor: d.floor, uuid: d.uuid, groups: d.groups });
return;
}
}
}
error(ws, msg, "no such device");
},
addGroup: (ws, msg) => {
var group = msg.group;
if (typeof group !== "string" || !group.length) {
error(ws, msg, "no group name");
} else {
var dev = findDevice(msg.uuid);
if (dev instanceof Object) {
dev.addGroup(group);
send(ws, msg, "ok");
} else {
error(ws, msg, `no such device ${msg.uuid}`);
}
}
},
removeGroup: (ws, msg) => {
var group = msg.group;
if (typeof group !== "string" || !group.length) {
error(ws, msg, "no group name");
} else {
var dev = findDevice(msg.uuid);
if (dev instanceof Object) {
dev.removeGroup(group);
send(ws, msg, "ok");
} else {
error(ws, msg, `no such device ${msg.uuid}`);
}
}
},
setGroup: (ws, msg) => {
var group = msg.group;
if (typeof group !== "string" || !group.length) {
error(ws, msg, "no group name");
} else {
var dev = findDevice(msg.uuid);
if (dev instanceof Object) {
dev.groups = [group];
send(ws, msg, "ok");
saveFilesInCloud(["devices.json"]);
homework.saveDevices();
} else {
error(ws, msg, `no such device ${msg.uuid}`);
}
}
},
addDevice: (ws, msg) => {
if (!("name" in msg) || typeof msg.name !== "string" || !msg.name.length) {
error(ws, msg, "no device name");
return;
}
let uuid = "virtual:" + msg.name.replace(/[\W]+/g, "_");
if (findDevice(uuid)) {
error(ws, msg, "device already exists");
return;
}
var dev = new homework.Device("Virtual", { uuid: uuid });
dev.name = msg.name;
homework.addDevice(dev);
send(ws, msg, "ok");
saveFilesInCloud(["devices.json"]);
homework.saveDevices();
},
removeDevice: (ws, msg) => {
if (!("uuid" in msg) || typeof msg.uuid !== "string" || !msg.uuid.length) {
error(ws, msg, "no device uuid");
return;
}
var dev = findDevice(msg.uuid);
if (!dev) {
error(ws, msg, "device not found");
return;
}
if (!dev.virtual) {
error(ws, msg, "can't remove non-virtual devices");
return;
}
homework.removeDevice(dev);
send(ws, msg, "ok");
saveFilesInCloud(["devices.json"]);
homework.saveDevices();
},
addDeviceValue: (ws, msg) => {
if (!("name" in msg) || typeof msg.name !== "string" || !msg.name.length) {
error(ws, msg, "no device name");
return;
}
if ("template" in msg && typeof msg.template !== "string") {
error(ws, msg, "no device template");
return;
}
if (!("uuid" in msg) || typeof msg.uuid !== "string" || !msg.uuid.length) {
error(ws, msg, "no device uuid");
return;
}
var dev = findDevice(msg.uuid);
if (!dev) {
error(ws, msg, "device not found");
return;
}
if (!dev.virtual) {
error(ws, msg, "can't add value for non-virtual devices");
return;
}
if (msg.name in dev.values) {
error(ws, msg, "device already has a value by that name");
return;
}
var data = {};
var range = /^\s*([0-9]+)\s*-\s*([0-9]+)\s*$/.exec(msg.template);
if (range) {
var low = parseInt(range[1]);
var high = parseInt(range[2]);
if (low > high)
data.range = [high, low];
else
data.range = [low, high];
data.valueType = "range";
} else if (!msg.template || !msg.template.length) {
data.valueType = "string";
} else {
var a = msg.template.split(";");
data.values = Object.create(null);
for (var i = 0; i < a.length; ++i) {
data.values[a[i]] = a[i];
}
data.valueType = "array";
}
dev.addValue(new homework.Device.Value(msg.name, data));
send(ws, msg, "ok");
saveFilesInCloud(["devices.json"]);
homework.saveDevices();
},
removeDeviceValue: (ws, msg) => {
if (!("name" in msg) || typeof msg.name !== "string" || !msg.name.length) {
error(ws, msg, "no device name");
return;
}
if (!("uuid" in msg) || typeof msg.uuid !== "string" || !msg.uuid.length) {
error(ws, msg, "no device uuid");
return;
}
var dev = findDevice(msg.uuid);
if (!dev) {
error(ws, msg, "device not found");
return;
}
if (!dev.virtual) {
error(ws, msg, "can't remove value for non-virtual devices");
return;
}
if (msg.name in dev.values) {
dev.removeValue(msg.name);
send(ws, msg, "ok");
} else {
error(ws, msg, "no such value");
}
},
rules: (ws, msg) => {
const rs = homework.rules;
if (rs instanceof Array) {
var complete = (from, array) => {
// first element of the array is the key in 'from'
var f = from[array[0]];
if (f === undefined) {
throw `No event/action named '${array[0]}' registered`;
}
// Console.log(`--${array[0]}--`);
// call consecutive complete with the rest of the array
var ret = { name: array[0], steps: [] };
var len = array.length - 1;
for (var i = 0; i < len; ++i) {
var rest = array.slice(1);
rest.splice(i);
var comps = f.completion.apply(null, rest);
// Console.log("comps for " + JSON.stringify(rest) + ": " + JSON.stringify(comps) + "(val: " + array[i + 1] + ")");
ret.steps.push({ alternatives: comps, value: array[i + 1] });
}
return ret;
};
try {
const ret = rs.map((r) => {
var fmt = r.format();
var acts = fmt.actions;
// actions is an array of arrays
for (var a = 0; a < acts.length; ++a) {
acts[a] = complete(homework.actions, acts[a]);
}
var evts = fmt.events;
// events is an arary of arrays of arrays
for (var es = 0; es < evts.length; ++es) {
for (var e = 0; e < evts[es].length; ++e) {
evts[es][e] = complete(homework.events, evts[es][e]);
}
}
return fmt;
});
send(ws, msg, ret);
} catch (e) {
error(ws, msg, `Exception mapping rule: ${e}`);
}
} else {
error(ws, msg, "no devices");
}
},
ruleTypes: (ws, msg) => {
const ret = {
events: Object.keys(homework.events),
actions: Object.keys(homework.actions)
};
send(ws, msg, ret);
},
eventCompletions: (ws, msg) => {
const args = msg.args;
if (!(args instanceof Array)) {
error(ws, msg, "args needs to be an array");
} else {
const evt = homework.events[args[0]];
if (!(typeof evt === "object") || !("completion" in evt)) {
error(ws, msg, "no event");
} else {
// Console.log("asking for completions", args.slice(1));
const ret = evt.completion.apply(null, args.slice(1));
// Console.log("got", ret);
send(ws, msg, ret);
}
}
},
actionCompletions: (ws, msg) => {
const args = msg.args;
if (!(args instanceof Array)) {
error(ws, msg, "args needs to be an array");
} else {
const act = homework.actions[args[0]];
if (!(typeof act === "object") || !("completion" in act)) {
error(ws, msg, "no action");
} else {
// Console.log("asking for completions", args.slice(1));
const ret = act.completion.apply(null, args.slice(1));
// Console.log("got", ret);
send(ws, msg, ret);
}
}
},
createRule: (ws, msg) => {
if (typeof msg.rule !== "object") {
error(ws, msg, "No rule");
return;
}
const desc = msg.rule;
if (typeof desc.name !== "string" || !desc.name.length) {
error(ws, msg, "No name");
return;
}
const events = desc.events;
const actions = desc.actions;
if (!(events instanceof Array)) {
error(ws, msg, "Events is not an array");
return;
}
if (!(actions instanceof Array)) {
error(ws, msg, "Actions is not an array");
return;
}
var rule = new Rule(desc.name);
var andCount = 0;
for (var es = 0; es < events.length; ++es) {
if (!(events[es] instanceof Array)) {
error(ws, msg, `Event is not an array: ${es}`);
return;
}
var ands = [];
for (var e = 0; e < events[es].length; ++e) {
var tr = events[es][e].trigger;
var event = events[es][e].event;
if (!(event instanceof Array) || !event.length) {
error(ws, msg, `Sub event is not an array: ${es}:${e}`);
return;
}
const ector = homework.events[event[0]];
if (typeof ector !== "object" || !("ctor" in ector)) {
error(ws, msg, `Sub event doesn't have a constructor ${event[0]}`);
return;
}
try {
ands.push({ trigger: tr, and: construct(ector.ctor, event.slice(1)) });
++andCount;
} catch (e) {
error(ws, msg, `Error in event ctor ${event[0]}: ${e}`);
return;
}
}
rule.and.apply(rule, ands);
}
if (!andCount) {
error(ws, msg, `No events for rule`);
return;
}
var thens = [];
for (var as = 0; as < actions.length; ++as) {
var action = actions[as];
if (!(action instanceof Array) || !action.length) {
error(ws, msg, `Action is not an array: ${as}`);
return;
}
const actor = homework.actions[action[0]];
if (typeof actor !== "object" || !("ctor" in actor)) {
error(ws, msg, `Sub action doesn't have a constructor ${action[0]}`);
return;
}
try {
thens.push(construct(actor.ctor, action.slice(1)));
} catch (e) {
error(ws, msg, `Error in action ctor ${action[0]}: ${e}`);
return;
}
}
if (!thens.length) {
error(ws, msg, `No actions for rule`);
return;
}
rule.then.apply(rule, thens);
homework.removeRuleByName(rule.name);
homework.addRule(rule);
send(ws, msg, { name: desc.name, success: true });
homework.saveRules();
saveFilesInCloud(["rules.json"]);
},
removeRule: (ws, msg) => {
if (!("name" in msg)) {
error(ws, msg, "no name in message");
return;
}
if (homework.removeRuleByName(msg.name)) {
send(ws, msg, "ok");
} else {
error(ws, msg, "rule not found");
}
},
devicedata: (ws, msg) => {
// return a list of all rooms and floors
var rooms = Object.create(null);
var floors = Object.create(null);
const devs = homework.devices;
for (var i = 0; i < devs.length; ++i) {
var dev = devs[i];
if (dev.room)
rooms[dev.room] = true;
if (dev.floor)
floors[dev.floor] = true;
}
var ret = {
rooms: Object.keys(rooms),
floors: Object.keys(floors),
types: homework.DeviceTypes
};
send(ws, msg, ret);
},
values: (ws, msg) => {
const devs = homework.devices;
if (!("devuuid" in msg)) {
error(ws, msg, "no devuuid in message");
return;
}
var dev;
for (var i = 0; i < devs.length; ++i) {
if (devs[i].uuid == msg.devuuid) {
dev = devs[i];
break;
}
}
if (!dev) {
error(ws, msg, "unknown device");
return;
}
var ret = [];
for (var v in dev.values) {
let val = dev.values[v];
ret.push({ name: val.name, value: val.value, raw: val.raw,
values: val.values, range: val.range, units: val.units,
readOnly: val.readOnly, type: val.type });
}
send(ws, msg, ret);
},
getValue: (ws, msg) => {
const devs = homework.devices;
if (!("devuuid" in msg)) {
error(ws, msg, "no devuuid in message");
return;
}
var dev;
for (var i = 0; i < devs.length; ++i) {
if (devs[i].uuid == msg.devuuid) {
dev = devs[i];
break;
}
}
if (!dev) {
error(ws, msg, "unknown device");
return;
}
if (!("valname" in msg)) {
error(ws, msg, "no valname in message");
return;
}
if (!(msg.valname in dev.values)) {
error(ws, msg, "valname not found");
return;
}
const val = dev.values[msg.valname];
const ret = { name: val.name, value: val.value, raw: val.raw,
values: val.values, range: val.range, units: val.units,
readOnly: val.readOnly, type: val.type };
send(ws, msg, ret);
},
setName: (ws, msg) => {
const devs = homework.devices;
if (!("devuuid" in msg)) {
error(ws, msg, "no devuuid in message");
return;
}
var dev;
for (var i = 0; i < devs.length; ++i) {
if (devs[i].uuid == msg.devuuid) {
dev = devs[i];
break;
}
}
if (!dev) {
error(ws, msg, "unknown device");
return;
}
if (typeof msg.name !== "string" || !msg.name.length) {
error(ws, msg, `invalid name ${msg.name}`);
return;
}
dev.name = msg.name;
send(ws, msg, "ok");
saveFilesInCloud(["devices.json"]);
homework.saveDevices();
},
setRoom: (ws, msg) => {
const devs = homework.devices;
if (!("devuuid" in msg)) {
error(ws, msg, "no devuuid in message");
return;
}
var dev;
for (var i = 0; i < devs.length; ++i) {
if (devs[i].uuid == msg.devuuid) {
dev = devs[i];
break;
}
}
if (!dev) {
error(ws, msg, "unknown device");
return;
}
if (typeof msg.room !== "string") {
error(ws, msg, `invalid room ${msg.room}`);
return;
}
if (msg.room.length > 0)
dev.room = msg.room;
else
dev.room = undefined;
send(ws, msg, "ok");
saveFilesInCloud(["devices.json"]);
homework.saveDevices();
},
setFloor: (ws, msg) => {
const devs = homework.devices;
if (!("devuuid" in msg)) {
error(ws, msg, "no devuuid in message");
return;
}
var dev;
for (var i = 0; i < devs.length; ++i) {
if (devs[i].uuid == msg.devuuid) {
dev = devs[i];
break;
}
}
if (!dev) {
error(ws, msg, "unknown device");
return;
}
if (typeof msg.floor !== "string") {
error(ws, msg, `invalid floor ${msg.floor}`);
return;
}
if (msg.floor.length > 0)
dev.floor = msg.floor;
else
dev.floor = undefined;
send(ws, msg, "ok");
saveFilesInCloud(["devices.json"]);
homework.saveDevices();
},
setType: (ws, msg) => {
const devs = homework.devices;
if (!("devuuid" in msg)) {
error(ws, msg, "no devuuid in message");
return;
}
var dev;
for (var i = 0; i < devs.length; ++i) {
if (devs[i].uuid == msg.devuuid) {
dev = devs[i];
break;
}
}
if (!dev) {
error(ws, msg, "unknown device");
return;
}
if (typeof msg.devtype !== "string") {
error(ws, msg, `invalid type ${msg.devtype}`);
return;
}
if (dev.virtual) {
error(ws, msg, "can't change a virtual device to be non-virtual");
return;
} else if (msg.devtype == "Virtual") {
error(ws, msg, "can't change a non-virtual device to be virtual");
return;
}
dev.type = msg.devtype;
send(ws, msg, "ok");
saveFilesInCloud(["devices.json"]);
homework.saveDevices();
},
setValue: (ws, msg) => {
const devs = homework.devices;
if (!("devuuid" in msg)) {
error(ws, msg, "no devuuid in message");
return;
}
var dev;
for (var i = 0; i < devs.length; ++i) {
if (devs[i].uuid == msg.devuuid) {
dev = devs[i];
break;
}
}
if (!dev) {
error(ws, msg, "unknown device");
return;
}
if (!("valname" in msg)) {
error(ws, msg, "no valname in message");
return;
}
if (!(msg.valname in dev.values)) {
error(ws, msg, "valname not found");
return;
}
if (!("value" in msg)) {
error(ws, msg, "no value in message");
return;
}
const val = dev.values[msg.valname];
if (val.readOnly) {
error(ws, msg, "read only value");
return;
}
val.value = msg.value;
send(ws, msg, "ok");
},
addValue: (ws, msg) => {
const devs = homework.devices;
if (!("devuuid" in msg)) {
error(ws, msg, "no devuuid in message");
return;
}
var dev;
for (var i = 0; i < devs.length; ++i) {
if (devs[i].uuid == msg.devuuid) {
dev = devs[i];
break;
}
}
if (!dev) {
error(ws, msg, "unknown device");
return;
}
if (!("valname" in msg)) {
error(ws, msg, "no valname in message");
return;
}
if (!(msg.valname in dev.values)) {
error(ws, msg, "valname not found");
return;
}
if (!("delta" in msg)) {
error(ws, msg, "no delta in message");
return;
}
const val = dev.values[msg.valname];
if (val.readOnly) {
error(ws, msg, "read only value");
return;
}
val.value = val.raw + msg.delta;
send(ws, msg, "ok");
},
variables: (ws, msg) => {
// JSON.stringify screws me over
var ret = Object.create(null);
for (var k in Variable.variables) {
ret[k] = Variable.variables[k];
if (ret[k] === undefined)
ret[k] = null;
}
send(ws, msg, { variables: ret });
},
setVariables: (ws, msg) => {
var name;
for (name in msg.variables) {
if (!(name in Variable.variables)) {
error(ws, msg, `variable ${name} doesn't exist`);
return;
}
}
for (name in msg.variables) {
Variable.change(name, msg.variables[name]);
}
send(ws, msg, { success: true });
},
createVariable: (ws, msg) => {
if (typeof msg.name !== "string" || !msg.name.length) {
error(ws, msg, `no name for variable`);
return;
}
if (!Variable.create(msg.name)) {
error(ws, msg, `variable ${msg.name} already exists`);
return;
}
send(ws, msg, { success: true });
const obj = { type: "variableUpdated", name: msg.name, value: null };
sendToAll(obj);
sendCloud(obj);
},
timers: (ws, msg) => {
const sub = msg.sub;
var ret = Object.create(null);
const serializeTimers = (timers, type, ret) => {
var out = Object.create(null);
for (var n in timers) {
var t = timers[n];
out[n] = {
state: t.state,
started: t.started,
timeout: t.timeout
};
}
ret[type] = out;
};
if (sub == "timeout" || sub == "all") {
serializeTimers(Timer.timeout, "timeout", ret);
}
if (sub == "interval" || sub == "all") {
serializeTimers(Timer.interval, "interval", ret);
}
if (sub == "schedule" || sub == "all") {
var out = Object.create(null);
for (var k in Timer.schedule) {
out[k] = Timer.schedule[k].date;
}
ret.schedule = out;
}
ret["now"] = Date.now();
send(ws, msg, { timers: ret });
},
stopTimer: (ws, msg) => {
var err = "";
if (msg.sub in Timer) {
if (msg.name in Timer[msg.sub]) {
var t = Timer[msg.sub][msg.name];
if ("stop" in t) {
t.stop();
} else {
err = "No stop";
}
} else {
err = "Timer out found";
}
} else {
err = "Timer group not found";
}
if (err != "") {
error(ws, msg, `${err} for ${msg.name} in group ${msg.sub}`);
} else {
send(ws, msg, { success: true });
}
},
restartTimer: (ws, msg) => {
var err = "";
if (msg.sub in Timer) {
if (msg.name in Timer[msg.sub]) {
var t = Timer[msg.sub][msg.name];
if ("start" in t) {
t.start(msg.value);
} else {
err = "No start";
}
} else {
err = "Timer out found";
}
} else {
err = "Timer group not found";
}
if (err != "") {
error(ws, msg, `${err} for ${msg.name} in group ${msg.sub}`);
} else {
send(ws, msg, { success: true });
}
},
createTimer: (ws, msg) => {
if (isempty(msg.name)) {
error(ws, msg, `No name for timer`);
return;
}
if (isempty(msg.sub)) {
error(ws, msg, `No type for timer ${msg.name}`);
return;
}
if (msg.sub === "schedule" && isempty(msg.value)) {
error(ws, msg, `No value for timer ${msg.name}`);
return;
}
try {
Timer.create(msg.sub, msg.name, msg.value);
} catch (e) {
error(ws, msg, `Couldn't create timer: ${e}`);
return;
}
send(ws, msg, { success: true });
},
setSchedules: (ws, msg) => {
var name;
for (name in msg.schedules) {
if (!(name in Timer.schedule)) {
error(ws, msg, `schedule ${name} doesn't exist`);
return;
}
}
var err = "";
for (name in msg.schedules) {
try {
Timer.schedule[name].date = msg.schedules[name];
} catch (e) {
err += JSON.stringify(e) + "\n";
}
}
if (err != "") {
error(ws, msg, err);
} else {
send(ws, msg, { success: true });
}
},
web: (ws, msg) => {
if ("path" in msg) {
var path = msg.path;
var q = path.indexOf('?');
if (q !== -1)
path = path.substr(0, q);
WebServer.get(path, (statusCode, headers, body, binary) => {
send(ws, msg, { statusCode: statusCode, headers: headers, body: body, binary: binary });
});
} else {
error(ws, msg, "no path in msg");
}
},
scenes: (ws, msg) => {
var hwscenes = homework.scenes;
var scenes = [];
for (var i = 0; i < hwscenes.length; ++i) {
var scene = hwscenes[i];
scenes.push({ name: scene.name, values: scene.values });
}
send(ws, msg, scenes);
},
removeScene: (ws, msg) => {
if (!("name" in msg)) {
error(ws, msg, "no name in msg");
return;
}
if (homework.removeScene(msg.name)) {
send(ws, msg, "ok");
saveFilesInCloud(["scenes.json"]);
homework.saveScenes();
} else {
error(ws, msg, `no such scene: ${msg.name}`);
}
},
setScene: (ws, msg) => {
if (!("name" in msg)) {
error(ws, msg, "no name in msg");
return;
}
if (!("values" in msg)) {
error(ws, msg, "no values in msg");
return;
}
if (typeof msg.values !== "object") {
error(ws, msg, "values is not an object");
return;
}
homework.removeScene(msg.name);
homework.addScene(new Scene(msg.name, msg.values));
send(ws, msg, "ok");
saveFilesInCloud(["scenes.json"]);
homework.saveScenes();
},
triggerScene: (ws, msg) => {
if (!("name" in msg)) {
error(ws, msg, "no name in msg");
return;
}
let scenes = homework.scenes;
for (var i = 0; i < scenes.length; ++i) {
if (scenes[i].name == msg.name) {
scenes[i].trigger();
send(ws, msg, "ok");
return;
}
}
error(ws, msg, `no such scene: ${msg.name}`);
},
restore: (ws, msg) => {
if (!("file" in msg)) {
error(ws, msg, "no file name in msg");
return;
}
let fn = msg.file;
if (!("sha256" in msg)) {
requestCloud({ type: "loadFile", fileName: `${fn}.json` }).then((data) => {
for (var idx in data) {
data[idx].date = dateFormat(data[idx].date, "dddd, mmmm dS, yyyy, h:MM:ss TT");
}
send(ws, msg, data);
}).catch((err) => {
error(ws, msg, err);
});
} else {
requestCloud({ type: "loadFile", fileName: `${fn}.json`, sha256: msg.sha256 }).then((data) => {
switch (fn) {
case "rules":
console.log("restoring rules", data);
homework.restoreRules(data);
homework.saveRules();
break;
case "scenes":
console.log("restoring scenes", data);
homework.restoreScenes(data);
homework.saveScenes();
break;
case "devices":
//homework.removeVirtualDevices();
console.log("restoring devices", data);
for (let uuid in data) {
// find this device
let dev = findDevice(uuid);
let desc = data[uuid];
if (dev) {
if ("name" in desc)
dev.name = desc.name;
if ("room" in desc)
dev.room = desc.room;
if ("floor" in desc)
dev.floor = desc.floor;
if ("groups" in desc)
dev.groups = desc.groups;
if ("type" in desc)
dev.type = desc.type;
} else {
// virtual? if so, create
}
}
homework.saveDevices();
break;
}
send(ws, msg, "ok");
}).catch((err) => {
error(ws, msg, err);
});
}
},
standardGet: (ws, msg) => {
if (!("uuid" in msg)) {
error(ws, msg, "no device uuid");
return;
}
var dev = findDevice(msg.uuid);
if (!dev) {
error(ws, msg, "device not found");
return;
}
if (!("name" in msg)) {
error(ws, msg, "no name in msg");
return;
}
try {
var ret = dev.standardGet(msg.name);
send(ws, msg, ret);
} catch (e) {
error(ws, msg, e.message);
}
},
standardMeta: (ws, msg) => {
if (!("uuid" in msg)) {
error(ws, msg, "no device uuid");
return;
}
var dev = findDevice(msg.uuid);
if (!dev) {
error(ws, msg, "device not found");
return;
}
if (!("name" in msg)) {
error(ws, msg, "no name in msg");
return;
}
try {
var ret = dev.standardMeta(msg.name);
send(ws, msg, ret);
} catch (e) {
error(ws, msg, e.message);
}
},
standardSet: (ws, msg) => {
if (!("uuid" in msg)) {
error(ws, msg, "no device uuid");
return;
}
var dev = findDevice(msg.uuid);
if (!dev) {
error(ws, msg, "device not found");
return;
}
if (!("name" in msg)) {
error(ws, msg, "no name in msg");
return;
}
if (!("value" in msg)) {
error(ws, msg, "no value in msg");
return;
}
try {
dev.standardSet(msg.name, msg.value);
send(ws, msg, "ok");
} catch (e) {
error(ws, msg, e.message);
}
},
logs: (ws, msg) => {
if (!("sub" in msg)) {
error(ws, msg, "no sub");
return;
}
var logs = Console.logs(msg.sub);
if (logs == undefined) {
error(ws, msg, `no logs for ${msg.sub}`);
return;
}
send(ws, msg, logs);
},
ifttt: (ws, msg) => {
if (msg.ifttt.method && msg.ifttt.method in types) {
types[msg.ifttt.method](ws, msg.ifttt.data);
} else {
error(ws, msg, `Can't find action for method ${msg.ifttt.method}`);
}
}
};
function cloud(ws, msg)
{
Console.log("cloud", msg);
}
var extraTypes = Object.create(null);
const HWWebSocket = {
_ready: undefined,
_cloud: undefined,
_cloudOk: false,
_cloudPending: [],
_cloudInterval: undefined,
_cloudSentReady: false,
sendCloud: function(d) {
if (this._cloud && this._cloudOk) {
while (this._cloudPending.length > 0) {
try {
this._cloud.send(JSON.stringify(this._cloudPending[0]));
this._cloudPending.splice(0, 1);
} catch (e) {
if (d)
this._cloudPending.push(d);
return;
}
}
if (d) {
var data = JSON.stringify(d);
try {
this._cloud.send(data);
} catch (e) {
this._cloudPending.push(d);
}
}
} else if (this._cloud) {
this._cloudPending.push(d);
} else {
Console.log("no cloud");
}
},
_setupWS: function(ws) {
ws.on("message", (data, flags) => {
// we only support rpc for now
var msg;
try {
msg = JSON.parse(data);
} catch (e) {
Console.log("invalid ws message:", data);
ws.send(JSON.stringify({ error: "invalid message" }));
return;
}
if (typeof msg !== "object") {
Console.log("ws message is not an object:", msg);
ws.send(JSON.stringify({ error: "message is not an object" }));
return;
}
if (!("id" in msg)) {
if ("type" in msg) {
if (msg.type == "cloud") {
this._cloudOk = true;
this.sendCloud();
return;
} else if (msg.type == "error") {
Console.error("cloud error:", msg.msg);
return;
}
}
if ("cid" in msg) {
resolveCloud(msg);
return;
}
Console.log("no id in message", msg);
ws.send(JSON.stringify({ error: "message has no id" }));
return;
}
if ("type" in msg && msg.type in types) {
types[msg.type](ws, msg);
} else if ("type" in msg && msg.type in extraTypes) {
extraTypes[msg.type](ws, msg);
} else {
Console.log("unhandled ws message:", msg);
}
});
},
_cloudConnect: function(cfg, state)
{
const again = () => {
const timeout = state.next;
state.next *= 10;
if (state.next > (10 * 60))
state.next = 10 * 60;
setTimeout(() => { this._cloudConnect(cfg, state); }, timeout * 1000);
};
const stop = () => {
this._cloud = undefined;
this._cloudOk = false;
this._cloudSentReady = false;
if (this._cloudInterval) {
clearInterval(this._cloudInterval);
this._cloudInterval = undefined;
}
};
try {
this._cloud = new WebSocket("wss://www.homework.software:443/user/websocket",
//this._cloud = new WebSocket("ws://192.168.1.22:3000/user/websocket",
{ headers: {
"X-Homework-Key": cfg.key
}});
this._cloud.on("open", () => {
state.next = 1;
Console.log("cloud available");
this._setupWS(this._cloud);
if (this._ready && !this._cloudSentReady) {
this._cloudSentReady = true;
Console.log("telling cloud we're ready (1)");
this.sendCloud({ type: "ready", ready: true });
}
this._cloudInterval = setInterval(() => {
Console.log("ping cloud");
this._cloud.ping();
}, cfg.cloudPingInterval || (20 * 1000 * 60));
saveFilesInCloud();
});
this._cloud.on("close", () => {
Console.log("cloud gone");
rejectCloud();
stop();
again();
});
this._cloud.on("error", (err) => {
Console.log("cloud error", err);
rejectCloud();
this._cloud.close();
stop();
again();
});
} catch (err) {
Console.log("cloud exception", err);
stop();
again();
}
},
init: function(hw, cfg) {
Console = hw.Console;
Console.on("logOut", (l) => {
const obj = { type: "log", stream: "out", log: l };
sendToAll(obj);
});
Console.on("logError", (l) => {
const obj = { type: "log", stream: "error", log: l };
sendToAll(obj);
});
sendCloud = this.sendCloud.bind(this);
Variable.on("changed", (name) => {
const obj = { type: "variableUpdated", name: name, value: Variable.variables[name] };
sendToAll(obj);
this.sendCloud(obj);
});
this._ready = false;
if (cfg && cfg.key) {
let connectState = { next: 1 };
this._cloudConnect(cfg, connectState);
} else {
Console.log("no cloud key, not connecting");
}
homework = hw;
homework.on("valueUpdated", (value) => {
const obj = { type: "valueUpdated", valueUpdated: { devuuid: value.device ? value.device.uuid : null, valname: value.name, value: value.value, raw: value.raw }};
sendToAll(obj);
this.sendCloud(obj);
});
homework.on("timerUpdated", (timer) => {
const t = Timer[timer.type][timer.name];
if (timer.type === "schedule") {
timer.value = t.date;
} else {
timer.value = {
state: t.state,
started: t.started,
timeout: t.timeout
};
}
const obj = { type: "timerUpdated", timerUpdated: timer };
sendToAll(obj);
this.sendCloud(obj);
});
homework.on("ready", () => {
this._ready = true;
sendToAll({ type: "ready", ready: true });
if (!this._cloudSentReady) {
Console.log("telling cloud we're ready (2)");
this.sendCloud({ type: "ready", ready: true });
this._cloudSentReady = true;
}
});
wss = new WebSocketServer({ port: 8093 });
wss.on("connection", (ws) => {
connections.push(ws);
if (this._ready) {
ws.send(JSON.stringify({ type: "ready", ready: true }));
}
this._setupWS(ws);
ws.on("close", () => {
const idx = connections.indexOf(ws);
if (idx !== -1) {
connections.splice(idx, 1);
}
});
});
},
registerCommand: function(cmd, cb) {
extraTypes[cmd] = cb;
},
send: send,
error: error
};
module.exports = HWWebSocket;