--- /dev/null
+**/__pycache__
+debian/*debhelper*
+debian/*.substvars
+debian/files
+debian/tbcncollector/
+.pybuild
+build
+tbcncollector.egg-info/
--- /dev/null
+# Daemon to collect beacon data from ThermoBeacon units and store in sqlite
--- /dev/null
+tbcncollector (0.01) experimental; urgency=low
+
+ * Initial version
+
+ -- Eugene Crosser <crosser@average.org> Tue, 09 Jul 2024 22:38:11 +0200
--- /dev/null
+Source: tbcncollector
+Maintainer: Eugene Crosser <crosser@average.org>
+Section: misc
+Priority: optional
+Standards-Version: 4.5.1
+X-Python-Version: >= 3.6
+Homepage: http://www.average.org/tbcncollector
+Build-Depends: black,
+ debhelper-compat (= 12),
+ dh-python,
+ mypy,
+ pylint,
+ python3-all,
+ python3-setuptools,
+ python3-bleak
+
+Package: tbcncollector
+Architecture: all
+Section: python
+Depends: adduser,
+ python3-bleak,
+ ${misc:Depends},
+ ${python3:Depends}
+Description: Daemon to collect data from ThermoBeacons
+ Stores beacon data: temperature and humidity in sqlite
--- /dev/null
+License: MIT
+
+Copyright (c) 2022 Eugene Crosser
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null
+# Environment for tbcncollector
+OPTIONS=""
--- /dev/null
+README.md var/lib/tbcncollector
--- /dev/null
+#!/bin/sh
+
+set -e
+adduser --system --group --home /var/lib/tbcncollector tbcncollector
+install --owner tbcncollector --group tbcncollector --directory /var/lib/tbcncollector
+
+#DEBHELPER#
+
+exit 0
--- /dev/null
+#!/usr/bin/make -f
+
+export PYBUILD_NAME=tbcncollector
+#export PYBUILD_BEFORE_TEST=cp -r mypystubs {build_dir}
+#export PYBUILD_AFTER_TEST=rm -rf {build_dir}/mypystubs
+
+%:
+ dh $@ --with python3 --buildsystem pybuild
--- /dev/null
+3.0 (native)
--- /dev/null
+[Unit]
+Description=ThermoBeacon Collector Service
+
+[Service]
+Type=simple
+EnvironmentFile=-/etc/default/tbcncollector
+ExecStart=python3 -m tbcncollector $OPTIONS
+KillSignal=INT
+Restart=on-failure
+StandardOutput=journal
+StandardError=inherit
+User=tbcncollector
+Group=tbcncollector
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null
+from setuptools import setup
+from re import findall
+
+with open("debian/changelog", "r") as clog:
+ _, version, _ = findall(
+ r"(?P<src>.*) \((?P<version>.*)\) (?P<suite>.*); .*",
+ clog.readline().strip(),
+ )[0]
+
+setup(
+ name="tbcncollector",
+ version=version,
+ description="Daemon to collect reports from ThermoBeacon devices",
+ url="http://www.average.org/tbcncollector/",
+ author="Eugene Crosser",
+ author_email="crosser@average.org",
+ install_requires=["bleak"],
+ license="MIT",
+ packages=[
+ "tbcncollector",
+ ],
+ long_description=open("README.md").read(),
+)
--- /dev/null
+""" TermoBeacon Collector daemon """
+
+import asyncio
+from sys import argv
+from sqlite3 import connect
+from struct import unpack
+from time import time
+
+from bleak import BleakScanner
+
+db = None
+INIT = (
+ "create table if not exists autoinc(num int)",
+ "insert into autoinc(num) select 0 where not exists"
+ " (select 1 from autoinc)",
+ "create table if not exists data(dt int, id int, addr text, batt float,"
+ " temp float, humid float, tminc int, rssi int, primary key(dt, id))"
+ " without rowid",
+ "create temp trigger before insert on data begin"
+ " update autoinc set num=num+1;"
+ " end",
+)
+lastcommit = 0.0
+
+def detection_callback(dev, data):
+ global lastcommit
+ # print("address", dev.address, "name", dev.name, "rssi", data.rssi)
+ if dev.address.startswith("A3:E4:"):
+ for k, v in data.manufacturer_data.items():
+ if len(v) == 18:
+ b, t, h, x, y, z = unpack("HHHBBB", v[8:17])
+ tm = (z<<16) + (y<<8) + x
+ dic = {"addr": dev.address,
+ "batt": b/1000,
+ "temp": t/16,
+ "humid": h/16,
+ "time": tm,
+ "rssi": data.rssi,
+ }
+ # bat 2.3 is empty, 3.1 is full
+ # print(dic)
+ db.execute("""\
+ insert into data (dt, id, addr, batt, temp, humid, tminc)
+ values (unixepoch(), (select num from autoinc),
+ :addr, :batt, :temp, :humid, :time)""",
+ dic)
+ now = time()
+ if now - lastcommit > 100.0:
+ db.commit()
+ lastcommit = now
+
+async def main():
+ async with BleakScanner(detection_callback=detection_callback):
+ await asyncio.Future()
+
+async def shutdown():
+ db.commit()
+ db.close()
+ print("Shutdown complete")
+
+if __name__ == "__main__":
+ dbname = argv[1] if len(argv) == 2 else "/var/lib/tbcncollector/raw.db"
+ db = connect(dbname)
+ for cmd in INIT:
+ db.execute(cmd)
+ lastcommit = time()
+ try:
+ asyncio.run(main())
+ except KeyboardInterrupt:
+ asyncio.run(shutdown())