-- Run: lua test/bletemp.lua package.path = "sdcard/.lua/apps/BLETemp/?.lua;sdcard/.lua/lib/?.lua;" .. "lib/esp32-lua-api/lua/lib/?.lua;test/?.lua;" .. package.path local sensors = require "sensors" -- ATC1441: 13-byte service data, big-endian. temp 23.4C, humidity 55%, batt 90%. local atc1441 = string.char(0x10, 0x16, 0x1A, 0x18) .. string.char(0, 1, 2, 3, 4, 5) -- MAC .. string.char(0x00, 0xEA) -- temp 234 = 23.4C .. string.char(0x37) -- humidity 55 .. string.char(0x5A) -- battery 90 .. string.char(0x0B, 0x54, 0x01) -- mV, counter local reading = sensors.parse(atc1441) assert(reading, "atc1441 parsed nil") assert(reading.tempC == 23.4, reading.tempC) assert(reading.humidity == 55, reading.humidity) assert(reading.battery == 90, reading.battery) -- pvvx: 15-byte service data, little-endian. temp 21.55C, humidity 48.20%. local pvvx = string.char(0x12, 0x16, 0x1A, 0x18) .. string.char(0, 1, 2, 3, 4, 5) -- MAC .. string.char(0x73, 0x08) -- temp 2163 = 21.63C .. string.char(0xD4, 0x12) -- humidity 4820 = 48.20% .. string.char(0x54, 0x0B) -- mV .. string.char(0x5A) -- battery 90 .. string.char(0x01, 0x00) -- counter, flags reading = sensors.parse(pvvx) assert(reading, "pvvx parsed nil") assert(reading.tempC == 21.63, reading.tempC) assert(reading.humidity == 48.2, reading.humidity) assert(reading.battery == 90, reading.battery) -- Negative temperature, big-endian two's complement: -1.0C = 0xFFF6. local cold = string.char(0x10, 0x16, 0x1A, 0x18) .. string.char(0, 1, 2, 3, 4, 5) .. string.char(0xFF, 0xF6) .. string.char(0x37, 0x5A, 0x0B, 0x54, 0x01) assert(sensors.parse(cold).tempC == -1.0, sensors.parse(cold).tempC) -- Govee H5075, captured off the air: 22.7C, 64.5% RH, 60% battery. local govee = string.char(0x09, 0xFF, 0x88, 0xEC) .. string.char(0x00, 0x03, 0x79, 0x3D) -- packed 227645 = temp*1000 + hum*10 .. string.char(0x3C, 0x00) -- battery, trailing pad reading = sensors.parse(govee) assert(reading, "govee parsed nil") assert(reading.tempC == 22.7, reading.tempC) assert(reading.humidity == 64.5, reading.humidity) assert(reading.battery == 60, reading.battery) -- Govee negative temperature: bit 23 of the packed value is the sign. local goveeCold = string.char(0x09, 0xFF, 0x88, 0xEC) .. string.char(0x00, 0x80, 0x27, 0x22) -- 0x800000 | 10018 = -1.0C, 1.8% RH .. string.char(0x3C, 0x00) assert(sensors.parse(goveeCold).tempC == -1.0, sensors.parse(goveeCold).tempC) -- BTHome v2 (pvvx default), captured off the air: 22.2C, 62.77% RH, 31% batt. local bthome = string.char(0x0E, 0x16, 0xD2, 0xFC) .. string.char(0x40) -- device info: v2, unencrypted .. string.char(0x00, 0x5B) -- packet id .. string.char(0x01, 0x1F) -- battery 31% .. string.char(0x02, 0xAC, 0x08) -- temp 2220 = 22.20C .. string.char(0x03, 0x85, 0x18) -- humidity 6277 = 62.77% reading = sensors.parse(bthome) assert(reading, "bthome parsed nil") assert(reading.tempC == 22.2, reading.tempC) assert(reading.humidity == 62.77, reading.humidity) assert(reading.battery == 31, reading.battery) -- Encrypted BTHome cannot be read, and an unknown object id stops the walk -- before the temperature that follows it. assert(sensors.parse(string.char(0x07, 0x16, 0xD2, 0xFC, 0x41, 0x02, 0xAC, 0x08)) == nil) assert(sensors.parse(string.char(0x08, 0x16, 0xD2, 0xFC, 0x40, 0xF9, 0x00, 0x02, 0xAC)) == nil) -- Neither 0x181A service data nor Govee manufacturer data. assert(sensors.parse(string.char(0x02, 0x01, 0x06)) == nil) -- App renders a parsed sensor and starts observing the 0x181A service. local device = require "fake_device" device.bleDevices = { { name = "Living Room", address = "aa:bb", rssi = -55, payload = atc1441 }, { name = "GVH5075_CB50", address = "ee:ff", rssi = -64, payload = govee }, { name = "TH_NURSERY", address = "11:22", rssi = -88, payload = bthome }, { name = "Junk", address = "cc:dd", rssi = -70, payload = string.char(0x02, 0x01, 0x06) }, } device.install() device.start "sdcard/.lua/apps/BLETemp/main.lua" assert(device.bleObserving) assert(device.bleFilter.services[1] == "181A") assert(device.bleFilter.manufacturers[1] == 0xEC88) assert(device.labelled "Living Room") assert(device.labelled "23.4 C") assert(device.labelled "GVH5075_CB50") assert(device.labelled "22.7 C") assert(device.labelled "64.5% RH") assert(device.labelled "60% batt") assert(device.labelled "0s ago") assert(device.labelled "TH_NURSERY") assert(device.labelled "22.2 C") assert(not device.labelled "Junk") -- no 0x181A service data, so it is not a sensor -- A new reading from a known sensor updates that row alone; a new sensor is a -- new row, so the screen is rebuilt. local built = tree.getCount() device.bleDevices[1].payload = cold device.invalidated = {} device.fireTimers() assert(#device.invalidated == 1, #device.invalidated .. " nodes repainted, want 1") assert(tree.getCount() == built, "a reading update rebuilt the screen") assert(device.labelled "-1.0 C") device.bleDevices[#device.bleDevices + 1] = { name = "Bedroom", address = "99:88", rssi = -60, payload = pvvx } device.fireTimers() assert(device.labelled "Bedroom", "a new sensor did not appear") assert(tree.getCount() > built, "a new sensor did not rebuild the screen") -- A sensor that stops advertising keeps its row and ages in place: still one -- node repainted, no rebuild. built = tree.getCount() device.bleDevices = {} device.now = device.now + 125000 device.invalidated = {} device.fireTimers() assert(tree.getCount() == built, "a departed sensor rebuilt the screen") assert(#device.invalidated == 4, #device.invalidated .. " nodes repainted, want 4 age fields") assert(device.labelled "TH_NURSERY", "a departed sensor lost its row") assert(device.labelled "22.2 C") assert(device.labelled "62.8% RH") assert(device.labelled "2m ago") print "ok"