first commit
This commit is contained in:
214
lywsd03mmc-exporter.go
Normal file
214
lywsd03mmc-exporter.go
Normal file
@@ -0,0 +1,214 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user