hw-server
Version:
Home automation system
389 lines (358 loc) • 11.5 kB
JavaScript
/*global require,process,homework,setTimeout,module,global*/
;
const path = require("path");
const fs = require("fs");
var packageObj = undefined;
function packageObject()
{
if (packageObj)
return packageObj;
const fn = module.filename;
const idx = fn.indexOf("/src/homework.js");
if (idx == -1) {
throw new Error(`Couldn't parse module filename ${fn}`);
}
var jsonfn = fn.substr(0, idx) + path.sep + "package.json";
try {
var json = JSON.parse(fs.readFileSync(jsonfn));
} catch (e) {
throw new Error(`Couldn't parse package.json from ${jsonfn}`);
}
if (typeof json !== "object" || !("version" in json)) {
throw new Error(`Couldn't find version in package.json from ${jsonfn}`);
}
packageObj = json;
return json;
}
require("sugar");
const Config = require("./config.js");
const Device = require("./device.js");
const Console = require("./console.js");
const WebSocket = require("./websocket.js");
const WebServer = require("./webserver.js");
const Rule = require("./rule.js");
const Modules = require("./modules.js");
const Timer = require("./timer.js");
const Variable = require("./variable.js");
const Scene = require("./scene.js");
const db = require("jsonfile");
global.homework = {
_events: Object.create(null),
_actions: Object.create(null),
_deviceSchemas: Object.create(null),
_devices: [],
_scenes: [],
_deviceinfo: undefined,
_rules: [],
_triggers: [],
_triggerTimer: undefined,
_pendingRules: [],
_cfg: undefined,
_Device: Device.Device,
_Scene: Scene.Scene,
_Type: Device.Type,
_Console: Console,
_Timer: Timer,
_Variable: Variable,
_WebServer: WebServer.Server,
_WebSocket: WebSocket,
_restored: false,
_serverVersion: packageObject().version,
_moduleProtocol: packageObject()["software.homework"].moduleProtocol,
_rulePriorities: { High: 0, Medium: 1, Low: 2 },
get rulePriorities() {
return this._rulePriorities;
},
get serverVersion() {
return this._serverVersion;
},
get moduleProtocol() {
return this._moduleProtocol;
},
registerEvent: function(name, ctor, completion, deserialize) {
if (name in this._events)
return false;
this._events[name] = { ctor: ctor, completion: completion, deserialize: deserialize };
return true;
},
registerAction: function(name, ctor, completion, deserialize) {
if (name in this._actions)
return false;
this._actions[name] = { ctor: ctor, completion: completion, deserialize: deserialize };
return true;
},
registerDevice: function(type, schema) {
if (type in this._deviceSchemas) {
if (!this._deviceSchemas[type].equals(schema))
throw new Error(`Device schema for ${type} already registered`);
} else {
this._deviceSchemas[type] = schema;
}
},
addDevice: function(device) {
var t = device.type;
if (t in this._deviceSchemas) {
if (!this._deviceSchemas[t].verify(device)) {
throw new Error(`Device schema mismatch for ${t} -> ${device.fullName}`);
}
} else if (t != "Unknown") {
throw new Error(`Invalid device type ${t}`);
}
this._devices.push(device);
},
removeDevice: function(device) {
this._devices.remove((el) => { return el.uuid == device.uuid; });
if (this._deviceinfo && device.uuid in this._deviceinfo) {
delete this._deviceinfo[device.uuid];
}
},
addScene: function(scene) {
this._scenes.push(scene);
},
removeScene: function(scene) {
var len = this._scenes.length;
if (typeof scene === "object")
this._scenes.remove((el) => { return Object.is(scene, el); });
else if (typeof scene === "string")
this._scenes.remove((el) => { return el.name === scene; });
return (len !== this._scenes.length);
},
addRule: function(rule) {
for (var i = 0; i < this._rules.length; ++i) {
if (rule.name == this._rules[i].name) {
console.trace(`duplicate rule ${rule.name}`);
process.exit();
}
}
this._rules.push(rule);
},
removeRule: function(rule) {
var len = this._rules.length;
this._rules.remove((el) => { if (Object.is(rule, el)) { rule.destroy(); return true; } return false; });
return this._rules.length < len;
},
removeRuleByName: function(name) {
var len = this._rules.length;
this._rules.remove((el) => { if (el.name == name) { el.destroy(); return true; } return false; });
return this._rules.length < len;
},
valueUpdated: function(value) {
this._emit("valueUpdated", value);
},
modulesReady: function() {
this._emit("ready");
},
timerUpdated: function(type, name) {
this._emit("timerUpdated", { type: type, name: name });
},
get events() {
return this._events;
},
get actions() {
return this._actions;
},
get devices() {
return this._devices;
},
get scenes() {
return this._scenes;
},
get rules() {
return this._rules;
},
get config() {
return this._cfg;
},
get Console() {
return this._Console;
},
get Device() {
return this._Device;
},
get DeviceTypes() {
return Object.keys(this._deviceSchemas);
},
get Scene() {
return this._Scene;
},
get Type() {
return this._Type;
},
get Timer() {
return this._Timer;
},
get Variable() {
return this._Variable;
},
get WebServer() {
return this._WebServer;
},
get WebSocket() {
return this._WebSocket;
},
get restored() {
return this._restored;
},
triggerRule: function(rule) {
if (rule) {
this._triggers.push(rule);
if (!this._triggerTimer) {
this._triggerTimer = setTimeout(() => {
this.triggerRule();
}, 0);
}
} else {
var tr = this._triggers;
this._triggers = [];
this._triggerTimer = undefined;
for (var pri = this.rulePriorities.High; pri <= this.rulePriorities.Low; ++pri) {
for (var k = 0; k < tr.length; ++k) {
tr[k]._trigger(pri);
}
}
}
},
saveRules: function(cb) {
var rules = [], i;
for (i = 0; i < this._rules.length; ++i) {
// true = overwrite
rules.push(this._rules[i].serialize());
}
// merge with pendingRules if we still have any
for (i = 0; i < this._pendingRules.length; ++i) {
rules.push(Rule.SerializePending(this._pendingRules[i]));
}
Console.log(rules);
if (cb) {
cb("rules.json", rules);
} else {
db.writeFileSync(path.join(Config.path, "rules.json"), rules, { spaces: 4 });
}
},
saveDevices: function(cb) {
var devices = this._deviceinfo || Object.create(null);
for (var i = 0; i < this._devices.length; ++i) {
var dev = this._devices[i];
devices[dev.uuid] = { name: dev.name, room: dev.room, floor: dev.floor, groups: dev.groups, type: dev.type };
if (dev.virtual) {
// save values as well
var vals = Object.create(null);
for (var v in dev.values) {
vals[v] = dev.values[v].data;
}
devices[dev.uuid].values = vals;
}
}
if (cb) {
cb("devices.json", devices);
} else {
db.writeFileSync(path.join(Config.path, "devices.json"), devices, { spaces: 4 });
}
},
saveScenes: function(cb) {
var scenes = [];
for (var i = 0; i < this._scenes.length; ++i) {
var scene = this._scenes[i];
scenes.push({ name: scene.name, values: scene.values });
}
if (cb) {
cb("scenes.json", scenes);
} else {
db.writeFileSync(path.join(Config.path, "scenes.json"), scenes, { spaces: 4 });
}
},
save: function(cb) {
this.saveRules(cb);
this.saveDevices(cb);
this.saveScenes(cb);
},
restoreRules: function(rules) {
this._rules = [];
this._pendingRules = rules;
this.loadRules();
},
restoreScenes: function(scenes) {
this._scenes = [];
if (scenes instanceof Array) {
for (var i = 0; i < scenes.length; ++i) {
this.addScene(new this.Scene(scenes[i].name, scenes[i].values));
}
}
},
restore: function(args) {
db.readFile(path.join(Config.path, "rules.json"), (err, obj) => {
if (obj) {
this.restoreRules(obj);
} else {
if (err.code !== "ENOENT")
console.error("error loading rules", err);
}
});
Config.load(this, () => {
this.WebServer.serve(homework, this.config.webserver);
db.readFile(path.join(Config.path, "devices.json"), (err, obj) => {
this._deviceinfo = obj;
Device.Device.init(this, obj);
db.readFile(path.join(Config.path, "scenes.json"), (err, obj) => {
Scene.init(this);
this.restoreScenes(obj);
Timer.init(this);
Variable.init(this);
WebSocket.init(this, this.config.websocket);
db.readFile(path.join(Config.path, "modules.json"), (err, obj) => {
Modules.init(this, obj);
this._restored = true;
});
});
});
});
},
loadRules: function() {
this._pendingRules = this._pendingRules.filter((rule) => {
var r = Rule.Deserialize(homework, rule);
if (r) {
Console.log("restored rule", r.name);
homework.addRule(r);
return false;
}
return true;
});
},
utils: require("./utils.js")
};
homework.utils.onify(homework);
homework._initOns();
Rule.init(homework);
Console.init(homework);
Console.on("shutdown", () => {
Console.cleanup();
if (!homework.restored) {
process.exit();
} else {
homework.save();
Modules.shutdown((data) => {
if (data) {
console.log("modules.json", data);
db.writeFileSync(path.join(Config.path, "modules.json"), data, { spaces: 4 });
}
process.exit();
});
}
});
homework.restore(require('minimist')(process.argv.slice(2)));
/*
// fake device
var hey = new Device("hey");
var heyval = new Device.Value("mode", {"on": 1, "off": 2});
setInterval(() => {
if (heyval.value === "on") {
heyval.update(2);
} else {
heyval.update(1);
}
}, 1000);
hey.addValue(heyval);
homework.addDevice(hey);
homework.loadRules();
*/