mirror of https://github.com/espruino/BangleApps
Add Xiaomi Mijia temperature sensor display
parent
8425f0b50a
commit
67e269486b
|
@ -0,0 +1 @@
|
||||||
|
0.01: Create mitherm app with support for pvvx firmware only
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwhC/AH4Ac5gWVhnM4AWVAAIYTCwQABCywYRIoYADJJwWHDB4RD5sz7hJPFIlP//0MRxFE6f/AAM9JJgWE4gWCAANMDBZcEn4XE+ZiKFwhcBCYPdDYRiEGAoXDLgf97vfMQwXILggXFMQYXHLgoXB6czMQoXHLgQXJMQQXG4YWEI44ABngXGh4XHF4v/+DAGC6DXGC5BHGC509F4IXTdwIABV4gXOIwIABJAoX/C6p3Xa4a/UABAXfgczABswC/4XmAH4A/ABY"))
|
|
@ -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();
|
Binary file not shown.
After Width: | Height: | Size: 863 B |
|
@ -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}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue