diff --git a/apps/sleepsummary/ChangeLog b/apps/sleepsummary/ChangeLog
new file mode 100644
index 0000000000..1a3bc17573
--- /dev/null
+++ b/apps/sleepsummary/ChangeLog
@@ -0,0 +1 @@
+0.01: New app!
diff --git a/apps/sleepsummary/README.md b/apps/sleepsummary/README.md
new file mode 100644
index 0000000000..c8276f2dbe
--- /dev/null
+++ b/apps/sleepsummary/README.md
@@ -0,0 +1,65 @@
+# 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.
+## App
+This module provides an app where you can see in-depth details about your sleep. There are two pages:
+1. Sleep score and description
+2. Detailed stats about all scores, and time asleep and wakeup time.
+
+## 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
+- Use True Sleep - Whether or not to use True Sleep from sleeplog. If not checked, uses consecutive sleep instead
+- Show Message - 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)
+- Ideal Deep Sleep - How much deep sleep per night should qualify as a deep sleep score of 95 (more than this gives you 100)
+- Ideal Sleep Time - 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
diff --git a/apps/sleepsummary/Screenshot.png b/apps/sleepsummary/Screenshot.png
new file mode 100644
index 0000000000..7d38079a86
Binary files /dev/null and b/apps/sleepsummary/Screenshot.png differ
diff --git a/apps/sleepsummary/app.js b/apps/sleepsummary/app.js
new file mode 100644
index 0000000000..2417936b39
--- /dev/null
+++ b/apps/sleepsummary/app.js
@@ -0,0 +1,210 @@
+var Layout = require("Layout");
+// Load data once when the app starts
+var data = require("sleepsummary").getSummaryData();
+var score = data.overallSleepScore; // The overall score used for the bar graph
+var pageActive = 1; // Start on page 1
+var txtInfo="";
+// Convert milliseconds → 12-hour time string (H:MMa/p)
+function msToTimeStr(ms) {
+ ms = Math.round(ms);
+ let totalMins = Math.floor(ms / 60000);
+ let h = Math.floor(totalMins / 60) % 24;
+ let m = totalMins % 60;
+ let ampm = h >= 12 ? "p" : "a";
+
+ let hour12 = h % 12;
+ if (hour12 === 0) hour12 = 12;
+
+ let mm = m.toString().padStart(2, "0");
+
+ return `${hour12}:${mm}${ampm}`;
+}
+
+// Convert total minutes → H:MM time duration string
+function minsToTimeStr(mins) {
+ mins = Math.round(mins);
+ if (!mins || mins < 0) return "--:--";
+
+ let h = Math.floor(mins / 60);
+ let m = mins % 60;
+ let mm = m.toString().padStart(2,"0");
+ return `${h}:${mm}`;
+}
+
+// Custom renderer for the score bar
+function drawGraph(l) {
+ let w = 160;
+ let pad=3;
+ let currentScore = score;
+
+ g.setColor(g.theme.fg);
+ g.fillRect({x:l.x, y:l.y, w:w, h:12,r:1000}); // Draw background container
+ g.setColor("#808080");
+ g.fillRect({x:l.x+pad, y:l.y+pad, w:(w-(2*pad)), h:12-(pad*2),r:10000});
+
+ // Set color based on score (Green > Yellow > Orange > Red)
+ g.setColor("#0F0");
+ if(currentScore < 75) g.setColor("#FF0");
+ if(currentScore < 60) g.setColor("#FF8000");
+ if(currentScore < 40) g.setColor("#F00");
+
+ // Draw the score bar fill
+ g.fillRect({x:l.x+pad, y:l.y+pad, w:currentScore*((w-(2*pad))/100), h:12-(pad*2),r:10000});
+}
+
+
+if(data.avgWakeUpTime-data.wakeUpTime>20){
+ txtInfo+="You woke up earlier than usual today";
+}else if(data.avgWakeUpTime-data.wakeUpTime<-20){
+ txtInfo+="You woke up later than usual today";
+}else{
+ txtInfo+="You woke up around the same time as usual today";
+}
+if(score>90){
+ if(data.avgWakeUpTime-data.wakeUpTime<-20) txtInfo+=",and ";
+ else txtInfo+=",but ";
+ txtInfo+="Your sleep was likely to be restful and restorative"
+}else if(score<60){
+ if(data.avgWakeUpTime-data.wakeUpTime>20) txtInfo+=",and ";
+ else txtInfo+=",but ";
+ //difference in wakeup Time
+ txtInfo+="Your sleep was not likely to be restful"
+}else{
+
+ //difference in wakeup Time
+ txtInfo+=", and you likely had a moderately restorative sleep."
+}
+
+
+
+// Layout definition for Page 1 (Score)
+var page1Layout = new Layout({
+ type: "v",valign:1, c: [
+
+ {
+ type:"v", c: [
+ {type:undefined, height:5},
+ {type:"txt" ,filly:0, label:"Sleep Summary", font:"Vector:17", halign:0, id:"title",pad:3},
+ // Display initial score value
+ {type:"txt", label:`Sleep Score: ${score}%`, font:"9%", pad:5, id:"sleepScore"},
+ {type:"custom", render:drawGraph, height:15, width:165, id:"scoreBar",pad:7},
+ {type:undefined, height:37}, // spacer
+ {type:"txt", label:txtInfo, font:"8%", pad:5, id:"infoTxt",wrap:true,width:g.getWidth()-10},
+ {type:undefined, filly:1},
+ ]
+ }
+ ]
+});
+
+// Layout definition for Page 2 (Stats)
+var page2Layout = new Layout({
+ type: "v", c: [
+ {type:undefined, height:7}, // spacer
+ {type:"txt" ,filly:0, label:"Time Stats", font:"Vector:17", halign:0, id:"title",height:14,pad:1},
+ {
+ type:"v", c: [
+ {type:"h", c:[
+ {
+ type:"v", pad:3, c:[
+ {type:"txt", label:"", font:"8%",halign:1,pad:4},
+ {type:"txt", label:"Wk Up:", font:"8%",halign:1},
+ {type:"txt", label:"Sleep:", font:"8%",halign:1},
+ ]
+ },
+ {
+ type:"v", pad:3, c:[
+ {type:"txt", label:"Today", font:"8%",pad:4},
+ {type:"txt", label:"--:--", font:"8%", id:"todayWakeupTime"},
+ {type:"txt", label:"--:--", font:"8%", id:"todaySleepTime"},
+ ]
+ },
+ {
+ type:"v", pad:3, c:[
+ {type:"txt", label:"Avg", font:"8%",pad:4},
+ {type:"txt", label:"--:--", font:"8%", id:"avgWakeupTime"},
+ {type:"txt", label:"--:--", font:"8%", id:"avgSleepTime"},
+ ]
+ }
+ ]},
+ {type:"txt" ,filly:0, label:"Scores", font:"Vector:17", halign:0, id:"title",height:17,pad:1},
+ {type:"h", c:[
+ {
+ type:"v", pad:2, c:[
+ {type:"txt", label:"Wake Up:", font:"8%",halign:1},
+ {type:"txt", label:"Deep Sleep:", font:"8%",halign:1},
+ {type:"txt", label:"Duration:", font:"8%",halign:1}
+ ]
+ },
+ {
+ type:"v", pad:2, c:[
+ {type:"txt", label:"---", font:"8%",halign:1,id:"wkUpScore"},
+ {type:"txt", label:"---", font:"8%",halign:1,id:"deepSleepScore"},
+ {type:"txt", label:"---", font:"8%",halign:1,id:"durationScore"},
+ ]
+ }]}
+ ]
+ }
+ ]
+});
+
+
+
+
+
+
+
+
+function draw() {
+ g.clear();
+ Bangle.drawWidgets();
+
+ if(pageActive==1){
+ // Update the label and render Page 1
+ page1Layout.sleepScore.label=`Sleep Score: ${score}`;
+ page1Layout.infoTxt=txtInfo;
+
+ page1Layout.render();
+ }
+ else{
+ // Update all labels for Page 2
+ page2Layout.todayWakeupTime.label = msToTimeStr(data.wakeUpTime || 0);
+ page2Layout.avgWakeupTime.label = msToTimeStr(data.avgWakeUpTime || 0);
+ page2Layout.todaySleepTime.label = minsToTimeStr(data.sleepDuration || 0);
+
+ page2Layout.avgSleepTime.label = minsToTimeStr(data.avgSleepTime || 0);
+
+ page2Layout.wkUpScore.label = data.wkUpSleepScore || "---";
+ page2Layout.deepSleepScore.label = data.deepSleepScore || "---";
+ page2Layout.durationScore.label = data.durationSleepScore || "---";
+
+ page2Layout.render();
+ }
+}
+
+Bangle.on('swipe', (direction) => {
+ // direction == -1 is usually swipe right (next page)
+ if (direction == -1) {
+ if(pageActive!=2){
+ Bangle.buzz(40);
+ pageActive=2;
+ draw();
+ }
+ // direction == 1 is usually swipe left (previous page)
+ } else if (direction == 1) {
+ if(pageActive!=1){
+ Bangle.buzz(40);
+ pageActive=1;
+ draw();
+ }
+ }
+});
+
+// Initial draw, wrapped in a timeout
+setTimeout(draw,200);
+
+
+// Set up the app to behave like a clock
+Bangle.setUI("clock");
+delete Bangle.CLOCK;
+
+Bangle.loadWidgets();
diff --git a/apps/sleepsummary/app.png b/apps/sleepsummary/app.png
new file mode 100644
index 0000000000..f6742eb2ad
Binary files /dev/null and b/apps/sleepsummary/app.png differ
diff --git a/apps/sleepsummary/appicon.js b/apps/sleepsummary/appicon.js
new file mode 100644
index 0000000000..93a1fa8c32
--- /dev/null
+++ b/apps/sleepsummary/appicon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4kA///9PTxEF+PQ9td1P991lmN+mOG99tymerXXjPH/nl1U1DoJj/AH4AYgUiAAgXQCwoYQFwwABkAuPl10kVJzOeGBwuCz//4W///6C6PPpn2o1s+xIOC4W/lAsBl+yC6Ov34XB1+CC6Mq5X5GYKQPC4XkxfyJITAPB4N//dilf+C6AwBknia5sRiMQGAoAFFJAXGMIQWMC44AQC7dVqoXUgoXBqAXTCwIABgczABMwC60zC+x3DC6anDC6Ud7oAC6YX/C4IARC/4Xla4IAMgIX/C5oA/AH4ANA="))
diff --git a/apps/sleepsummary/boot.js b/apps/sleepsummary/boot.js
new file mode 100644
index 0000000000..e33eb0d52d
--- /dev/null
+++ b/apps/sleepsummary/boot.js
@@ -0,0 +1,93 @@
+ {
+ let getMsPastMidnight=function(unixTimestamp) {
+
+ 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 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");
+ Bangle.load("sleepsummarymsg.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().getDate();
+ 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 for the first time
+ require("sleepsummary").recordData();
+
+ if(settings.showMessage){
+ setTimeout(showSummary,settings.messageDelay)
+ }
+
+
+ }
+
+ }
+ }
+
+ //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"] = {
+ 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
+ };
+ }
+ }
diff --git a/apps/sleepsummary/metadata.json b/apps/sleepsummary/metadata.json
new file mode 100644
index 0000000000..2f0c44eab3
--- /dev/null
+++ b/apps/sleepsummary/metadata.json
@@ -0,0 +1,31 @@
+ {
+ "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":"sleepsummarymsg.app.js","url":"msgapp.js"},
+ {"name":"sleepsummary.img","url":"appicon.js","evaluate":true}
+ ],
+ "data":[
+ {"name":"sleepsummarydata.json"},
+ {"name":"sleepsummary.settings.json"},
+ {"name":"sleepsummarydatacache.json"}
+ ],
+ "screenshots":[
+ {"url":"Screenshot.png"}
+ ]
+}
diff --git a/apps/sleepsummary/module.js b/apps/sleepsummary/module.js
new file mode 100644
index 0000000000..4409bdb7f7
--- /dev/null
+++ b/apps/sleepsummary/module.js
@@ -0,0 +1,257 @@
+
+{
+ //Creator: RKBoss6
+ //The calculations used are very resource-heavy, so we calculate once, and offload to a cache for the day.
+
+ let getMsPastMidnight=function(unixTimestamp) {
+
+ 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;
+ };
+
+ let averageNumbers=function(runningAvg,totalCycles,newData){
+ return ((runningAvg*totalCycles)+newData)/(totalCycles+1);
+
+ };
+ let getDate=function(){
+ 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);
+ }
+
+ let getAvgData=function(){
+ return Object.assign({
+
+ avgSleepTime: 0,
+ totalCycles:0,
+ avgWakeUpTime:0,
+ promptLastShownDay:"",
+
+ }, require('Storage').readJSON("sleepsummarydata.json", true) || {});
+ }
+
+ let getSettings=function() {
+ return Object.assign({
+ useTrueSleep:true,
+ timeSinceAwake: 1800000,
+ showMessage:true,
+ deepSleepHours:5,
+ idealSleepHours:10,
+
+ }, require('Storage').readJSON("sleepsummary.settings.json", true) || {});
+ };
+ //l
+ let writeAvgData=function(data){
+ require("Storage").writeJSON("sleepsummarydata.json", data);
+
+ };
+
+ let deleteAvgData=function(){
+ require("Storage").erase("sleepsummarydata.json");
+ };
+
+ let getSleepData=function(){
+ var data=require("sleeplog").getStats(Date.now(), 24*60*60*1000);
+ var totalSleep=data.consecSleep;
+ if(getSettings().useTrueSleep) totalSleep=data.deepSleep+data.lightSleep;
+
+ return { calculatedAt: data.calculatedAt,
+ deepSleep: data.deepSleep,
+ lightSleep: data.lightSleep,
+ awakeSleep: data.awakeSleep,
+ consecSleep: data.consecSleep,
+ awakeTime: data.awakeTime,
+ notWornTime: data.notWornTime,
+ unknownTime: data.unknownTime,
+ logDuration: data.logDuration,
+ firstDate: data.firstDate,
+ lastDate: data.lastDate,
+ totalSleep: totalSleep,
+ trueSleep:data.deepSleep+data.lightSleep,
+ awakeSince:getMsPastMidnight(global.sleeplog.info.awakeSince)
+ };
+
+
+ }
+
+
+
+
+
+
+ // takes in an object with {score, weight}
+ function getWeightedScore(components) {
+ // sum of weights
+ let totalWeight = 0;
+ for (let key in components) totalWeight += components[key].weight;
+
+ // avoid division by zero
+ if (totalWeight === 0) return 0;
+
+ let score = 0;
+ for (let key in components) {
+ let s = components[key].score;
+ let w = components[key].weight;
+ score += (s * (w / totalWeight));
+ }
+
+ return Math.round(score);
+ }
+ let generateScore = function(value, target) {
+ if (value >= target) {
+ let extra = Math.min(1, (value - target) / target);
+ return Math.round(95 + extra * 5); // perfect = 95, max = 100
+ } else {
+ return Math.round((value / target) * 95);
+ }
+ }
+
+
+ let getSleepScore=function(){
+
+ var sleepData=getSleepData();
+ var settings=getSettings();
+ var summaryData=getAvgData();
+
+ //only if enabled in Health
+ //var hrmScore;
+
+ return getWeightedScore({
+ duration:
+ {score: generateScore(sleepData.consecSleep/60,settings.idealSleepHours), weight: 0.6},
+ deepSleep:
+ {score: generateScore(sleepData.deepSleep/60,settings.deepSleepHours), weight: 0.3},
+ averageSleep:
+ {score:generateScore(sleepData.totalSleep,summaryData.avgSleepTime),weight:0.15},
+ averageWakeup:
+ {score:generateScore(getMsPastMidnight(sleepData.awakeSince),summaryData.avgWakeUpTime),weight:0.1},
+ });
+ }
+
+
+
+
+
+ let getAllSleepScores=function(){
+ var data=getAvgData();
+ var sleepData=getSleepData();
+ var settings=getSettings();
+ return {
+ durationScore:generateScore(sleepData.consecSleep/60,settings.idealSleepHours),
+ deepSleepScore:generateScore(sleepData.deepSleep/60,settings.deepSleepHours),
+ avgWakeUpScore: generateScore(getMsPastMidnight(sleepData.awakeSince),data.avgWakeUpTime),
+ avgSleepTimeScore:generateScore(sleepData.totalSleep,data.avgSleepTime),
+ overallScore:getSleepScore(),
+ }
+ };
+ let writeCachedData=function(data){
+ require("Storage").writeJSON("sleepsummarydatacache.json",data);
+ }
+ let getCachedData=function(){
+ var data=Object.assign({
+
+ wakeUpTime:0,
+ overallSleepScore:0,
+ deepSleepScore:0,
+ wkUpSleepScore:0,
+ durationSleepScore:0,
+ consecSleep:0,
+ trueSleep:0,
+ dayLastUpdated:100,
+
+ }, require('Storage').readJSON("sleepsummarydatacache.json", true) || {});
+ data.sleepDuration=getSettings().useTrueSleep?data.trueSleep:data.consecSleep;
+ return data;
+ }
+
+ let calcAndCache=function(){
+ let today=new Date().getDate();
+ let scores=getAllSleepScores();
+ let slpData=getSleepData();
+ let cachedData=getCachedData();
+ //cache data
+ cachedData.overallSleepScore=scores.overallScore;
+ cachedData.deepSleepScore=scores.deepSleepScore;
+ cachedData.wkUpSleepScore=scores.avgWakeUpScore;
+ cachedData.durationSleepScore=scores.avgSleepTimeScore;
+ cachedData.consecSleep=slpData.consecSleep;
+ cachedData.trueSleep=slpData.trueSleep;
+ cachedData.dayLastUpdated=today;
+ cachedData.wakeUpTime=slpData.awakeSince;
+ writeCachedData(cachedData);
+
+
+
+ }
+
+
+ let getSummaryData=function(){
+
+
+ let avgData=getAvgData();
+ let cachedData=getCachedData()
+ let today=new Date().getDate();
+ // Check if data is up to date for today
+ if(cachedData.dayLastUpdated!=today){
+ //has not cached for today, do it now
+ calcAndCache();
+ //re-run this function to get the new data
+ cachedData= getCachedData();
+
+ }
+
+ // we now have up-to-date cache data, return merged cachedData and avgData to user
+
+ return Object.assign({}, avgData, cachedData);
+
+
+ }
+
+ function recordSleepStats(){
+ var today = new Date().getDate();
+ var sleepData=getSleepData();
+ var data=getAvgData();
+ //Wakeup time
+ var wakeUpTime=sleepData.awakeSince;
+ var avgWakeUpTime=averageNumbers(data.avgWakeUpTime,data.totalCycles,wakeUpTime);
+ data.avgWakeUpTime=avgWakeUpTime;
+ var timeRecorded=getDate();
+ //sleep time in minutes
+ var time=sleepData.totalSleep;
+
+
+ var avgSleepTime = averageNumbers(data.avgSleepTime, data.totalCycles, time);
+ data.avgSleepTime = avgSleepTime;
+
+ data.promptLastShownDay=today;
+ data.dateLastRecorded=timeRecorded;
+
+ data.totalCycles+=1;
+ writeAvgData(data);
+ // recalculate new data for cache
+ calcAndCache();
+ };
+
+ exports.deleteData = deleteAvgData;
+ exports.recalculate=calcAndCache;
+ exports.getSummaryData=getSummaryData;
+ exports.recordData=recordSleepStats;
+ exports.getSettings=getSettings;
+ exports.getSleepData=getSleepData;
+
+
+
+
+}
+
diff --git a/apps/sleepsummary/msgapp.js b/apps/sleepsummary/msgapp.js
new file mode 100644
index 0000000000..f8047c5f06
--- /dev/null
+++ b/apps/sleepsummary/msgapp.js
@@ -0,0 +1,43 @@
+//If the user has a timeout to return to the clock, this allows it to be ignored
+Bangle.setUI("clock");
+
+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";
+}
+
+let summaryData=require("sleepsummary").getSummaryData()
+let score=summaryData.overallSleepScore;
+var message=""; //"You slept for "+ formatTime(summaryData.sleepDuration/60)
+if(summaryData.avgWakeUpTime-summaryData.wakeUpTime>20){
+ message+="You woke up earlier than usual today";
+}else if(summaryData.avgWakeUpTime-summaryData.wakeUpTime<-20){
+ message+="You woke up later than usual today";
+}else{
+ message+="You woke up around the same time as usual today";
+}
+message+=", with a sleep score of "+score+".";
+
+
+E.showPrompt(message, {
+ title: "Good Morning!",
+ buttons: { "Dismiss": 1, "Open App":2},
+
+}).then(function (answer) {
+ if(answer==1){
+ Bangle.load();
+ }else{
+ Bangle.load("sleepsummary.app.js");
+ }
+});
+
diff --git a/apps/sleepsummary/settings.js b/apps/sleepsummary/settings.js
new file mode 100644
index 0000000000..97ffe921b6
--- /dev/null
+++ b/apps/sleepsummary/settings.js
@@ -0,0 +1,94 @@
+(function(back) {
+ var FILE = "sleepsummary.settings.json";
+ // Load settings
+ var settings = require("sleepsummary").getSettings();
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, settings);
+ }
+
+ E.showMenu({
+ "" : { "title" : "Sleep Summary" },
+ "< Back" : () => back(),
+ 'Use True Sleep': {
+ value: !!settings.useTrueSleep,
+ onchange: v => {
+ settings.useTrueSleep = v;
+ writeSettings();
+ }
+ },
+ 'Show Message': {
+ value: !!settings.showMessage,
+ onchange: v => {
+ settings.showMessage = v;
+ writeSettings();
+ }
+
+ },
+ 'Message Delay': {
+ value: 0|settings.messageDelay,
+ min: 0, max: 7200000,
+ step:5,
+ onchange: v => {
+ settings.messageDelay = v;
+ writeSettings();
+ },
+ format : v => {
+ let h = Math.floor(v/60000);
+ let m = v % 60;
+ let str = "";
+ if (h) str += h+"h";
+ if (m) str += " "+m+"m";
+ return str || "0m";
+ }
+ },
+ 'Ideal Deep Sleep': {
+ value: 0|settings.deepSleepHours*60,
+ min: 60, max: 600,
+ step:15,
+ onchange: v => {
+ settings.deepSleepHours = v/60;
+ writeSettings();
+ },
+ format : v => {
+ let h = Math.floor(v/60);
+ let m = v % 60;
+ let str = "";
+ if (h) str += h+"h";
+ if (m) str += " "+m+"m";
+ return str || "0m";
+ }
+ },
+ 'Ideal Sleep Time': {
+ value: 0|settings.idealSleepHours*60,
+ min: 120, max: 60*14 ,
+ step:15,
+ onchange: v => {
+ settings.idealSleepHours = v/60,
+ writeSettings();
+ },
+ format : v => {
+ let h = Math.floor(v/60);
+ let m = v % 60;
+ let str = "";
+ if (h) str += h+"h";
+ if (m) str += " "+m+"m";
+ return str || "0m";
+ }
+ },
+ 'Clear Data': function () {
+ E.showPrompt("Are you sure you want to delete all averaged data?", {title:"Confirmation"})
+ .then(function(v) {
+ if (v) {
+ require("sleepsummary").deleteData();
+ E.showAlert("Cleared data!",{title:"Cleared!"}).then(function(v) {eval(require("Storage").read("sleepsummary.settings.js"))(()=>load())});
+ } else {
+ eval(require("Storage").read("sleepsummary.settings.js"))(()=>load());
+
+ }
+ });
+ },
+ });
+
+})
+