215 lines
4.6 KiB
Go
215 lines
4.6 KiB
Go
// lywsd03mmc-exporter - a Prometheus exporter for the LYWSD03MMC BLE thermometer
|
|
|
|
// Copyright (C) 2020 Leah Neukirchen <leah@vuxu.org>
|
|
// Licensed under the terms of the MIT license, see LICENSE.
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-ble/ble"
|
|
"github.com/go-ble/ble/examples/lib/dev"
|
|
)
|
|
|
|
|
|
// Reading represents a Temperature|Humidity readings
|
|
type Reading struct {
|
|
Temperature float64
|
|
Humidity float64
|
|
}
|
|
|
|
const Sensor = "LYWSD03MMC"
|
|
const TelinkVendorPrefix = "a4:c1:38"
|
|
|
|
var EnvironmentalSensingUUID = ble.UUID16(0x181a)
|
|
var XiaomiIncUUID = ble.UUID16(0xfe95)
|
|
|
|
const ExpiryAtc = 2.5 * 10 * time.Second
|
|
const ExpiryStock = 2.5 * 10 * time.Minute
|
|
const ExpiryConn = 2.5 * 10 * time.Second
|
|
|
|
var expirers = make(map[string]*time.Timer)
|
|
var expirersLock sync.Mutex
|
|
|
|
|
|
|
|
var (
|
|
configFile = flag.String("config_file", "config.ini", "Config file location")
|
|
)
|
|
|
|
var config, ERR = NewConfig(*configFile)
|
|
|
|
func bump(mac string, expiry time.Duration) {
|
|
expirersLock.Lock()
|
|
if t, ok := expirers[mac]; ok {
|
|
t.Reset(expiry)
|
|
} else {
|
|
expirers[mac] = time.AfterFunc(expiry, func() {
|
|
fmt.Printf("expiring %s\n", mac)
|
|
expirersLock.Lock()
|
|
delete(expirers, mac)
|
|
expirersLock.Unlock()
|
|
})
|
|
}
|
|
expirersLock.Unlock()
|
|
}
|
|
|
|
func macWithColons(mac string) string {
|
|
return strings.ToUpper(fmt.Sprintf("%s:%s:%s:%s:%s:%s",
|
|
mac[0:2],
|
|
mac[2:4],
|
|
mac[4:6],
|
|
mac[6:8],
|
|
mac[8:10],
|
|
mac[10:12]))
|
|
}
|
|
|
|
func macWithoutColons(mac string) string {
|
|
return strings.ReplaceAll(strings.ToUpper(mac), ":", "")
|
|
}
|
|
|
|
func decodeSign(i uint16) int {
|
|
if i < 32768 {
|
|
return int(i)
|
|
} else {
|
|
return int(i) - 65536
|
|
}
|
|
}
|
|
|
|
func registerData(data []byte, frameMac string, rssi int) {
|
|
if len(data) != 13 {
|
|
return
|
|
}
|
|
|
|
mac := fmt.Sprintf("%X", data[0:6])
|
|
|
|
if mac != frameMac {
|
|
return
|
|
}
|
|
|
|
temp := float64(decodeSign(binary.BigEndian.Uint16(data[6:8]))) / 10.0
|
|
hum := float64(data[8])
|
|
batp := float64(data[9])
|
|
batv := float64(binary.BigEndian.Uint16(data[10:12])) / 1000.0
|
|
//frame := float64(data[12])
|
|
|
|
bump(mac, ExpiryAtc)
|
|
|
|
logTemperature(mac, temp)
|
|
logHumidity(mac, hum)
|
|
logBatteryPercent(mac, batp)
|
|
logVoltage(mac, batv)
|
|
logRssi(mac, rssi)
|
|
var r = Reading{temp, hum}
|
|
config.MQTT.Publish(mac,&r)
|
|
}
|
|
|
|
func advHandler(a ble.Advertisement) {
|
|
mac := strings.ReplaceAll(strings.ToUpper(a.Addr().String()), ":", "")
|
|
|
|
for _, sd := range a.ServiceData() {
|
|
if sd.UUID.Equal(EnvironmentalSensingUUID) {
|
|
registerData(sd.Data, mac, a.RSSI())
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
func logTemperature(mac string, temp float64) {
|
|
log.Printf("%s thermometer_temperature_celsius %.1f\n", mac, temp)
|
|
}
|
|
|
|
func logHumidity(mac string, hum float64) {
|
|
log.Printf("%s thermometer_humidity_ratio %.0f\n", mac, hum)
|
|
}
|
|
|
|
func logVoltage(mac string, batv float64) {
|
|
log.Printf("%s thermometer_battery_volts %.3f\n", mac, batv)
|
|
}
|
|
|
|
func logBatteryPercent(mac string, batp float64) {
|
|
log.Printf("%s thermometer_battery_ratio %.0f\n", mac, batp)
|
|
}
|
|
|
|
func logRssi(mac string, rssi int) {
|
|
log.Printf("%s thermometer_rssi %d\n", mac, rssi)
|
|
}
|
|
|
|
func decodeAtcTemp(mac string) func(req []byte) {
|
|
return func(req []byte) {
|
|
temp := float64(decodeSign(binary.LittleEndian.Uint16(req[0:2]))) / 10.0
|
|
bump(mac, ExpiryConn)
|
|
logTemperature(mac, temp)
|
|
}
|
|
}
|
|
|
|
func decodeAtcHumidity(mac string) func(req []byte) {
|
|
return func(req []byte) {
|
|
hum := float64(binary.LittleEndian.Uint16(req[0:2])) / 100.0
|
|
bump(mac, ExpiryConn)
|
|
logHumidity(mac, hum)
|
|
}
|
|
}
|
|
|
|
func decodeAtcBattery(mac string) func(req []byte) {
|
|
return func(req []byte) {
|
|
batp := float64(req[0])
|
|
bump(mac, ExpiryConn)
|
|
logBatteryPercent(mac, batp)
|
|
}
|
|
}
|
|
|
|
// ToString converts a Reading to a string
|
|
func (r *Reading) String() string {
|
|
return fmt.Sprintf("Temperature: %.04f; Humidity: %.04f", r.Temperature, r.Humidity)
|
|
}
|
|
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
//config, err := NewConfig(*configFile)
|
|
//if err != nil {
|
|
// log.Fatal("Unable to parse configuration")
|
|
//}
|
|
|
|
log.Printf("MQTT broker: %s", config.MQTT.Server())
|
|
if err := config.MQTT.Connect("xiaomi"); err != nil {
|
|
log.Print("[main] Unable to connect to MQTT broker")
|
|
}
|
|
|
|
|
|
deviceID := flag.Int("i", 0, "use device hci`N`")
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(os.Stderr,
|
|
"Usage: %s [FLAGS...] \n", os.Args[0])
|
|
flag.PrintDefaults()
|
|
}
|
|
flag.Parse()
|
|
|
|
device, err := dev.NewDevice("default", ble.OptDeviceID(*deviceID))
|
|
if err != nil {
|
|
log.Fatal("oops: ", err)
|
|
}
|
|
|
|
ble.SetDefaultDevice(device)
|
|
|
|
ctx := ble.WithSigHandler(context.Background(), nil)
|
|
|
|
telinkVendorFilter := func(a ble.Advertisement) bool {
|
|
return strings.HasPrefix(a.Addr().String(), TelinkVendorPrefix)
|
|
}
|
|
err = ble.Scan(ctx, true, advHandler, telinkVendorFilter)
|
|
if err != nil {
|
|
log.Fatal("oops: %s", err)
|
|
}
|
|
}
|