1
0
Fork 0

Merge pull request #2679 from storm64/sleeplog

[sleeplog] Improving triggers and web interface
master
Gordon Williams 2023-04-12 10:28:07 +01:00 committed by GitHub
commit 14cc283679
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 87 additions and 31 deletions

View File

@ -7,4 +7,6 @@
0.10: Complete rework off this app! 0.10: Complete rework off this app!
0.10beta: Add interface.html to view saved log data, add "View log" function for debugging log, send data for gadgetbridge, change caching for global getStats 0.10beta: Add interface.html to view saved log data, add "View log" function for debugging log, send data for gadgetbridge, change caching for global getStats
0.11: Prevent module not found error 0.11: Prevent module not found error
0.12: Improve README, option to add functions triggered by status changes or time periods, remove old log (<0.10) conversion 0.12: Improve README, option to add functions triggered by status changes or time periods, remove old log (<0.10) conversion
0.13: Prevent to stay in consecutive sleep if not worn, correct trigger calling, add trigger object itself as argument to the fn function
0.14: Add "Delete all logfiles before" to interface.html, display all logfiles in the interface

View File

@ -7,11 +7,11 @@ This app logs and displays the following states:
It is using the built in movement calculation to decide your sleeping state. While charging it is assumed that you are not wearing the watch and if the status changes to _deep sleep_ the internal heartrate sensor is used to detect if you are wearing the watch. It is using the built in movement calculation to decide your sleeping state. While charging it is assumed that you are not wearing the watch and if the status changes to _deep sleep_ the internal heartrate sensor is used to detect if you are wearing the watch.
#### Explanations #### Explanations
* __Detection of Sleep__ * __Detection of Sleep__
The movement value of bangle's build in health event that is triggered every 10 minutes is checked against the thresholds for light and deep sleep. If the measured movement is lower or equal to the __Deep Sleep__-threshold a deep sleep phase is detected for the last 10 minutes. If the threshold is exceeded but not the __Light Sleep__-threshold than the last timeperiod is detected as light sleep phase. On exceeding even this threshold it is assumed that you were awake. The movement value of bangle's build in health event that is triggered every 10 minutes is checked against the thresholds for light and deep sleep. If the measured movement is lower or equal to the __Deep Sleep__-threshold a deep sleep phase is detected for the last 10 minutes. If the threshold is exceeded but not the __Light Sleep__-threshold than the last timeperiod is detected as light sleep phase. On exceeding even this threshold it is assumed that you were awake.
* __True Sleep__ * __True Sleep__
The true sleep value is a simple addition of all registered sleeping periods. The true sleep value is a simple addition of all registered sleeping periods.
* __Consecutive Sleep__ * __Consecutive Sleep__
In addition the consecutive sleep value tries to predict the complete time you were asleep, even the very light sleeping periods when an awake period is detected based on the registered movements. All periods after a sleeping period will be summarized until the first following non sleeping period that is longer then the maximal awake duration (__Max Awake__). If this sum is lower than the minimal consecutive sleep duration (__Min Consecutive__) it is not considered, otherwise it will be added to the consecutive sleep value. In addition the consecutive sleep value tries to predict the complete time you were asleep, even the very light sleeping periods when an awake period is detected based on the registered movements. All periods after a sleeping period will be summarized until the first following non sleeping period that is longer then the maximal awake duration (__Max Awake__). If this sum is lower than the minimal consecutive sleep duration (__Min Consecutive__) it is not considered, otherwise it will be added to the consecutive sleep value.
Logfiles are not removed on un-/reinstall to prevent data loss. Logfiles are not removed on un-/reinstall to prevent data loss.
@ -19,7 +19,7 @@ Logfiles are not removed on un-/reinstall to prevent data loss.
| Filename (* _example_) | Content | Removeable in | | Filename (* _example_) | Content | Removeable in |
|------------------------------|-----------------|-------------------| |------------------------------|-----------------|-------------------|
| `sleeplog.log (StorageFile)` | recent logfile | App Web Interface | | `sleeplog.log (StorageFile)` | recent logfile | App Web Interface |
| `sleeplog_1234.log`* | old logfiles | App Web Interface | | `sleeplog_1234.log`* | past logfiles | App Web Interface |
| `sleeplog_123456.csv`* | debugging files | Web IDE | | `sleeplog_123456.csv`* | debugging files | Web IDE |
@ -100,13 +100,20 @@ Logfiles are not removed on un-/reinstall to prevent data loss.
Available through the App Loader when your watch is connected. Available through the App Loader when your watch is connected.
- __view data__ - A list of all found logfiles with following options for each file:
Display the data to each timestamp in a table. - __view data__
- __save csv-file__ Display the data to each timestamp in a table.
Download a csv-file with the data to each timestamp. - __save csv-file__
The time format is chooseable beneath the file list. Download a csv-file with the data to each timestamp.
- __delete file__ The time format is chooseable beneath the file list.
Deletes the logfile from the watch. __Please backup your data first!__ - __delete file__
Deletes the logfile from the watch. __Please backup your data first!__
- __csv time format__
__JavaScript (milliseconds since 1970)__ /
_UNIX (seconds since 1970)_ /
_Office (days since 1900)_
- __delete all logfiles before__
Deletes all logfile before the given date from the watch. __Please backup your data first!__
--- ---
### Timestamps and Files ### Timestamps and Files
@ -184,11 +191,12 @@ if (typeof (global.sleeplog || {}).trigger === "object") {
from: 0, // 0 as default, in ms, first time fn will be called from: 0, // 0 as default, in ms, first time fn will be called
to: 24*60*60*1000, // 24h as default, in ms, last time fn will be called to: 24*60*60*1000, // 24h as default, in ms, last time fn will be called
// reference time to from & to is rounded to full minutes // reference time to from & to is rounded to full minutes
fn: function(data) { print(data); } // function to be executed fn: function(data, thisTriggerEntry) { print(data); } // function to be executed
}; };
} }
``` ```
The passed data object has the following properties:
The passed __data__ object has the following properties:
- timestamp: of the status change as date object, - timestamp: of the status change as date object,
(should be around 10min. before "now", the actual call of the function) (should be around 10min. before "now", the actual call of the function)
- status: value of the new status (0-4), - status: value of the new status (0-4),
@ -199,10 +207,16 @@ The passed data object has the following properties:
- prevConsecutive: if changed the value of the previous status (0-2) else undefined - prevConsecutive: if changed the value of the previous status (0-2) else undefined
If you want to use other variables or functions from the trigger object inside the trigger fn function, you will find them inside the __thisTriggerEntry__ object, as the this keyword is not working in this scenario. The function itself (the fn property) is not passed inside the thisTriggerEntry object.
--- ---
### Worth Mentioning ### Worth Mentioning
--- ---
#### To do list #### To do list
- Optimize interface.html:
- Open logfile through require("Storage") instead of require("sleeplog").
- Give feedback how much files have been deleted on "delete all logfiles before".
- Check translations. - Check translations.
- Add more functionallities to interface.html. - Add more functionallities to interface.html.
- Enable receiving data on the Gadgetbridge side + testing. - Enable receiving data on the Gadgetbridge side + testing.

View File

@ -235,6 +235,8 @@ if (sleeplog.conf.enabled) {
// reset consecutive status // reset consecutive status
data.consecutive = 0; data.consecutive = 0;
} }
// reset consecutive sleep if not worn
if (data.status === 1) this.consecutive = 1;
// check if consecutive unknown // check if consecutive unknown
if (!this.consecutive) { if (!this.consecutive) {
// check if long enough asleep or too long awake // check if long enough asleep or too long awake
@ -265,10 +267,13 @@ if (sleeplog.conf.enabled) {
// go through all triggers // go through all triggers
triggers.forEach(key => { triggers.forEach(key => {
// read entry to key // read entry to key
var entry = this.trigger[key]; 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;
// check if the event matches the entries requirements // check if the event matches the entries requirements
if (typeof entry.fn === "function" && (changed || !entry.onChange) && if (typeof entry.fn === "function" && (changed || !entry.onChange) &&
(entry.from || 0) <= time && (entry.to || 24 * 60 * 60 * 1000) >= time) (from <= to ? from <= time && time <= to : time <= to || from <= time))
// and call afterwards with status data // and call afterwards with status data
setTimeout(entry.fn, 100, { setTimeout(entry.fn, 100, {
timestamp: new Date(data.timestamp), timestamp: new Date(data.timestamp),
@ -276,7 +281,7 @@ if (sleeplog.conf.enabled) {
consecutive: data.consecutive, consecutive: data.consecutive,
prevStatus: data.status === this.status ? undefined : this.status, prevStatus: data.status === this.status ? undefined : this.status,
prevConsecutive: data.consecutive === this.consecutive ? undefined : this.consecutive prevConsecutive: data.consecutive === this.consecutive ? undefined : this.consecutive
}); }, (e => {delete e.fn; return e;})(entry.clone()));
}); });
} }

View File

@ -38,7 +38,7 @@ function viewLog(logData, filename) {
<td>` + <td>` +
new Date(entry[0] * 6E5).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}) + ` new Date(entry[0] * 6E5).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}) + `
</td> </td>
<td style="text-align: right"><div class="container"` + <td style="text-align: right"><div class="container"` +
(duration >= 60 ? ` style="background-color: hsl(192, 50%, ` + (duration >= 60 ? ` style="background-color: hsl(192, 50%, ` +
(duration > 480 ? 50 : 100 - Math.floor(duration / 60) * 50 / 8) + (duration > 480 ? 50 : 100 - Math.floor(duration / 60) * 50 / 8) +
`%)">` : `>`) + `%)">` : `>`) +
@ -84,7 +84,7 @@ function readLog(date, callback) {
function deleteFile(filename, callback) { function deleteFile(filename, callback) {
if (window.confirm("Do you really want to remove " + filename)) { if (window.confirm("Do you really want to remove " + filename)) {
Util.showModal("Deleting..."); Util.showModal("Deleting " + filename + " ...");
if (filename.endsWith(" (StorageFile)")) { if (filename.endsWith(" (StorageFile)")) {
Util.eraseStorageFile(filename, () => { Util.eraseStorageFile(filename, () => {
Util.hideModal(); Util.hideModal();
@ -99,15 +99,29 @@ function deleteFile(filename, callback) {
} }
} }
function deleteBefore(dateString, callback) {
date = new Date(dateString);
if (window.confirm("Do you really want to remove all data before " + date.toLocaleDateString(undefined))) {
Util.showModal("Deleting all data before" + date.toLocaleDateString(undefined) + " ...");
Puck.eval(`require("Storage").list(/^sleeplog_\\d+.log$/)` +
`.filter(file => (parseInt(file.match(/\\d+/)[0]) + 0.25) * 12096E5 < ` + date.valueOf() + ` - 12096E5)` +
`.map(file => require("Storage").erase(file)).length`, count => {
Util.hideModal();
window.alert(count + " files deleted");
callback();
})
}
}
function viewFiles() { function viewFiles() {
Util.showModal("Loading..."); Util.showModal("Loading...");
domTable.innerHTML = ""; domTable.innerHTML = "";
Puck.eval(`require("Storage").list(/^sleeplog_\\d\\d\\d\\d\\.log$/)`, files => { Puck.eval(`require("Storage").list(/^sleeplog_\\d+.log$/)`, files => {
// add active log // add active log
files.push("" + Math.floor(Date.now() / 12096E5 - 0.25)); files.push("" + Math.floor(Date.now() / 12096E5 - 0.25));
files = files.map(file => { return { files = files.map(file => { return {
filename: file.length === 4 ? "sleeplog.log (StorageFile)" : file, filename: file.length === 4 ? "sleeplog.log (StorageFile)" : file,
date: (parseInt(file.match(/\d{4}/)[0]) + 0.25) * 12096E5 date: (parseInt(file.match(/\d+/)[0]) + 0.25) * 12096E5
}}); }});
files = files.sort((a, b) => a.date - b.date); files = files.sort((a, b) => a.date - b.date);
var html = ` var html = `
@ -146,10 +160,10 @@ function viewFiles() {
<div class="container"> <div class="container">
<form class="form-horizontal"> <form class="form-horizontal">
<div class="form-group"> <div class="form-group">
<div class="col-3 col-sm-12"> <div class="col-sm-12">
<label class="form-label"><b>csv time format</b></label> <label class="form-label"><b>csv time format</b></label>
</div> </div>
<div class="col-9 col-sm-12"> <div class="col-sm-12">
<select class="form-select" id="csvTime"> <select class="form-select" id="csvTime">
<option>JavaScript (milliseconds since 1970)</option> <option>JavaScript (milliseconds since 1970)</option>
<option>UNIX (seconds since 1970)</option> <option>UNIX (seconds since 1970)</option>
@ -158,6 +172,23 @@ function viewFiles() {
</div> </div>
</div> </div>
</form> </form>
</div>
<div class="container">
<form class="form-horizontal">
<div class="form-group">
<div class="col-sm-12">
<label class="form-label"><b>Delete all logfiles before</b></label>
</div>
<div class="col-sm-10">
<input class="form-input" id="delBeforeDate" type="date" value="2022-01-01">
</div>
<div class="col-mx-auto">
<button class="btn tooltip btn-error" data-tooltip="delete old files" task="delBefore">
<i class="icon icon-delete"></i>
</button>
</div>
</div>
</form>
</div>`; </div>`;
domTable.innerHTML = html; domTable.innerHTML = html;
Util.hideModal(); Util.hideModal();
@ -166,12 +197,16 @@ function viewFiles() {
buttons[i].addEventListener("click", event => { buttons[i].addEventListener("click", event => {
var button = event.currentTarget; var button = event.currentTarget;
var task = button.getAttribute("task"); var task = button.getAttribute("task");
var filename = button.getAttribute("filename"); if (task === "delBefore") {
var date = button.getAttribute("date") - 0; deleteBefore(document.getElementById("delBeforeDate").value, () => viewFiles());
if (!task || !filename || !date) return; } else {
if (task === "view") readLog(date, logData => viewLog(logData, filename)); var filename = button.getAttribute("filename");
else if (task === "csv") readLog(date, logData => saveCSV(logData, date, date + 12096E5)); var date = button.getAttribute("date") - 0;
else if (task === "del") deleteFile(filename, () => viewFiles()); if (!task || !filename || !date) return;
if (task === "view") readLog(date, logData => viewLog(logData, filename));
else if (task === "csv") readLog(date, logData => saveCSV(logData, date, date + 12096E5));
else if (task === "del") deleteFile(filename, () => viewFiles());
}
}); });
} }
}); });

View File

@ -123,7 +123,7 @@ exports = {
// free ram // free ram
files = undefined; files = undefined;
// check if log from files is available // check if log from files is available
if (filesLog.length) { if (filesLog.length) {
// remove unwanted entries // remove unwanted entries
filesLog = filesLog.filter((entry, index, filesLog) => ( filesLog = filesLog.filter((entry, index, filesLog) => (

View File

@ -2,7 +2,7 @@
"id":"sleeplog", "id":"sleeplog",
"name":"Sleep Log", "name":"Sleep Log",
"shortName": "SleepLog", "shortName": "SleepLog",
"version": "0.12", "version": "0.14",
"description": "Log and view your sleeping habits. This app is using the built in movement calculation.", "description": "Log and view your sleeping habits. This app is using the built in movement calculation.",
"icon": "app.png", "icon": "app.png",
"type": "app", "type": "app",