Files
pico-weather/main.go
T
gballan e5d78e8423
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
[Issue #8] - adding MQTT reconnect if the connection drops
2026-09-12 02:58:57 -04:00

281 lines
7.3 KiB
Go

package main
import (
"context"
"fmt"
"image/color"
"io"
"machine"
"strconv"
"time"
"tinygo.org/x/drivers/ssd1306"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/proggy"
)
var StateTopic = fmt.Sprintf("homeassistant/sensor/%s/state", NodeID)
// -------------------------------------------------------------
// Minimal MQTT 3.1.1 Client
// -------------------------------------------------------------
type SimpleMQTT struct {
rw io.ReadWriter
}
func NewSimpleMQTT(rw io.ReadWriter) *SimpleMQTT {
return &SimpleMQTT{rw: rw}
}
func (m *SimpleMQTT) Connect(clientID, username, password string) error {
flags := byte(0x02) // Clean session
if username != "" {
flags |= 0x80
}
if password != "" {
flags |= 0x40
}
// Variable header: MQTT (proto name), Level 4 (MQTT 3.1.1), flags, KeepAlive 0s (disabled)
varHeader := []byte{0x00, 0x04, 'M', 'Q', 'T', 'T', 0x04, flags, 0x00, 0x00}
payload := encodeString(clientID)
if username != "" {
payload = append(payload, encodeString(username)...)
}
if password != "" {
payload = append(payload, encodeString(password)...)
}
body := append(varHeader, payload...)
packet := append([]byte{0x10}, encodeLength(len(body))...)
packet = append(packet, body...)
if _, err := m.rw.Write(packet); err != nil {
return err
}
resp := make([]byte, 4)
if _, err := io.ReadFull(m.rw, resp); err != nil {
return err
}
if resp[0] != 0x20 || resp[3] != 0x00 {
return fmt.Errorf("MQTT connection refused: code %d", resp[3])
}
return nil
}
func (m *SimpleMQTT) Publish(topic string, payload []byte, retain bool) error {
cmd := byte(0x30)
if retain {
cmd |= 0x01
}
body := append(encodeString(topic), payload...)
packet := append([]byte{cmd}, encodeLength(len(body))...)
packet = append(packet, body...)
_, err := m.rw.Write(packet)
return err
}
func encodeString(s string) []byte {
b := []byte(s)
length := uint16(len(b))
return append([]byte{byte(length >> 8), byte(length & 0xFF)}, b...)
}
func encodeLength(length int) []byte {
var encoded []byte
for {
digit := byte(length % 128)
length /= 128
if length > 0 {
digit |= 0x80
}
encoded = append(encoded, digit)
if length == 0 {
break
}
}
return encoded
}
// -------------------------------------------------------------
// Home Assistant Auto-Discovery
// -------------------------------------------------------------
type haSensorDef struct {
id string
name string
unit string
class string
valTpl string
}
func registerHADiscovery(mqtt *SimpleMQTT, sensor ClimateSensor) error {
deviceJSON := fmt.Sprintf(`"device":{"identifiers":["%s"],"name":"%s","model":"Pico 2 W (%s)","manufacturer":"Raspberry Pi"}`,
NodeID, NodeName, sensor.Name())
sensors := []haSensorDef{
{"temperature", "Temperature", "°C", "temperature", "{{ value_json.temperature }}"},
{"humidity", "Humidity", "%", "humidity", "{{ value_json.humidity }}"},
}
if sensor.Name() == "BME280" {
sensors = append(sensors, haSensorDef{
id: "pressure",
name: "Pressure",
unit: "hPa",
class: "atmospheric_pressure",
valTpl: "{{ value_json.pressure }}",
})
}
for _, s := range sensors {
topic := fmt.Sprintf("homeassistant/sensor/%s/%s/config", NodeID, s.id)
payload := fmt.Sprintf(`{"name":"%s","has_entity_name":true,"unique_id":"%s_%s","device_class":"%s","state_class":"measurement","unit_of_measurement":"%s","state_topic":"%s","value_template":"%s",%s}`,
s.name, NodeID, s.id, s.class, s.unit, StateTopic, s.valTpl, deviceJSON)
if err := mqtt.Publish(topic, []byte(payload), true); err != nil {
return err
}
println("Registered HA discovery:", s.id)
}
return nil
}
// -------------------------------------------------------------
// Panic Handler & Helper Functions
// -------------------------------------------------------------
func panicErr(msg string, err error) {
if err != nil {
println("FATAL:", msg, "-", err.Error())
}
for {
time.Sleep(500 * time.Millisecond)
}
}
func connectMQTT(netStack *NetworkStack, port uint16) (*SimpleMQTT, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
conn, err := netStack.DialTCP(ctx, MQTTHost, port)
if err != nil {
return nil, err
}
mqtt := NewSimpleMQTT(conn)
if err := mqtt.Connect(ClientID, MQTTUser, MQTTPass); err != nil {
return nil, err
}
return mqtt, nil
}
func main() {
time.Sleep(2 * time.Second)
// Configure I2C0 bus
i2c := machine.I2C0
err := i2c.Configure(machine.I2CConfig{
Frequency: 400 * machine.KHz,
SDA: machine.GPIO4,
SCL: machine.GPIO5,
})
if err != nil {
panicErr("I2C configuration", err)
}
// Instantiate nil oled variable
var oled *ssd1306.Device
white := color.RGBA{R: 255, G: 255, B: 255, A: 255}
var dummy [1]byte
if err := i2c.ReadRegister(0x3C, 0x00, dummy[:]); err == nil {
println("SSD1306 detected at 0x3C")
dev := ssd1306.NewI2C(i2c)
dev.Configure(ssd1306.Config{
Address: 0x3C,
Width: 128,
Height: 64,
})
oled = dev
oled.ClearBuffer()
tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 16, "Booting...", white)
oled.Display()
} else {
println("SSD1306 not found — running headless")
}
// Initialize the configured sensor
sensor, err := InitSensor(i2c, SensorType)
if err != nil {
panicErr("Sensor init", err)
}
fmt.Printf("Started climate monitor using: %s\n", sensor.Name())
netStack, err := InitNetwork(WifiSSID, WifiPass)
if err != nil {
panicErr("network init", err)
}
portNum, _ := strconv.Atoi(MQTTPort)
println("Connecting MQTT session...")
mqtt, err := connectMQTT(netStack, uint16(portNum))
if err != nil {
panicErr("MQTT connect", err)
}
println("MQTT connected.")
if err := registerHADiscovery(mqtt, sensor); err != nil {
println("Warning: HA discovery failed:", err.Error())
}
for {
t, p, h, hasPres, err := sensor.ReadValues()
if err != nil {
println(fmt.Sprintf("%s read error: %s", sensor.Name(), err.Error()))
} else {
var stateJSON string
if hasPres {
stateJSON = fmt.Sprintf(`{"temperature":%.2f,"humidity":%.2f,"pressure":%.2f}`, t, h, p)
} else {
stateJSON = fmt.Sprintf(`{"temperature":%.2f,"humidity":%.2f}`, t, h)
}
if err := mqtt.Publish(StateTopic, []byte(stateJSON), false); err != nil {
println("MQTT publish error:", err.Error(), "- attempting reconnect...")
newMqtt, recErr := connectMQTT(netStack, uint16(portNum))
if recErr != nil {
println("MQTT reconnect failed:", recErr.Error())
} else {
println("MQTT reconnected successfully.")
mqtt = newMqtt
_ = registerHADiscovery(mqtt, sensor)
_ = mqtt.Publish(StateTopic, []byte(stateJSON), false)
}
} else {
println(fmt.Sprintf("[%s] Published: %s", NodeName, stateJSON))
}
if oled != nil {
oled.ClearBuffer()
tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 14, fmt.Sprintf("Node: %s", NodeName), white)
tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 30, fmt.Sprintf("Temp: %.1f C", t), white)
tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 46, fmt.Sprintf("Hum: %.1f %%", h), white)
if hasPres {
tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 60, fmt.Sprintf("Pres: %.1f hPa", p), white)
} else {
tinyfont.WriteLine(oled, &proggy.TinySZ8pt7b, 0, 60, fmt.Sprintf("Sens: %s", sensor.Name()), white)
}
oled.Display()
}
}
time.Sleep(UpdateInterval)
}
}