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 000000000..81d6bb24f Binary files /dev/null and b/apps/mitherm/app.png differ diff --git a/apps/mitherm/metadata.json b/apps/mitherm/metadata.json new file mode 100644 index 000000000..a8da6fd26 --- /dev/null +++ b/apps/mitherm/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "mitherm", + "name": "Xiaomi Mijia Temperature and Humidity display", + "shortName": "MiTherm", + "version": "0.01", + "description": "Reads and displays data from Xiaomi temperature/humidity sensors running custom firmware", + "icon": "app.png", + "tags": "xiaomi,mi,ble,bluetooth,thermometer,humidity", + "readme": "README.md", + "supports": ["BANGLEJS", "BANGLEJS2"], + "storage": [ + {"name":"mitherm.app.js","url":"app.js"}, + {"name":"mitherm.img","url":"app-icon.js","evaluate":true} + ] +}