Merge branch 'master' into add_weather.json_support_to_mtnclock

pull/2561/head
Sam Sorensen 2023-02-05 21:54:20 -07:00
commit e33db79ab3
17 changed files with 684 additions and 4 deletions

View File

@ -3,3 +3,4 @@
0.03: Use default Bangle formatter for booleans
0.04: Remove calibration with current voltage (Calibrate->Auto) as it is now handled by settings app
Allow automatic calibration on every charge longer than 3 hours
0.05: Add back button to settings menu.

View File

@ -2,7 +2,7 @@
"id": "powermanager",
"name": "Power Manager",
"shortName": "Power Manager",
"version": "0.04",
"version": "0.05",
"description": "Allow configuration of warnings and thresholds for battery charging and display.",
"icon": "app.png",
"type": "bootloader",

View File

@ -23,6 +23,7 @@
'': {
'title': 'Power Manager'
},
"< Back" : back,
'Monotonic percentage': {
value: !!settings.forceMonoPercentage,
onchange: v => {

223
apps/sched/interface.html Normal file
View File

@ -0,0 +1,223 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
<script src="../../core/lib/interface.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ical.js/0.0.3/ical.min.js"></script>
<script>
let dataElement = document.getElementById("data");
let alarms;
let schedSettings;
function readFile(input) {
document.getElementById('upload').disabled = true;
const offsetMinutes = document.getElementById("offsetMinutes").value;
for(let i=0; i<input.files.length; i++) {
const reader = new FileReader();
reader.addEventListener("load", () => {
const jCalData = ICAL.parse(reader.result);
const comp = new ICAL.Component(jCalData[1]);
// Fetch the VEVENT part
comp.getAllSubcomponents('vevent').forEach(vevent => {
event = new ICAL.Event(vevent);
const exists = alarms.some(alarm => alarm.id === event.uid);
const alarm = eventToAlarm(event, offsetMinutes*60*1000);
renderAlarm(alarm, exists);
if (exists) {
alarms = alarms.filter(alarm => alarm.id !== event.uid); // remove if already exists
const tr = document.querySelector(`.event-row[data-uid='${event.uid}']`);
document.getElementById('events').removeChild(tr);
}
alarms.push(alarm);
});
}, false);
reader.readAsText(input.files[i], "UTF-8");
}
}
function dateToMsSinceMidnight(date) {
const dateMidnight = new Date(date);
dateMidnight.setHours(0,0,0,0);
return date - dateMidnight;
}
function dateFromAlarm(alarm) {
const date = new Date(alarm.date);
return new Date(date.getTime() + alarm.t);
}
function getAlarmDefaults() {
const date = new Date();
return {
on: true,
t: dateToMsSinceMidnight(date),
dow: 127,
date: date.toISOString().substring(0,10),
last: 0,
rp: "defaultRepeat" in schedSettings ? schedSettings.defaultRepeat : false,
vibrate: "defaultAlarmPattern" in schedSettings ? schedSettings.defaultAlarmPattern : "::",
as: false,
};
}
function eventToAlarm(event, offsetMs) {
const dateOrig = event.startDate.toJSDate();
const date = offsetMs ? new Date(dateOrig - offsetMs) : dateOrig;
const alarm = {...getAlarmDefaults(), ...{
id: event.uid,
msg: event.summary,
t: dateToMsSinceMidnight(date),
date: date.toISOString().substring(0,10),
data: {end: event.endDate.toJSDate().toISOString()}
}};
if (offsetMs) { // Alarm time is not real event time, so do a backup
alarm.data.time = dateOrig.toISOString();
}
return alarm;
}
function upload() {
Util.showModal("Saving...");
Util.writeStorage("sched.json", JSON.stringify(alarms), () => {
location.reload(); // reload so we see current data
});
}
function renderAlarm(alarm, exists) {
const localDate = dateFromAlarm(alarm);
const tr = document.createElement('tr');
tr.classList.add('event-row');
tr.dataset.uid = alarm.id;
const tdTime = document.createElement('td');
tr.appendChild(tdTime);
const inputTime = document.createElement('input');
inputTime.type = "datetime-local";
inputTime.classList.add('event-date');
inputTime.classList.add('form-input');
inputTime.dataset.uid = alarm.id;
inputTime.value = localDate.toISOString().slice(0,16);
inputTime.onchange = (e => {
const date = new Date(inputTime.value);
alarm.t = dateToMsSinceMidnight(date);
alarm.date = date.toISOString().substring(0,10);
});
tdTime.appendChild(inputTime);
const tdSummary = document.createElement('td');
tr.appendChild(tdSummary);
const inputSummary = document.createElement('input');
inputSummary.type = "text";
inputSummary.classList.add('event-summary');
inputSummary.classList.add('form-input');
inputSummary.dataset.uid = alarm.id;
inputSummary.maxLength=40;
const realHumanStartTime = alarm.data?.time ? ' ' + (new Date(alarm.data.time)).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : '';
const summary = (alarm.msg?.substring(0, inputSummary.maxLength) || "");
inputSummary.value = summary.endsWith(realHumanStartTime) ? summary : summary + realHumanStartTime;
inputSummary.onchange = (e => {
alarm.msg = inputSummary.value;
});
tdSummary.appendChild(inputSummary);
inputSummary.onchange();
const tdInfo = document.createElement('td');
tr.appendChild(tdInfo);
const buttonDelete = document.createElement('button');
buttonDelete.classList.add('btn');
buttonDelete.classList.add('btn-action');
tdInfo.prepend(buttonDelete);
const iconDelete = document.createElement('i');
iconDelete.classList.add('icon');
iconDelete.classList.add('icon-delete');
buttonDelete.appendChild(iconDelete);
buttonDelete.onclick = (e => {
alarms = alarms.filter(a => a !== alarm);
document.getElementById('events').removeChild(tr);
});
document.getElementById('events').appendChild(tr);
document.getElementById('upload').disabled = false;
}
function addAlarm() {
const alarm = getAlarmDefaults();
renderAlarm(alarm);
alarms.push(alarm);
}
function getData() {
Util.showModal("Loading...");
Util.readStorage('sched.json',data=>{
alarms = JSON.parse(data || "[]") || [];
Util.readStorage('sched.settings.json',data=>{
schedSettings = JSON.parse(data || "{}") || {};
Util.hideModal();
alarms.forEach(alarm => {
if (alarm.date) {
renderAlarm(alarm, true);
}
});
});
});
}
// Called when app starts
function onInit() {
getData();
}
</script>
</head>
<body>
<h4>Manage dated events</h4>
<div class="float-right">
<button class="btn" onclick="addAlarm()">
<i class="icon icon-plus"></i>
</button>
</div>
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Summary</th>
<th></th>
</tr>
</thead>
<tbody id="events">
</tbody>
</table>
<div class="divider"></div>
<div class="form-horizontal">
<div class="form-group">
<div class="col-5 col-xs-12">
<label class="form-label" for="fileinput">Add from iCalendar file</label>
</div>
<div class="col-7 col-xs-12">
<input id="fileinput" class="form-input" type="file" onchange="readFile(this)" accept=".ics,.ifb,.ical,.ifbf" multiple/>
</div>
</div>
<div class="form-group">
<div class="col-5 col-xs-12">
<label class="form-label" for="fileinput">Minutes to alarm in advance</label>
</div>
<div class="col-7 col-xs-12">
<input id="offsetMinutes" class="form-input" type="number" value="0" min="0" step="5"/>
</div>
</div>
</div>
<div class="divider"></div>
<button id="upload" class="btn btn-primary" onClick="upload()" disabled>Upload</button>
<button id="reload" class="btn" onClick="location.reload()">Reload</button>
</body>
</html>

View File

@ -10,6 +10,7 @@
"provides_modules" : ["sched"],
"default" : true,
"readme": "README.md",
"interface": "interface.html",
"storage": [
{"name":"sched.boot.js","url":"boot.js"},
{"name":"sched.js","url":"sched.js"},

2
apps/tempgraph/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: 3/Feb/2023 Added 'Temperature Graph' app to depository.

36
apps/tempgraph/README.md Normal file
View File

@ -0,0 +1,36 @@
# Temperature Graph
**Temperature Graph** (tempgraph) is a Bangle.js 2 app for recording graphs of the temperature for various time periods from 10 minutes to 7 days long. It samples the watch's temperature sensor 150 times while creating a graph, regardless of the time period selected.
### Menu Options
* **Widgets** Toggles the watch's widgets on and off. With them off gives you a bigger graph when viewing it.
* **Duration** Select the time period for drawing the graph, from 10 minutes to 7 days long.
* **Draw Graph** Draws the graph.
* Tapping the screen toggles the graph between Celsius (red) and Fahrenheit (blue).
* Pressing the watch button takes you back to the menu. **Note:** While the graph can still be viewed after returning to the menu, you can't continue recording it if you had returned to the menu before the time period was up. The graph is saved in the watch though so it's still there the next time you start the app.
* **Show Graph** Shows the last drawn graph.
* Tapping the screen toggles the graph between Celsius (red) and Fahrenheit (blue).
* Pressing the watch button takes you back to the menu.
* **Save Graph** Sends a screengrab of the graph to the Espruino Web IDE from where you can save it as you would any image on a webpage.
* **Save Data** Sends a CSV file of the graph's temperature data to the Espruino Web IDE where you can save it for further use. I suggest you use the Espruino Web IDE's Terminal Logger (selected in the IDE's Settings/General) to record the data as it's sent. This is the easiest way to save it as a text file.
* **Show Temp** Shows the current temperature.
### Note
Using the watch in a normal fashion can raise the temperature it's sensing to quite a few degrees above the surrounding temperature and it may take half an hour or so to drop to close to the surrounding temperature. After that it seems to give quite accurate readings, assuming the thermometer I've been comparing it to is itself reasonably accurate. So best to load the app then not touch the watch for half an hour before starting a recording. This is assuming you're not wearing the app and are just using it to record the temperature where you've put the watch. You could of course wear it and it'll still draw a graph, which might also be useful.
### Screenshots
![](screenshot_1.png)
![](screenshot_2.png)
![](screenshot_3.png)
### Creator
Carl Read ([mail](mailto:cread98@orcon.net.nz), [github](https://github.com/CarlR9))
#### License
[MIT License](LICENSE)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+goA/AH4AgrHXABFYF0XXkYAK64utGENYFxoABSTxeHXYJglF+UAAIQvEBApfhE4UAF4IDBFwZf/X7hfsR4K/tL96/vRwpf/X/5fJGYK/tL9u02i/tF4KOFL/6/XF4ZftR4K/tL96/vRwpf/X/5fJGYK/tL96/vRwpf/X7gADF8ouBGA4v/F/6/urAmGABFYF7pgIL0owPF0KSC64AIRj4A/AH4ACA="))

395
apps/tempgraph/app.js Normal file
View File

@ -0,0 +1,395 @@
// Temperature Graph
// BangleJS Script
Bangle.setBarometerPower(true,"tempgraph");
Bangle.loadWidgets();
var wids=WIDGETS;
var widsOn=true;
var rm=null;
var gt=null;
var dg=null;
var Layout=require("Layout");
var C=true;
var temp,tempMode,readErrCnt,watchButton2;
var graph=require("Storage").readJSON("tempgraph.json",true);
if(graph==undefined) {
graph=[];
}
var timesData=[
// dur=duration, u=time units, d=divisions on graph, s=seconds per unit.
{dur:10,u:"Mins",d:5,s:60},
{dur:20,u:"Mins",d:4,s:60},
{dur:30,u:"Mins",d:3,s:60},
{dur:40,u:"Mins",d:4,s:60},
{dur:1,u:"Hr",d:4,s:3600},
{dur:2,u:"Hrs",d:4,s:3600},
{dur:3,u:"Hrs",d:3,s:3600},
{dur:4,u:"Hrs",d:4,s:3600},
{dur:6,u:"Hrs",d:6,s:3600},
{dur:8,u:"Hrs",d:4,s:3600},
{dur:12,u:"Hrs",d:6,s:3600},
{dur:16,u:"Hrs",d:4,s:3600},
{dur:20,u:"Hrs",d:5,s:3600},
{dur:1,u:"Day",d:4,s:3600},
{dur:2,u:"Days",d:4,s:86400},
{dur:3,u:"Days",d:3,s:86400},
{dur:4,u:"Days",d:4,s:86400},
{dur:5,u:"Days",d:5,s:86400},
{dur:6,u:"Days",d:6,s:86400},
{dur:7,u:"Days",d:7,s:86400}
];
var times=[];
for(n=0;n<timesData.length;n++){
times.push(timesData[n].dur+" "+timesData[n].u);
}
var durInd=0;
var duration=times[durInd];
function drawWids(){
g.clear();
if(widsOn){
WIDGETS=wids;
Bangle.drawWidgets();
} else {
WIDGETS={};
}
}
function openMenu(){
drawWids();
E.showMenu(menu);
}
function redoMenu(){
clearInterval(rm);
E.showMenu();
openMenu();
}
function refreshMenu(){
rm = setInterval(redoMenu,100);
}
function getF(c){
// Get Fahrenheit temperature from Celsius.
return c*1.8+32;
}
function getT(){
Bangle.getPressure().then(p=>{
temp=p.temperature;
if(tempMode=="drawGraph"&&graph.length>0&&Math.abs(graph[graph.length-1].temp-temp)>10&&readErrCnt<2){
// A large change in temperature may be a reading error. ie. A 0C or less reading after
// a 20C reading. So if this happens, the reading is repeated up to 2 times to hopefully
// skip such errors.
readErrCnt++;
print("readErrCnt "+readErrCnt);
return;
}
clearInterval(gt);
readErrCnt=0;
switch (tempMode){
case "showTemp":
showT();
break;
case "drawGraph":
var date=new Date();
var dateStr=require("locale").date(date).trim();
var hrs=date.getHours();
var mins=date.getMinutes();
var secs=date.getSeconds();
graph.push({
temp:temp,
date:dateStr,
hrs:hrs,
mins:mins,
secs:secs
});
if(graph.length==1){
graph[0].dur=durInd;
}
require("Storage").writeJSON("tempgraph.json", graph);
if(graph.length==150){
clearInterval(dg);
}
drawG();
}
});
}
function getTemp(){
readErrCnt=0;
gt = setInterval(getT,800);
}
function setButton(){
var watchButton=setWatch(function(){
clearInterval(gt);
clearInterval(dg);
clearWatch(watchButton);
Bangle.removeListener("touch",screenTouch);
openMenu();
},BTN);
Bangle.on('touch',screenTouch);
}
function setButton2(){
watchButton2=setWatch(function(){
clearWatch(watchButton2);
openMenu();
},BTN);
}
function zPad(n){
return n.toString().padStart(2,0);
}
function screenTouch(n,ev){
if(ev.y>23&&ev.y<152){
C=C==false;
drawG(false);
}
}
function drawG(){
function cf(t){
if(C){
return t;
}
return getF(t);
}
drawWids();
var top=1;
var bar=21;
var barBot=175-22;
if(widsOn){
top=25;
bar=bar+24;
barBot=barBot-24;
}
var low=graph[0].temp;
var hi=low;
for(n=0;n<graph.length;n++){
var t=graph[n].temp;
if(low>t){
low=t;
}
if(hi<t){
hi=t;
}
}
var tempHi=Math.ceil((cf(hi)+2)/10)*10;
var tempLow=Math.floor((cf(low)-2)/10)*10;
var div=2;
if(tempHi-tempLow>10){
div=5;
}
if(C){
g.setColor(1,0,0);
}else{
g.setColor(0,0,1);
}
var step=(barBot-bar)/((tempHi-tempLow)/div);
for(n=0;n<graph.length;n++){
var pos=tempLow-cf(graph[n].temp);
g.drawLine(n+3,pos*(step/div)+barBot,n+3,barBot+3);
}
g.fillRect(161,barBot+5,174,barBot+20);
g.setColor(1,1,1);
g.setFont("6x8:2");
if(C){
g.drawString("C",163,barBot+5);
}else{
g.drawString("F",163,barBot+5);
}
g.setColor(0,0,0);
g.setFont6x15();
g.drawString("Temperature Graph - "+times[graph[0].dur],1,top);
g.drawRect(2,bar-4,153,barBot+4);
g.setFont("6x8:1");
var num=tempHi;
for(n=bar;n<=barBot;n=n+step){
g.drawLine(3,n,152,n);
g.drawString(num.toString().padStart(3," "),155,n-4);
num=num-div;
}
step=151/timesData[graph[0].dur].d;
for(n=step+2;n<152;n=n+step){
g.drawLine(n,bar-4,n,barBot+4);
}
grSt=graph[0];
g.drawString("Start: "+grSt.date+" "+grSt.hrs+":"+zPad(grSt.mins),1,barBot+6);
var lastT=graph[graph.length-1].temp;
g.drawString("Last Reading:",1,barBot+14);
g.setColor(1,0,0);
g.drawString(lastT.toFixed(1)+"C",85,barBot+14);
g.setColor(0,0,1);
g.drawString(getF(lastT).toFixed(1)+"F",121,barBot+14);
process.memory(true);
}
function drawGraph(){
setButton();
tempMode="drawGraph";
durInd=times.indexOf(duration);
graph=[];
getTemp();
dg=setInterval(getTemp,1000*timesData[durInd].dur*timesData[durInd].s/150);
}
function showGraph(){
setButton();
drawG();
}
function noBluetooth(){
if(NRF.getSecurityStatus().connected){
return false;
}else{
message("Error! Your\nBangle Watch\ncurrently has\nno Bluetooth\nconnection.");
return true;
}
}
function saveGraph(){
if(noBluetooth()){
return;
}
drawG();
g.flip();
g.dump();
message("Graph has\nbeen sent\nto Web IDE\nfor saving.\n");
}
function saveData(){
if(noBluetooth()){
return;
}
drawG();
g.flip();
print("Temperature Graph - "+times[graph[0].dur]+"\n");
print("\"Date\",\"Time\",\"Celsius\",\"Fahrenheit\"");
for(n=0;n<graph.length;n++){
var gr=graph[n];
print("\""+gr.date+"\",\""+gr.hrs+":"+zPad(gr.mins)+":"+zPad(gr.secs)+"\","+gr.temp+","+getF(gr.temp));
}
message("Data has\nbeen sent\nto Web IDE\nfor saving.\n");
}
function message(mes){
setButton2();
var messageLO=new Layout({
type:"v",c:[
{type:"txt",font:"6x8:2",width:171,label:mes,id:"label"},
{type:"btn",font:"6x8:2",pad:3,label:"OK",cb:l=>exit()},
],lazy:true
});
drawWids();
messageLO.render();
}
function showT(){
tempLO.lab1.label=tempLO.lab3.label;
tempLO.lab2.label=tempLO.lab4.label;
tempLO.lab3.label=tempLO.lab5.label;
tempLO.lab4.label=tempLO.lab6.label;
tempLO.lab5.label=temp.toFixed(2)+"C";
tempLO.lab6.label=getF(temp).toFixed(2)+"F";
tempLO.render();
}
function exit(){
clearWatch(watchButton2);
openMenu();
}
function showTemp(){
tempMode="showTemp";
setButton2();
tempLO=new Layout({
type:"v",c:[
{type:"h",c:[
{type:"txt",pad:5,col:"#f77",font:"6x8:2",label:" ",id:"lab1"},
{type:"txt",pad:5,col:"#77f",font:"6x8:2",label:" ",id:"lab2"}
]},
{type:"h",c:[
{type:"txt",pad:5,col:"#f77",font:"6x8:2",label:" ",id:"lab3"},
{type:"txt",pad:5,col:"#77f",font:"6x8:2",label:" ",id:"lab4"}
]},
{type:"h",c:[
{type:"txt",pad:5,col:"#f00",font:"6x8:2",label:" ",id:"lab5"},
{type:"txt",pad:5,col:"#00f",font:"6x8:2",label:" ",id:"lab6"}
]},
{type:"h",c:[
{type:"btn",pad:2,font:"6x8:2",label:"Temp",cb:l=>getTemp()},
{type:"btn",pad:2,font:"6x8:2",label:"Exit",cb:l=>exit()}
]}
]
},{lazy:true});
tempLO.render();
getTemp();
}
var menu={
"":{
"title":" Temp. Graph"
},
"Widgets":{
value:widsOn,
format:vis=>vis?"Hide":"Show",
onchange:vis=>{
widsOn=vis;
refreshMenu();
}
},
"Duration":{
value:times.indexOf(duration),
min:0,max:times.length-1,step:1,wrap:true,
format:tim=>times[tim],
onchange:(dur)=>{
duration=times[dur];
}
},
"Draw Graph":function(){
E.showMenu();
drawGraph();
},
"Show Graph" : function(){
E.showMenu();
if(graph.length>0){
showGraph();
}else{
message("No graph to\nshow as no\ngraph has been\ndrawn yet.");
}
},
"Save Graph" : function(){
E.showMenu();
if(graph.length>0){
saveGraph();
}else{
message("No graph to\nsave as no\ngraph has been\ndrawn yet.");
}
},
"Save Data" : function(){
E.showMenu();
if(graph.length>0){
saveData();
}else{
message("No data to\nsave as no\ngraph has been\ndrawn yet.");
}
},
"Show Temp":function(){
E.showMenu();
showTemp();
}
};
openMenu();

BIN
apps/tempgraph/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -0,0 +1,19 @@
{ "id": "tempgraph",
"name": "Temperature Graph",
"shortName":"Temp Graph",
"version":"0.01",
"description": "An app for recording the temperature for time periods ranging from 10 minutes to 7 days.",
"icon": "app.png",
"type": "app",
"tags": "temperature,tempgraph,graph",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"tempgraph.app.js","url":"app.js"},
{"name":"tempgraph.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"tempgraph.json"}
],
"screenshots": [{"url":"screenshot_1.png"},{"url":"screenshot_2.png"},{"url":"screenshot_3.png"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -4,3 +4,4 @@
Add option to show seconds
0.03: Fix Bell not appearing on alarms > 24h and redrawing interval
Update to match the default alarm widget, and not show itself when an alarm is hidden.
0.04: Fix check for active alarm

View File

@ -2,7 +2,7 @@
"id": "widalarmeta",
"name": "Alarm & Timer ETA",
"shortName": "Alarm ETA",
"version": "0.03",
"version": "0.04",
"description": "A widget that displays the time to the next Alarm or Timer in hours and minutes, maximum 24h (configurable).",
"icon": "widget.png",
"type": "widget",

View File

@ -9,10 +9,10 @@
function draw() {
const times = alarms
.map(alarm => {
.map(alarm =>
alarm.hidden !== true
&& require("sched").getTimeToAlarm(alarm)
})
)
.filter(a => a !== undefined);
const next = times.length > 0 ? Math.min.apply(null, times) : 0;
let calcWidth = 0;