mirror of https://github.com/espruino/BangleApps
Support naming, Farenheit, and alerts.
Add ability to rename tags. Add Sauna, Fridge & Freezer alert. Support °F based on locale. Add possibility to name tag "Porch". How could I forget that? I need it myself!pull/1447/head
parent
405de6d6c0
commit
8350108d88
|
@ -1,3 +1,4 @@
|
||||||
0.01: Hello Ruuvi Watch!
|
0.01: Hello Ruuvi Watch!
|
||||||
0.02: Clear gfx on startup.
|
0.02: Clear gfx on startup.
|
||||||
0.03: Improve design and code, reduce flicker.
|
0.03: Improve design and code, reduce flicker.
|
||||||
|
0.04: Ability to rename tags. Sauna, Fridge & Freezer alert. Support °F based on locale.
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
Watch the status of [RuuviTags](https://ruuvi.com) in range.
|
Watch the status of [RuuviTags](https://ruuvi.com) in range.
|
||||||
|
|
||||||
|
By Marc Englund [GitHub](https://github.com/emarc) | [Twitter](https://twitter.com/marcenglund)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- Id
|
- Id
|
||||||
|
@ -9,18 +11,23 @@ Watch the status of [RuuviTags](https://ruuvi.com) in range.
|
||||||
- Humidity (%)
|
- Humidity (%)
|
||||||
- Pressure (hPa)
|
- Pressure (hPa)
|
||||||
- Battery voltage
|
- Battery voltage
|
||||||
|
- Reading "freshness" (age)
|
||||||
Also shows how "fresh" the data is (age of reading).
|
- Ability to name tags
|
||||||
|
- Alerts for Sauna, Fridge, Freezer
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- Scans for devices when launched and every N seconds.
|
- Scans for devices when launched and every N seconds.
|
||||||
- Page trough devices with BTN1/BTN3.
|
- Page trough devices with left/right swipe or BTN1/BTN3.
|
||||||
- Trigger scan with BTN2.
|
- Page past last/first to trigger scan.
|
||||||
|
- BTN2 = Menu; name tag & trigger scan
|
||||||
|
- Change locale (via App Loader) to get Farenheit.
|
||||||
|
|
||||||
## Todo / ideas
|
## Todo / ideas
|
||||||
|
|
||||||
- Settings for scan frequency, units
|
- Bangle 2 support (I don't have one, let me know if you want to help with testing!)
|
||||||
- Allow to "name" known devices
|
- Settings for scan frequency
|
||||||
- Include more data
|
- Settings for alert limits
|
||||||
|
- Alert for "Wine cellar"
|
||||||
|
- Alert for Washer & Dryer (stops shaking = ready)
|
||||||
- Support older Ruuvi protocols
|
- Support older Ruuvi protocols
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "Ruuvi Watch",
|
"name": "Ruuvi Watch",
|
||||||
"shortName":"Ruuvi Watch",
|
"shortName":"Ruuvi Watch",
|
||||||
"icon": "ruuviwatch.png",
|
"icon": "ruuviwatch.png",
|
||||||
"version":"0.03",
|
"version":"0.04",
|
||||||
"description": "Keep an eye on RuuviTag devices (https://ruuvi.com). For RuuviTags using the v5 format.",
|
"description": "Keep an eye on RuuviTag devices (https://ruuvi.com). For RuuviTags using the v5 format.",
|
||||||
"readme":"README.md",
|
"readme":"README.md",
|
||||||
"tags": "bluetooth",
|
"tags": "bluetooth",
|
||||||
|
|
|
@ -7,11 +7,21 @@ require("Storage").write("ruuviwatch.info", {
|
||||||
|
|
||||||
const lookup = {};
|
const lookup = {};
|
||||||
const ruuvis = [];
|
const ruuvis = [];
|
||||||
|
const names = require("Storage").readJSON("RuuviNames") || {};
|
||||||
let current = 0;
|
let current = 0;
|
||||||
let scanning = false;
|
let scanning = false;
|
||||||
|
|
||||||
|
let paused = false;
|
||||||
|
|
||||||
const SCAN_FREQ = 1000 * 30;
|
const SCAN_FREQ = 1000 * 30;
|
||||||
|
|
||||||
|
// ALERT LIMITS
|
||||||
|
LIMIT_SAUNA = 60;
|
||||||
|
LIMIT_FRIDGE = 4;
|
||||||
|
LIMIT_FREEZER = -18;
|
||||||
|
// TODO add wine cellar limits
|
||||||
|
// TODO configurable limits
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
const FONT_L = "Vector:60";
|
const FONT_L = "Vector:60";
|
||||||
const FONT_M = "Vector:20";
|
const FONT_M = "Vector:20";
|
||||||
|
@ -80,8 +90,8 @@ function p(data) {
|
||||||
int2Hex(data[OFFSET + 23]),
|
int2Hex(data[OFFSET + 23]),
|
||||||
].join(":");
|
].join(":");
|
||||||
|
|
||||||
robject.name =
|
robject.id = int2Hex(data[OFFSET + 22]) + int2Hex(data[OFFSET + 23]);
|
||||||
"Ruuvi " + int2Hex(data[OFFSET + 22]) + int2Hex(data[OFFSET + 23]);
|
|
||||||
return robject;
|
return robject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +124,7 @@ function drawAge() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function redrawAge() {
|
function redrawAge() {
|
||||||
|
if (paused) return;
|
||||||
const originalColor = g.getColor();
|
const originalColor = g.getColor();
|
||||||
g.clearRect(0, SCANNING_Y - 10, g.getWidth(), SCANNING_Y + 10);
|
g.clearRect(0, SCANNING_Y - 10, g.getWidth(), SCANNING_Y + 10);
|
||||||
g.setFont(FONT_S);
|
g.setFont(FONT_S);
|
||||||
|
@ -128,9 +139,15 @@ function redrawAge() {
|
||||||
g.setColor(originalColor);
|
g.setColor(originalColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getName(id) {
|
||||||
|
let name = names[id] || "Ruuvi";
|
||||||
|
return name + " (" + id + ")";
|
||||||
|
}
|
||||||
|
|
||||||
function redraw() {
|
function redraw() {
|
||||||
g.clear();
|
g.clear();
|
||||||
g.setColor("#ffffff");
|
g.setColor("#ffffff");
|
||||||
|
g.setFontAlign(0, 0);
|
||||||
|
|
||||||
if (ruuvis.length > 0 && ruuvis[current]) {
|
if (ruuvis.length > 0 && ruuvis[current]) {
|
||||||
const ruuvi = ruuvis[current];
|
const ruuvi = ruuvis[current];
|
||||||
|
@ -145,14 +162,22 @@ function redraw() {
|
||||||
|
|
||||||
// name
|
// name
|
||||||
g.setFont(FONT_M);
|
g.setFont(FONT_M);
|
||||||
g.drawString(ruuvi.name, CENTER, NAME_Y);
|
g.drawString(getName(ruuvi.id), CENTER, NAME_Y);
|
||||||
|
|
||||||
// age
|
// age
|
||||||
redrawAge();
|
redrawAge();
|
||||||
|
|
||||||
// temp
|
// temp
|
||||||
g.setFont(FONT_L);
|
g.setFont(FONT_L);
|
||||||
g.drawString(ruuvi.temperature.toFixed(2) + "°c", CENTER, TEMP_Y);
|
if (
|
||||||
|
(ruuvi.name.startsWith("Sauna") && ruuvi.temperature > LIMIT_SAUNA) ||
|
||||||
|
(ruuvi.name.startsWith("Fridge") && ruuvi.temperature > LIMIT_FRIDGE) ||
|
||||||
|
(ruuvi.name.startsWith("Freezer") && ruuvi.temperature > LIMIT_FREEZER)
|
||||||
|
) {
|
||||||
|
g.setColor("#ffe800");
|
||||||
|
}
|
||||||
|
g.drawString(getTempString(ruuvi.temperature), CENTER, TEMP_Y);
|
||||||
|
g.setColor("#ffffff");
|
||||||
|
|
||||||
// humid & pressure
|
// humid & pressure
|
||||||
g.setFont(FONT_M);
|
g.setFont(FONT_M);
|
||||||
|
@ -175,8 +200,28 @@ function redraw() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTempString(temp) {
|
||||||
|
// workaround: built-in 'locale' looses precision :-(
|
||||||
|
let unit = "°C";
|
||||||
|
const isF = require("locale").temp(1).endsWith("F");
|
||||||
|
if (isF) {
|
||||||
|
unit = "°F";
|
||||||
|
temp = (temp + 40) * 1.8 - 40;
|
||||||
|
}
|
||||||
|
return temp.toFixed(2) + unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attention(message) {
|
||||||
|
// message ignored for now
|
||||||
|
Bangle.beep();
|
||||||
|
Bangle.beep();
|
||||||
|
Bangle.beep();
|
||||||
|
Bangle.buzz();
|
||||||
|
}
|
||||||
|
|
||||||
function scan() {
|
function scan() {
|
||||||
if (scanning) return;
|
if (scanning) return;
|
||||||
|
if (paused) return;
|
||||||
scanning = true;
|
scanning = true;
|
||||||
NRF.findDevices(
|
NRF.findDevices(
|
||||||
function (devices) {
|
function (devices) {
|
||||||
|
@ -184,11 +229,36 @@ function scan() {
|
||||||
devices.forEach((device) => {
|
devices.forEach((device) => {
|
||||||
const data = p(device.data);
|
const data = p(device.data);
|
||||||
data.time = new Date().getTime();
|
data.time = new Date().getTime();
|
||||||
const idx = lookup[data.name];
|
data.name = names[data.id] || "Ruuvi";
|
||||||
|
|
||||||
|
const idx = lookup[data.id];
|
||||||
if (idx !== undefined) {
|
if (idx !== undefined) {
|
||||||
|
const old = ruuvis[idx];
|
||||||
|
if (
|
||||||
|
data.name.startsWith("Sauna") &&
|
||||||
|
old.temperature < LIMIT_SAUNA &&
|
||||||
|
data.temperature > LIMIT_SAUNA
|
||||||
|
) {
|
||||||
|
current = idx;
|
||||||
|
attention(data.name + " ready!");
|
||||||
|
} else if (
|
||||||
|
data.name.startsWith("Fridge") &&
|
||||||
|
old.temperature < LIMIT_FRIDGE &&
|
||||||
|
data.temperature > LIMIT_FRIDGE
|
||||||
|
) {
|
||||||
|
current = idx;
|
||||||
|
attention(data.name + " warning!");
|
||||||
|
} else if (
|
||||||
|
data.name.startsWith("Freezer") &&
|
||||||
|
old.temperature < LIMIT_FREEZER &&
|
||||||
|
data.temperature > LIMIT_FREEZER
|
||||||
|
) {
|
||||||
|
current = idx;
|
||||||
|
attention(data.name + " warning!");
|
||||||
|
}
|
||||||
ruuvis[idx] = data;
|
ruuvis[idx] = data;
|
||||||
} else {
|
} else {
|
||||||
lookup[data.name] = ruuvis.push(data) - 1;
|
lookup[data.id] = ruuvis.push(data) - 1;
|
||||||
foundNew = true;
|
foundNew = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -202,23 +272,195 @@ function scan() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setName(newName) {
|
||||||
|
const ruuvi = ruuvis[current];
|
||||||
|
ruuvi.name = newName;
|
||||||
|
names[ruuvi.id] = ruuvi.name;
|
||||||
|
require("Storage").writeJSON("RuuviNames", names);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() {
|
||||||
|
E.showMenu();
|
||||||
|
paused = false;
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMenu() {
|
||||||
|
// TODO make this DRY + indicate current in menu
|
||||||
|
if (!ruuvis.length) {
|
||||||
|
scan();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
paused = true;
|
||||||
|
const ruuvi = ruuvis[current];
|
||||||
|
const id = ruuvi.id;
|
||||||
|
const name = getName(id);
|
||||||
|
|
||||||
|
var mainmenu = {
|
||||||
|
"": { title: name },
|
||||||
|
"Scan now": function () {
|
||||||
|
closeMenu();
|
||||||
|
scan();
|
||||||
|
},
|
||||||
|
"Rename tag": function () {
|
||||||
|
E.showMenu(namemenu);
|
||||||
|
},
|
||||||
|
"< Back": function () {
|
||||||
|
closeMenu();
|
||||||
|
}, // remove the menu
|
||||||
|
};
|
||||||
|
// Submenu
|
||||||
|
var namemenu = {
|
||||||
|
"": { title: "Rename " + name },
|
||||||
|
Ruuvi: function () {
|
||||||
|
setName("Ruuvi");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Indoors: function () {
|
||||||
|
setName("Indoors");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Downstairs: function () {
|
||||||
|
setName("Downstairs");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Upstairs: function () {
|
||||||
|
setName("Upstairs");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Attic: function () {
|
||||||
|
setName("Attic");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Basement: function () {
|
||||||
|
setName("Basement");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Kitchen: function () {
|
||||||
|
setName("Kitchen");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Pantry: function () {
|
||||||
|
setName("Pantry");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
"Living room": function () {
|
||||||
|
setName("Living room");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
"Dining room": function () {
|
||||||
|
setName("Dining room");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Office: function () {
|
||||||
|
setName("Office");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Bedroom: function () {
|
||||||
|
setName("Bedroom");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Bathroom: function () {
|
||||||
|
setName("Bathroom");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Sauna: function () {
|
||||||
|
setName("Sauna");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
"Wine cellar": function () {
|
||||||
|
setName("Wine cellar");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Outdoors: function () {
|
||||||
|
setName("Outdoors");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Porch: function () {
|
||||||
|
setName("Porch");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Backyard: function () {
|
||||||
|
setName("Backyard");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Garage: function () {
|
||||||
|
setName("Garage");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Greenhouse: function () {
|
||||||
|
setName("Greenhouse");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Shed: function () {
|
||||||
|
setName("Shed");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Fridge: function () {
|
||||||
|
setName("Fridge");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Freezer: function () {
|
||||||
|
setName("Freezer");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Dryer: function () {
|
||||||
|
setName("Dryer");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
Washer: function () {
|
||||||
|
setName("Washer");
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
|
"< Back": function () {
|
||||||
|
E.showMenu(mainmenu);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Actually display the menu
|
||||||
|
E.showMenu(mainmenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPage() {
|
||||||
|
current++;
|
||||||
|
if (current >= ruuvis.length) {
|
||||||
|
current = 0;
|
||||||
|
scan();
|
||||||
|
}
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevPage() {
|
||||||
|
current--;
|
||||||
|
if (current < 0) {
|
||||||
|
current = ruuvis.length - 1;
|
||||||
|
scan();
|
||||||
|
}
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
// START
|
// START
|
||||||
|
Bangle.on("swipe", function (dir) {
|
||||||
|
if (paused) return;
|
||||||
|
if (dir > 0) {
|
||||||
|
prevPage();
|
||||||
|
} else {
|
||||||
|
nextPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
// Button 1 pages up
|
// Button 1 pages up
|
||||||
setWatch(
|
setWatch(
|
||||||
() => {
|
() => {
|
||||||
current--;
|
if (paused) return;
|
||||||
if (current < 0) {
|
prevPage();
|
||||||
current = ruuvis.length - 1;
|
|
||||||
}
|
|
||||||
redraw();
|
|
||||||
},
|
},
|
||||||
BTN1,
|
BTN1,
|
||||||
{ repeat: true }
|
{ repeat: true }
|
||||||
);
|
);
|
||||||
// button triggers scan
|
// button triggers menu
|
||||||
setWatch(
|
setWatch(
|
||||||
() => {
|
() => {
|
||||||
scan();
|
if (paused) return;
|
||||||
|
showMenu();
|
||||||
},
|
},
|
||||||
BTN2,
|
BTN2,
|
||||||
{ repeat: true }
|
{ repeat: true }
|
||||||
|
@ -226,11 +468,8 @@ setWatch(
|
||||||
// button 3 pages down
|
// button 3 pages down
|
||||||
setWatch(
|
setWatch(
|
||||||
() => {
|
() => {
|
||||||
current++;
|
if (paused) return;
|
||||||
if (current >= ruuvis.length) {
|
nextPage();
|
||||||
current = 0;
|
|
||||||
}
|
|
||||||
redraw();
|
|
||||||
},
|
},
|
||||||
BTN3,
|
BTN3,
|
||||||
{ repeat: true }
|
{ repeat: true }
|
||||||
|
|
2
core
2
core
|
@ -1 +1 @@
|
||||||
Subproject commit 187af1527e0b830c804049aae834ed658ffeed08
|
Subproject commit 3093d78a5d752cbf03ea8f9a1a7c0b50b9c8123b
|
Loading…
Reference in New Issue