From 8350108d880ce4682ee6bde66f14baaaae449655 Mon Sep 17 00:00:00 2001 From: Marc Englund Date: Sun, 13 Feb 2022 22:21:19 +0200 Subject: [PATCH] Support naming, Farenheit, and alerts. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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! --- apps/ruuviwatch/ChangeLog | 1 + apps/ruuviwatch/README.md | 21 ++- apps/ruuviwatch/metadata.json | 2 +- apps/ruuviwatch/ruuviwatch.app.js | 275 ++++++++++++++++++++++++++++-- core | 2 +- 5 files changed, 274 insertions(+), 27 deletions(-) diff --git a/apps/ruuviwatch/ChangeLog b/apps/ruuviwatch/ChangeLog index 15ff601f0..7a1d5db21 100644 --- a/apps/ruuviwatch/ChangeLog +++ b/apps/ruuviwatch/ChangeLog @@ -1,3 +1,4 @@ 0.01: Hello Ruuvi Watch! 0.02: Clear gfx on startup. 0.03: Improve design and code, reduce flicker. +0.04: Ability to rename tags. Sauna, Fridge & Freezer alert. Support °F based on locale. \ No newline at end of file diff --git a/apps/ruuviwatch/README.md b/apps/ruuviwatch/README.md index c96e032d7..27e323628 100644 --- a/apps/ruuviwatch/README.md +++ b/apps/ruuviwatch/README.md @@ -2,6 +2,8 @@ Watch the status of [RuuviTags](https://ruuvi.com) in range. +By Marc Englund [GitHub](https://github.com/emarc) | [Twitter](https://twitter.com/marcenglund) + ![Ruuvi Watch in action](/BangleApps/apps/ruuviwatch/ruuviwatch-in-action.jpg) - Id @@ -9,18 +11,23 @@ Watch the status of [RuuviTags](https://ruuvi.com) in range. - Humidity (%) - Pressure (hPa) - Battery voltage - -Also shows how "fresh" the data is (age of reading). +- Reading "freshness" (age) +- Ability to name tags +- Alerts for Sauna, Fridge, Freezer ## Usage - Scans for devices when launched and every N seconds. -- Page trough devices with BTN1/BTN3. -- Trigger scan with BTN2. +- Page trough devices with left/right swipe or BTN1/BTN3. +- Page past last/first to trigger scan. +- BTN2 = Menu; name tag & trigger scan +- Change locale (via App Loader) to get Farenheit. ## Todo / ideas -- Settings for scan frequency, units -- Allow to "name" known devices -- Include more data +- Bangle 2 support (I don't have one, let me know if you want to help with testing!) +- Settings for scan frequency +- Settings for alert limits +- Alert for "Wine cellar" +- Alert for Washer & Dryer (stops shaking = ready) - Support older Ruuvi protocols diff --git a/apps/ruuviwatch/metadata.json b/apps/ruuviwatch/metadata.json index 413d96153..eab9f64bf 100644 --- a/apps/ruuviwatch/metadata.json +++ b/apps/ruuviwatch/metadata.json @@ -2,7 +2,7 @@ "name": "Ruuvi Watch", "shortName":"Ruuvi Watch", "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.", "readme":"README.md", "tags": "bluetooth", diff --git a/apps/ruuviwatch/ruuviwatch.app.js b/apps/ruuviwatch/ruuviwatch.app.js index 674319dd8..249303382 100644 --- a/apps/ruuviwatch/ruuviwatch.app.js +++ b/apps/ruuviwatch/ruuviwatch.app.js @@ -7,11 +7,21 @@ require("Storage").write("ruuviwatch.info", { const lookup = {}; const ruuvis = []; +const names = require("Storage").readJSON("RuuviNames") || {}; let current = 0; let scanning = false; +let paused = false; + 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 const FONT_L = "Vector:60"; const FONT_M = "Vector:20"; @@ -80,8 +90,8 @@ function p(data) { int2Hex(data[OFFSET + 23]), ].join(":"); - robject.name = - "Ruuvi " + int2Hex(data[OFFSET + 22]) + int2Hex(data[OFFSET + 23]); + robject.id = int2Hex(data[OFFSET + 22]) + int2Hex(data[OFFSET + 23]); + return robject; } @@ -114,6 +124,7 @@ function drawAge() { } function redrawAge() { + if (paused) return; const originalColor = g.getColor(); g.clearRect(0, SCANNING_Y - 10, g.getWidth(), SCANNING_Y + 10); g.setFont(FONT_S); @@ -128,9 +139,15 @@ function redrawAge() { g.setColor(originalColor); } +function getName(id) { + let name = names[id] || "Ruuvi"; + return name + " (" + id + ")"; +} + function redraw() { g.clear(); g.setColor("#ffffff"); + g.setFontAlign(0, 0); if (ruuvis.length > 0 && ruuvis[current]) { const ruuvi = ruuvis[current]; @@ -145,14 +162,22 @@ function redraw() { // name g.setFont(FONT_M); - g.drawString(ruuvi.name, CENTER, NAME_Y); + g.drawString(getName(ruuvi.id), CENTER, NAME_Y); // age redrawAge(); // temp 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 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() { if (scanning) return; + if (paused) return; scanning = true; NRF.findDevices( function (devices) { @@ -184,11 +229,36 @@ function scan() { devices.forEach((device) => { const data = p(device.data); data.time = new Date().getTime(); - const idx = lookup[data.name]; + data.name = names[data.id] || "Ruuvi"; + + const idx = lookup[data.id]; 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; } else { - lookup[data.name] = ruuvis.push(data) - 1; + lookup[data.id] = ruuvis.push(data) - 1; 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 +Bangle.on("swipe", function (dir) { + if (paused) return; + if (dir > 0) { + prevPage(); + } else { + nextPage(); + } +}); // Button 1 pages up setWatch( () => { - current--; - if (current < 0) { - current = ruuvis.length - 1; - } - redraw(); + if (paused) return; + prevPage(); }, BTN1, { repeat: true } ); -// button triggers scan +// button triggers menu setWatch( () => { - scan(); + if (paused) return; + showMenu(); }, BTN2, { repeat: true } @@ -226,11 +468,8 @@ setWatch( // button 3 pages down setWatch( () => { - current++; - if (current >= ruuvis.length) { - current = 0; - } - redraw(); + if (paused) return; + nextPage(); }, BTN3, { repeat: true } diff --git a/core b/core index 187af1527..3093d78a5 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 187af1527e0b830c804049aae834ed658ffeed08 +Subproject commit 3093d78a5d752cbf03ea8f9a1a7c0b50b9c8123b