Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bd077aa
Add sleep summary logging and notification functions
RKBoss6 Sep 15, 2025
5c10d3f
Create module.js
RKBoss6 Sep 15, 2025
4d331f5
Implement sleep summary display with custom layout
RKBoss6 Sep 15, 2025
a183ad2
Add metadata.json for Sleep Summary app
RKBoss6 Sep 15, 2025
52ba8df
Add files via upload
RKBoss6 Sep 15, 2025
62f32e0
Update metadata.json
RKBoss6 Sep 15, 2025
133cedd
Add README for sleepsummary module
RKBoss6 Sep 15, 2025
274ec8b
Add settings management for Sleep Summary app
RKBoss6 Sep 15, 2025
91111db
Add author field to metadata.json
RKBoss6 Sep 15, 2025
b8fc56f
Format README settings section with bold text
RKBoss6 Sep 15, 2025
a05a70c
Merge branch 'espruino:master' into Sleep-Summary
RKBoss6 Sep 15, 2025
d915f16
Add timeSinceAwake setting to sleep summary
RKBoss6 Sep 15, 2025
e9b659f
Add files via upload
RKBoss6 Sep 16, 2025
e3fea03
Add app icon entry to metadata.json
RKBoss6 Sep 16, 2025
9cce58e
Add appicon.js for sleep summary app
RKBoss6 Sep 16, 2025
3215939
Fix formatting in metadata.json
RKBoss6 Sep 16, 2025
7a1e0f0
Implement time conversion functions in app.js
RKBoss6 Sep 16, 2025
3c17a70
Refactor getMsPastMidnight and update sleep calculations
RKBoss6 Sep 16, 2025
2d5543a
Add condition to check awakeSince is not zero
RKBoss6 Sep 16, 2025
d115a2e
Revise ideal deep sleep and sleep time thresholds
RKBoss6 Sep 16, 2025
f818d22
Refactor UI setup for sleep summary app
RKBoss6 Sep 16, 2025
6bb869a
Add dependencies and provides_modules to metadata
RKBoss6 Sep 16, 2025
4ddf56a
Merge branch 'espruino:master' into Sleep-Summary
RKBoss6 Sep 17, 2025
beb5279
Merge branch 'espruino:master' into Sleep-Summary
RKBoss6 Sep 25, 2025
4b51a92
Merge branch 'espruino:master' into Sleep-Summary
RKBoss6 Oct 9, 2025
bcd5f02
Update settings.js
RKBoss6 Oct 9, 2025
427a194
Refactor sleep summary logging and awake checks
RKBoss6 Oct 9, 2025
655e175
Refactor sleep summary module for settings and scores
RKBoss6 Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions apps/sleepsummary/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Sleep Summary
This provides the module `sleepsummary`, which collects sleep data from `Sleep Log`, and generates a sleep score for you, based on average wakeup times, duration and more. The scores will generally trend higher in the first week that you use the module, however, the most accurate scores come the longer you use the module.

The module also comes with an app to see detailed statistics of your sleep compared to your averages prior.
All data is stored only on your device.

It is highly reccomended to use HRM with `sleeplog` for increased accuracy in sleep tracking, leading to a more accurate sleep score here. To enable this, turn on HRM intervals in the `Health` app.
## Formula

The module takes in several data points:
- How long you slept compared to your average
- Duration compared to ideal duration set in settings
- Deep sleep length compared to ideal deep sleep set in settings
- When you woke up compared to your average

The module then averages those individual scores with a weight added to get a score out of 100.
## Settings
- <b>Use True Sleep</b> - Whether or not to use True Sleep from sleeplog. If not checked, uses consecutive sleep instead
- <b>Show Message</b> - Whether or not to show a good morning message / prompt when you wake up (this may not be exactly when you wake up, depending on how accurate your settings for Sleeplog are)
- <b>Ideal Deep Sleep</b> - How much deep sleep per night should qualify as a deep sleep score of 95 (more than this gives you 100)
- <b>Ideal Sleep Time</b> - How much sleep per night should qualify as a sleep duration score of 95 (more than this gives you 100)

## Development
To use the module, do `require("sleepsummary")` followed by any function from the list below.

- `require("sleepsummary").recordData();` - Records current sleep data for the averages. It is best to only do this once a day, and is already automatically handled by the module.

- `require("sleepsummary").getSummaryData();` - Returns the following:
- `avgSleepTime` - The average time you sleep for, returned in minutes
- `totalCycles` - How many times that the average was calculated / recorded
- `avgWakeUpTime` - The average time you wake up at every day, returned in ms (milliseconds) past midnight
- `promptLastShownDay` - The day of the week that the good morning prompt was last shown (0-6). This is only used by the boot.js file as a way to know if it needs to show the prompt today

- `require("sleepsummary").getSleepData();` - Returns the following about this night's sleep (Most of the data comes directly from `require("sleeplog").getStats(Date.now(), 24*60*60*1000)`:
- `calculatedAt` - When the data was calculated
- `deepSleep` - How long in minutes you spent in deep sleep
- `lightSleep` - How long in minutes you spent in light sleep
- `awakeSleep` - How long you spent awake during sleep periods
- `consecSleep` - How long in minutes your consecutive sleep is
- `awakeTime` - How long you are awake for
- `notWornTime` - How long the watch was detected as not worn
- `unknownTime` - Time spent unknown
- `logDuration` - Time spent logging
- `firstDate` - Unix timestamp of the first log entry in the stats
- `lastDate`: Unix timestamp of the last log entry in the stats
- `totalSleep`: Total minutes of sleep for this night using consecutive sleep or true sleep, depending on what's selected in settings
- `awakeSince` - Time you woke up at, in ms past midnight

- `require("sleepsummary").getSleepScores();` - Returns the following sleep scores:
- `durationScore` - Sleep duration compared to ideal duration set in settings.
- `deepSleepScore` - Deep sleep length compared to ideal deep sleep set in settings
- `avgWakeUpScore` - When you woke up compared to your average
- `avgSleepTimeScore` - How long you slept compared to your average
- `overallScore` - The overall sleep score, calculated as a weighted average of all the other scores

- `require("sleepsummary").deleteData();` - Deletes learned data, automatically relearns the next time `recordData()` is called.


## Creator
RKBoss6
Binary file added apps/sleepsummary/Screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 108 additions & 0 deletions apps/sleepsummary/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
var Layout = require("Layout");
var sleepScore = require("sleepsummary").getSleepScores().overallScore;

// Convert unix timestamp (s or ms) → HH:MM
function msToTimeStr(ms) {
// convert ms → minutes
let totalMins = Math.floor(ms / 60000);
let h = Math.floor(totalMins / 60) % 24; // hours in 0–23
let m = totalMins % 60; // minutes
let ampm = h >= 12 ? "p" : "a";

// convert to 12-hour clock, where 0 → 12
let hour12 = h % 12;
if (hour12 === 0) hour12 = 12;

// pad minutes
let mm = m.toString().padStart(2, "0");

return `${hour12}:${mm}${ampm}`;
}

function minsToTimeStr(mins) {
let h = Math.floor(mins / 60) % 24; // hours 0–23
let m = mins % 60; // minutes 0–59
let mm = m.toString().padStart(2,"0");
return `${h}:${mm}`;
}



// Custom renderer for the battery bar
function drawGraph(l) {
let w = 160;
g.setColor(g.theme.fg);
g.drawRect(l.x, l.y, l.x+w, l.y+10); // outline
g.setColor("#00F")
if(g.theme.dark)g.setColor("#0F0");
g.fillRect(l.x, l.y, l.x+(sleepScore*1.65), l.y+10); // fill
}

// Layout definition
var pageLayout = new Layout({
type: "v", c: [
{type:undefined, height:7}, // spacer
{type:"txt",filly:0, label:"Sleep Summary", font:"Vector:17", halign:0, id:"title",height:17,pad:3},
{
type:"v", c: [
{type:"txt", label:"Sleep Score: --", font:"8%", pad:5, id:"sleepScore"},
{type:"custom", render:drawGraph, height:15, width:165, id:"scoreBar"},

{type:undefined, height:4}, // spacer
{type:"txt", label:"Time Stats", font:"9%"},
{type:"h", c:[
{
type:"v", pad:3, c:[
{type:"txt", label:"", font:"9%",halign:1,pad:4},
{type:"txt", label:"Wake Up:", font:"8%",halign:1},
{type:"txt", label:"Sleep:", font:"8%",halign:1},
]
},
{
type:"v", pad:3, c:[
{type:"txt", label:"Today", font:"9%",pad:4},
{type:"txt", label:"3:40a", font:"8%", id:"todayWakeupTime"},
{type:"txt", label:"7:40", font:"8%", id:"todaySleepTime"},
]
},
{
type:"v", pad:3, c:[
{type:"txt", label:"Avg", font:"9%",pad:4},
{type:"txt", label:"6:33a", font:"8%", id:"avgWakeupTime"},
{type:"txt", label:"7:54", font:"8%", id:"avgSleepTime"},
]
}
]}
]
}

]
}, {lazy:true});

// Update function
function draw() {
var sleepData=require("sleepsummary").getSleepData();
var data=require("sleepsummary").getSummaryData();
pageLayout.sleepScore.label = "Sleep score: "+sleepScore;
pageLayout.todayWakeupTime.label = msToTimeStr(sleepData.awakeSince);
pageLayout.avgWakeupTime.label = msToTimeStr(data.avgWakeUpTime);
pageLayout.todaySleepTime.label = minsToTimeStr(sleepData.totalSleep);
pageLayout.avgSleepTime.label = minsToTimeStr(data.avgSleepTime);
pageLayout.render();
}


// Initial draw
g.clear();
draw();


// We want this app to behave like a clock:
// i.e. show launcher when middle button pressed
Bangle.setUI("clock");
// But the app is not actually a clock
// This matters for widgets that hide themselves for clocks, like widclk or widclose
delete Bangle.CLOCK;

Bangle.loadWidgets();
Bangle.drawWidgets();
Binary file added apps/sleepsummary/app.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/sleepsummary/appicon.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

119 changes: 119 additions & 0 deletions apps/sleepsummary/boot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{
let getMsPastMidnight=function(unixTimestamp) {

Check warning on line 2 in apps/sleepsummary/boot.js

View workflow job for this annotation

GitHub Actions / build

'getMsPastMidnight' is assigned a value but never used

const dateObject = new Date(unixTimestamp);

const hours = dateObject.getHours();
const minutes = dateObject.getMinutes();
const seconds = dateObject.getSeconds();
const milliseconds = dateObject.getMilliseconds();

const msPastMidnight = (hours * 3600000) + (minutes * 60000) + (seconds * 1000) + milliseconds;
return msPastMidnight;
};
function formatTime(hours) {
let h = Math.floor(hours); // whole hours
let m = Math.round((hours - h) * 60); // leftover minutes

// handle rounding like 1.9999 → 2h 0m
if (m === 60) {
h += 1;
m = 0;
}

if (h > 0 && m > 0) return h + "h " + m + "m";
if (h > 0) return h + "h";
return m + "m";
}


function logNow(msg) {
let filename="sleepsummarylog.json";
let storage = require("Storage");

// load existing log (or empty array if file doesn't exist)
let log = storage.readJSON(filename,1) || [];

// get human-readable time
let d = new Date();
let timeStr = d.getFullYear() + "-" +
("0"+(d.getMonth()+1)).slice(-2) + "-" +
("0"+d.getDate()).slice(-2) + " " +
("0"+d.getHours()).slice(-2) + ":" +
("0"+d.getMinutes()).slice(-2) + ":" +
("0"+d.getSeconds()).slice(-2)+", MSG: "+msg;

// push new entry
log.push(timeStr);

// keep file from growing forever
if (log.length > 200) log = log.slice(-200);

// save back
storage.writeJSON(filename, log);
}

let showSummary=function(){
logNow("shown")
var sleepData=require("sleepsummary").getSleepData();
var sleepScore=require("sleepsummary").getSleepScores().overallSleepScore;
//sleepData.consecSleep
var message="You slept for "+ formatTime(sleepData.consecSleep/60) +", with a sleep score of "+sleepScore;

E.showPrompt(message, {
title: "Good Morning!",
buttons: { "Dismiss": 1, "Open App":2},

}).then(function (answer) {
if(answer==1){
Bangle.load();
}else{
load("sleepsummary.app.js");
}
});
}


function checkIfAwake(data,thisTriggerEntry){

logNow("checked, prev status: "+data.prevStatus+", current status: "+data.status+", promptLastShownDay: "+require("sleepsummary").getSummaryData().promptLastShownDay);

let today = new Date().getDay();
if(require("sleepsummary").getSummaryData().promptLastShownDay!=today){
//if coming from sleep
if (data.status==2&&(data.previousStatus==3||data.previousStatus==4)) {
var settings=require("sleepsummary").getSettings();

//woke up
if(settings.showMessage){
setTimeout(showSummary,settings.messageDelay)
}

require("sleepsummary").recordData();
}

}
}

//Force-load module
require("sleeplog");

// first ensure that the sleeplog trigger object is available (sleeplog is enabled)
if (typeof (global.sleeplog || {}).trigger === "object") {
// then add your parameters with the function to call as object into the trigger object
sleeplog.trigger["sleepsummary"] = {

Check warning on line 104 in apps/sleepsummary/boot.js

View workflow job for this annotation

GitHub Actions / build

'sleeplog' is not defined
onChange: true, // false as default, if true call fn only on a status change
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
// reference time to from & to is rounded to full minutes
fn: function(data, thisTriggerEntry) {

checkIfAwake(data,thisTriggerEntry);



} // function to be executed
};
}

}
29 changes: 29 additions & 0 deletions apps/sleepsummary/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"id": "sleepsummary",
"name": "Sleep Summary",
"shortName": "Sleep Summ.",
"author":"RKBoss6",
"version": "0.01",
"description": "Adds a module that learns sleeping habits over time and provides a sleep score based on how good of a sleep you got",
"icon": "app.png",
"type": "app",
"tags": "health",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"dependencies" : { "sleeplog":"app" },
"provides_modules" : ["sleepsummary"],
"storage": [
{"name":"sleepsummary.app.js","url":"app.js"},
{"name":"sleepsummary.boot.js","url":"boot.js"},
{"name":"sleepsummary","url":"module.js"},
{"name":"sleepsummary.settings.js","url":"settings.js"},
{"name":"sleepsummary.img","url":"appicon.js","evaluate":true}
],
"data":[
{"name":"sleepsummarydata.json"},
{"name":"sleepsummary.settings.json"}
],
"screenshots":[
{"url":"Screenshot.png"}
]
}
Loading
Loading