commit
1ff81628e8
|
|
@ -0,0 +1 @@
|
|||
v0.01: New app!
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
(function() {
|
||||
var batt;
|
||||
//updates values
|
||||
|
||||
|
||||
function getHrsFormatted(hrsLeft){
|
||||
|
||||
var daysLeft = hrsLeft / 24;
|
||||
daysLeft = Math.round(daysLeft);
|
||||
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;
|
||||
return 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 : () => {
|
||||
|
||||
var img = drawBatt();
|
||||
var data=require("smartbatt").get();
|
||||
|
||||
//update clock info according to batt state
|
||||
if (Bangle.isCharging()) {
|
||||
return { text: batt+"%", img };
|
||||
}
|
||||
else{
|
||||
return { text: getHrsFormatted(data.hrsLeft), img };
|
||||
}
|
||||
},
|
||||
|
||||
show : function() {
|
||||
this.interval = setInterval(()=>{
|
||||
updateDisplay();
|
||||
this.emit('redraw');
|
||||
}, 300000);
|
||||
},
|
||||
|
||||
hide : function() {
|
||||
clearInterval(this.interval);
|
||||
this.interval = undefined;
|
||||
}
|
||||
},
|
||||
{ name : "AvgDrainage",
|
||||
get : () => {
|
||||
var img = drawBatt()
|
||||
var data=require("smartbatt").get();
|
||||
return { text: data.avgDrainage.toFixed(2)+"/h", img };
|
||||
},
|
||||
|
||||
show : function() {
|
||||
this.interval = setInterval(()=>{
|
||||
this.emit('redraw');
|
||||
}, 300000);
|
||||
},
|
||||
|
||||
hide : function() {
|
||||
clearInterval(this.interval);
|
||||
this.interval = undefined;
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 403 KiB |
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
});
|
||||
})
|
||||
Loading…
Reference in New Issue