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
|
2024-03-22 16:59:09 +00:00
|
|
|
wearTemp: 19.5, // temperature threshold to count as worn
|
2022-05-20 15:17:30 +00:00
|
|
|
}, require("Storage").readJSON("sleeplog.json", true) || {})
|
|
|
|
};
|
|
|
|
|
|
|
|
// check if service is enabled
|
2024-04-01 15:27:15 +00:00
|
|
|
if (global.sleeplog.conf.enabled) {
|
2022-05-20 15:17:30 +00:00
|
|
|
// 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
|
2024-04-01 15:27:15 +00:00
|
|
|
E.on('kill', global.sleeplog.saveStatus);
|
|
|
|
Bangle.on('health', global.sleeplog.health);
|
2022-05-20 15:17:30 +00:00
|
|
|
|
|
|
|
// restore saved status
|
|
|
|
this.restoreStatus();
|
|
|
|
},
|
|
|
|
|
|
|
|
// define function to stop the service, it will be restarted on reload if enabled
|
|
|
|
stop: function() {
|
2022-02-12 00:43:58 +00:00
|
|
|
// remove all listeners
|
2024-04-01 15:27:15 +00:00
|
|
|
Bangle.removeListener('health', global.sleeplog.health);
|
|
|
|
E.removeListener('kill', global.sleeplog.saveStatus);
|
2022-05-20 15:17:30 +00:00
|
|
|
|
|
|
|
// 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-02-15 18:47:15 +00:00
|
|
|
}
|
|
|
|
}
|
2022-05-20 15:17:30 +00:00
|
|
|
|
|
|
|
// write restored values into global object
|
|
|
|
global.sleeplog = Object.assign(this, restore);
|
2022-02-15 18:47:15 +00:00
|
|
|
},
|
|
|
|
|
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
|
2024-04-01 15:27:15 +00:00
|
|
|
if (!global.sleeplog.info.saveUpToDate || force) {
|
2022-05-20 15:17:30 +00:00
|
|
|
// save status, consecutive status and info timestamps to restore on reload
|
2024-04-01 15:27:15 +00:00
|
|
|
var save = [global.sleeplog.info.lastCheck, global.sleeplog.info.awakeSince, global.sleeplog.info.asleepSince];
|
2022-11-10 16:04:54 +00:00
|
|
|
// add debuging status if active
|
2024-04-01 15:27:15 +00:00
|
|
|
if (global.sleeplog.debug) save.push(global.sleeplog.debug.writeUntil, global.sleeplog.debug.fileid);
|
2022-05-20 15:17:30 +00:00
|
|
|
|
|
|
|
// 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
|
2024-04-01 15:27:15 +00:00
|
|
|
if (force) save = (global.sleeplog.info.lastChange / 6E5) + "," +
|
|
|
|
global.sleeplog.status + "," + global.sleeplog.consecutive + "\n" + save;
|
2022-05-20 15:17:30 +00:00
|
|
|
|
|
|
|
// append saved data to StorageFile
|
|
|
|
require("Storage").open("sleeplog.log", "a").write(save);
|
|
|
|
|
|
|
|
// clear save string to free ram
|
|
|
|
save = undefined;
|
|
|
|
}
|
2022-02-11 08:29:02 +00:00
|
|
|
},
|
|
|
|
|
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
|
2024-03-07 23:08:48 +00:00
|
|
|
// 1 = not worn, 2 = awake, 3 = light sleep, 4 = deep sleep
|
2022-05-20 15:17:30 +00:00
|
|
|
data.status = Bangle.isCharging() ? 1 :
|
2024-04-01 15:27:15 +00:00
|
|
|
data.movement <= global.sleeplog.conf.deepTh ? 4 :
|
|
|
|
data.movement <= global.sleeplog.conf.lightTh ? 3 : 2;
|
2022-05-20 15:17:30 +00:00
|
|
|
|
2024-03-07 23:08:48 +00:00
|
|
|
// check if changing to deep sleep from non sleeping
|
2024-04-01 15:27:15 +00:00
|
|
|
if (data.status === 4 && global.sleeplog.status <= 2) {
|
|
|
|
global.sleeplog.checkIsWearing((isWearing, data) => {
|
2024-03-21 19:58:08 +00:00
|
|
|
// correct status
|
|
|
|
if (!isWearing) data.status = 1;
|
|
|
|
// set status
|
2024-04-01 15:27:15 +00:00
|
|
|
global.sleeplog.setStatus(data);
|
2024-03-21 19:58:08 +00:00
|
|
|
}, data);
|
2024-03-22 12:02:25 +00:00
|
|
|
} else {
|
|
|
|
// set status
|
2024-04-01 15:27:15 +00:00
|
|
|
global.sleeplog.setStatus(data);
|
2022-02-12 00:43:58 +00:00
|
|
|
}
|
2022-05-20 15:17:30 +00:00
|
|
|
},
|
|
|
|
|
2024-03-21 19:58:08 +00:00
|
|
|
// check wearing status either based on HRM or temperature as set in settings
|
2022-05-20 15:17:30 +00:00
|
|
|
checkIsWearing: function(returnFn, data) {
|
2024-03-22 16:59:09 +00:00
|
|
|
if (this.conf.wearTemp !== 19.5) {
|
2024-03-21 19:58:08 +00:00
|
|
|
return returnFn(!Bangle.isCharging() && E.getTemperature() >= this.conf.wearTemp, data);
|
|
|
|
}
|
|
|
|
|
2022-05-20 15:17:30 +00:00
|
|
|
// create a temporary object to store data and functions
|
2024-04-01 15:27:15 +00:00
|
|
|
const tmpWearingCheck = {
|
2024-03-22 12:22:31 +00:00
|
|
|
// define temporary hrm listener function to read the wearing status
|
|
|
|
hrmListener: hrm => tmpWearingCheck.isWearing = hrm.isWearing,
|
|
|
|
// set default wearing status
|
|
|
|
isWearing: false,
|
2022-05-20 15:17:30 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// enable HRM
|
|
|
|
Bangle.setHRMPower(true, "wearingCheck");
|
|
|
|
// wait until HRM is initialised
|
2024-04-01 15:27:15 +00:00
|
|
|
setTimeout((returnFn, data, tmpWearingCheck) => {
|
2022-05-20 15:17:30 +00:00
|
|
|
// add HRM listener
|
|
|
|
Bangle.on('HRM-raw', tmpWearingCheck.hrmListener);
|
|
|
|
// wait for two cycles (HRM working on 60Hz)
|
2024-04-01 15:27:15 +00:00
|
|
|
setTimeout((returnFn, data, tmpWearingCheck) => {
|
2022-05-20 15:17:30 +00:00
|
|
|
// remove listener and disable HRM
|
|
|
|
Bangle.removeListener('HRM-raw', tmpWearingCheck.hrmListener);
|
|
|
|
Bangle.setHRMPower(false, "wearingCheck");
|
|
|
|
// call return function with status
|
2024-04-01 15:27:15 +00:00
|
|
|
returnFn(tmpWearingCheck.isWearing, data);
|
|
|
|
}, 34, returnFn, data, tmpWearingCheck);
|
|
|
|
}, 2500, returnFn, data, tmpWearingCheck);
|
2022-05-20 15:17:30 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
2023-04-05 14:47:09 +00:00
|
|
|
// reset consecutive sleep if not worn
|
|
|
|
if (data.status === 1) this.consecutive = 1;
|
2022-05-20 15:17:30 +00:00
|
|
|
// 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-02-12 00:43:58 +00:00
|
|
|
}
|
|
|
|
}
|
2022-05-20 15:17:30 +00:00
|
|
|
|
|
|
|
// check if the status has changed
|
2022-11-16 21:46:49 +00:00
|
|
|
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
|
2022-11-17 09:04:57 +00:00
|
|
|
var time = new Date();
|
2022-11-16 22:26:23 +00:00
|
|
|
time = (time.getHours() * 60 + time.getMinutes()) * 60 * 1000;
|
2022-11-16 21:46:49 +00:00
|
|
|
// go through all triggers
|
|
|
|
triggers.forEach(key => {
|
|
|
|
// read entry to key
|
2023-04-05 14:47:09 +00:00
|
|
|
let entry = this.trigger[key];
|
|
|
|
// set from and to values to default if unset
|
|
|
|
let from = entry.from || 0;
|
|
|
|
let to = entry.to || 24 * 60 * 60 * 1000;
|
2022-11-16 21:46:49 +00:00
|
|
|
// check if the event matches the entries requirements
|
|
|
|
if (typeof entry.fn === "function" && (changed || !entry.onChange) &&
|
2023-04-05 14:47:09 +00:00
|
|
|
(from <= to ? from <= time && time <= to : time <= to || from <= time))
|
2022-11-16 21:46:49 +00:00
|
|
|
// and call afterwards with status data
|
2022-11-17 08:26:51 +00:00
|
|
|
setTimeout(entry.fn, 100, {
|
2022-11-16 21:46:49 +00:00
|
|
|
timestamp: new Date(data.timestamp),
|
2022-11-17 09:26:52 +00:00
|
|
|
status: data.status,
|
|
|
|
consecutive: data.consecutive,
|
|
|
|
prevStatus: data.status === this.status ? undefined : this.status,
|
|
|
|
prevConsecutive: data.consecutive === this.consecutive ? undefined : this.consecutive
|
2023-04-05 14:47:09 +00:00
|
|
|
}, (e => {delete e.fn; return e;})(entry.clone()));
|
2022-11-10 16:04:54 +00:00
|
|
|
});
|
2022-11-16 21:46:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// cache change into a known consecutive state
|
|
|
|
var changeIntoConsec = data.consecutive;
|
2022-09-12 21:15:39 +00:00
|
|
|
|
2022-11-16 21:46:49 +00:00
|
|
|
// 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;
|
|
|
|
}
|
2022-09-12 21:15:39 +00:00
|
|
|
|
2022-05-26 08:41:56 +00:00
|
|
|
// send status to gadgetbridge
|
|
|
|
var gb_kinds = "unknown,not_worn,activity,light_sleep,deep_sleep";
|
2023-06-19 18:19:32 +00:00
|
|
|
Bluetooth.println("");
|
2022-05-26 08:41:56 +00:00
|
|
|
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);
|
|
|
|
|
2022-05-26 09:27:36 +00:00
|
|
|
// 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();
|
|
|
|
}
|
2022-08-17 21:21:35 +00:00
|
|
|
// remove module from cache if cached
|
|
|
|
if (Modules.getCached().includes("sleeplog")) Modules.removeCached("sleeplog");
|
2022-05-26 09:27:36 +00:00
|
|
|
}
|
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() {
|
2022-05-26 09:27:36 +00:00
|
|
|
// 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();
|
2022-08-17 21:21:35 +00:00
|
|
|
// 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;
|
2022-09-12 21:15:39 +00:00
|
|
|
},
|
|
|
|
|
2022-11-16 21:46:49 +00:00
|
|
|
// define trigger object
|
|
|
|
trigger: {}
|
2024-04-01 15:27:15 +00:00
|
|
|
}, global.sleeplog);
|
2022-02-12 00:43:58 +00:00
|
|
|
|
2022-02-11 08:29:02 +00:00
|
|
|
// initial starting
|
|
|
|
global.sleeplog.start();
|
2022-05-20 15:17:30 +00:00
|
|
|
} else {
|
|
|
|
// clear global object from ram
|
|
|
|
delete global.sleeplog;
|
2022-02-11 08:29:02 +00:00
|
|
|
}
|