Files
lywsd03mmc-mqtt-exporter/lywsd03mmc-exporter.go
2022-12-08 14:59:39 +01:00

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)
}
}