nomiku
Version:
Client library for controlling Nomiku WiFi devices
221 lines (202 loc) • 5.82 kB
JavaScript
/**
* The Device object manages the state of an individual device
*
* @constructor
*
* @param {object} config Object with {hwid,id,name} of device
*/
var lodash=require('lodash/core')
function Device(options) {
if (!(this instanceof Device)) {
return new Device(options);
}
this.id = options.id;
this.hwid = options.hwid;
this.name = options.name;
this.optimistic = true;
if (options.hasOwnProperty('optimistic')) {
this.optimistic = options.optimistic;
}
this.getState=this.getState.bind(this);
this.updateState=this.updateState.bind(this);
this.endProvisional=this.endProvisional.bind(this);
this._setState=this._setState.bind(this);
this.set=this.set.bind(this);
this.lastState = {};
this.state = {};
this.confirmedState = {};
this.provisional = {};
}
/**
* Returns MQTT topic for device state
*
* @method getTopic
*/
Device.prototype.getTopic = function getTopic() {
return `nom2/${this.hwid}/get/+`
}
const stateParser = {
temp: parseFloat,
setpoint: parseFloat,
showF: (x) => { return (x==='1'); },
state: parseInt,
recipeID: parseInt,
recipeTitle: (title) => { return title; },
timerRunning: (x) => { return (x==='1'); },
timerSecs: parseInt,
timerEnd: parseInt
}
/**
* Returns full state detail for device
*
* @method getState
*/
Device.prototype.getState = function getState() {
var s=Object.hasOwnProperty.bind(this.state);
var isValid=(s('temp') && s('setpoint') && s('showF') && s('state') && s('timerRunning') &&
((this.state.timerRunning && s('timerEnd')) || (!this.state.timerRunning && s('timerSecs'))));
var isNew=!lodash.isEqual(this.lastState,this.state);
return {
id:this.id,
new:isNew,
provisional:this.provisional,
state:this.state,
valid:isValid
}
}
/**
* Update device state based on MQTT message on a topic with a payload
*
* @method updateState
* @param {string} topic - Last part of split MQTT topic
* @param {string} payload - Payload of message
*/
Device.prototype.updateState = function updateState(topic, payload) {
this.lastState = lodash.clone(this.state);
if (topic === 'timer') {
let timerInt = parseInt(payload);
if (timerInt>3000000) {
//timer is running
this.updateState('timerRunning', '1')
this.updateState('timerEnd', payload)
} else {
//timer is not running
this.updateState('timerRunning', '0')
this.updateState('timerSecs', payload)
}
}
if (stateParser.hasOwnProperty(topic)) {
if (this.provisional.hasOwnProperty(topic) && this.provisional[topic]) {
//topic is provisional, add to confirmedState object:
this.confirmedState[topic]=stateParser[topic](payload)
//then check if it confirms the latest provisional state
if (this.confirmedState[topic]===this.state[topic]) {
this.provisional[topic]=false;
}
} else {
this.state[topic]=stateParser[topic](payload)
}
}
return this.getState()
}
/**
* Restore state to confirmed state
*
* @method endProvisional
*/
Device.prototype.endProvisional = function endProvisional() {
this.lastState = lodash.clone(this.state);
for (key in this.provisional) {
if (this.provisional[key]) {
this.state[key] = this.confirmedState[key];
this.provisional[key] = false;
}
}
return this.getState();
}
const allowSet = ['timerRunning', 'timerSecs', 'timerEnd', 'setpoint', 'recipeID', 'recipeTitle', 'showF','state']
const timerSet = ['timerRunning', 'timerSecs', 'timerEnd']
/**
* Update device state based on set state change and return change
*
* @method _setState
* @param {function} callback - Called with (updateState, fullState)
* @param {object} stateChange - Changes to state
*/
Device.prototype._setState = function _setState(callback, stateChange) {
this.lastState = lodash.clone(this.state);
var setTimer=false
var updateState={}
for (key in stateChange) {
if (allowSet.indexOf(key) > -1) {
if (this.optimistic) {
this.state[key]=stateChange[key]
this.provisional[key]=true
}
updateState[key]=stateChange[key]
}
if (timerSet.indexOf(key) > -1) {
setTimer=true
}
}
if (setTimer) {
if (this.state.timerRunning) {
updateState['timer']=this.state.timerEnd
} else {
updateState['timer']=this.state.timerSecs
}
}
var fullState=this.getState();
return callback(updateState, fullState)
}
/**
* Functions to set the state. Mostly syntactic sugar on _setState.
*
* @method set
* @param {function} callback - Called with (updateState, fullState)
*/
Device.prototype.set = function set(callback) {
var setState=this._setState.bind(this,callback)
return {
off:() => {
return setState({state:0})
},
on:() => {
return setState({state:1})
},
recipe:(recipe) => {
let newState={
recipeID:recipe.id || 0,
recipeTitle:recipe.title || "",
setpoint:recipe.temp,
state:1,
timerRunning:false,
timerSecs:recipe.time || 0
}
return setState(newState);
},
setpoint:(setpoint) => {
return setState({setpoint:Math.round(setpoint*100)/100.0});
},
state:(state) => {
return setState(state);
},
timer:{
start:() => {
let timerEnd=Math.round(Date.now()/1000)+this.state.timerSecs
return setState({timerEnd:timerEnd,timerRunning:true})
},
stop:() => {
let timerSecs=this.state.timerEnd - Math.round(Date.now()/1000)
return setState({timerSecs:timerSecs,timerRunning:false});
},
set:(secs) => {
return setState({timerSecs:secs,timerRunning:false});
}
},
units: (unit) => {
return setState({showF:(unit==='F')});
}
}
}
module.exports = Device