83a2d8c963
atc.lua becomes sensors.lua: a shared AD-structure walker now feeds three decoders -- ATC1441/pvvx on service data 0x181A, Govee H5075's packed 24-bit manufacturer data 0xEC88, and BTHome v2 on 0xFCD2, which is what pvvx firmware advertises by default. Both new formats were decoded from adverts captured off the air and are asserted from those bytes. Sensors are remembered once seen, so one that stops advertising ages in place instead of vanishing, and the list only grows. Every field is its own node -- name, temperature, humidity, battery, age -- so the one second tick repaints the fields that moved rather than rebuilding: a rebuild is left only for a newly discovered sensor, which has no node to dirty. Each sensor is a card. The emulator has no Bluetooth controller, so the skill now says to keep BLE off it entirely rather than chase a panic.
137 lines
5.7 KiB
Lua
137 lines
5.7 KiB
Lua
-- 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"
|