fix(gateway): use exact payload size filter and improve robustness

- Use exact 12-byte payload size filter instead of minimum to avoid
  false positives from non-Thingy devices with same company_id
- Discard CO2 value 0xFFFFFFFF indicating sensor not ready or failed
- Downgrade unknown key and empty payload logs to DEBUG level
- Add on_connect callback to confirm successful broker connection
- Add on_publish callback to confirm message delivery to broker
- Rename MIN_PAYLOAD_SIZE to EXPECTED_PAYLOAD_SIZE for clarity

Validated: data received and confirmed by broker after Thingy reboot
This commit is contained in:
DjeAvd
2026-05-14 21:50:45 +02:00
committed by Klagarge
parent 3aac195626
commit da5d20688b

View File

@@ -24,6 +24,13 @@ class Gateway:
KEY_TEMP = 0x03 KEY_TEMP = 0x03
KEY_CO2 = 0x04 KEY_CO2 = 0x04
# Sentinel value indicating sensor failure or not ready
INVALID_VALUE = 0xFFFFFFFF
# Expected payload size in bytes:
# 4 keys (1B each) + window(1B) + humidity(1B) + temp(2B) + co2(4B) = 12 bytes
EXPECTED_PAYLOAD_SIZE = 12
def __init__(self, config: dict): def __init__(self, config: dict):
self.gateway_id = config["gateway_id"] self.gateway_id = config["gateway_id"]
self.mqtt_broker = config["mqtt"]["broker"] self.mqtt_broker = config["mqtt"]["broker"]
@@ -81,6 +88,9 @@ class Gateway:
0x02 : humidity (1 byte, integer %) 0x02 : humidity (1 byte, integer %)
0x03 : temperature (2 bytes big-endian, integer / 10 = degrees C) 0x03 : temperature (2 bytes big-endian, integer / 10 = degrees C)
0x04 : CO2 ppm (4 bytes big-endian, integer) 0x04 : CO2 ppm (4 bytes big-endian, integer)
Values equal to 0xFFFFFFFF indicate sensor failure or not ready
and are discarded.
""" """
result = {} result = {}
i = 0 # no preamble to skip — company_id is not in raw bytes i = 0 # no preamble to skip — company_id is not in raw bytes
@@ -100,10 +110,16 @@ class Gateway:
result["temp"] = raw / 10 result["temp"] = raw / 10
i += 2 i += 2
elif key == self.KEY_CO2 and i + 3 < len(data): elif key == self.KEY_CO2 and i + 3 < len(data):
result["co2_ppm"] = int.from_bytes(data[i:i+4], byteorder='big') co2 = int.from_bytes(data[i:i+4], byteorder='big')
# 0xFFFFFFFF indicates sensor not ready or failed
if co2 != self.INVALID_VALUE:
result["co2_ppm"] = co2
else:
log.debug(f"CO2 sensor not ready — discarding value 0xFFFFFFFF")
i += 4 i += 4
else: else:
log.warning(f"Unknown key 0x{key:02x} at offset {i-1}") # Unknown key — likely a non-Thingy device, ignore silently
log.debug(f"Unknown key 0x{key:02x} at offset {i-1}")
break break
return result return result
@@ -132,15 +148,19 @@ class Gateway:
if 0xffff not in adv_data.manufacturer_data: if 0xffff not in adv_data.manufacturer_data:
return return
# company_id 0xffff is defined in the firmware spec
# the raw bytes do not include the company_id itself
raw = adv_data.manufacturer_data[0xffff] raw = adv_data.manufacturer_data[0xffff]
# Filter on exact payload size to avoid false positives from
# other BLE devices using the same company_id
if len(raw) != self.EXPECTED_PAYLOAD_SIZE:
log.debug(f"{device.address} | ignored — unexpected payload size: {len(raw)}")
return
log.debug(f"{device.address} | Thingy detected, raw: {list(raw)}") log.debug(f"{device.address} | Thingy detected, raw: {list(raw)}")
data = self.decode_payload(raw) data = self.decode_payload(raw)
if not data: if not data:
log.warning(f"{device.address} | empty decoded payload") log.debug(f"{device.address} | empty decoded payload — ignored")
return return
log.debug(f"{device.address} | decoded: {data}") log.debug(f"{device.address} | decoded: {data}")