Last active
August 27, 2025 01:36
-
-
Save ddlsmurf/1d1ee3aedd4aee6d3b800490d3e6dde7 to your computer and use it in GitHub Desktop.
BTHome javascript definitions (see https://bthome.io/format/ )
This file contains hidden or 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
[ | |
{ | |
"id": 0, | |
"name": "packet id", | |
"example": { | |
"data": "0009", | |
"result": "9" | |
}, | |
"kind": "number", | |
"size": 1 | |
}, | |
{ | |
"id": 1, | |
"name": "battery", | |
"unit": { | |
"factor": 1, | |
"label": "%" | |
}, | |
"example": { | |
"data": "0161", | |
"result": "97" | |
}, | |
"kind": "number", | |
"size": 1 | |
}, | |
{ | |
"id": 2, | |
"name": "temperature", | |
"unit": { | |
"factor": 0.01, | |
"label": "°C" | |
}, | |
"example": { | |
"data": "02CA09", | |
"result": "25.06" | |
}, | |
"kind": "number", | |
"size": 2, | |
"signed": true | |
}, | |
{ | |
"id": 3, | |
"name": "humidity", | |
"unit": { | |
"factor": 0.01, | |
"label": "%" | |
}, | |
"example": { | |
"data": "03BF13", | |
"result": "50.55" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 4, | |
"name": "pressure", | |
"unit": { | |
"factor": 0.01, | |
"label": "hPa" | |
}, | |
"example": { | |
"data": "04138A01", | |
"result": "1008.83" | |
}, | |
"kind": "number", | |
"size": 3 | |
}, | |
{ | |
"id": 5, | |
"name": "illuminance", | |
"unit": { | |
"factor": 0.01, | |
"label": "lux" | |
}, | |
"example": { | |
"data": "05138A14", | |
"result": "13460.67" | |
}, | |
"kind": "number", | |
"size": 3 | |
}, | |
{ | |
"id": 6, | |
"name": "mass (kg)", | |
"unit": { | |
"factor": 0.01, | |
"label": "kg" | |
}, | |
"example": { | |
"data": "065E1F", | |
"result": "80.3" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 7, | |
"name": "mass (lb)", | |
"unit": { | |
"factor": 0.01, | |
"label": "lb" | |
}, | |
"example": { | |
"data": "073E1D", | |
"result": "74.86" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 8, | |
"name": "dewpoint", | |
"unit": { | |
"factor": 0.01, | |
"label": "°C" | |
}, | |
"example": { | |
"data": "08CA06", | |
"result": "17.38" | |
}, | |
"kind": "number", | |
"size": 2, | |
"signed": true | |
}, | |
{ | |
"id": 9, | |
"name": "count", | |
"unit": { | |
"factor": 1, | |
"label": "" | |
}, | |
"example": { | |
"data": "0960", | |
"result": "96" | |
}, | |
"kind": "number", | |
"size": 1 | |
}, | |
{ | |
"id": 10, | |
"name": "energy", | |
"unit": { | |
"factor": 0.001, | |
"label": "kWh" | |
}, | |
"example": { | |
"data": "0A138A14", | |
"result": "1346.067" | |
}, | |
"kind": "number", | |
"size": 3 | |
}, | |
{ | |
"id": 11, | |
"name": "power", | |
"unit": { | |
"factor": 0.01, | |
"label": "W" | |
}, | |
"example": { | |
"data": "0B021B00", | |
"result": "69.14" | |
}, | |
"kind": "number", | |
"size": 3 | |
}, | |
{ | |
"id": 12, | |
"name": "voltage", | |
"unit": { | |
"factor": 0.001, | |
"label": "V" | |
}, | |
"example": { | |
"data": "0C020C", | |
"result": "3.074" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 13, | |
"name": "pm2.5", | |
"unit": { | |
"factor": 1, | |
"label": "ug/m3" | |
}, | |
"example": { | |
"data": "0D120C", | |
"result": "3090" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 14, | |
"name": "pm10", | |
"unit": { | |
"factor": 1, | |
"label": "ug/m3" | |
}, | |
"example": { | |
"data": "0E021C", | |
"result": "7170" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 15, | |
"name": "generic boolean", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Off", | |
"On" | |
] | |
}, | |
{ | |
"id": 16, | |
"name": "power", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Off", | |
"On" | |
] | |
}, | |
{ | |
"id": 17, | |
"name": "opening", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Closed", | |
"Open" | |
] | |
}, | |
{ | |
"id": 18, | |
"name": "co2", | |
"unit": { | |
"factor": 1, | |
"label": "ppm" | |
}, | |
"example": { | |
"data": "12E204", | |
"result": "1250" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 19, | |
"name": "tvoc", | |
"unit": { | |
"factor": 1, | |
"label": "ug/m3" | |
}, | |
"example": { | |
"data": "133301", | |
"result": "307" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 20, | |
"name": "moisture", | |
"unit": { | |
"factor": 0.01, | |
"label": "%" | |
}, | |
"example": { | |
"data": "14020C", | |
"result": "30.74" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 21, | |
"name": "battery", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Normal", | |
"Low" | |
] | |
}, | |
{ | |
"id": 22, | |
"name": "battery charging", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Not Charging", | |
"Charging" | |
] | |
}, | |
{ | |
"id": 23, | |
"name": "carbon monoxide", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Not detected", | |
"Detected" | |
] | |
}, | |
{ | |
"id": 24, | |
"name": "cold", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Normal", | |
"Cold" | |
] | |
}, | |
{ | |
"id": 25, | |
"name": "connectivity", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Disconnected", | |
"Connected" | |
] | |
}, | |
{ | |
"id": 26, | |
"name": "door", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Closed", | |
"Open" | |
] | |
}, | |
{ | |
"id": 27, | |
"name": "garage door", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Closed", | |
"Open" | |
] | |
}, | |
{ | |
"id": 28, | |
"name": "gas", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Clear", | |
"Detected" | |
] | |
}, | |
{ | |
"id": 29, | |
"name": "heat", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Normal", | |
"Hot" | |
] | |
}, | |
{ | |
"id": 30, | |
"name": "light", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"No light", | |
"Light detected" | |
] | |
}, | |
{ | |
"id": 31, | |
"name": "lock", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Locked", | |
"Unlocked" | |
] | |
}, | |
{ | |
"id": 32, | |
"name": "moisture", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Dry", | |
"Wet" | |
] | |
}, | |
{ | |
"id": 33, | |
"name": "motion", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Clear", | |
"Detected" | |
] | |
}, | |
{ | |
"id": 34, | |
"name": "moving", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Not moving", | |
"Moving" | |
] | |
}, | |
{ | |
"id": 35, | |
"name": "occupancy", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Clear", | |
"Detected" | |
] | |
}, | |
{ | |
"id": 36, | |
"name": "plug", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Unplugged", | |
"Plugged in" | |
] | |
}, | |
{ | |
"id": 37, | |
"name": "presence", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Away", | |
"Home" | |
] | |
}, | |
{ | |
"id": 38, | |
"name": "problem", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"OK", | |
"Problem" | |
] | |
}, | |
{ | |
"id": 39, | |
"name": "running", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Not Running", | |
"Running" | |
] | |
}, | |
{ | |
"id": 40, | |
"name": "safety", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Unsafe", | |
"Safe" | |
] | |
}, | |
{ | |
"id": 41, | |
"name": "smoke", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Clear", | |
"Detected" | |
] | |
}, | |
{ | |
"id": 42, | |
"name": "sound", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Clear", | |
"Detected" | |
] | |
}, | |
{ | |
"id": 43, | |
"name": "tamper", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Off", | |
"On" | |
] | |
}, | |
{ | |
"id": 44, | |
"name": "vibration", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Clear", | |
"Detected" | |
] | |
}, | |
{ | |
"id": 45, | |
"name": "window", | |
"kind": "boolean", | |
"size": 1, | |
"boolLabels": [ | |
"Closed", | |
"Open" | |
] | |
}, | |
{ | |
"id": 46, | |
"name": "humidity", | |
"unit": { | |
"factor": 1, | |
"label": "%" | |
}, | |
"example": { | |
"data": "2E23", | |
"result": "35" | |
}, | |
"kind": "number", | |
"size": 1 | |
}, | |
{ | |
"id": 47, | |
"name": "moisture", | |
"unit": { | |
"factor": 1, | |
"label": "%" | |
}, | |
"example": { | |
"data": "2F23", | |
"result": "35" | |
}, | |
"kind": "number", | |
"size": 1 | |
}, | |
{ | |
"id": 58, | |
"name": "button", | |
"size": 1, | |
"kind": "trigger", | |
"enum": [ | |
[ | |
0, | |
"None" | |
], | |
[ | |
1, | |
"press" | |
], | |
[ | |
2, | |
"double_press" | |
], | |
[ | |
3, | |
"triple_press" | |
], | |
[ | |
4, | |
"long_press" | |
], | |
[ | |
5, | |
"long_double_press" | |
], | |
[ | |
6, | |
"long_triple_press" | |
], | |
[ | |
128, | |
"hold_press" | |
] | |
] | |
}, | |
{ | |
"id": 60, | |
"name": "dimmer", | |
"size": 2, | |
"kind": "trigger_with_count", | |
"enum": [ | |
[ | |
0, | |
"None" | |
], | |
[ | |
1, | |
"rotate left" | |
], | |
[ | |
2, | |
"rotate right" | |
] | |
] | |
}, | |
{ | |
"id": 61, | |
"name": "count", | |
"unit": { | |
"factor": 1, | |
"label": "" | |
}, | |
"example": { | |
"data": "3D0960", | |
"result": "24585" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 62, | |
"name": "count", | |
"unit": { | |
"factor": 1, | |
"label": "" | |
}, | |
"example": { | |
"data": "3E2A2C0960", | |
"result": "1611213866" | |
}, | |
"kind": "number", | |
"size": 4 | |
}, | |
{ | |
"id": 63, | |
"name": "rotation", | |
"unit": { | |
"factor": 0.1, | |
"label": "°" | |
}, | |
"example": { | |
"data": "3F020C", | |
"result": "307.4" | |
}, | |
"kind": "number", | |
"size": 2, | |
"signed": true | |
}, | |
{ | |
"id": 64, | |
"name": "distance (mm)", | |
"unit": { | |
"factor": 1, | |
"label": "mm" | |
}, | |
"example": { | |
"data": "400C00", | |
"result": "12" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 65, | |
"name": "distance (m)", | |
"unit": { | |
"factor": 0.1, | |
"label": "m" | |
}, | |
"example": { | |
"data": "414E00", | |
"result": "7.8" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 66, | |
"name": "duration", | |
"unit": { | |
"factor": 0.001, | |
"label": "s" | |
}, | |
"example": { | |
"data": "424E3400", | |
"result": "13.390" | |
}, | |
"kind": "number", | |
"size": 3 | |
}, | |
{ | |
"id": 67, | |
"name": "current", | |
"unit": { | |
"factor": 0.001, | |
"label": "A" | |
}, | |
"example": { | |
"data": "434E34", | |
"result": "13.39" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 68, | |
"name": "speed", | |
"unit": { | |
"factor": 0.01, | |
"label": "m/s" | |
}, | |
"example": { | |
"data": "444E34", | |
"result": "133.90" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 69, | |
"name": "temperature", | |
"unit": { | |
"factor": 0.1, | |
"label": "°C" | |
}, | |
"example": { | |
"data": "451101", | |
"result": "27.3" | |
}, | |
"kind": "number", | |
"size": 2, | |
"signed": true | |
}, | |
{ | |
"id": 70, | |
"name": "UV index", | |
"unit": { | |
"factor": 0.1, | |
"label": "" | |
}, | |
"example": { | |
"data": "4632", | |
"result": "5.0" | |
}, | |
"kind": "number", | |
"size": 1 | |
}, | |
{ | |
"id": 71, | |
"name": "volume", | |
"unit": { | |
"factor": 0.1, | |
"label": "L" | |
}, | |
"example": { | |
"data": "478756", | |
"result": "2215.1" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 72, | |
"name": "volume", | |
"unit": { | |
"factor": 1, | |
"label": "mL" | |
}, | |
"example": { | |
"data": "48DC87", | |
"result": "34780" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 73, | |
"name": "volume flow rate", | |
"unit": { | |
"factor": 0.001, | |
"label": "m3/hr" | |
}, | |
"example": { | |
"data": "49DC87", | |
"result": "34.780" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 74, | |
"name": "voltage", | |
"unit": { | |
"factor": 0.1, | |
"label": "V" | |
}, | |
"example": { | |
"data": "4A020C", | |
"result": "307.4" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 75, | |
"name": "gas", | |
"unit": { | |
"factor": 0.001, | |
"label": "m3" | |
}, | |
"example": { | |
"data": "4B138A14", | |
"result": "1346.067" | |
}, | |
"kind": "number", | |
"size": 3 | |
}, | |
{ | |
"id": 76, | |
"name": "gas", | |
"unit": { | |
"factor": 0.001, | |
"label": "m3" | |
}, | |
"example": { | |
"data": "4C41018A01", | |
"result": "25821.505" | |
}, | |
"kind": "number", | |
"size": 4 | |
}, | |
{ | |
"id": 77, | |
"name": "energy", | |
"unit": { | |
"factor": 0.001, | |
"label": "kWh" | |
}, | |
"example": { | |
"data": "4d12138a14", | |
"result": "344593.170" | |
}, | |
"kind": "number", | |
"size": 4 | |
}, | |
{ | |
"id": 78, | |
"name": "volume", | |
"unit": { | |
"factor": 0.001, | |
"label": "L" | |
}, | |
"example": { | |
"data": "4E87562A01", | |
"result": "19551.879" | |
}, | |
"kind": "number", | |
"size": 4 | |
}, | |
{ | |
"id": 79, | |
"name": "water", | |
"unit": { | |
"factor": 0.001, | |
"label": "L" | |
}, | |
"example": { | |
"data": "4F87562A01", | |
"result": "19551.879" | |
}, | |
"kind": "number", | |
"size": 4 | |
}, | |
{ | |
"id": 80, | |
"name": "timestamp", | |
"unit": { | |
"factor": 1, | |
"label": "s since epoch" | |
}, | |
"example": { | |
"data": "505d396164", | |
"result": "2023-05-14T19:41:17.000Z" | |
}, | |
"kind": "epochS", | |
"size": 4 | |
}, | |
{ | |
"id": 81, | |
"name": "acceleration", | |
"unit": { | |
"factor": 0.001, | |
"label": "m/s²" | |
}, | |
"example": { | |
"data": "518756", | |
"result": "22.151" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 82, | |
"name": "gyroscope", | |
"unit": { | |
"factor": 0.001, | |
"label": "°/s" | |
}, | |
"example": { | |
"data": "528756", | |
"result": "22.151" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 83, | |
"name": "text", | |
"example": { | |
"data": "530C48656C6C6F20576F726C6421", | |
"result": "Hello World!" | |
}, | |
"size": -1, | |
"kind": "utf8" | |
}, | |
{ | |
"id": 84, | |
"name": "raw", | |
"example": { | |
"data": "540C48656C6C6F20576F726C6421", | |
"result": "48656c6c6f20576f726c6421" | |
}, | |
"size": -1, | |
"kind": "hex" | |
}, | |
{ | |
"id": 85, | |
"name": "volume storage", | |
"unit": { | |
"factor": 0.001, | |
"label": "L" | |
}, | |
"example": { | |
"data": "5587562A01", | |
"result": "19551.879" | |
}, | |
"kind": "number", | |
"size": 4 | |
}, | |
{ | |
"id": 86, | |
"name": "conductivity", | |
"unit": { | |
"factor": 1, | |
"label": "µS/cm" | |
}, | |
"example": { | |
"data": "56E803", | |
"result": "1000" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 87, | |
"name": "temperature", | |
"unit": { | |
"factor": 1, | |
"label": "°C" | |
}, | |
"example": { | |
"data": "57EA", | |
"result": "-22" | |
}, | |
"kind": "number", | |
"size": 1, | |
"signed": true | |
}, | |
{ | |
"id": 88, | |
"name": "temperature", | |
"unit": { | |
"factor": 0.35, | |
"label": "°C" | |
}, | |
"example": { | |
"data": "58EA", | |
"result": "-7.7" | |
}, | |
"kind": "number", | |
"size": 1, | |
"signed": true | |
}, | |
{ | |
"id": 89, | |
"name": "count", | |
"unit": { | |
"factor": 1, | |
"label": "" | |
}, | |
"example": { | |
"data": "59EA", | |
"result": "-22" | |
}, | |
"kind": "number", | |
"size": 1, | |
"signed": true | |
}, | |
{ | |
"id": 90, | |
"name": "count", | |
"unit": { | |
"factor": 1, | |
"label": "" | |
}, | |
"example": { | |
"data": "5AEAEA", | |
"result": "-5398" | |
}, | |
"kind": "number", | |
"size": 2, | |
"signed": true | |
}, | |
{ | |
"id": 91, | |
"name": "count", | |
"unit": { | |
"factor": 1, | |
"label": "" | |
}, | |
"example": { | |
"data": "5BEA0234EA", | |
"result": "-365690134" | |
}, | |
"kind": "number", | |
"size": 4, | |
"signed": true | |
}, | |
{ | |
"id": 92, | |
"name": "power", | |
"unit": { | |
"factor": 0.01, | |
"label": "W" | |
}, | |
"example": { | |
"data": "5C02FBFFFF", | |
"result": "-12.78" | |
}, | |
"kind": "number", | |
"size": 4, | |
"signed": true | |
}, | |
{ | |
"id": 93, | |
"name": "current", | |
"unit": { | |
"factor": 0.001, | |
"label": "A" | |
}, | |
"example": { | |
"data": "5D02EA", | |
"result": "-5.63" | |
}, | |
"kind": "number", | |
"size": 2, | |
"signed": true | |
}, | |
{ | |
"id": 94, | |
"name": "direction", | |
"unit": { | |
"factor": 0.01, | |
"label": "°" | |
}, | |
"example": { | |
"data": "5E9F8C", | |
"result": "359.99" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 95, | |
"name": "precipitation", | |
"unit": { | |
"factor": 0.1, | |
"label": "mm" | |
}, | |
"example": { | |
"data": "5FA00F", | |
"result": "400.0" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 96, | |
"name": "channel", | |
"unit": { | |
"factor": 1, | |
"label": "" | |
}, | |
"example": { | |
"data": "6001", | |
"result": "01" | |
}, | |
"kind": "number", | |
"size": 1 | |
}, | |
{ | |
"id": 240, | |
"name": "device type id", | |
"example": { | |
"data": "F00100", | |
"result": "1" | |
}, | |
"kind": "number", | |
"size": 2 | |
}, | |
{ | |
"id": 241, | |
"name": "firmware version", | |
"example": { | |
"data": "F100010204", | |
"result": "4.2.1.0" | |
}, | |
"kind": "version", | |
"size": 4 | |
}, | |
{ | |
"id": 242, | |
"name": "firmware version", | |
"example": { | |
"data": "F2000106", | |
"result": "6.1.0" | |
}, | |
"kind": "version", | |
"size": 3 | |
} | |
] |
This file contains hidden or 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
#!/usr/bin/env node | |
function run_this_in_the_browser_and_pass_downloaded_json_path_as_first_argument() { | |
// Visit https://bthome.io/format/ | |
(function tablesToJSON() { | |
function downloadBlob(blob, mime) { | |
if (typeof blob === "string") blob = new Blob([blob], { type: mime || "application/octet-stream" }); | |
const url = URL.createObjectURL(blob); | |
window.location.assign(url); | |
URL.revokeObjectURL(url); | |
} | |
const result = [...document.querySelectorAll("table")].map(function tableToJSON(table) { | |
let headers; | |
const rows = []; | |
table.querySelectorAll("tr").forEach(row => { | |
const dataRow = [...row.querySelectorAll("td")].map(x => x.innerText); | |
if (dataRow.length) | |
rows.push(dataRow); | |
else { | |
const isHeaders = [...row.querySelectorAll("th")].map(x => x.innerText); | |
if (!isHeaders.length) console.error("Skipping unknown row", row); | |
else if (headers) console.error("Ignoring after first header row", row, isHeaders) | |
headers = isHeaders; | |
} | |
}); | |
return { headers, rows }; | |
}); | |
downloadBlob(JSON.stringify(result, null, 2)); | |
})(); | |
} | |
if (!process.argv[2]) throw new Error(`Missing argument: the downloaded JSON file (see this source code)`); | |
const tables = JSON.parse(require('fs').readFileSync(process.argv[2], "utf8")); | |
const expectedHeaders = [ | |
'Object id,Property,Data type,Factor,Example,Result,Unit', | |
'Object id,Property,Data type,Example,Result', | |
'Object id,Device type,Event id,Event type,Event property,Example,Result', | |
'Object id,Property,Data Type,Example,Result', | |
'Object id,Property,Data Type,Example,Result' | |
]; | |
tables.forEach((t, i) => { if (t.headers.join(",") != expectedHeaders[i]) throw new Error(`Expected data type column ${JSON.stringify(t.headers)}`)}); | |
const numericSensors = tables[0]; | |
const binarySensors = tables[1]; | |
const eventSensors = tables[2]; | |
const deviceInfo = tables[3]; | |
const packetInfo = tables[4]; | |
const tablesWithDataTypes = [ numericSensors, binarySensors, deviceInfo, packetInfo ]; | |
const dataTypes = { | |
'sint8 (1 byte)': { kind: 'number', size: 1, signed: true, }, | |
'sint16 (2 bytes)': { kind: 'number', size: 2, signed: true, }, | |
// sint24 is not supported, hoping it doesn't appear | |
'sint32 (4 bytes)': { kind: 'number', size: 4, signed: true, }, | |
'uint8 (1 byte)': { kind: 'number', size: 1 }, // "uint8 (1 byte)" (the space after the number isn't normal) | |
'uint16 (2 bytes)': { kind: 'number', size: 2 }, | |
'uint24 (3 bytes)': { kind: 'number', size: 3 }, | |
'uint32 (4 bytes)': { kind: 'number', size: 4 }, | |
"see below": { size: -1 }, | |
get(label) { return this[label.replace(/\s/g, ' ')] }, | |
} | |
// Check all types are known | |
tablesWithDataTypes.forEach(({rows}) => rows.forEach(row => { | |
if (!dataTypes.get(row[2])) throw new Error(`Unknown data type ${JSON.stringify(row[2])}`) | |
})); | |
const parseHex = (str) => parseInt(/^0x([0-9a-f]{2})$/i.exec(str)[1], 16); | |
const parseNumericSensors = (table) => | |
table.rows.map(row => { | |
const dataType = dataTypes.get(row[2]); | |
const common = { | |
id: parseHex(row[0]), | |
name: row[1], | |
...( | |
row.length === 7 | |
? { | |
unit: { factor: parseFloat(row[3]), label: row[6], }, | |
example: { data: row[4].replace(/\s+/g, ''), result: row[5] }, | |
} | |
: { | |
example: { data: row[3].replace(/\s+/g, ''), result: row[4] }, | |
} | |
), | |
...dataType, | |
}; | |
// numericSensors | |
if (common.id === 80 && common.name === "timestamp") { | |
common.unit = { factor: 1, label: "s since epoch" }; | |
common.example.result = "2023-05-14T19:41:17.000Z"; | |
common.kind = "epochS"; | |
} else if (common.id === 83 && common.name === "text") { | |
delete common.unit; | |
common.kind = "utf8"; | |
} else if (common.id === 84 && common.name === "raw") { | |
delete common.unit; | |
common.kind = "hex"; | |
common.example.result = common.example.result.replace(/\s+/g, ''); | |
} | |
// deviceInfo | |
else if ((common.id === 241 || common.id === 242) && common.name === "firmware version") | |
common.kind = "version"; | |
return common; | |
}); | |
const parseBinarySensors = (table) => | |
table.rows.map(row => { | |
const dataType = dataTypes.get(row[2]); | |
if (dataType.size !== 1 || dataType.signed) throw new Error(`Invalid for bool sensor`); | |
return { | |
id: parseHex(row[0]), | |
name: row[1], | |
...dataType, | |
boolLabels: [ | |
/False = ([^)]*)/i.exec(row[4])[1], | |
/True = ([^)]*)/i.exec(row[4])[1], | |
], | |
kind: 'boolean', | |
}; | |
}); | |
const parseEventSensors = (table = eventSensors) => { | |
const result = []; | |
table.rows.forEach(row => { | |
const enumValue = [ parseHex(row[2]), row[3] ]; | |
if (row[0]) { | |
const size = (row[5].length / 2) - 1; | |
result.push({ | |
id: parseHex(row[0]), | |
name: row[1], | |
size, | |
kind: size === 1 ? 'trigger' : (size === 2 ? 'trigger_with_count' : `Unknown size in event sensor ${size}`), | |
enum: [], | |
}) | |
} | |
result[result.length - 1].enum.push(enumValue); | |
}); | |
return result; | |
} | |
const sensors = [].concat( | |
parseNumericSensors(numericSensors), | |
parseNumericSensors(deviceInfo), | |
parseEventSensors(), | |
parseNumericSensors(packetInfo), | |
parseBinarySensors(binarySensors), | |
); | |
function byteLengthPrefixedStringFromBuffer(buffer, offset, encoding = 'utf8') { | |
if (buffer.length < offset + 1) throw new Error(`Not enough data`); | |
const size = buffer[offset]; | |
if (buffer.length < offset + size + 1) throw new Error(`Not enough data`); | |
return [size + 1, buffer.subarray(offset + 1, offset + 1 + size).toString(encoding)]; | |
} | |
function numberFromBuffer(buffer, offset, size, signed) { | |
if (buffer.length < offset + size) throw new Error(`Not enough data`); | |
const functionName = `read${signed ? 'I' : 'Ui'}nt${size * 8}${size > 1 ? 'LE' : ''}`; | |
if (functionName === 'readInt24LE') throw new Error(`Signed 3 byte is not supported`); | |
else if (functionName === 'readUint24LE') { | |
let num = 0; | |
for (let i = 2; i >= 0; i--) | |
num = (num << 8) | buffer[offset + i]; | |
return num; | |
} else | |
return buffer[functionName](offset); | |
} | |
/** @returns [bytesRead, value, sensor] */ | |
function readSensor(buffer, offset) { | |
const sensorID = buffer[offset++]; | |
const sensor = sensors.find(sensor => sensor.id === sensorID); | |
if (!sensor) throw new Error(`Unknown sensor ID ${sensorID}`); | |
switch (sensor.kind) { | |
case "number": | |
case "version": | |
case "epochS": | |
const number = numberFromBuffer(buffer, offset, sensor.size, sensor.signed); | |
return [ sensor.size + 1, number, sensor ]; | |
case "hex": | |
case "utf8": | |
const [ bytesRead, value ] = byteLengthPrefixedStringFromBuffer(buffer, offset, sensor.kind); | |
return [ bytesRead + 1, value, sensor ]; | |
case "trigger": | |
case "trigger_with_count": | |
const hasCount = sensor.kind === "trigger_with_count"; | |
const eventId = numberFromBuffer(buffer, offset, 1); | |
const enumEntry = sensor.enum.find(e => e[0] === eventId); | |
const result = { trigger: enumEntry ? enumEntry[1] : `0x${value.toString(16)}` }; | |
if (hasCount) result.arg = numberFromBuffer(buffer, offset + 1, 1); | |
return [ hasCount ? 3 : 2, result, sensor ]; | |
case "boolean": | |
return [ 2, numberFromBuffer(buffer, offset, 1) != 0, sensor ]; | |
default: | |
throw new Error(`Unknown sensor value kind: ${JSON.stringify(sensor)}`); | |
} | |
} | |
function getEnums() { | |
const triggers = {}; | |
sensors.forEach(sensor => { | |
if (sensor.enum) triggers[sensor.name] = sensor.enum; | |
}); | |
return triggers; | |
} | |
/** @returns [bytesRead, formattedValue, value, sensor] */ | |
function formatReadSensor(buffer, offset) { | |
const [ size, value, sensor ] = readSensor(buffer, offset); | |
let formatted = value; | |
const formatters = { | |
number: () => sensor.unit ? value * sensor.unit.factor : value, | |
epochS: () => new Date(value * 1000).toISOString(), | |
version: () => new Array(sensor.size).fill(0).map((_, i) => (value >> (sensor.size - i - 1) * 8) & 0xff).join("."), | |
boolean: () => sensor.boolLabels[value ? 1 : 0], | |
}; | |
if (sensor.kind in formatters) formatted = formatters[sensor.kind](); | |
return [ size, formatted, value, sensor ]; | |
} | |
const areFloatsEqual = (a, b) => Math.abs(a - b) < 1e-6; | |
// Check example.result against example.data when available | |
sensors.forEach(sensorDUT => { | |
if (!sensorDUT.example?.data) return; | |
if (!sensorDUT.example?.result) return; | |
const [size, formatted, value, sensor] = formatReadSensor(Buffer.from(sensorDUT.example.data, "hex"), 0); | |
if (typeof formatted == "number") { | |
if (!areFloatsEqual(formatted, parseFloat(sensorDUT.example?.result))) | |
throw new Error(`Sensor example error, ${JSON.stringify(formatted)} != ${JSON.stringify(sensorDUT.example?.result)}`); | |
} else if (formatted !== sensorDUT.example?.result) | |
throw new Error(`Sensor example error, ${JSON.stringify(formatted)} != ${JSON.stringify(sensorDUT.example?.result)}`); | |
}); | |
sensors.sort((a, b) => a.id - b.id); | |
// sensors.sort((a, b) => a.name.localeCompare(b.name)); | |
console.log(JSON.stringify(sensors, null, 2)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment