From 67e269486bab419ac7f2968484f55beda2ecd14b Mon Sep 17 00:00:00 2001 From: Mo Abrahams <3750013+dashavoo@users.noreply.github.com> Date: Fri, 16 Dec 2022 23:56:41 +0000 Subject: [PATCH] Add Xiaomi Mijia temperature sensor display --- apps/mitherm/ChangeLog | 1 + apps/mitherm/README.md | 22 +++++ apps/mitherm/app-icon.js | 1 + apps/mitherm/app.js | 172 +++++++++++++++++++++++++++++++++++++ apps/mitherm/app.png | Bin 0 -> 863 bytes apps/mitherm/metadata.json | 15 ++++ 6 files changed, 211 insertions(+) create mode 100644 apps/mitherm/ChangeLog create mode 100644 apps/mitherm/README.md create mode 100644 apps/mitherm/app-icon.js create mode 100644 apps/mitherm/app.js create mode 100644 apps/mitherm/app.png create mode 100644 apps/mitherm/metadata.json diff --git a/apps/mitherm/ChangeLog b/apps/mitherm/ChangeLog new file mode 100644 index 000000000..630459c15 --- /dev/null +++ b/apps/mitherm/ChangeLog @@ -0,0 +1 @@ +0.01: Create mitherm app with support for pvvx firmware only diff --git a/apps/mitherm/README.md b/apps/mitherm/README.md new file mode 100644 index 000000000..cdf3daa61 --- /dev/null +++ b/apps/mitherm/README.md @@ -0,0 +1,22 @@ +Reads BLE advertisement data from Xiaomi temperature/humidity sensors running the +`pvvx` custom firmware (https://github.com/pvvx/ATC_MiThermometer). + +## Features + +* Display temperature +* Display humidity +* Display battery state of sensor +* Auto-refresh every 5 minutes +* Manual refresh on demand +* Add aliases for MAC addresses to easily recognise devices + +## Planned features + +* Supprt for other advertising formats: + * atc1441 format + * BTHome + * Xiaomi Mijia format +* Configurable auto-refresh interval +* Configurable scan length (currently 30s) +* Alerts when temperature outside defined limits (with a widget or bootcode to + work when app is inactive) diff --git a/apps/mitherm/app-icon.js b/apps/mitherm/app-icon.js new file mode 100644 index 000000000..2e8737704 --- /dev/null +++ b/apps/mitherm/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4Ac5gWVhnM4AWVAAIYTCwQABCywYRIoYADJJwWHDB4RD5sz7hJPFIlP//0MRxFE6f/AAM9JJgWE4gWCAANMDBZcEn4XE+ZiKFwhcBCYPdDYRiEGAoXDLgf97vfMQwXILggXFMQYXHLgoXB6czMQoXHLgQXJMQQXG4YWEI44ABngXGh4XHF4v/+DAGC6DXGC5BHGC509F4IXTdwIABV4gXOIwIABJAoX/C6p3Xa4a/UABAXfgczABswC/4XmAH4A/ABY")) diff --git a/apps/mitherm/app.js b/apps/mitherm/app.js new file mode 100644 index 000000000..b7abdb2fc --- /dev/null +++ b/apps/mitherm/app.js @@ -0,0 +1,172 @@ +var filterTemperature = [{ + serviceData: { + "181a": {} + } +}]; +var results = {}; +var macs = []; + +var aliases = require("Storage").readJSON("mitherm.json", true); +if (!aliases) aliases = {}; + +var lastSeen = {}; +var current = 0; +var scanning = false; +var timeoutDraw; +var timeoutScan; + + +const scan = function() { + if (!scanning) { // Don't start scanning if already doing so. + scanning = true; + if (timeoutScan) clearTimeout(timeoutScan); + timeoutScan = setTimeout(scan, 300000); // Scan again in 5 minutes. + drawScanState(scanning); + NRF.findDevices(function(devices) { + onDevices(devices); + }, { + filters: filterTemperature, + timeout: 30000 // Scan for 30s + }); + } +}; + + +const onDevices = function(devices) { + let now = Date.now(); + for (let i = 0; i < devices.length; i++) { + let device = devices[i]; + + let processedData = extractData(device.data); + console.log({ + rssi: device.rssi, + data: processedData + }); + if (!macs.includes(processedData.MAC)) { + macs.push(processedData.MAC); + } + results[processedData.MAC] = processedData; + lastSeen[processedData.MAC] = now; + } + console.log("Scan complete."); + scanning = false; + writeOutput(); +}; + + +const extractData = function(thedata) { + let data = DataView(thedata); + let MAC = []; + for (let i = 9; i > 3; i--) { + MAC.push(data.getUint8(i, true).toString(16).padStart(2, "0")); + } + out = { + size: data.getUint8(0, true), + uid: data.getUint8(1, true), + UUID: data.getUint16(2, true), + MAC: MAC.join(":"), + temperature: data.getInt16(10, true) * 0.01, + humidity: data.getUint16(12, true) * 0.01, + battery_mv: data.getUint16(14, true), + battery_level: data.getUint8(16, true), + }; + return out; +}; + + +const writeOutput = function() { + let now = Date.now(); + if (timeoutDraw) clearTimeout(timeoutDraw); + timeoutDraw = setTimeout(writeOutput, 60000); // Refresh in 1 minute. + g.clear(true); + Bangle.drawWidgets(); + g.reset(); + drawScanState(scanning); + + if (macs.length == 0) return; + + processedData = results[macs[current]]; + g.setFont12x20(2); + g.drawString(`${processedData.temperature.toFixed(2)}°C`, 10, 30); + g.drawString(`${processedData.humidity.toFixed(2)} %`, 10, 70); + + g.setFont6x15(); + g.drawString(`${((now - lastSeen[macs[current]]) / 60000).toFixed(0)} min ago`, 10, 130); + g.drawString(`${processedData.battery_level} % battery`, 80, 130); + g.drawString(` ${processedData.MAC in aliases ? aliases[processedData.MAC] : processedData.MAC}: ${current + 1} / ${macs.length}`, 10, 150); +}; + + +const scrollDevices = function(directionLR) { + // Swipe left or right to move between devices. + current -= directionLR; // inverted feels a more familiar gesture. + if (current + 1 > macs.length) + current = 0; + if (current < 0) + current = macs.length - 1; + writeOutput(); +}; + +const drawScanState = function(state) { + if (state) + g.fillRect(160, 160, 170, 170); + else + g.clearRect(160, 160, 170, 170); +}; + +const setAlias = function(mac, alias) { + if (alias === "") { + delete aliases[mac]; + } + else { + aliases[mac] = alias; + require("Storage").writeJSON("mitherm.json", aliases); + } +}; + +const changeAlias = function(mac) { + g.clear(); + require("textinput").input((mac in aliases) ? aliases[mac] : "").then(function(text) { + setAlias(mac, text); + setUI(); + writeOutput(); + }); +}; + + +const setUI = function() { + Bangle.setUI({ + mode: "custom", + swipe: scrollDevices, + btn: function() { + E.showMenu(actionsMenu); + } + }); +}; + + +const actionsMenu = { + "": { + "title": "-- Actions --", + "back": function() { + E.showMenu(); + }, + "remove": function() { + setUI(); + writeOutput(); + }, + }, + "Scan now": function() { + scan(); + E.showMenu(); + }, + "Edit alias": function() { + changeAlias(macs[current]); + }, +}; + +setUI(); +Bangle.loadWidgets(); +g.setClipRect(Bangle.appRect); +scan(); +writeOutput(); diff --git a/apps/mitherm/app.png b/apps/mitherm/app.png new file mode 100644 index 0000000000000000000000000000000000000000..81d6bb24f8dd9d8bc1e59ecc20ffbf2744ffd293 GIT binary patch literal 863 zcmV-l1EBngP)C~OT?JZL=V9)^A6*YCOc`M522%(A>ADh3-C*JXLD!YY+l{;(AndvldAp(8 zfjqjdM1RJHNck(w(7dF}0n)B>{$e<;K7jN3BXEHdt*~K>fC*YRs2rf_Iv)(d*>)T7 z`TzjZx33U-Ejm2FDWOJnXFA#b!5xG%1F0WU&0L~YH{md;`_a0Sz zzp$ko{O9ai7Yj>}?0KU&SA02ybZ11>ja&kB{f+X$5M0p_)#3rH6b}G=^1b@p-3Q@} zj=)xbf~poP&17zUQMT**8mN*iGx+qkh`L(&xxf{ygZ=Cs@KyC#ycvO%{HPg|*a1Em zg6rO>?&4|SD-XdH9VK#uWDg{qSNZ~cu#viu;wJZBzdt=(ppGHF7{vGXrfE*Xrd)eK zYYFvdY>4o|#sqcd9B3_}T=0A9AX%}4%brCtR0WidqZt6=~0Vm>iN^ekTt5x~b|@Sm@iJ@=oh0UwVs&1HxH&;52-QgiZT^Rp=EyUa9~Ap-1U z&vcFH!I(Xe$tVtSqR2FrAp+9d_p6N6GR