Skip to content

Commit f3cb3ac

Browse files
committed
Allow iSpindel discovery with no prior entry in user config file
1 parent ffc53dc commit f3cb3ac

File tree

4 files changed

+322
-15
lines changed

4 files changed

+322
-15
lines changed

src/scripts/modules/configuration.js

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ var defaultConfigValues = function() {
1212
'relayDelayPostON' : parseInt(180),
1313
'relayDelayPostOFF' : parseInt(480),
1414
'iSpindels' : [
15-
{ "name":"iSpindel", "timeout":60 }
15+
{ "name":"iSpindel", "timeout":120 }
1616
]
1717
};
1818
};
@@ -187,22 +187,34 @@ Configuration.prototype.setIspindelTimeout = function (key, val) {
187187

188188
var config = this._configuration;
189189
var found = false;
190-
config.iSpindels.forEach( function(item) {
191-
if (item.name == key ) {
192-
found = true;
193-
item.timeout = val;
194-
}
195-
});
190+
if (config.iSpindels) {
191+
config.iSpindels.forEach( function(item) {
192+
if (item.name == key ) {
193+
found = true;
194+
item.timeout = val;
195+
}
196+
});
197+
}
196198
if (!found) {
197-
config.iSpindels.push({"name":key, "timeout":val});
199+
if (config.iSpindels) {
200+
config.iSpindels.push({"name":key, "timeout":val});
201+
} else {
202+
config.iSpindels = [];
203+
config.iSpindels.push({"name":key, "timeout":val});
204+
}
198205
}
199206
fs.writeFileSync(this._configFileName, JSON.stringify(config));
200207
};
201208

202209
Configuration.prototype.newIspindel = function (name) {
203210
var config = this._configuration;
204211
var defaultTimeout = defaultConfigValues().iSpindels[0].timeout;
205-
config.iSpindels.push({"name":name, "timeout":defaultTimeout});
212+
if (config.iSpindels) {
213+
config.iSpindels.push({"name":name, "timeout":defaultTimeout});
214+
} else {
215+
config.iSpindels = [];
216+
config.iSpindels.push({"name":name, "timeout":defaultTimeout});
217+
}
206218
fs.writeFileSync(this._configFileName, JSON.stringify(config));
207219
this.loadConfigFromFile();
208220
};

src/scripts/modules/fhem.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,14 @@ class fhemDevice extends Sensor {
9797
var config = configObj.getConfiguration();
9898

9999
this.timeout;
100-
for (var i=0;i<config.iSpindels.length;i++) {
101-
//console.log("Comparing " + config.iSpindels[i].name + " vs. " + raw.name);
102-
if (config.iSpindels[i].name == raw.name) {
103-
this.timeout = 1000 * parseInt(config.iSpindels[i].timeout);
104-
//console.log("Comparing was OK");
105-
break;
100+
if (config.iSpindels) {
101+
for (var i=0;i<config.iSpindels.length;i++) {
102+
//console.log("Comparing " + config.iSpindels[i].name + " vs. " + raw.name);
103+
if (config.iSpindels[i].name == raw.name) {
104+
this.timeout = 1000 * parseInt(config.iSpindels[i].timeout);
105+
//console.log("Comparing was OK");
106+
break;
107+
}
106108
}
107109
}
108110
if (! this.timeout) {

src/scripts/modules/iSpindel.js

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
/*
2+
The only communication from an iSpindel device is its periodic data report.
3+
We have no way of determining whether an iSpindel device is being used
4+
(or multiple devices) other than the data reports. We therefore start out
5+
assuming there are no iSPindel devices. As each device submits a data report,
6+
we instantiate an iSpindelDevice object and add it to iSpindelDeviceList.
7+
8+
The iSpindelDevice object maintains a record of the last received data report
9+
along with the time it was received. If we haven't heard from a device for
10+
some predermined time, it is removed from iSpindelDeviceList by deviceReaper()
11+
which runs periodically (started when the first iSpindel is detected).
12+
13+
Data reports are not frequent - the isPindel documentation mentions:
14+
"With an update interval of 30min it was possible to achive a
15+
battery lifetime of almost 3 months!" (sic).
16+
Therefore when each data report is received, we emit "iSpindel_reading" so that
17+
any interested modules can immediately do something with the newly received
18+
data.
19+
*/
20+
import { eventEmitter } from "./gpioworker";
21+
import Configuration from "./configuration";
22+
import Sensor from './sensor';
23+
24+
25+
/*
26+
FHEM arithmetic from
27+
https://github.com/universam1/iSpindel/blob/master/docs/upload-FHEM_en.md
28+
*/
29+
var correctPlato = function (plato, temp) {
30+
var k;
31+
32+
if (plato < 5) k = [56.084, -0.17885, -0.13063];
33+
else if (plato >= 5) k = [69.685, -1.367, -0.10621];
34+
else if (plato >= 10) k = [77.782, -1.7288, -0.10822];
35+
else if (plato >= 15) k = [87.895, -2.3601, -0.10285];
36+
else if (plato >= 20) k = [97.052, -2.7729, -0.10596];
37+
38+
var cPlato = k[0] + k[1] * temp + k[2] * temp*temp;
39+
return plato - cPlato/100;
40+
};
41+
var calcPlato = function (tilt, temp) {
42+
// Get this from Excel Calibration at 20 Degrees
43+
var plato=0.00438551*tilt*tilt + 0.13647658*tilt - 6.968821422;
44+
45+
return correctPlato(plato, temp);
46+
};
47+
48+
var iSpindelDeviceList = [];
49+
var searchDeviceListByChipId = function (Id) {
50+
var duplicates = 0;
51+
var i, duplicate, result;
52+
do {
53+
for (i=0;i<iSpindelDeviceList.length;i++) {
54+
if (iSpindelDeviceList[i].raw.chipId == Id) {
55+
if (result) {
56+
duplicate = i;
57+
duplicates += 1;
58+
} else {
59+
result = iSpindelDeviceList[i];
60+
}
61+
}
62+
}
63+
if (duplicates > 0) {
64+
console.log("Removing duplicate device: " + iSpindelDeviceList[duplicate].raw.chipId);
65+
iSpindelDeviceList.splice(duplicate, 1);
66+
duplicates -= 1;
67+
}
68+
} while (duplicates > 0);
69+
70+
return result;
71+
};
72+
/*
73+
var searchDeviceListByName = function (name) {
74+
var result;
75+
for (var i=0;i<iSpindelDeviceList.length;i++) {
76+
if (iSpindelDeviceList[i].name == name) {
77+
return iSpindelDeviceList[i];
78+
}
79+
}
80+
return result;
81+
};
82+
*/
83+
84+
class iSpindelDevice extends Sensor {
85+
constructor (raw) {
86+
console.log("Creating new iSpindelDevice object from: " + JSON.stringify(raw));
87+
super(raw.name);
88+
this.raw = raw;
89+
//this.name = raw.name;
90+
//this.chipId = raw.chipId;
91+
this.date = new Date();
92+
93+
// Check for existing configuration
94+
var configObj = new Configuration();
95+
var config = configObj.getConfiguration();
96+
97+
this.timeout;
98+
if (config.iSpindel ) {
99+
for (var i=0;i<config.iSpindels.length;i++) {
100+
//console.log("Comparing " + config.iSpindels[i].name + " vs. " + raw.name);
101+
if (config.iSpindels[i].name == raw.name) {
102+
this.timeout = 1000 * parseInt(config.iSpindels[i].timeout);
103+
//console.log("Comparing was OK");
104+
break;
105+
}
106+
}
107+
}
108+
if (! this.timeout) {
109+
// No timeout from configuration so need to make one up
110+
// We expect reports (at least) every 30mins,
111+
// so a check every 10mins chould be plenty.
112+
// 10 * 60 * 1000 = 600000
113+
configObj.newIspindel(raw.name);
114+
this.timeout = raw.timeout || 600000;
115+
console.log("Couldn't find " + raw.name + ". Using default timeout (" + (this.timeout/1000) + "s).");
116+
}
117+
118+
// Periodically check whether to remove this device
119+
this.reaper = setInterval(this.deviceReaper,parseInt(this.timeout/10),this);
120+
121+
}
122+
123+
static iSpindelCount () {
124+
console.log("iSpindelCount(): " + iSpindelDeviceList.length);
125+
}
126+
127+
static newReading (reading) {
128+
console.log("New iSpindel reading: " + reading);
129+
var obj = {};
130+
131+
try {
132+
var data = JSON.parse(reading);
133+
/*
134+
console.log("name = " + data.name);
135+
console.log(" ID = " + data.ID);
136+
console.log("token = " + data.token);
137+
console.log("angle = " + data.angle);
138+
console.log(" temp = " + data.temperature);
139+
console.log(" batt = " + data.battery);
140+
console.log(" grav = " + data.gravity);
141+
*/
142+
} catch (err) {
143+
console.log("newReading() Can't parse " + reading);
144+
return;
145+
}
146+
147+
obj.chipId = data.ID;
148+
obj.name = data.name;
149+
obj.tilt = data.angle;
150+
obj.temp = data.temperature;
151+
obj.batt = data.battery;
152+
obj.grav = data.gravity;
153+
//obj.plato = calcPlato(obj.tilt, obj.temp);
154+
155+
var device = searchDeviceListByChipId(obj.chipId);
156+
if (device) {
157+
//console.log("Already have device: " + device.raw.chipId + " at " + device.stamp);
158+
device.update(obj);
159+
} else {
160+
//console.log("Adding new iSpindel device (" + obj.chipId + ")");
161+
iSpindelDeviceList.push(new iSpindelDevice(obj));
162+
eventEmitter.emit('iSpindel_new_device');
163+
}
164+
165+
eventEmitter.emit("iSpindel_reading", obj);
166+
}
167+
168+
// Return a list of chip ids
169+
static sensors () {
170+
var returnList = [];
171+
iSpindelDeviceList.forEach( function (sensor) {
172+
returnList.push(sensor);
173+
});
174+
return returnList;
175+
}
176+
177+
update (raw) {
178+
this.raw = raw;
179+
this.name = raw.name;
180+
this.date = new Date();
181+
//console.log("update(raw) grav = " + this.raw.grav);
182+
}
183+
184+
static devices () {
185+
return iSpindelDeviceList;
186+
}
187+
188+
get stamp () {
189+
return this.date;
190+
}
191+
192+
get chipId () {
193+
return this.raw.chipId;
194+
}
195+
get tilt () {
196+
return this.raw.tilt;
197+
}
198+
get temp () {
199+
return this.raw.temp;
200+
}
201+
get batt () {
202+
return this.raw.batt;
203+
}
204+
get grav () {
205+
return this.raw.grav;
206+
}
207+
get plato () {
208+
return calcPlato(this.raw.tilt, this.raw.temp);
209+
}
210+
211+
/*
212+
If the device is in the first x% of its life (time to be reaped)
213+
then fresh is true, otherwise false
214+
*/
215+
get fresh () {
216+
var device = searchDeviceListByChipId(this.raw.chipId);
217+
if (! device) return false;
218+
219+
if ((new Date() - new Date(device.stamp)) < parseInt(device.timeout/5) ) {
220+
return true;
221+
} else {
222+
return false;
223+
}
224+
}
225+
226+
/* val seconds */
227+
setNewTimeout (val) {
228+
var newTimeout = parseInt(val);
229+
if (newTimeout < 10) return;
230+
this.timeout = 1000 * newTimeout;
231+
if (this.reaper) clearInterval(this.reaper);
232+
233+
this.reaper = setInterval(this.deviceReaper, this.timeout/10, this);
234+
}
235+
236+
/*
237+
We expect reports (at least) every 30mins,
238+
so if nothing heard from a device for twice that time, remove it.
239+
60 * 60 * 1000 = 3600000
240+
*/
241+
deviceReaper (caller) {
242+
var reap = false;
243+
var i;
244+
for (i=0;i<iSpindelDeviceList.length;i++) {
245+
if (iSpindelDeviceList[i].name == caller.name ) {
246+
//console.log("dur: " + (new Date() - new Date(iSpindelDeviceList[i].stamp)));
247+
//console.log("timeout = " + caller.timeout);
248+
if ((new Date() - new Date(iSpindelDeviceList[i].stamp)) > caller.timeout ) {
249+
//console.log("Planning removal of " + iSpindelDeviceList[i].name + ", caller = " + caller.name);
250+
reap = true;
251+
}
252+
break;
253+
}
254+
}
255+
if (reap ) {
256+
console.log("Reaping " + iSpindelDeviceList[i].chipId + ", caller = " + caller.name);
257+
clearInterval(caller.reaper);
258+
iSpindelDeviceList.splice(i, 1);
259+
/*
260+
Others (running jobs using this device) may be interested thath it's gone
261+
*/
262+
eventEmitter.emit("iSpindel_reaped", caller);
263+
}
264+
}
265+
266+
}
267+
268+
export default iSpindelDevice;
269+
export const newReading = iSpindelDevice.newReading;
270+
export const iSpindelCount = iSpindelDevice.iSpindelCount;
271+
272+
/* ex:set ai shiftwidth=2 inputtab=spaces smarttab noautotab: */
273+

src/scripts/modules/sensor.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
const NAME = Symbol();
3+
4+
class Sensor {
5+
constructor (val) {
6+
this[NAME] = val;
7+
8+
//console.log("New Sensor device (" + val + ")");
9+
}
10+
11+
set name (val) {}
12+
get name () { return this[NAME]; }
13+
set id (val) {}
14+
get id () { return this[NAME]; }
15+
16+
}
17+
export default Sensor;
18+
19+
20+
/* ex:set ai shiftwidth=2 inputtab=spaces smarttab noautotab: */

0 commit comments

Comments
 (0)