Skip to content

Commit

Permalink
Initial iSpindel support
Browse files Browse the repository at this point in the history
  • Loading branch information
cwilling committed Sep 6, 2017
1 parent 9b41911 commit cbea9a6
Show file tree
Hide file tree
Showing 8 changed files with 812 additions and 171 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ SERVER_FILES = \
src/scripts/modules/server.js

CLIENT_FILES = \
styles/brewable.css \
src/scripts/status.js \

TEST_FILES = test-status.js \
Expand Down
35 changes: 29 additions & 6 deletions src/scripts/modules/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@ var defaultConfigValues = function() {
'multiSensorMeanWeight' : parseInt(50),
'relayDelayPostON' : parseInt(180),
'relayDelayPostOFF' : parseInt(480),
'iSpindelA' : {
"name":"iSpindel",
"calfactor0":0.00438551,
"calfactor1":0.13647658,
"calfactor2":6.968821422
}
'iSpindels' : [
{ "name":"iSpindel", "timeout":60 }
]
};
};

Expand Down Expand Up @@ -59,6 +56,7 @@ Configuration.prototype.loadConfigFromFile = function () {
console.log("conf file: " + path.join(this._projectConfigDir, this._project + ".conf"));
try {
this._configuration = JSON.parse(fs.readFileSync(this._configFileName, 'utf8'));
console.log("config from file: " + JSON.stringify(this._configuration));
}
catch (err) {
console.log("Error reading " + this._configFileName + ". Using default configuration");
Expand Down Expand Up @@ -183,6 +181,31 @@ Configuration.prototype.setFudgeFactor = function (key, val) {
fs.writeFileSync(this._configFileName, JSON.stringify(config));
};

Configuration.prototype.setIspindelTimeout = function (key, val) {
console.log("setIspindelTimeout() " + key + ", val " + val);

var config = this._configuration;
var found = false;
config.iSpindels.forEach( function(item) {
if (item.name == key ) {
found = true;
item.timeout = val;
}
});
if (!found) {
config.iSpindels.push({"name":key, "timeout":val});
}
fs.writeFileSync(this._configFileName, JSON.stringify(config));
};

Configuration.prototype.newIspindel = function (name) {
var config = this._configuration;
var defaultTimeout = defaultConfigValues().iSpindels[0].timeout;
config.iSpindels.push({"name":name, "timeout":defaultTimeout});
fs.writeFileSync(this._configFileName, JSON.stringify(config));
this.loadConfigFromFile();
};

/********
atest = new Configuration();
var config = atest.getConfiguration();
Expand Down
222 changes: 222 additions & 0 deletions src/scripts/modules/fhem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
The only communication from an iSpindel device is its periodic data report.
We have no way of determining whether an iSpindel device is being used
(or multiple devices) other than the data reports. We therefore start out
assuming there are no iSPindel devices. As each device submits a data report,
we instantiate an fhemDevice object and add it to fhemDeviceList.
The fhemDevice object maintains a record of the last received data report
along with the time it was received. If we haven't heard from a device for
some predermined time, it is removed from fhemDeviceList by deviceReaper()
which runs periodically (started when the first iSpindel is detected).
Data reports are not frequent - the isPindel documentation mentions:
"With an update interval of 30min it was possible to achive a
battery lifetime of almost 3 months!" (sic).
Therefore when each data report is received, we emit "fhem_reading" so that
any interested modules can immediately do something with the newly received
data.
*/
import querystring from "querystring";
import { eventEmitter } from "./gpioworker";
import Configuration from "./configuration";


/*
FHEM arithmetic from
https://github.com/universam1/iSpindel/blob/master/docs/upload-FHEM_en.md
*/
var correctPlato = function (plato, temp) {
var k;

if (plato < 5) k = [56.084, -0.17885, -0.13063];
else if (plato >= 5) k = [69.685, -1.367, -0.10621];
else if (plato >= 10) k = [77.782, -1.7288, -0.10822];
else if (plato >= 15) k = [87.895, -2.3601, -0.10285];
else if (plato >= 20) k = [97.052, -2.7729, -0.10596];

var cPlato = k[0] + k[1] * temp + k[2] * temp*temp;
return plato - cPlato/100;
};
var calcPlato = function (tilt, temp) {
// Get this from Excel Calibration at 20 Degrees
var plato=0.00438551*tilt*tilt + 0.13647658*tilt - 6.968821422;

return correctPlato(plato, temp);
};

var fhemDeviceList = [];
var searchDeviceListByName = function (name) {
var result;
for (var i=0;i<fhemDeviceList.length;i++) {
if (fhemDeviceList[i].name == name) {
return fhemDeviceList[i];
}
}
return result;
};

class fhemDevice {
constructor (raw) {
console.log("Creating new fhemDevice object from: " + JSON.stringify(raw));
this.raw = raw;
this.name = raw.name;
this.date = new Date();

// Check for existing configuration
var configObj = new Configuration();
var config = configObj.getConfiguration();
console.log("Configurations: " + JSON.stringify(config));
console.log("Configurations: " + Object.keys(config));
console.log("Configurations: " + Object.keys(config.iSpindels));

this.timeout;
for (var i=0;i<config.iSpindels.length;i++) {
console.log("Comparing " + config.iSpindels[i].name + " vs. " + raw.name);
if (config.iSpindels[i].name == raw.name) {
this.timeout = 1000 * parseInt(config.iSpindels[i].timeout);
console.log("Comparing was OK");
break;
}
}
if (! this.timeout) {
// No timeout from configuration so need to make one up
// We expect reports (at least) every 30mins,
// so a check every 10mins chould be plenty.
// 10 * 60 * 1000 = 600000
configObj.newIspindel(raw.name);
this.timeout = raw.timeout || 600000;
console.log("Couldn't find " + raw.name + ". Using default timeout (" + (this.timeout/1000) + "s).");
}

// Periodically check whether to remove this device
this.reaper = setInterval(this.deviceReaper,parseInt(this.timeout/10),this);
}

static newReading (reading) {
//console.log("New reading: " + reading);
var obj = {};

try {
var parsed = querystring.parse(reading);
//console.log("= " + JSON.stringify(parsed));

var command = parsed["cmd.Test"].split(" ");
//console.log("command = " + command);
} catch (err) {
console.log("newReading() Can't parse " + reading);
return;
}

if (command.length != 6 ) {
console.log("newReading() Incorrect element count while parsing object: " + JSON.stringify(command));
return;
}
obj.name = command[1];
obj.tilt = parseFloat(command[2]);
obj.temp = parseFloat(command[3]);
obj.batt = parseFloat(command[4]);
obj.grav = parseFloat(command[5]);
obj.plato = calcPlato(obj.tilt, obj.temp);

var device = searchDeviceListByName(obj.name);
if (device) {
console.log("Already have device: " + device.name + " at " + device.stamp);
device.update(obj);
} else {
console.log("Adding new device");
fhemDeviceList.push(new fhemDevice(obj));
}

eventEmitter.emit("fhem_reading", obj);
}

update (raw) {
this.raw = raw;
this.name = raw.name;
this.date = new Date();
}

static devices () {
return fhemDeviceList;
}

get stamp () {
return this.date;
}

get tilt () {
return this.raw.tilt;
}
get temp () {
return this.raw.temp;
}
get batt () {
return this.raw.batt;
}
get grav () {
return this.raw.grav;
}
get plato () {
return calcPlato(this.raw.tilt, this.raw.temp);
}

/*
If the device is in the first x% of its life (time to be reaped)
then fresh is true, otherwise false
*/
get fresh () {
var device = searchDeviceListByName(this.name);
if (! device) return false;

if ((new Date() - new Date(device.stamp)) < parseInt(device.timeout/5) ) {
return true;
} else {
return false;
}
}

/* val seconds */
setNewTimeout (val) {
var newTimeout = parseInt(val);
if (newTimeout < 10) return;
this.timeout = 1000 * newTimeout;
if (this.reaper) clearInterval(this.reaper);

this.reaper = setInterval(this.deviceReaper, this.timeout/10, this);
}

/*
We expect reports (at least) every 30mins,
so if nothing heard from a device for twice that time, remove it.
60 * 60 * 1000 = 3600000
*/
deviceReaper (caller) {
//console.log("Reaping ... fhemDeviceList length = " + fhemDeviceList.length);
var reap = false;
var i;
for (i=0;i<fhemDeviceList.length;i++) {
if (fhemDeviceList[i].name == caller.name ) {
//console.log("dur: " + (new Date() - new Date(fhemDeviceList[i].stamp)));
//console.log("timeout = " + caller.timeout);
if ((new Date() - new Date(fhemDeviceList[i].stamp)) > caller.timeout ) {
console.log("Planning removal of " + fhemDeviceList[i].name + ", caller = " + caller.name);
reap = true;
}
break;
}
}
if (reap ) {
console.log("Removing " + fhemDeviceList[i].name + ", caller = " + caller.name);
clearInterval(caller.reaper);
fhemDeviceList.splice(i, 1);
}
}

}

export default fhemDevice;
export const newReading = fhemDevice.newReading;

/* ex:set ai shiftwidth=2 inputtab=spaces smarttab noautotab: */

42 changes: 37 additions & 5 deletions src/scripts/modules/gpioworker.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import events from "events";
var eventEmitter = new events.EventEmitter();

import SensorDevice from "./sensor";
import fhemDevice from "./fhem";
import Relay from "./sainsmartrelay";
import Configuration from "./configuration";
import JobProcessor from "./jobprocessor";
Expand Down Expand Up @@ -133,16 +134,20 @@ function gpioWorker (input_queue, output_queue) {
//export default gpioWorker;
export { gpioWorker, eventEmitter };

/*
Just for testing.
We should really just call liveUpdate which should extract data from
any/all devices (including the one whose data report triggered the
'fhem_reading' event which is probably what brought us here.
*/
gpioWorker.prototype.fhemReading = function (reading) {
//console.log("fhemReading() " + JSON.stringify(reading));
var name = reading.name;
var tilt = reading.tilt;
var temp = reading.temp;
var batt = reading.batt;
console.log("fhem() " + name + ", " + tilt + ", " + temp + ", " + batt);

//var plato = calcPlato(tilt, temp);
//console.log("plato = " + plato);
var grav = reading.grav;
console.log("fhemReading() " + name + ", " + tilt + ", " + temp + ", " + batt + ", " + grav);
};

gpioWorker.prototype.sensorDevices = function () {
Expand All @@ -161,6 +166,7 @@ gpioWorker.prototype.sensorDevices = function () {
var bval = parseInt(b.id.substr(0,b.id.search("-")),16) + parseInt(b.id.substr(b.id.search("-") + 1),16);
return aval - bval;
});
console.log("deviceList = " + JSON.stringify(deviceList));
return deviceList;
};

Expand Down Expand Up @@ -214,6 +220,15 @@ gpioWorker.prototype.liveUpdate = function () {
});
//console.log("liveUpdate(): " + JSON.stringify(sensor_state));

var fhemDevices = fhemDevice.devices();
fhemDevices.forEach( function (item) {
if (item.fresh) {
console.log("FHEM device: " + item.name + ", " + item.stamp);
sensor_state.push({'sensorId':item.name, 'temperature':item.temp, 'tilt':item.tilt, 'batt':item.batt, 'grav':item.grav, 'stamp':item.stamp});
}
});
//console.log("liveUpdate(): " + JSON.stringify(sensor_state));

for (var i=0;i<this.relay.deviceCount();i++) {
relay_state.push([this.relay.isOn(i+1), this.relay.isDelayed(i+1)]);
}
Expand Down Expand Up @@ -297,12 +312,13 @@ gpioWorker.prototype.processMessage = function () {

gpioWorker.prototype.load_startup_data = function (msg) {
//console.log("load_startup_data():");
var configObj = new Configuration();

var jdata = JSON.stringify({
'type':'startup_data',
'data': {
'testing': this.brewtest(),
'config' : this.configObj.getConfiguration(),
'config' : configObj.getConfiguration(),
'the_end': 'orange'
}
});
Expand Down Expand Up @@ -338,6 +354,22 @@ gpioWorker.prototype.config_change = function (msg) {
});
//this.configuration.sensorFudgeFactors[msg.data['sensorFudgeFactors']] = msg.data['fudge'];
this.configObj.setFudgeFactor(msg.data['sensorFudgeFactors'], msg.data['fudge']);
} else if (keys[0] == 'iSpindels') {
console.log("config_change() iSpindels: " + msg.data['iSpindels'] + " (" + msg.data['timeout'] + ")");

// First update the configuration
this.configObj.setIspindelTimeout(msg.data['iSpindels'], msg.data['timeout']);

// Now apply to the device itself
var fhemDevices = fhemDevice.devices();
fhemDevices.forEach( function (item) {
console.log("reconfig FHEM device: " + item.name + ", " + item.stamp);
if (item.name == msg.data['iSpindels']) {
console.log("Found the right one:" + item.name);
item.setNewTimeout(msg.data['timeout']);
}
});

} else if (keys[0] == 'multiSensorMeanWeight') {
this.configObj.setMultiSensorMeanWeight(msg.data['multiSensorMeanWeight']);
} else if (keys[0] == 'relayDelayPostON' || keys[0] == 'relayDelayPostOFF') {
Expand Down
Loading

0 comments on commit cbea9a6

Please sign in to comment.