[sleeplog] Improving triggers and web interface

to 0.13:
- Prevent to stay in consecutive sleep if not worn.
- Correct calling triggers where last time is earlier then first time.
- Add the trigger object itself as argument to the fn function to be able to store data or functions inside the trigger object.

to 0.14:
improvements initiated by #2677
- Add a option to delete all logfiles before a specific date from the watch
- Correct regex file search from `\d\d\d\d` / `\d{4}` to `\d+`
pull/2679/head
Scharf 2023-04-05 16:47:09 +02:00
parent a33a6df463
commit 00a9db1bf0
6 changed files with 107 additions and 57 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

@ -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 |
@ -28,16 +28,16 @@ Logfiles are not removed on un-/reinstall to prevent data loss.
--- ---
#### Controls: #### Controls:
- __swipe left & right__ - __swipe left & right__
to change the displayed day to change the displayed day
- __touch the "title"__ (e.g. `Night to Fri 20/05/2022`) - __touch the "title"__ (e.g. `Night to Fri 20/05/2022`)
to enter day selection prompt to enter day selection prompt
- __touch the info area__ - __touch the info area__
to change the displayed information to change the displayed information
(by default: consecutive & true sleeping) (by default: consecutive & true sleeping)
- __touch the wrench__ (upper right corner) - __touch the wrench__ (upper right corner)
to enter the settings to enter the settings
- __use back button widget__ (upper left corner) - __use back button widget__ (upper left corner)
exit the app exit the app
#### View: #### View:
@ -55,42 +55,42 @@ Logfiles are not removed on un-/reinstall to prevent data loss.
### Settings Usage ### Settings Usage
--- ---
- __Thresholds__ submenu - __Thresholds__ submenu
Changes take effect from now on, not retrospective! Changes take effect from now on, not retrospective!
- __Max Awake__ | maximal awake duration - __Max Awake__ | maximal awake duration
_10min_ / _20min_ / ... / __60min__ / ... / _120min_ _10min_ / _20min_ / ... / __60min__ / ... / _120min_
- __Min Consecutive__ | minimal consecutive sleep duration - __Min Consecutive__ | minimal consecutive sleep duration
_10min_ / _20min_ / ... / __30min__ / ... / _120min_ _10min_ / _20min_ / ... / __30min__ / ... / _120min_
- __Deep Sleep__ | deep sleep threshold - __Deep Sleep__ | deep sleep threshold
_30_ / _31_ / ... / __100__ / ... / _200_ _30_ / _31_ / ... / __100__ / ... / _200_
- __Light Sleep__ | light sleep threshold - __Light Sleep__ | light sleep threshold
_100_ / _110_ / ... / __200__ / ... / _400_ _100_ / _110_ / ... / __200__ / ... / _400_
- __Reset to Default__ | reset to bold values above - __Reset to Default__ | reset to bold values above
- __BreakToD__ | time of day to break view - __BreakToD__ | time of day to break view
_0:00_ / _1:00_ / ... / __12:00__ / ... / _23:00_ _0:00_ / _1:00_ / ... / __12:00__ / ... / _23:00_
- __App Timeout__ | app specific lock timeout - __App Timeout__ | app specific lock timeout
__0s__ / _10s_ / ... / _120s_ __0s__ / _10s_ / ... / _120s_
- __Enabled__ | completely en-/disables the background service - __Enabled__ | completely en-/disables the background service
__on__ / _off_ __on__ / _off_
- __Debugging__ submenu - __Debugging__ submenu
- __View log__ | display logfile data - __View log__ | display logfile data
Select the logfile by its starting time. Select the logfile by its starting time.
Thresholds are shown as line with its value. Thresholds are shown as line with its value.
- __swipe left & right__ - __swipe left & right__
to change displayed duration to change displayed duration
- __swipe up & down__ - __swipe up & down__
to change displayed value range to change displayed value range
- __touch the graph__ - __touch the graph__
to change between light & dark colors to change between light & dark colors
- __use back button widget__ (upper left corner) - __use back button widget__ (upper left corner)
to go back to the logfile selection to go back to the logfile selection
- __Enabled__ | en-/disables debugging - __Enabled__ | en-/disables debugging
_on_ / __off__ _on_ / __off__
- __write File__ | toggles if a logfile is written - __write File__ | toggles if a logfile is written
_on_ / __off__ _on_ / __off__
- __Duration__ | duration for writing into logfile - __Duration__ | duration for writing into logfile
_1h_ / _2h_ / ... / __12h__ / _96_ _1h_ / _2h_ / ... / __12h__ / _96_
- The following data is logged to a csv-file: - The following data is logged to a csv-file:
_timestamp_ (in days since 1900-01-01 00:00 UTC used by office software) _, movement, status, consecutive, asleepSince, awakeSince, bpm, bpmConfidence_ _timestamp_ (in days since 1900-01-01 00:00 UTC used by office software) _, movement, status, consecutive, asleepSince, awakeSince, bpm, bpmConfidence_
@ -100,33 +100,40 @@ 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
--- ---
1. externally visible/usable timestamps (in `global.sleeplog`) are formatted as Bangle timestamps: 1. externally visible/usable timestamps (in `global.sleeplog`) are formatted as Bangle timestamps:
seconds since 1970-01-01 00:00 UTC seconds since 1970-01-01 00:00 UTC
2. internally used and logged (to `sleeplog.log (StorageFile)`) is within the highest available resolution: 2. internally used and logged (to `sleeplog.log (StorageFile)`) is within the highest available resolution:
10 minutes since 1970-01-01 00:00 UTC (`Bangle / (10 * 60 * 1000)`) 10 minutes since 1970-01-01 00:00 UTC (`Bangle / (10 * 60 * 1000)`)
3. debug .csv file ID (`sleeplog_123456.csv`) has a hourly resolution: 3. debug .csv file ID (`sleeplog_123456.csv`) has a hourly resolution:
hours since 1970-01-01 00:00 UTC (`Bangle / (60 * 60 * 1000)`) hours since 1970-01-01 00:00 UTC (`Bangle / (60 * 60 * 1000)`)
4. logged timestamps inside the debug .csv file are formatted for office calculation software: 4. logged timestamps inside the debug .csv file are formatted for office calculation software:
days since 1900-01-01 00:00 UTC (`Bangle / (24 * 60 * 60 * 1000) + 25569`) days since 1900-01-01 00:00 UTC (`Bangle / (24 * 60 * 60 * 1000) + 25569`)
5. every 14 days the `sleeplog.log (StorageFile)` is reduced and old entries are moved into separat files for each fortnight (`sleeplog_1234.log`) but still accessible though the app: 5. every 14 days the `sleeplog.log (StorageFile)` is reduced and old entries are moved into separat files for each fortnight (`sleeplog_1234.log`) but still accessible though the app:
fortnights since 1970-01-04 12:00 UTC (converted with `require("sleeplog").msToFn(Bangle)` and `require("sleeplog").fnToMs(fortnight)`) fortnights since 1970-01-04 12:00 UTC (converted with `require("sleeplog").msToFn(Bangle)` and `require("sleeplog").fnToMs(fortnight)`)
- __Logfiles from before 0.10:__ - __Logfiles from before 0.10:__
timestamps and sleeping status of old logfiles are automatically converted on your first consecutive sleep or manually by `require("sleeplog").convertOldLog()` timestamps and sleeping status of old logfiles are automatically converted on your first consecutive sleep or manually by `require("sleeplog").convertOldLog()`
- __View logged data:__ - __View logged data:__
if you'd like to view your logged data in the IDE, you can access it with `require("sleeplog").printLog(since, until)` or `require("sleeplog").readLog(since, until)` to view the raw data if you'd like to view your logged data in the IDE, you can access it with `require("sleeplog").printLog(since, until)` or `require("sleeplog").readLog(since, until)` to view the raw data
since & until in Bangle timestamp, e.g. `require("sleeplog").printLog(Date()-24*60*60*1000, Date())` for the last 24h since & until in Bangle timestamp, e.g. `require("sleeplog").printLog(Date()-24*60*60*1000, Date())` for the last 24h
@ -136,11 +143,11 @@ Available through the App Loader when your watch is connected.
--- ---
#### Access statistics #### Access statistics
- Last Asleep Time [Date]: - Last Asleep Time [Date]:
`Date(sleeplog.awakeSince)` `Date(sleeplog.awakeSince)`
- Last Awake Duration [ms]: - Last Awake Duration [ms]:
`Date() - sleeplog.awakeSince` `Date() - sleeplog.awakeSince`
- Last Statistics [object]: - Last Statistics [object]:
``` ```
// get stats of the last night (period as displayed inside the app) // get stats of the last night (period as displayed inside the app)
// as this might be the mostly used function the data is cached inside the global object // as this might be the mostly used function the data is cached inside the global object
@ -184,21 +191,25 @@ 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:
- timestamp: of the status change as date object, The passed __data__ object has the following properties:
- 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),
(0 = unknown, 1 = not worn, 2 = awake, 3 = light sleep, 4 = deep sleep) (0 = unknown, 1 = not worn, 2 = awake, 3 = light sleep, 4 = deep sleep)
- consecutive: value of the new status (0-2), - consecutive: value of the new status (0-2),
(0 = unknown, 1 = no consecutive sleep, 2 = consecutive sleep) (0 = unknown, 1 = no consecutive sleep, 2 = consecutive sleep)
- prevStatus: if changed the value of the previous status (0-4) else undefined, - prevStatus: if changed the value of the previous status (0-4) else undefined,
- 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
--- ---

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) {
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$/).forEach(file => {
date = (parseInt(file.match(/\\d+/)[0]) + 0.25) * 12096E5
print(date < ` + date + `)
})`, () => {
Util.hideModal();
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" filename="" date="">
<i class="icon icon-delete"></i>
</button>
</div>
</div>
</form>
</div>`; </div>`;
domTable.innerHTML = html; domTable.innerHTML = html;
Util.hideModal(); Util.hideModal();
@ -172,6 +203,7 @@ function viewFiles() {
if (task === "view") readLog(date, logData => viewLog(logData, filename)); if (task === "view") readLog(date, logData => viewLog(logData, filename));
else if (task === "csv") readLog(date, logData => saveCSV(logData, date, date + 12096E5)); else if (task === "csv") readLog(date, logData => saveCSV(logData, date, date + 12096E5));
else if (task === "del") deleteFile(filename, () => viewFiles()); else if (task === "del") deleteFile(filename, () => viewFiles());
else if (task === "delBefore") deleteBefore(document.getElementById("delBeforeDate").value);
}); });
} }
}); });

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",