-
-
Save ssievert42/1305e4a036d8a3d4a6e14229c2eb01e5 to your computer and use it in GitHub Desktop.
Espruino BTHome module, modified to use encryption
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Copyright (c) 2023 Gordon Williams. See the file LICENSE for copying permission. */ | |
/* | |
Module for creating BTHome.io compatible Advertisements | |
https://www.espruino.com/BTHome | |
*/ | |
let advCounter = 0; | |
let changedCounter = false; | |
let key = []; | |
let hex2buf = function(str) { | |
if (str.length % 2 != 0) { | |
return null; | |
} | |
let length = str.length / 2; | |
let buf = Uint8Array(length); | |
for (let i=0; i<length; i++) { | |
buf[i] = parseInt(str.slice(i * 2, i * 2 + 2), 16); | |
} | |
return buf; | |
}; | |
let buf2hex = function(buf) { | |
let str = ""; | |
for (let i=0; i<buf.length; i++) { | |
str += buf[i].toString(16).padStart(2, '0'); | |
} | |
return str; | |
}; | |
let genKey = function() { | |
let key = Uint8Array(16); | |
for (let i=0; i<key.length; i++) { | |
key[i] = Math.abs(E.hwRand()%256); | |
} | |
return key; | |
}; | |
exports.setKey = function(keyHexStr) { | |
key = hex2buf(keyHexStr); | |
require("Storage").write("bthome.key", buf2hex(key)); | |
}; | |
exports.setRandomKey = function() { | |
key = genKey(); | |
require("Storage").write("bthome.key", buf2hex(key)); | |
}; | |
exports.getKey = function() { | |
init(); | |
return buf2hex(key); | |
}; | |
exports.setCounter = function(counter) { | |
advCounter = counter; | |
changedCounter = true; | |
}; | |
exports.getCounter = function() { | |
return advCounter; | |
}; | |
let initDone = false; | |
let init = function() { | |
if (initDone) return; | |
let storedKey = require("Storage").read("bthome.key"); | |
if (storedKey != undefined) { | |
key = hex2buf(storedKey); | |
} else { | |
exports.setRandomKey(); | |
} | |
let storedCounter = require("Storage").read("bthome.counter"); | |
if (storedCounter != undefined) { | |
advCounter = parseInt(storedCounter); | |
} | |
initDone = true; | |
}; | |
E.on("kill", () => { | |
if (changedCounter) { | |
require("Storage").write("bthome.counter", advCounter.toString()); | |
} | |
}); | |
let generateNonce = function(counterStr){ | |
let macAddress = NRF.getAddress().replaceAll(":",""); | |
let uuid = "d2fc"; | |
let nonce = macAddress + uuid + "41" + counterStr; | |
return nonce; | |
}; | |
let concatArrays = function(arrays) { | |
let length = 0; | |
arrays.forEach((arr) => { | |
length += arr.length; | |
}); | |
let combined = Uint8Array(length); | |
let index = 0; | |
arrays.forEach((arr) => { | |
if (arr.forEach != undefined) { | |
arr.forEach((v) => { | |
combined[index] = v; | |
index++; | |
}); | |
} else { | |
for (let i=0; i<arr.length; i++) { | |
combined[index] = arr[i]; | |
index++; | |
} | |
} | |
}); | |
return combined; | |
}; | |
exports.getAdvertisement = function(devices) { | |
init(); | |
const b16 = (id,v)=>[id,v&255, (v>>8)&255]; | |
const b24 = (id,v)=>[id,v&255, (v>>8)&255, (v>>16)&255]; | |
const b32 = (id,v)=>[id,v&255, (v>>8)&255, (v>>16)&255, (v>>24)&255]; | |
const DEV = { | |
battery : e => [ 1, E.clip(Math.round(e.v),0,100)], // 0..100, int | |
temperature : e => b16(2, Math.round(e.v*100)), // degrees C, floating point | |
count : e => [ 0x0F, e.v], // 0..255, int | |
count16 : e => b16(0x3D, e.v), // 0..65535, int | |
count32 : e => b32(0x3E, e.v), // 0..0xFFFFFFFF, int | |
current : e => b16(0x5D, Math.round(e.v*1000)), // amps, floating point | |
duration : e => b16(0x42, Math.round(e.v*1000)), // seconds, floating point | |
energy : e => b32(0x4D, Math.round(e.v*1000)), // kWh, floating point | |
gas : e => b32(0x4C, e.v), // gas (m3), int (32 bit version) | |
humidity : e => [0x2E, Math.round(e.v)], // humidity %, int | |
humidity16 : e => b16(3, Math.round(e.v*100)), // humidity %, floating point | |
power : e => b24(0x0B, Math.round(e.v*100)), // power (W), floating point | |
pressure : e => b24(4, Math.round(e.v*100)), // pressure (hPa), floating point | |
voltage : e => b16(0x0C, Math.round(e.v*1000)), // voltage (V), floating point | |
co2 : e => b16(0x12, Math.round(e.v)), // co2 (ppm), int, factor=1 | |
tvoc : e => b16(0x13, Math.round(e.v)), // TVOC (ug/m3), int, factor=1 | |
text : e => { let t = ""+e.v; return [ 0x53, t.length ].concat(t.split("").map(c=>c.charCodeAt())); }, // text string | |
button_event : e => { | |
const events=["none","press","double_press","triple_press","long_press","long_double_press","long_triple_press"]; | |
if (!events.includes(e.v)) throw new Error(`Unknown event type ${E.toJS(e.v)}`); | |
return [0x3A, events.indexOf(e.v)]; | |
}, | |
dimmer_event : e => { | |
var n = 0; | |
if (e.v<0) return [0x3C, 1, -e.v]; // left | |
if (e.v>0) return [0x3C, 2, e.v]; // right | |
return [0x3C, 0, 0]; | |
} | |
}; | |
const BOOL = { | |
battery_low : 0x15, | |
battery_charge : 0x16, | |
cold : 0x18, | |
connected : 0x19, | |
door : 0x1A, | |
garage_door : 0x1B, | |
boolean : 0x0F, | |
heat : 0x1D, | |
light : 0x1E, | |
locked : 0x1F, | |
motion : 0x21, | |
moving : 0x22, | |
occupancy : 0x23, | |
opening : 0x11, | |
power_on : 0x10, | |
presence : 0x25, | |
problem : 0x26, | |
tamper : 0x2B, | |
vibration : 0x2C | |
}; | |
let adv = [ | |
/* BTHome Device Information | |
bit 0: "Encryption flag" | |
bit 1: "Reserved for future use" | |
bit 2: Trigger based device flag (0 = we advertise all the time) | |
bit 1: "Reserved for future use" | |
bit 5-7: "BTHome Version" = 2 */ | |
0x41, | |
]; | |
advCounter = (advCounter+1)&2147483647; | |
changedCounter = true; | |
let counterStr = advCounter.toString(16).padStart(8, "0"); | |
let nonce = generateNonce(counterStr); | |
let data = [].concat.apply([], devices.map(dev => { | |
if (dev.type in DEV) return DEV[dev.type](dev); | |
if (dev.type in BOOL) return [BOOL[dev.type], dev.v?1:0]; | |
throw new Error(`Unknown device type ${E.toJS(dev.type)}`); | |
}).sort((a,b) => a[0]-b[0])); | |
let encrypted = AES.ccmEncrypt(data, key, hex2buf(nonce), 4); | |
adv = concatArrays([adv, encrypted.data, hex2buf(counterStr), encrypted.tag]); | |
return { | |
0xFCD2 : adv | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Simply upload this file to storage with the name "BTHomeCrypt" and use it like the original BTHome module:
Edit: Key generation is now built in and done automagically on the first call to
getAdvertisement()
.This uses a counter that is incremented for each advertisement.
If HomeAssistant isn't showing new advertisements, chances are that (for some reason) the counter is lower than in an already received advertisement - setting the counter to a higher value may help: