Skip to content

Create Smart Battery Module #3944

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c37dce1
add base module
RKBoss6 Jul 23, 2025
0b39121
Create metadata.json
RKBoss6 Jul 23, 2025
255e34e
Create README.md
RKBoss6 Jul 23, 2025
0bcde8e
Add files via upload
RKBoss6 Jul 23, 2025
ebc31b5
Update module.js
RKBoss6 Jul 23, 2025
d4d4a7e
Update module.js
RKBoss6 Jul 23, 2025
276bf0c
add charging checks, made logging optional
RKBoss6 Jul 23, 2025
b170e9b
Fix for extra semicolon syntax error
RKBoss6 Jul 23, 2025
cb9b22a
Update module.js
RKBoss6 Jul 23, 2025
9081a02
Create ChangeLog
RKBoss6 Jul 24, 2025
746351d
Create settings.js
RKBoss6 Jul 24, 2025
27f2156
Update metadata.json
RKBoss6 Jul 24, 2025
49a2780
Update README.md
RKBoss6 Jul 24, 2025
143a7a4
Update README.md
RKBoss6 Jul 24, 2025
99f7987
Update metadata.json
RKBoss6 Jul 24, 2025
fdab17a
Delete log file in delete(), new algorithm for weighted average over …
RKBoss6 Jul 25, 2025
d05c0b1
Use log file dynamically as getSettings()
RKBoss6 Jul 25, 2025
3fcd5fe
Add checks for extreme fluctuation, log with human-readable date format
RKBoss6 Jul 27, 2025
0d2cf7b
Shift time logging into logEntry() function
RKBoss6 Jul 27, 2025
bde2e81
Only skip due to fluctuation if total cycles>=10, to let initial data…
RKBoss6 Jul 28, 2025
f4d5bca
Update README.md
RKBoss6 Jul 28, 2025
c97d817
Update README.md
RKBoss6 Jul 28, 2025
d28d762
Remove extreme fluctuation check, because of accuracy decline
RKBoss6 Aug 1, 2025
2901c34
Fix lint error, return more values in get()
RKBoss6 Aug 2, 2025
611fa88
Fix avgDrainage returning undefined
RKBoss6 Aug 2, 2025
a0dd97c
Create clkinfo.js from @RelapsingCertainly's gist, added avgDrainage …
RKBoss6 Aug 2, 2025
09f6803
Update metadata.json
RKBoss6 Aug 2, 2025
d132ab0
Update README.md for clockInfos, add @RelapsingCertainly to contributors
RKBoss6 Aug 2, 2025
2b56b2e
remove variables from memory, increase redraw time to conserve batter…
RKBoss6 Aug 2, 2025
a199abb
Update metadata.json
RKBoss6 Aug 2, 2025
9eaff57
Fix battery not drawing
RKBoss6 Aug 2, 2025
6aa2027
Merge branch 'espruino:master' into smartbatt-module
RKBoss6 Aug 3, 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
1 change: 1 addition & 0 deletions apps/smartbatt/ChangeLog
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v0.01: New app!
48 changes: 48 additions & 0 deletions apps/smartbatt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Smart Battery Module
A module for providing a truly accurate battery life in terms of days. The module learns from daily usage and drainage patterns, and extrapolates that. As you use it more, and the battery keeps draining, the predictions should become more accurate.

Because the Bangle.js battery percent fluctuates naturally, it is highly recomended to use the `Power Manager` app and enable monotonic/stable percentage to stabilize the percentage, and reduce fluctuations. This may help provide more accurate readings.
## Upon Install
Use an app that needs this module, like `Smart Battery Widget`.
When this app is installed, <i><b>do not rely on it for the first 24-30 hours.</b></i>
The module might return different data than expected, or a severely low prediction. Give it time. It will learn from drainage rates, which needs the battery to drain. If your watch normally lasts for a long time on one charge, it will take longer for the module to return an accurate reading.

If you think something is wrong with the predictions after 3 days, try clearing the data, and let it rebuild again from scratch.
## Clock Infos
The module provides two clockInfos:
- Days left
- Learned drainage rate per hour

## Settings
### Clear Data - Clears all learned data.
Use this when you switch to a new clock or change the battery drainage in a fundamental way. The app averages drainage over time, and so you might just want to restart the learned data to be more accurate for the new configurations you have implemented.
### Logging - Enables logging for stats that this module uses.
To view the log file, go to the [Web IDE](https://www.espruino.com/ide/#), click on the storage icon (4 discs), and scroll to the file named `smartbattlog.json`. From there, you can view the file, copy to editor, or save it to your computer.
Logs:
* The time in a human-readable format (hh:mm:ss, mm:dd:yy) when the record event was triggered
* The current battery percentage
* The last saved battery percentage
* The change in hours between the time last recorded and now
* The average or learned drainage for battery per hour
* The status of that record event:
* Recorded
* Skipped due to battery fluctuations or no change
* Invalid time between the two periods (first record)
## Functions
From any app, you can call `require("smartbatt")` and then one of the functions below:
* `require("smartbatt").record()` - Attempts to record the battery and push it to the average.
* `require("smartbatt").get()` - Returns an object that contains:


* `hrsRemaining` - Hours remaining
* `avgDrainage` - Learned battery drainage per hour
* `totalCycles` - Total times the battery has been recorded and averaged
* `totalHours` - Total hours recorded
* `batt` - Current battery level


* `require("smartbatt").deleteData()` - Deletes all learned data. (Automatically re-learns)
## Creator
- RKBoss6
## Contributors
- RelapsingCertainly
88 changes: 88 additions & 0 deletions apps/smartbatt/clkinfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
(function() {
var img;
var batt;
//updates values


function getHrsFormatted(hrsLeft){

var daysLeft = hrsLeft / 24;
daysLeft = Math.round(daysLeft);
if (Bangle.isCharging()) {
return batt+"%";
}
else if(daysLeft >= 1) {
return daysLeft+"d";
}

else {
return Math.round(hrsLeft)+"h";
}
}

//draws battery icon and fill bar
function drawBatt(){
batt =E.getBattery();
var s=24,g=Graphics.createArrayBuffer(24,24,1,{msb:true});
g.fillRect(0,6,s-3,18).clearRect(2,8,s-5,16).fillRect(s-2,10,s,15).fillRect(3,9,3+batt*(s-9)/100,15);
g.transparent=0;
img=g.asImage("string");
}

//calls both updates for values and icons.
//might split in the future since values updates once every five minutes so we dont need to call it in every minute while the battery can be updated once a minute.
function updateDisplay(){
drawBatt();
}

return {
name: "SmartBatt",
items: [
{ name : "BattStatus",
get : () => {

drawBatt();
var data=require("smartbatt").get();

//update clock info according to batt state
if (Bangle.isCharging()) {
return { text: batt+"%", img : img};
}
else{
return { text: getHrsFormatted(data.hrsLeft), img : img};
}
},

show : function() {
this.interval = setInterval(()=>{
updateDisplay();
this.emit('redraw');
}, 300000);
},

hide : function() {
clearInterval(this.interval);
this.interval = undefined;
}
},
{ name : "AvgDrainage",
get : () => {
drawBatt()
var data=require("smartbatt").get();
return { text: data.avgDrainage.toFixed(2)+"/h", img : img};
},

show : function() {
this.interval = setInterval(()=>{
this.emit('redraw');
}, 300000);
},

hide : function() {
clearInterval(this.interval);
this.interval = undefined;
}
}
]
};
})
Binary file added apps/smartbatt/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions apps/smartbatt/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"id": "smartbatt",
"name": "Smart Battery Module",
"shortName": "Smart Battery",
"version": "0.01",
"description": "Provides a `smartbatt` module that returns the battery in days, and learns from daily usage over time for accurate predictions.",
"icon": "icon.png",
"type": "module",
"tags": "tool,system,clkinfo",
"supports": ["BANGLEJS","BANGLEJS2"],
"provides_modules" : ["smartbatt"],
"readme": "README.md",
"storage": [
{"name":"smartbatt","url":"module.js"},
{"name":"smartbatt.settings.js","url":"settings.js"},
{"name":"smartbatt.clkinfo.js","url":"clkinfo.js"}
],
"data": [
{"name":"smartbatt.settings.json"}
]
}
136 changes: 136 additions & 0 deletions apps/smartbatt/module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
{
var dataFile = "smartbattdata.json";
var interval;
var storage = require("Storage");


var logFile = "smartbattlog.json";

function getSettings() {
return Object.assign({
//Record Interval stored in ms
doLogging: false
}, require('Storage').readJSON("smartbatt.settings.json", true) || {});
}

function logBatterySample(entry) {
let log = storage.readJSON(logFile, 1) || [];
//get human-readable time
let d = new Date();
entry.time = 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);

log.push(entry);
if (log.length > 100) log = log.slice(-100);

storage.writeJSON(logFile, log);
}


// Record current battery reading into current average
function recordBattery() {
let now = Date.now();
let data = getData();
let batt = E.getBattery();
let battChange = data.battLastRecorded - batt;
let deltaHours = (now - data.timeLastRecorded) / (1000 * 60 * 60);
// Default reason (in case we skip)
let reason = "Recorded";


if (battChange <= 0) {
reason = "Skipped: battery fluctuated or no change";
if (Math.abs(battChange) < 5) {
//less than 6% difference, average percents
var newBatt = (batt + data.battLastRecorded) / 2;
data.battLastRecorded = newBatt;
} else {
//probably charged, ignore average
data.battLastRecorded = batt;
}

storage.writeJSON(dataFile, data);
} else if (deltaHours <= 0 || !isFinite(deltaHours)) {
reason = "Skipped: invalid time delta";
data.timeLastRecorded = now;
data.battLastRecorded = batt;
storage.writeJSON(dataFile, data);
} else {
let weightCoefficient = 1;
let currentDrainage = battChange / deltaHours;
let newAvg = weightedAverage(data.avgBattDrainage, data.totalHours, currentDrainage, deltaHours * weightCoefficient);
data.avgBattDrainage = newAvg;
data.timeLastRecorded = now;
data.totalCycles += 1;
data.totalHours += deltaHours;
data.battLastRecorded = batt;
storage.writeJSON(dataFile, data);

reason = "Drainage recorded: " + currentDrainage.toFixed(3) + "%/hr";
}
if (getSettings().doLogging) {
// Always log the sample
logBatterySample({
battNow: batt,
battLast: data.battLastRecorded,
battChange: battChange,
deltaHours: deltaHours,
timeLastRecorded: data.timeLastRecorded,
avgDrainage: data.avgBattDrainage,
reason: reason
});
}
}

function weightedAverage(oldValue, oldWeight, newValue, newWeight) {
return (oldValue * oldWeight + newValue * newWeight) / (oldWeight + newWeight);
}



function getData() {
return storage.readJSON(dataFile, 1) || {
avgBattDrainage: 0,
battLastRecorded: E.getBattery(),
timeLastRecorded: Date.now(),
totalCycles: 0,
totalHours: 0,
};
}



// Estimate hours remaining
function estimateBatteryLife() {
let data = getData();
var batt = E.getBattery();
var hrsLeft = Math.abs(batt / data.avgBattDrainage);
return {
batt: batt,
hrsLeft: hrsLeft,
avgDrainage:data.avgBattDrainage,
totalHours:data.totalHours,
cycles:data.totalCycles
};
}

function deleteData() {
storage.erase(dataFile);
storage.erase(logFile);
}
// Expose public API
exports.record = recordBattery;
exports.deleteData = deleteData;
exports.get = estimateBatteryLife;
exports.changeInterval = function (newInterval) {
clearInterval(interval);
interval = setInterval(recordBattery, newInterval);
};
// Start recording every 5 minutes
interval = setInterval(recordBattery, 600000);
recordBattery(); // Log immediately
}
42 changes: 42 additions & 0 deletions apps/smartbatt/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

(function(back) {
var FILE = "smartbatt.settings.json";
// Load settings
var settings = Object.assign({
//Record Interval stored in ms
doLogging:false
}, require('Storage').readJSON(FILE, true) || {});

function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}

// Show the menu
E.showMenu({
"" : { "title" : "Smart Day Battery" },
"< Back" : () => back(),

'Clear Data': function () {
E.showPrompt("Are you sure you want to delete all learned data?", {title:"Confirm"})
.then(function(v) {
if (v) {
require("smartbatt").deleteData();
E.showMessage("Successfully cleared data!","Cleared");
} else {
eval(require("Storage").read("smartbatt.settings.js"))(()=>load());

}
});
},
'Log Battery': {
value: !!settings.doLogging, // !! converts undefined to false
onchange: v => {
settings.doLogging = v;
writeSettings();
}
// format: ... may be specified as a function which converts the value to a string
// if the value is a boolean, showMenu() will convert this automatically, which
// keeps settings menus consistent
},
});
})