281 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|