BangleApps/apps/sleeplog/boot.js

361 lines
13 KiB
JavaScript
Raw Normal View History

2022-05-20 15:17:30 +00:00
// sleeplog.status values:
// undefined = service stopped, 0 = unknown, 1 = not worn, 2 = awake, 3 = light sleep, 4 = deep sleep
// sleeplog.consecutive values:
// undefined = service stopped, 0 = unknown, 1 = no consecutive sleep, 2 = consecutive sleep
// create global object with settings
global.sleeplog = {
conf: Object.assign({
// main settings
enabled: true, // en-/disable completely
// threshold settings
maxAwake: 36E5, // [ms] maximal awake time to count for consecutive sleep
minConsec: 18E5, // [ms] minimal time to count for consecutive sleep
deepTh: 100, // threshold for deep sleep
lightTh: 200, // threshold for light sleep
}, require("Storage").readJSON("sleeplog.json", true) || {})
};
// check if service is enabled
if (sleeplog.conf.enabled) {
// assign functions to global object
global.sleeplog = Object.assign({
// define function to initialy start or restart the service
start: function() {
// add kill and health listener
E.on('kill', sleeplog.saveStatus);
Bangle.on('health', sleeplog.health);
// restore saved status
this.restoreStatus();
},
// define function to stop the service, it will be restarted on reload if enabled
stop: function() {
// remove all listeners
Bangle.removeListener('health', sleeplog.health);
2022-05-20 15:17:30 +00:00
E.removeListener('kill', sleeplog.saveStatus);
// save active values
this.saveStatus();
// reset active values
this.status = undefined;
this.consecutive = undefined;
},
// define function to restore active values on a restart or reload
restoreStatus: function() {
// define restore objects with default values
var restore = {
status: 0,
consecutive: 0,
info: {}
};
// open log file
var file = require("Storage").open("sleeplog.log", "r");
// read last 55 chars from log
file.read(file.getLength() - 52);
file = file.read(52);
// check if the log contains data
if (file) {
// remove unneeded data
file = file.trim().split("\n").reverse().filter((e, i) => i < 2);
// convert file into accessable array
file = file.map(e => e.split(",").map(e => parseInt(e)));
// add default data if no previous status is available
if (file.length < 2 || file[1].length !== 3) file.push([0, 0, 0]);
// check if data to restore has been saved
if (file[0].length > 3) {
// read data into restore object
restore = {
status: file[1][1],
consecutive: file[1][2],
info: {
lastChange: file[1][0] * 6E5,
lastCheck: file[0][1] * 6E5,
awakeSince: file[0][2] * 6E5,
asleepSince: file[0][3] * 6E5
}
};
// add debug if set
if (file[0].length === 6)
restore = Object.assign(restore, {
debug: file[0][4] ? {
writeUntil: file[0][4] * 6E5,
fileid: file[0][5]
} : true
});
// calculate timestamp in 10min steps, corrected to 10min ago
var timestamp = (Date.now() / 6E5 | 0) - 1;
// check if restored status not unknown and lastCheck was 20min before timestamp
if (restore.status && restore.info.lastCheck + 12E5 < timestamp) {
// set status and consecutive to unknown
restore.status = 0;
restore.consecutive = 0;
restore.info.lastChange = restore.info.lastCheck + 6E5;
restore.info.lastCheck = timestamp;
// write undefined status 10min after restored lastCheck
this.appendStatus(restore.info.lastChange, 0, 0);
} else {
// set saveUpToDate
restore.info.saveUpToDate = true;
}
}
}
2022-05-20 15:17:30 +00:00
// write restored values into global object
global.sleeplog = Object.assign(this, restore);
},
2022-05-20 15:17:30 +00:00
// define function to save active values on a stop or kill event
// - called by event listener: "this"-reference points to global
saveStatus: function(force) {
// check if global variable accessable
if (!global.sleeplog) return new Error("sleeplog: Can't save status, global object missing!");
// check saveUpToDate is not set or forced
if (!sleeplog.info.saveUpToDate || force) {
// save status, consecutive status and info timestamps to restore on reload
var save = [sleeplog.info.lastCheck, sleeplog.info.awakeSince, sleeplog.info.asleepSince];
// add debuging status if active
2022-05-20 15:17:30 +00:00
if (sleeplog.debug) save.push(sleeplog.debug.writeUntil, sleeplog.debug.fileid);
// stringify entries
save = "," + save.map((entry, index) => {
if (index < 4) entry /= 6E5; // store in 10min steps
return entry | 0; // sanitize
}).join(",") + "\n";
// add present status if forced
if (force) save = (sleeplog.info.lastChange / 6E5) + "," +
sleeplog.status + "," + sleeplog.consecutive + "\n" + save;
// append saved data to StorageFile
require("Storage").open("sleeplog.log", "a").write(save);
// clear save string to free ram
save = undefined;
}
},
2022-05-20 15:17:30 +00:00
// define health listener function
// - called by event listener: "this"-reference points to global
health: function(data) {
// check if global variable accessable
if (!global.sleeplog) return new Error("sleeplog: Can't process health event, global object missing!");
// check if movement is available
if (!data.movement) return;
// add timestamp rounded to 10min, corrected to 10min ago
data.timestamp = data.timestamp || ((Date.now() / 6E5 | 0) - 1) * 6E5;
// add preliminary status depending on charging and movement thresholds
data.status = Bangle.isCharging() ? 1 :
data.movement <= sleeplog.conf.deepTh ? 4 :
data.movement <= sleeplog.conf.lightTh ? 3 : 2;
// check if changing to deep sleep from non sleepling
if (data.status === 4 && sleeplog.status <= 2) {
// check wearing status
sleeplog.checkIsWearing((isWearing, data) => {
// correct status
if (!isWearing) data.status = 1;
// set status
sleeplog.setStatus(data);
}, data);
} else {
// set status
sleeplog.setStatus(data);
}
2022-05-20 15:17:30 +00:00
},
// define function to check if the bangle is worn by using the hrm
checkIsWearing: function(returnFn, data) {
// create a temporary object to store data and functions
global.tmpWearingCheck = {
// define temporary hrm listener function to read the wearing status
hrmListener: hrm => tmpWearingCheck.isWearing = hrm.isWearing,
// set default wearing status
isWearing: false,
};
// enable HRM
Bangle.setHRMPower(true, "wearingCheck");
// wait until HRM is initialised
setTimeout((returnFn, data) => {
// add HRM listener
Bangle.on('HRM-raw', tmpWearingCheck.hrmListener);
// wait for two cycles (HRM working on 60Hz)
setTimeout((returnFn, data) => {
// remove listener and disable HRM
Bangle.removeListener('HRM-raw', tmpWearingCheck.hrmListener);
Bangle.setHRMPower(false, "wearingCheck");
// cache wearing status
var isWearing = tmpWearingCheck.isWearing;
// clear temporary object
delete global.tmpWearingCheck;
// call return function with status
returnFn(isWearing, data);
}, 34, returnFn, data);
}, 2500, returnFn, data);
},
// define function to set the status
setStatus: function(data) {
// update lastCheck
this.info.lastCheck = data.timestamp;
// correct light sleep status to awake if
// previous status not deep sleep and not too long awake (asleepSince unset)
if (data.status === 3 && this.status !== 4 && !this.info.asleepSince) {
data.status = 2;
}
// cache consecutive status to check for changes later on
data.consecutive = this.consecutive;
// check if changing to deep sleep from non sleepling
if (data.status === 4 && this.status <= 2) {
// set asleepSince if undefined
this.info.asleepSince = this.info.asleepSince || data.timestamp;
// reset consecutive status
data.consecutive = 0;
// check if changing to awake
} else if (data.status === 2 && this.status > 2) {
// set awakeSince if undefined
this.info.awakeSince = this.info.awakeSince || data.timestamp;
// reset consecutive status
data.consecutive = 0;
}
// check if consecutive unknown
if (!this.consecutive) {
// check if long enough asleep or too long awake
if (data.status === 4 && this.info.asleepSince &&
this.info.asleepSince + this.conf.minConsec <= data.timestamp) {
// set consecutive sleep
data.consecutive = 2;
// reset awakeSince
this.info.awakeSince = 0;
} else if (data.status <= 2 && this.info.awakeSince &&
this.info.awakeSince + this.conf.maxAwake <= data.timestamp) {
// set non consecutive sleep
data.consecutive = 1;
// reset asleepSince
this.info.asleepSince = 0;
}
}
2022-05-20 15:17:30 +00:00
// check if the status has changed
var changed = data.status !== this.status || data.consecutive !== this.consecutive;
// read and check trigger entries
var triggers = Object.keys(this.trigger) || [];
if (triggers.length) {
// calculate time from timestamp in ms on full minutes
var time = data.timestamp;
time = ((time.getHours() * 60) + time.getMinutes() * 60) * 1000;
// go through all triggers
triggers.forEach(key => {
// read entry to key
var entry = this.trigger[key];
// check if the event matches the entries requirements
if (typeof entry.fn === "function" && (changed || !entry.onChange) &&
(entry.from || 0) <= time && (entry.to || 24 * 60 * 60 * 1000) >= time)
// and call afterwards with status data
setTimeout(fn, 100, {
timestamp: new Date(data.timestamp),
status: data.status === this.status ? undefined : data.status,
consecutive: data.consecutive === this.consecutive ? undefined : data.consecutive,
prevStatus: this.status,
prevConsecutive: this.consecutive
});
});
}
// cache change into a known consecutive state
var changeIntoConsec = data.consecutive;
// actions on a status change
if (changed) {
2022-05-20 15:17:30 +00:00
// append status
this.appendStatus(data.timestamp, data.status, data.consecutive);
// set new states and update lastChange
this.status = data.status;
this.consecutive = data.consecutive;
this.info.lastChange = data.timestamp;
// reset saveUpToDate status
delete this.info.saveUpToDate;
}
// send status to gadgetbridge
var gb_kinds = "unknown,not_worn,activity,light_sleep,deep_sleep";
Bluetooth.println(JSON.stringify({
t: "act",
act: gb_kinds.split(",")[data.status],
ts: data.timestamp
}));
2022-05-20 15:17:30 +00:00
// call debugging function if set
if (this.debug) require("sleeplog").debug(data);
// check if changed into known consecutive state
if (changeIntoConsec) {
// check if change is to consecutive sleep or not
if (changeIntoConsec === 2) {
// call move log function
require("sleeplog").moveLog();
} else {
// update stats cache if available
if (this.statsCache) this.statsCache = require("sleeplog").getStats();
}
// remove module from cache if cached
if (Modules.getCached().includes("sleeplog")) Modules.removeCached("sleeplog");
}
2022-05-20 15:17:30 +00:00
},
// define function to append the status to the StorageFile log
appendStatus: function(timestamp, status, consecutive) {
// exit on missing timestamp
if (!timestamp) return;
// reduce timestamp to 10min step
timestamp = timestamp / 6E5 | 0;
// append to StorageFile
require("Storage").open("sleeplog.log", "a").write(
[timestamp, status || 0, consecutive || 0].join(",") + "\n"
);
},
// define function to access stats of the last night
getStats: function() {
// check if stats cache is not defined or older than 12h
// if stats cache is set it will be updated on every change to non consecutive sleep
if (this.statsCache === undefined || this.statsCache.calculatedAt + 432E5 < Date.now()) {
2022-05-20 15:17:30 +00:00
// read stats of the last night into cache and remove module from cache
this.statsCache = require("sleeplog").getStats();
// remove module from cache if cached
if (Modules.getCached().includes("sleeplog")) Modules.removeCached("sleeplog");
2022-05-20 15:17:30 +00:00
}
// return stats cache
return this.statsCache;
},
// define trigger object
trigger: {}
2022-05-20 15:17:30 +00:00
}, sleeplog);
// initial starting
global.sleeplog.start();
2022-05-20 15:17:30 +00:00
} else {
// clear global object from ram
delete global.sleeplog;
}