From 6c556dae32215234ba1fc636445e4f9afdb12505 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sat, 4 Feb 2023 22:42:11 +0000 Subject: [PATCH 001/135] wip: clean go app structure --- api/api.go | 18 --- api/zone_controller.go | 36 ----- arduino/arduino.go | 175 -------------------- arduino/commands.go | 18 --- arduino/moisture_reading.go | 59 ------- arduino/moisture_sensor.go | 91 ----------- arduino/water_outlet.go | 40 ----- cmd/main.go | 7 + config/app_config.go | 68 -------- config/zone.go | 241 ---------------------------- homeassistant/zone_configuration.go | 84 ---------- main.go | 198 ----------------------- mqtt/client.go | 165 ------------------- pkg/app/app.go | 7 + {helpers => pkg/helpers}/slices.go | 0 world/moisture_level.go | 34 ---- 16 files changed, 14 insertions(+), 1227 deletions(-) delete mode 100644 api/api.go delete mode 100644 api/zone_controller.go delete mode 100644 arduino/arduino.go delete mode 100644 arduino/commands.go delete mode 100644 arduino/moisture_reading.go delete mode 100644 arduino/moisture_sensor.go delete mode 100644 arduino/water_outlet.go create mode 100644 cmd/main.go delete mode 100644 config/app_config.go delete mode 100644 config/zone.go delete mode 100644 homeassistant/zone_configuration.go delete mode 100644 main.go delete mode 100644 mqtt/client.go create mode 100644 pkg/app/app.go rename {helpers => pkg/helpers}/slices.go (100%) delete mode 100644 world/moisture_level.go diff --git a/api/api.go b/api/api.go deleted file mode 100644 index 7b344b8..0000000 --- a/api/api.go +++ /dev/null @@ -1,18 +0,0 @@ -package api - -import ( - "log" - "net/http" - - "github.com/mewejo/go-watering/config" -) - -func StartApi(app *config.Application) { - - zoneController := zoneController{ - app: app, - } - - http.HandleFunc("/api/zones/", zoneController.handle) - log.Fatal(http.ListenAndServe(":8080", nil)) -} diff --git a/api/zone_controller.go b/api/zone_controller.go deleted file mode 100644 index 0ad053b..0000000 --- a/api/zone_controller.go +++ /dev/null @@ -1,36 +0,0 @@ -package api - -import ( - "fmt" - "net/http" - "strconv" - - "github.com/mewejo/go-watering/config" - "github.com/mewejo/go-watering/world" -) - -type zoneController struct { - app *config.Application -} - -func (c zoneController) handle(w http.ResponseWriter, r *http.Request) { - percentage, err := strconv.Atoi(r.URL.Query().Get("target")) - - if err != nil { - fmt.Fprintf(w, "Bad percentage!") - return - } - - zoneIndex, err := strconv.Atoi(r.URL.Query().Get("zone")) - - if err != nil { - fmt.Fprintf(w, "Bad zone!") - return - } - - c.app.Zones[zoneIndex].TargetMoisture = world.MoistureLevel{ - Percentage: uint(percentage), - } - - fmt.Fprintf(w, "Set zone %v to %v pc", zoneIndex, percentage) -} diff --git a/arduino/arduino.go b/arduino/arduino.go deleted file mode 100644 index 353987f..0000000 --- a/arduino/arduino.go +++ /dev/null @@ -1,175 +0,0 @@ -package arduino - -import ( - "errors" - "log" - "strings" - "time" - - "go.bug.st/serial" -) - -type Arduino struct { - Port serial.Port -} - -func (a Arduino) SendCommand(command Command) error { - _, err := a.Port.Write([]byte(command)) - - return err -} - -func (a Arduino) ReadData(buffer []byte) (int, error) { - return a.Port.Read(buffer) -} - -func (a Arduino) ReadLine() string { - buff := make([]byte, 1) - data := "" - - for { - n, err := a.ReadData(buff) - - if err != nil { - log.Fatal(err) - } - - if n == 0 { - break - } - - data += string(buff[:n]) - - if strings.Contains(data, "\n") { - break - } - } - - return data -} - -func (a Arduino) ReadLines(until string) []string { - lines := []string{} - - for { - line := a.ReadLine() - - lines = append(lines, line) - - if strings.Contains(line, until) { - break - } - } - - return lines -} - -func (a Arduino) GetReadings() ([]MoistureReading, error) { - err := a.SendCommand(REQUEST_READINGS) - - if err != nil { - return nil, errors.New("could not request readings") - } - - time.Sleep(time.Millisecond * 250) - - readings := []MoistureReading{} - - for _, line := range a.ReadLines("READINGS_END") { - line = strings.TrimSuffix(line, "\n") - line = strings.TrimSuffix(line, "\r") - reading, err := MakeMoistureReadingFromString(line) - - if err != nil { - continue - } - - readings = append( - readings, - reading, - ) - } - - if len(readings) < 1 { - return nil, errors.New("no readings returned from Arduino") - } - - return readings, nil -} - -func (a Arduino) SetAllWaterState(state bool) error { - if state { - return a.SendCommand(WATER_ON) - } else { - return a.SendCommand(WATER_OFF) - } -} - -func (a Arduino) SetWaterState(outlet WaterOutlet, state bool) error { - - var err error - var command Command - - if state { - command, err = outlet.OnCommand() - } else { - command, err = outlet.OffCommand() - } - - if nil != err { - return err - } - - return a.SendCommand(command) -} - -func (a Arduino) WaitUntilReady() { - a.ReadLines("READY") -} - -func findArduinoPort() (string, error) { - ports, err := serial.GetPortsList() - - if err != nil { - log.Fatal(err) - } - - if len(ports) == 0 { - log.Fatal("no serial ports found!") - } - - for _, port := range ports { - if !strings.Contains(port, "ttyACM") { - continue - } - - return port, nil - } - - return "", errors.New("no devices found which look like an Arduino") -} - -func GetArduino() Arduino { - - arduinoPort, err := findArduinoPort() - - if err != nil { - log.Fatal("could not find Arduino port! " + err.Error()) - } - - mode := &serial.Mode{ - BaudRate: 9600, - } - - port, err := serial.Open(arduinoPort, mode) - - if err != nil { - log.Fatal("could not open Arduino port! " + err.Error()) - } - - arduino := Arduino{ - Port: port, - } - - return arduino -} diff --git a/arduino/commands.go b/arduino/commands.go deleted file mode 100644 index ae32a5f..0000000 --- a/arduino/commands.go +++ /dev/null @@ -1,18 +0,0 @@ -package arduino - -type Command string - -const ( - WATER_OFF Command = "a" - WATER_ON Command = "b" - WATER_1_ON Command = "c" - WATER_1_OFF Command = "d" - WATER_2_ON Command = "e" - WATER_2_OFF Command = "f" - WATER_3_ON Command = "g" - WATER_3_OFF Command = "h" - WATER_4_ON Command = "i" - WATER_4_OFF Command = "j" - REQUEST_READINGS Command = "k" - REQUEST_OUTLETS Command = "l" -) diff --git a/arduino/moisture_reading.go b/arduino/moisture_reading.go deleted file mode 100644 index fa554ee..0000000 --- a/arduino/moisture_reading.go +++ /dev/null @@ -1,59 +0,0 @@ -package arduino - -import ( - "errors" - "strconv" - "strings" - "time" - - "github.com/mewejo/go-watering/world" -) - -type MoistureReading struct { - Time time.Time - Raw int16 - Original world.MoistureLevel - Sensor MoistureSensor -} - -func MakeMoistureReadingFromString(line string) (MoistureReading, error) { - parts := strings.Split(line, ":") - - if parts[0] != "MS" { - return MoistureReading{}, errors.New("line was not a moisture reading") - - } - - sensorId, err := strconv.Atoi(parts[1]) - - if err != nil { - return MoistureReading{}, err - } - - rawValue, err := strconv.Atoi(parts[2]) - - if err != nil { - return MoistureReading{}, err - } - - percentageValue, err := strconv.Atoi(parts[3]) - - if err != nil { - return MoistureReading{}, err - } - - sensor, err := MoistureSensorFromId(sensorId) - - if err != nil { - return MoistureReading{}, err - } - - return MoistureReading{ - Time: time.Now(), - Sensor: sensor, - Raw: int16(rawValue), - Original: world.MoistureLevel{ - Percentage: uint(percentageValue), - }, - }, nil -} diff --git a/arduino/moisture_sensor.go b/arduino/moisture_sensor.go deleted file mode 100644 index 893a356..0000000 --- a/arduino/moisture_sensor.go +++ /dev/null @@ -1,91 +0,0 @@ -package arduino - -import ( - "errors" - "fmt" - "os" - - "github.com/mewejo/go-watering/homeassistant" -) - -type MoistureSensor int - -const ( - MOISTURE_SENSOR_1 MoistureSensor = iota - MOISTURE_SENSOR_2 - MOISTURE_SENSOR_3 - MOISTURE_SENSOR_4 - MOISTURE_SENSOR_5 - MOISTURE_SENSOR_6 -) - -func (ms MoistureSensor) GetHomeAssistantMoistureSensorConfiguration() homeassistant.MoistureSensorConfiguration { - c := homeassistant.NewMoistureSensorConfiguration() - c.Name = fmt.Sprintf("Moisture Sensor #%v", ms.GetId()) - c.ObjectId = ms.GetHomeAssistantObjectId() - c.UniqueId = ms.GetHomeAssistantObjectId() - c.StateTopic = ms.GetHomeAssistantStateTopic() - c.AvailabilityTopic = ms.GetHomeAssistantAvailabilityTopic() - c.Device = homeassistant.NewDeviceDetails() - - return c -} - -func (ms MoistureSensor) GetHomeAssistantStateTopic() string { - return fmt.Sprintf("%v/state", ms.GetHomeAssistantBaseTopic()) -} - -func (ms MoistureSensor) GetHomeAssistantAvailabilityTopic() string { - return fmt.Sprintf("%v/availability", ms.GetHomeAssistantBaseTopic()) -} - -func (ms MoistureSensor) GetHomeAssistantBaseTopic() string { - return fmt.Sprintf( - "%v/sensor/vegetable-soaker/%v", - os.Getenv("HOME_ASSISTANT_DISCOVERY_PREFIX"), - ms.GetHomeAssistantObjectId(), - ) -} - -func (ms MoistureSensor) GetHomeAssistantObjectId() string { - return fmt.Sprintf( - "moisture-sensor-%v", - ms.GetId(), - ) -} - -func (ms MoistureSensor) GetId() int { - if ms == MOISTURE_SENSOR_1 { - return 1 - } else if ms == MOISTURE_SENSOR_2 { - return 2 - } else if ms == MOISTURE_SENSOR_3 { - return 3 - } else if ms == MOISTURE_SENSOR_4 { - return 4 - } else if ms == MOISTURE_SENSOR_5 { - return 5 - } else if ms == MOISTURE_SENSOR_6 { - return 6 - } - - return 0 -} - -func MoistureSensorFromId(id int) (MoistureSensor, error) { - if id == 1 { - return MOISTURE_SENSOR_1, nil - } else if id == 2 { - return MOISTURE_SENSOR_2, nil - } else if id == 3 { - return MOISTURE_SENSOR_3, nil - } else if id == 4 { - return MOISTURE_SENSOR_4, nil - } else if id == 5 { - return MOISTURE_SENSOR_5, nil - } else if id == 6 { - return MOISTURE_SENSOR_6, nil - } - - return MOISTURE_SENSOR_1, errors.New("invalid moisture sensor ID") -} diff --git a/arduino/water_outlet.go b/arduino/water_outlet.go deleted file mode 100644 index 8bb9d7f..0000000 --- a/arduino/water_outlet.go +++ /dev/null @@ -1,40 +0,0 @@ -package arduino - -import "errors" - -type WaterOutlet int - -const ( - WATER_OUTLET_1 WaterOutlet = iota - WATER_OUTLET_2 - WATER_OUTLET_3 - WATER_OUTLET_4 -) - -func (o WaterOutlet) OnCommand() (Command, error) { - if o == WATER_OUTLET_1 { - return WATER_1_ON, nil - } else if o == WATER_OUTLET_2 { - return WATER_2_ON, nil - } else if o == WATER_OUTLET_3 { - return WATER_3_ON, nil - } else if o == WATER_OUTLET_4 { - return WATER_4_ON, nil - } - - return WATER_OFF, errors.New("invalid outlet") -} - -func (o WaterOutlet) OffCommand() (Command, error) { - if o == WATER_OUTLET_1 { - return WATER_1_OFF, nil - } else if o == WATER_OUTLET_2 { - return WATER_2_OFF, nil - } else if o == WATER_OUTLET_3 { - return WATER_3_OFF, nil - } else if o == WATER_OUTLET_4 { - return WATER_4_OFF, nil - } - - return WATER_OFF, errors.New("invalid outlet") -} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..e284418 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/mewejo/go-watering/pkg/app" + +func main() { + app.Start() +} diff --git a/config/app_config.go b/config/app_config.go deleted file mode 100644 index 263198e..0000000 --- a/config/app_config.go +++ /dev/null @@ -1,68 +0,0 @@ -package config - -import ( - "github.com/mewejo/go-watering/arduino" - "github.com/mewejo/go-watering/world" -) - -type Application struct { - Zones []*Zone -} - -func GetApplication() Application { - a := Application{ - Zones: []*Zone{ - { - Id: "zone-1", - Name: "Zone 1", - TargetMoisture: world.MoistureLevel{ - Percentage: 70, - }, - MoistureSensors: []arduino.MoistureSensor{ - arduino.MOISTURE_SENSOR_1, - arduino.MOISTURE_SENSOR_2, - }, - WaterOutlets: []arduino.WaterOutlet{ - arduino.WATER_OUTLET_1, - }, - }, - { - Id: "zone-2", - Name: "Zone 2", - TargetMoisture: world.MoistureLevel{ - Percentage: 70, - }, - MoistureSensors: []arduino.MoistureSensor{ - arduino.MOISTURE_SENSOR_3, - arduino.MOISTURE_SENSOR_4, - }, - WaterOutlets: []arduino.WaterOutlet{ - arduino.WATER_OUTLET_2, - }, - }, - { - Id: "zone-3", - Name: "Zone 3", - TargetMoisture: world.MoistureLevel{ - Percentage: 70, - }, - MoistureSensors: []arduino.MoistureSensor{ - arduino.MOISTURE_SENSOR_5, - arduino.MOISTURE_SENSOR_6, - }, - WaterOutlets: []arduino.WaterOutlet{ - arduino.WATER_OUTLET_3, - }, - }, - { - Id: "patio", - Name: "Patio", - WaterOutlets: []arduino.WaterOutlet{ - arduino.WATER_OUTLET_4, - }, - }, - }, - } - - return a -} diff --git a/config/zone.go b/config/zone.go deleted file mode 100644 index b5df388..0000000 --- a/config/zone.go +++ /dev/null @@ -1,241 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "log" - "os" - "time" - - "github.com/mewejo/go-watering/arduino" - "github.com/mewejo/go-watering/helpers" - "github.com/mewejo/go-watering/homeassistant" - "github.com/mewejo/go-watering/world" -) - -type Zone struct { - Id string // This will be user defined, used for API calls - Name string - TargetMoisture world.MoistureLevel - MoistureSensors []arduino.MoistureSensor - WaterOutlets []arduino.WaterOutlet - MoisureReadings []arduino.MoistureReading - Watering bool - WateringChangedAt time.Time - ForcedWatering bool -} - -func (z Zone) GetHomeAssistantHumidifierConfiguration() homeassistant.HumidifierConfiguration { - c := homeassistant.NewZoneHumidifierConfiguration() - c.ObjectId = z.GetHomeAssistantObjectId() - c.UniqueId = z.GetHomeAssistantObjectId() - c.Name = z.Name + " Soaker" - c.StateTopic = z.GetHomeAssistantStateTopic() - c.CommandTopic = z.GetHomeAssistantCommandTopic() - c.TargetHumidityTopic = z.GetHomeAssistantTargetHumidityTopic() - c.TargetHumidityStateTopic = z.GetHomeAssistantTargetStateHumidityTopic() - c.AvailabilityTopic = z.GetHomeAssistantAvailabilityTopic() - c.ModeStateTopic = z.GetHomeAssistantModeStateTopic() - c.ModeCommandTopic = z.GetHomeAssistantModeCommandTopic() - c.Device = homeassistant.NewDeviceDetails() - - return c -} - -func (z Zone) GetHomeAssistantMoistureSensorConfiguration() homeassistant.MoistureSensorConfiguration { - c := homeassistant.NewMoistureSensorConfiguration() - c.Name = z.Name + " Moisture" - c.ObjectId = z.GetHomeAssistantObjectId() - c.UniqueId = z.GetHomeAssistantObjectId() - c.StateTopic = z.GetHomeAssistantStateTopic() - c.AvailabilityTopic = z.GetHomeAssistantAvailabilityTopic() - c.Device = homeassistant.NewDeviceDetails() - - return c -} - -func (z Zone) GetHomeAssistantHumidifierBaseTopic() string { - return fmt.Sprintf( - "%v/humidifier/%v", - os.Getenv("HOME_ASSISTANT_DISCOVERY_PREFIX"), - z.GetHomeAssistantObjectId(), - ) -} - -func (z Zone) GetHomeAssistantMoistureSensorBaseTopic() string { - return fmt.Sprintf( - "%v/sensor/%v/average-moisture", - os.Getenv("HOME_ASSISTANT_DISCOVERY_PREFIX"), - z.GetHomeAssistantObjectId(), - ) -} - -func (z Zone) GetHomeAssistantModeStateTopic() string { - return fmt.Sprintf("%v/mode_state", z.GetHomeAssistantHumidifierBaseTopic()) -} - -func (z Zone) GetHomeAssistantModeCommandTopic() string { - return fmt.Sprintf("%v/mode_command", z.GetHomeAssistantHumidifierBaseTopic()) -} - -func (z Zone) GetHomeAssistantAvailabilityTopic() string { - return fmt.Sprintf("%v/availability", z.GetHomeAssistantHumidifierBaseTopic()) -} - -func (z Zone) GetHomeAssistantCommandTopic() string { - return fmt.Sprintf("%v/command", z.GetHomeAssistantHumidifierBaseTopic()) -} - -func (z Zone) GetHomeAssistantTargetHumidityTopic() string { - return fmt.Sprintf("%v/target", z.GetHomeAssistantHumidifierBaseTopic()) -} - -func (z Zone) GetHomeAssistantTargetStateHumidityTopic() string { - return fmt.Sprintf("%v/humidity_state", z.GetHomeAssistantHumidifierBaseTopic()) -} - -func (z Zone) GetHomeAssistantStateTopic() string { - return fmt.Sprintf("%v/state", z.GetHomeAssistantHumidifierBaseTopic()) -} - -func (z Zone) GetHomeAssistantObjectId() string { - return os.Getenv("HOME_ASSISTANT_OBJECT_ID_PREFIX") + z.Id -} - -func (z *Zone) SetForcedWateringState(ard arduino.Arduino, state bool) { - if z.ForcedWatering == state { - return - } - - z.ForcedWatering = state - z.WateringChangedAt = time.Now() - - z.EnforceWateringState(ard) -} - -func (z *Zone) SetWaterState(ard arduino.Arduino, state bool) { - if z.Watering == state { - return - } - - z.Watering = state - z.WateringChangedAt = time.Now() - - z.EnforceWateringState(ard) -} - -func (z Zone) EnforceWateringState(ard arduino.Arduino) { - for _, outlet := range z.WaterOutlets { - err := ard.SetWaterState(outlet, z.Watering || z.ForcedWatering) - - if err != nil { - log.Fatal("could not set water state for zone") - } - } -} - -func (z Zone) ShouldStartWatering() (bool, error) { - moistureLevel, err := z.AverageMoistureLevel() - - if err != nil { - return false, errors.New("could not get average moisture level for zone") - } - - // Already watering... don't need to start - if z.Watering { - return false, nil - } - - if moistureLevel.Percentage < z.TargetMoisture.HysteresisOnLevel().Percentage { - return true, nil - } - - return false, nil -} - -func (z Zone) ShouldStopWatering() (bool, error) { - moistureLevel, err := z.AverageMoistureLevel() - - if err != nil { - return false, errors.New("could not get average moisture level for zone") - } - - // Not watering... don't need to stop - if !z.Watering { - return false, nil - } - - if moistureLevel.Percentage > z.TargetMoisture.HysteresisOffLevel().Percentage { - return true, nil - } - - return false, nil -} - -func (z Zone) AverageMoistureLevel() (world.MoistureLevel, error) { - // Loop over the readings until we have one from each sensor - readingsReversed := make([]arduino.MoistureReading, len(z.MoisureReadings)) - copy(readingsReversed, z.MoisureReadings) - helpers.ReverseSlice(readingsReversed) - - sensorsFound := []arduino.MoistureSensor{} - readings := []world.MoistureLevel{} - - for _, reading := range readingsReversed { - if moistureSensorInSlice(reading.Sensor, sensorsFound) { - continue - } - - sensorsFound = append(sensorsFound, reading.Sensor) - readings = append(readings, reading.Original) - - if len(sensorsFound) == len(z.MoistureSensors) { - break - } - } - - if len(readings) < 1 { - return world.MoistureLevel{}, errors.New("no readings to make average from") - } - - if len(sensorsFound) != len(z.MoistureSensors) { - return world.MoistureLevel{}, errors.New("incomplete data (sensors), cannot calculate moisture level") - } - - if len(readings) != len(z.MoistureSensors) { - return world.MoistureLevel{}, errors.New("incomplete data (readings), cannot calculate moisture level") - } - - var totalPercentage uint - - for _, reading := range readings { - totalPercentage += reading.Percentage - } - - return world.MoistureLevel{ - Percentage: uint(totalPercentage / uint(len(readings))), - }, nil -} - -func (z *Zone) RecordMoistureReading(r arduino.MoistureReading) { - z.MoisureReadings = append(z.MoisureReadings, r) - limitMoistureReadings(&z.MoisureReadings, 100) -} - -func limitMoistureReadings(s *[]arduino.MoistureReading, length int) { - if len(*s) <= length { - return - } - - *s = (*s)[len(*s)-length:] -} - -func moistureSensorInSlice(s arduino.MoistureSensor, sensors []arduino.MoistureSensor) bool { - for _, v := range sensors { - if v == s { - return true - } - } - - return false -} diff --git a/homeassistant/zone_configuration.go b/homeassistant/zone_configuration.go deleted file mode 100644 index 4b654cd..0000000 --- a/homeassistant/zone_configuration.go +++ /dev/null @@ -1,84 +0,0 @@ -package homeassistant - -import "github.com/mewejo/go-watering/world" - -type HumidifierConfiguration struct { - StateTopic string `json:"state_topic"` - DeviceClass string `json:"device_class"` - Name string `json:"name"` - ObjectId string `json:"object_id"` - UniqueId string `json:"unique_id"` - CommandTopic string `json:"command_topic"` - TargetHumidityTopic string `json:"target_humidity_command_topic"` - TargetHumidityStateTopic string `json:"target_humidity_state_topic"` - AvailabilityTopic string `json:"availability_topic"` - Device DeviceDetails `json:"device"` - PayloadAvailable string `json:"payload_available"` - PayloadNotAvailable string `json:"payload_not_available"` - PayloadOn string `json:"payload_on"` - PayloadOff string `json:"payload_off"` - Optimistic bool `json:"optimistic"` - StateValueTemplate string `json:"state_value_template"` - ModeCommandTopic string `json:"mode_command_topic"` - ModeStateTopic string `json:"mode_state_topic"` - Modes []string `json:"modes"` -} - -type MoistureSensorConfiguration struct { - Name string `json:"name"` - DeviceClass string `json:"device_class"` - ObjectId string `json:"object_id"` - UniqueId string `json:"unique_id"` - StateTopic string `json:"state_topic"` - StateValueTemplate string `json:"value_template"` - AvailabilityTopic string `json:"availability_topic"` - UnitOfMeasurement string `json:"unit_of_measurement"` - Device DeviceDetails `json:"device"` - PayloadAvailable string `json:"payload_available"` - PayloadNotAvailable string `json:"payload_not_available"` -} - -type DeviceDetails struct { - Identifier string `json:"identifiers"` - Name string `json:"name"` - Model string `json:"model"` - Manufacturer string `json:"manufacturer"` -} - -type ZoneState struct { - MoistureLevel world.MoistureLevel `json:"moisture"` - State string `json:"state"` -} - -func NewDeviceDetails() DeviceDetails { - d := DeviceDetails{} - d.Manufacturer = "Josh Bonfield" - d.Model = "Go Watering" - d.Identifier = "vegetable-soaker" - d.Name = "Vegetable Soaker" - - return d -} - -func NewMoistureSensorConfiguration() MoistureSensorConfiguration { - c := MoistureSensorConfiguration{} - c.DeviceClass = "moisture" - c.StateValueTemplate = "{{ value_json.moisture.percentage }}" - c.UnitOfMeasurement = "%" - c.PayloadAvailable = "online" - c.PayloadNotAvailable = "offline" - - return c -} - -func NewZoneHumidifierConfiguration() HumidifierConfiguration { - c := HumidifierConfiguration{} - c.DeviceClass = "humidifier" - c.PayloadAvailable = "online" - c.PayloadNotAvailable = "offline" - c.PayloadOn = "on" - c.PayloadOff = "off" - c.StateValueTemplate = "{{ value_json.state }}" - c.Modes = []string{"normal"} - return c -} diff --git a/main.go b/main.go deleted file mode 100644 index 5e9d7f1..0000000 --- a/main.go +++ /dev/null @@ -1,198 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "os/signal" - "syscall" - "time" - - "github.com/joho/godotenv" - - mqttLib "github.com/eclipse/paho.mqtt.golang" - "github.com/mewejo/go-watering/api" - "github.com/mewejo/go-watering/arduino" - "github.com/mewejo/go-watering/config" - "github.com/mewejo/go-watering/mqtt" -) - -var mqttClient mqttLib.Client - -func main() { - - if godotenv.Load() != nil { - log.Fatal("Error loading .env file") - } - - app := config.GetApplication() - - fmt.Println("Connecting to MQTT broker...") - - mqttClient = mqtt.GetClient() - - fmt.Println("Connected to MQTT!") - - fmt.Println("Publishing Home Assistant auto discovery...") - - for _, zone := range app.Zones { - mqtt.PublishHomeAsssitantAutoDiscovery(mqttClient, *zone, []arduino.MoistureSensor{ - arduino.MOISTURE_SENSOR_1, - arduino.MOISTURE_SENSOR_2, - arduino.MOISTURE_SENSOR_3, - arduino.MOISTURE_SENSOR_4, - arduino.MOISTURE_SENSOR_5, - arduino.MOISTURE_SENSOR_6, - }) - - mqtt.PublishHomeAssistantAvailability(mqttClient, *zone) - - token, _ := mqtt.PublishHomeAssistantState(mqttClient, *zone) - - if token != nil { - token.Wait() - } - - mqtt.PublishHomeAssistantTargetHumidity(mqttClient, *zone).Wait() - mqtt.PublishHomeAssistantModeState(mqttClient, *zone).Wait() - } - - fmt.Println("Waiting until Arduino is ready...") - - ard := arduino.GetArduino() - - ard.WaitUntilReady() - - fmt.Println("The Arduino is ready!") - - setupCloseHandler(ard) - - go maintainMoistureLevels(ard, &app) - go readMoistureLevels(ard, &app) - go enforceZoneWaterOutletStates(ard, &app) - - fmt.Println("Starting API...") - - api.StartApi(&app) -} - -func setupCloseHandler(ard arduino.Arduino) { - c := make(chan os.Signal) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - go func() { - <-c - fmt.Println("Exiting... turning water off") - ard.SendCommand(arduino.WATER_OFF) - ard.Port.Close() - os.Exit(0) - }() -} - -func enforceZoneWaterOutletStates(ard arduino.Arduino, app *config.Application) { - ticker := time.NewTicker(60 * time.Second) - - quit := make(chan struct{}) - - go func() { - for { - select { - case <-ticker.C: - for _, zone := range app.Zones { - zone.EnforceWateringState(ard) - } - - case <-quit: - ticker.Stop() - return - } - } - }() -} - -func maintainMoistureLevels(ard arduino.Arduino, app *config.Application) { - ticker := time.NewTicker(10 * time.Second) - - quit := make(chan struct{}) - - go func() { - for { - select { - case <-ticker.C: - for _, zone := range app.Zones { - if len(zone.MoistureSensors) < 1 { - continue - } - - shouldNotBeWatering, err := zone.ShouldStopWatering() - - if err != nil { - continue - } - - if shouldNotBeWatering { - zone.SetWaterState(ard, false) - continue - } - - shouldStartWatering, err := zone.ShouldStartWatering() - - if err != nil { - continue - } - - if shouldStartWatering { - zone.SetWaterState(ard, true) - continue - } - } - - case <-quit: - ticker.Stop() - return - } - } - }() -} - -func readMoistureLevels(ard arduino.Arduino, app *config.Application) { - ticker := time.NewTicker(2 * time.Second) - - quit := make(chan struct{}) - - go func() { - for { - select { - case <-ticker.C: - readings, err := ard.GetReadings() - - if err != nil { - log.Fatal("Could not get readings from Arduino: " + err.Error()) - } - - processMoistureReadings(app, readings) - case <-quit: - ticker.Stop() - return - } - } - }() -} - -func processMoistureReadings(app *config.Application, readings []arduino.MoistureReading) { - for _, zone := range app.Zones { - for _, sensor := range zone.MoistureSensors { - for _, reading := range readings { - if sensor != reading.Sensor { - continue - } - - // This 3 level nesting feels nasty - - zone.RecordMoistureReading(reading) - mqtt.PublishHomeAssistantState(mqttClient, *zone) - mqtt.PublishHomeAssistantAvailability(mqttClient, *zone) - mqtt.PublishHomeAssistantTargetHumidity(mqttClient, *zone) - } - } - } -} diff --git a/mqtt/client.go b/mqtt/client.go deleted file mode 100644 index 91dc74c..0000000 --- a/mqtt/client.go +++ /dev/null @@ -1,165 +0,0 @@ -package mqtt - -import ( - "encoding/json" - "fmt" - "log" - "os" - "time" - - mqtt "github.com/eclipse/paho.mqtt.golang" - "github.com/mewejo/go-watering/arduino" - "github.com/mewejo/go-watering/config" - "github.com/mewejo/go-watering/homeassistant" -) - -var f mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) { - fmt.Printf("TOPIC: %s\n", msg.Topic()) - fmt.Printf("MSG: %s\n", msg.Payload()) -} - -func PublishHomeAssistantModeState(c mqtt.Client, zone config.Zone) mqtt.Token { - return c.Publish( - zone.GetHomeAssistantModeStateTopic(), - 0, - false, - "normal", // TODO - ) -} - -func PublishHomeAssistantTargetHumidity(c mqtt.Client, zone config.Zone) mqtt.Token { - return c.Publish( - zone.GetHomeAssistantTargetStateHumidityTopic(), - 0, - false, - fmt.Sprintf("%v", zone.TargetMoisture.Percentage), - ) -} - -func PublishHomeAssistantAvailability(c mqtt.Client, zone config.Zone) mqtt.Token { - return c.Publish( - zone.GetHomeAssistantAvailabilityTopic(), - 0, - false, - "online", - ) -} - -func PublishHomeAssistantState(c mqtt.Client, zone config.Zone) (mqtt.Token, error) { - - moistureLevel, err := zone.AverageMoistureLevel() - - if err != nil { - return nil, err - } - - state := homeassistant.ZoneState{} - state.State = "on" // TODO - state.MoistureLevel = moistureLevel - - json, err := json.Marshal(state) - - if err != nil { - return nil, err - } - - return c.Publish( - zone.GetHomeAssistantStateTopic(), - 0, - true, - json, - ), nil -} - -func PublishHomeAsssitantAutoDiscovery(c mqtt.Client, zone config.Zone, moistureSensors []arduino.MoistureSensor) { - - var topic string - var err error - var zoneConfigJson []byte - var token mqtt.Token - - // Main device - topic = fmt.Sprintf( - "%v/config", - zone.GetHomeAssistantHumidifierBaseTopic(), - ) - - zoneConfigJson, err = json.Marshal( - zone.GetHomeAssistantHumidifierConfiguration(), - ) - - if err != nil { - log.Fatal("Could not create Home Assistant config for zone humidifier: " + zone.Id) - } - - token = c.Publish(topic, 1, true, zoneConfigJson) - token.Wait() - - // No sensors, no average! - if len(zone.MoistureSensors) < 1 { - return - } - - // Now the average sensor - topic = fmt.Sprintf( - "%v/config", - zone.GetHomeAssistantMoistureSensorBaseTopic(), - ) - - zoneConfigJson, err = json.Marshal( - zone.GetHomeAssistantMoistureSensorConfiguration(), - ) - - if err != nil { - log.Fatal("Could not create Home Assistant config for zone average moisture sensor: " + zone.Id) - } - - token = c.Publish(topic, 1, true, zoneConfigJson) - token.Wait() - - // Now the sensors on their own - for _, sensor := range moistureSensors { - topic = fmt.Sprintf( - "%v/config", - sensor.GetHomeAssistantBaseTopic(), - ) - - sensorConfigJson, err := json.Marshal( - sensor.GetHomeAssistantMoistureSensorConfiguration(), - ) - - if err != nil { - log.Fatal( - fmt.Sprintf("Could not create Home Assistant config for zone moisture sensor: %v", sensor.GetId()), - ) - } - - token = c.Publish(topic, 1, true, sensorConfigJson) - token.Wait() - } -} - -func GetClient() mqtt.Client { - connectionString := fmt.Sprintf( - "tcp://%v:%v", - os.Getenv("MQTT_HOST"), - os.Getenv("MQTT_PORT"), - ) - - opts := mqtt.NewClientOptions() - opts.AddBroker(connectionString) - opts.SetClientID(os.Getenv("MQTT_CLIENT_ID")) - opts.SetUsername(os.Getenv("MQTT_USERNAME")) - opts.SetPassword(os.Getenv("MQTT_PASSWORD")) - opts.SetKeepAlive(2 * time.Second) - opts.SetDefaultPublishHandler(f) - opts.SetPingTimeout(1 * time.Second) - - c := mqtt.NewClient(opts) - - if token := c.Connect(); token.Wait() && token.Error() != nil { - panic(token.Error()) - } - - return c -} diff --git a/pkg/app/app.go b/pkg/app/app.go new file mode 100644 index 0000000..f737de6 --- /dev/null +++ b/pkg/app/app.go @@ -0,0 +1,7 @@ +package app + +import "fmt" + +func Start() { + fmt.Println("Hello, world!") +} diff --git a/helpers/slices.go b/pkg/helpers/slices.go similarity index 100% rename from helpers/slices.go rename to pkg/helpers/slices.go diff --git a/world/moisture_level.go b/world/moisture_level.go deleted file mode 100644 index 9db9a64..0000000 --- a/world/moisture_level.go +++ /dev/null @@ -1,34 +0,0 @@ -package world - -// Percentage of hysteresis to use (decimal, 0.05 = 5%) -const hysteresis = 0.05 - -type MoistureLevel struct { - Percentage uint `json:"percentage"` -} - -func percentageToMoistureLevel(level float64) MoistureLevel { - if level > 100 { - level = 100 - } - - if level < 0 { - level = 0 - } - - return MoistureLevel{ - Percentage: uint(level), - } -} - -func (ml MoistureLevel) HysteresisOnLevel() MoistureLevel { - return percentageToMoistureLevel( - float64(ml.Percentage) * (1.0 - hysteresis), - ) -} - -func (ml MoistureLevel) HysteresisOffLevel() MoistureLevel { - return percentageToMoistureLevel( - float64(ml.Percentage) * (1.0 + hysteresis), - ) -} From fdef06816ad00acc26185e0e037fabe57636a847 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sat, 4 Feb 2023 23:04:25 +0000 Subject: [PATCH 002/135] wip: basic app --- cmd/main.go | 13 +++++++++++-- pkg/app/app.go | 12 ++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index e284418..327818c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,7 +1,16 @@ package main -import "github.com/mewejo/go-watering/pkg/app" +import ( + "log" + + "github.com/joho/godotenv" + "github.com/mewejo/go-watering/pkg/app" +) func main() { - app.Start() + if godotenv.Load() != nil { + log.Fatal("Error loading .env file") + } + + app.Make().Run() } diff --git a/pkg/app/app.go b/pkg/app/app.go index f737de6..7734354 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -2,6 +2,14 @@ package app import "fmt" -func Start() { - fmt.Println("Hello, world!") +type App struct { + // +} + +func (app *App) Run() { + fmt.Println("Bonsoir, Elliot") +} + +func Make() *App { + return &App{} } From c08445f8d49cc8d8062b6b4cdb5959e3c8078e17 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 11:41:15 +0000 Subject: [PATCH 003/135] wip: rough plan --- pkg/app/app.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pkg/app/app.go b/pkg/app/app.go index 7734354..655b13f 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -8,6 +8,34 @@ type App struct { func (app *App) Run() { fmt.Println("Bonsoir, Elliot") + + /* + Make zone configurations + Connect to MQTT + Set LWT for availability topic (shared by all entities). Support modes normal/boost + Publish MQTT auto discovery for Zones (climate), moisture sensors, and outlets not attached to zones (or zones with no sensors?) + Find Arduino port + Open serial connection + Wait for heartbeat + Wait for above to be ready + Loop + Read & process sensors and water states + Publish zone states (freq as below) + Publish sensor states (every 5 min in prod, 2 sec in testing) + Publish outlets without zones states + Check for heartbeat - if none after X: + Close Arduino serial connection + Attempt to establish a new connection + Wait for a heartbeat + Loop as above + Program exit + Send water off command + Send unavailable topic to MQTT + Exit + + + */ + } func Make() *App { From fcc23be749027d93a1a1a9d9b00a4811bdbd16b4 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 15:58:44 +0000 Subject: [PATCH 004/135] wip: start of persistence --- cmd/main.go | 2 +- pkg/app/app.go | 11 ++- pkg/arduino/arduino.go | 115 ++++++++++++++++++++++ pkg/arduino/command.go | 18 ++++ pkg/helpers/slices.go | 7 -- pkg/model/moisture_level.go | 5 + pkg/model/moisture_reading.go | 8 ++ pkg/model/moisture_sensor.go | 7 ++ pkg/model/water_outlet.go | 6 ++ pkg/model/zone.go | 6 ++ pkg/persistence/moisture_reading_store.go | 45 +++++++++ pkg/slice/slices.go | 14 +++ 12 files changed, 233 insertions(+), 11 deletions(-) create mode 100644 pkg/arduino/arduino.go create mode 100644 pkg/arduino/command.go delete mode 100644 pkg/helpers/slices.go create mode 100644 pkg/model/moisture_level.go create mode 100644 pkg/model/moisture_reading.go create mode 100644 pkg/model/moisture_sensor.go create mode 100644 pkg/model/water_outlet.go create mode 100644 pkg/model/zone.go create mode 100644 pkg/persistence/moisture_reading_store.go create mode 100644 pkg/slice/slices.go diff --git a/cmd/main.go b/cmd/main.go index 327818c..1875853 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -12,5 +12,5 @@ func main() { log.Fatal("Error loading .env file") } - app.Make().Run() + app.NewApp().Run() } diff --git a/pkg/app/app.go b/pkg/app/app.go index 655b13f..673f067 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1,9 +1,14 @@ package app -import "fmt" +import ( + "fmt" + + "github.com/mewejo/go-watering/pkg/model" +) type App struct { - // + Zones []model.Zone + WaterOutlets []model.WaterOutlet } func (app *App) Run() { @@ -38,6 +43,6 @@ func (app *App) Run() { } -func Make() *App { +func NewApp() *App { return &App{} } diff --git a/pkg/arduino/arduino.go b/pkg/arduino/arduino.go new file mode 100644 index 0000000..cdcea87 --- /dev/null +++ b/pkg/arduino/arduino.go @@ -0,0 +1,115 @@ +package arduino + +import ( + "errors" + "log" + "strings" + + "go.bug.st/serial" +) + +type Arduino struct { + port serial.Port +} + +func (a Arduino) SendCommand(command Command) (int, error) { + return a.port.Write([]byte(command)) +} + +func (a Arduino) ReadData(buffer []byte) (int, error) { + return a.port.Read(buffer) +} + +func (a Arduino) ReadLines(until string) []string { + lines := []string{} + + for { + line := a.ReadLine() + + lines = append(lines, line) + + if strings.Contains(line, until) { + break + } + } + + return lines +} + +func findArduinoPort() (string, error) { + ports, err := serial.GetPortsList() + + if err != nil { + log.Fatal(err) + } + + if len(ports) == 0 { + log.Fatal("no serial ports found!") + } + + for _, port := range ports { + if !strings.Contains(port, "ttyACM") { + continue + } + + return port, nil + } + + return "", errors.New("no devices found which look like an Arduino") +} + +func (a Arduino) ReadLine() string { + buff := make([]byte, 1) + data := "" + + for { + n, err := a.ReadData(buff) + + if err != nil { + log.Fatal(err) + } + + if n == 0 { + break + } + + data += string(buff[:n]) + + if strings.Contains(data, "\n") { + break + } + } + + return data +} + +func (a *Arduino) ClosePort() error { + return a.port.Close() +} + +func (a *Arduino) FindAndOpenPort() error { + arduinoPort, err := findArduinoPort() + + if err != nil { + return err + } + + mode := &serial.Mode{ + BaudRate: 9600, + } + + port, err := serial.Open(arduinoPort, mode) + + if err != nil { + return err + } + + a.port = port + + return nil +} + +func NewArduino() *Arduino { + + return &Arduino{} +} diff --git a/pkg/arduino/command.go b/pkg/arduino/command.go new file mode 100644 index 0000000..ae32a5f --- /dev/null +++ b/pkg/arduino/command.go @@ -0,0 +1,18 @@ +package arduino + +type Command string + +const ( + WATER_OFF Command = "a" + WATER_ON Command = "b" + WATER_1_ON Command = "c" + WATER_1_OFF Command = "d" + WATER_2_ON Command = "e" + WATER_2_OFF Command = "f" + WATER_3_ON Command = "g" + WATER_3_OFF Command = "h" + WATER_4_ON Command = "i" + WATER_4_OFF Command = "j" + REQUEST_READINGS Command = "k" + REQUEST_OUTLETS Command = "l" +) diff --git a/pkg/helpers/slices.go b/pkg/helpers/slices.go deleted file mode 100644 index c90953f..0000000 --- a/pkg/helpers/slices.go +++ /dev/null @@ -1,7 +0,0 @@ -package helpers - -func ReverseSlice[S ~[]E, E any](s S) { - for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { - s[i], s[j] = s[j], s[i] - } -} diff --git a/pkg/model/moisture_level.go b/pkg/model/moisture_level.go new file mode 100644 index 0000000..34ec8e2 --- /dev/null +++ b/pkg/model/moisture_level.go @@ -0,0 +1,5 @@ +package model + +type MoistureLevel struct { + Percentage uint `json:"percentage"` +} diff --git a/pkg/model/moisture_reading.go b/pkg/model/moisture_reading.go new file mode 100644 index 0000000..fac5a41 --- /dev/null +++ b/pkg/model/moisture_reading.go @@ -0,0 +1,8 @@ +package model + +import "time" + +type MoistureReading struct { + Time time.Time + MoistureLevel MoistureLevel +} diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go new file mode 100644 index 0000000..14f6f38 --- /dev/null +++ b/pkg/model/moisture_sensor.go @@ -0,0 +1,7 @@ +package model + +type MoistureSensor struct { + Id uint + Name string + // TODO there will be props to translate the raw readings from Arduino into a percentage +} diff --git a/pkg/model/water_outlet.go b/pkg/model/water_outlet.go new file mode 100644 index 0000000..512a714 --- /dev/null +++ b/pkg/model/water_outlet.go @@ -0,0 +1,6 @@ +package model + +type WaterOutlet struct { + Id uint + Name string +} diff --git a/pkg/model/zone.go b/pkg/model/zone.go new file mode 100644 index 0000000..cf7c9e1 --- /dev/null +++ b/pkg/model/zone.go @@ -0,0 +1,6 @@ +package model + +type Zone struct { + Id uint + Name string +} diff --git a/pkg/persistence/moisture_reading_store.go b/pkg/persistence/moisture_reading_store.go new file mode 100644 index 0000000..2318cdd --- /dev/null +++ b/pkg/persistence/moisture_reading_store.go @@ -0,0 +1,45 @@ +package persistence + +import ( + "github.com/mewejo/go-watering/pkg/model" +) + +type moistureReadingStore struct { + sensor model.MoistureSensor + readings []model.MoistureReading +} + +func (s *moistureReadingStore) recordReading(r model.MoistureReading) { + s.readings = append(s.readings, r) + limitReadings(&s.readings, 100) +} + +func limitReadings(s *[]model.MoistureReading, length int) { + if len(*s) <= length { + return + } + + *s = (*s)[len(*s)-length:] +} + +var stores []moistureReadingStore + +func RecordReading(sensor model.MoistureSensor, reading model.MoistureReading) { + getOrMakeStore(sensor).recordReading(reading) +} + +func getOrMakeStore(sensor model.MoistureSensor) *moistureReadingStore { + for _, store := range stores { + if store.sensor == sensor { + return &store + } + } + + store := moistureReadingStore{ + sensor: sensor, + } + + stores = append(stores, store) + + return &store +} diff --git a/pkg/slice/slices.go b/pkg/slice/slices.go new file mode 100644 index 0000000..cdadba1 --- /dev/null +++ b/pkg/slice/slices.go @@ -0,0 +1,14 @@ +package slice + +func ReverseSliceInPlace[S ~[]E, E any](s S) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} + +func ReverseSlice[S ~[]E, E any](s S) S { + new := make([]E, len(s)) + copy(new, s) + ReverseSliceInPlace(new) + return new +} From 78cd7bd5400f8a29b13b64a3570ae085101d73ed Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 16:31:28 +0000 Subject: [PATCH 005/135] feat: sensor readings --- pkg/model/moisture_level.go | 6 +++++ pkg/model/moisture_reading.go | 41 ++++++++++++++++++++++++++++++++++- pkg/model/moisture_sensor.go | 26 +++++++++++++++++++--- pkg/number/change_range.go | 6 +++++ 4 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 pkg/number/change_range.go diff --git a/pkg/model/moisture_level.go b/pkg/model/moisture_level.go index 34ec8e2..3a5f2e1 100644 --- a/pkg/model/moisture_level.go +++ b/pkg/model/moisture_level.go @@ -3,3 +3,9 @@ package model type MoistureLevel struct { Percentage uint `json:"percentage"` } + +func NewMoistureLevel(percentage uint) MoistureLevel { + return MoistureLevel{ + Percentage: percentage, + } +} diff --git a/pkg/model/moisture_reading.go b/pkg/model/moisture_reading.go index fac5a41..522dfc3 100644 --- a/pkg/model/moisture_reading.go +++ b/pkg/model/moisture_reading.go @@ -1,8 +1,47 @@ package model -import "time" +import ( + "errors" + "strconv" + "strings" + "time" +) type MoistureReading struct { Time time.Time + raw uint MoistureLevel MoistureLevel } + +func (r *MoistureReading) CalculateMoistureLevelForSensor(sensor MoistureSensor) { + r.MoistureLevel = NewMoistureLevel( + sensor.mapRawReadingToPercentage(r.raw), + ) +} + +// Returns the reading, the sensor ID and an error +func NewMoistureReadingFromString(line string) (MoistureReading, uint, error) { + // MS:1:700:44 # MS:ID:RAW:PERCENTAGE + parts := strings.Split(line, ":") + + if parts[0] != "MS" { + return MoistureReading{}, 0, errors.New("line was not a moisture reading") + } + + sensorId, err := strconv.Atoi(parts[1]) + + if err != nil { + return MoistureReading{}, 0, err + } + + rawValue, err := strconv.Atoi(parts[2]) + + if err != nil { + return MoistureReading{}, 0, err + } + + return MoistureReading{ + Time: time.Now(), + raw: uint(rawValue), + }, uint(sensorId), nil +} diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go index 14f6f38..e66974e 100644 --- a/pkg/model/moisture_sensor.go +++ b/pkg/model/moisture_sensor.go @@ -1,7 +1,27 @@ package model +import "github.com/mewejo/go-watering/pkg/number" + type MoistureSensor struct { - Id uint - Name string - // TODO there will be props to translate the raw readings from Arduino into a percentage + Id uint + Name string + DryThreshold uint + WetThreshold uint +} + +func (ms MoistureSensor) mapRawReadingToPercentage(raw uint) uint { + return uint(number.ChangeRange( + float64(raw), + float64(ms.DryThreshold), + float64(ms.WetThreshold), + 0, + 100, + )) +} + +func NewMoistureSensor() MoistureSensor { + return MoistureSensor{ + DryThreshold: 500, + WetThreshold: 240, + } } diff --git a/pkg/number/change_range.go b/pkg/number/change_range.go new file mode 100644 index 0000000..3a6c4f3 --- /dev/null +++ b/pkg/number/change_range.go @@ -0,0 +1,6 @@ +package number + +// Source: https://www.arduino.cc/reference/en/language/functions/math/map/ +func ChangeRange(input float64, inputMin float64, inputMax float64, outputMin float64, outputMax float64) float64 { + return (input-inputMin)*(outputMax-outputMin)/(inputMax-inputMin) + outputMin +} From 199cd648306aee6ea4a2d43c6a0a5afa2d62738b Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 16:48:04 +0000 Subject: [PATCH 006/135] wip: zones --- pkg/model/moisture_level.go | 2 +- pkg/model/moisture_reading.go | 4 ++-- pkg/model/moisture_sensor.go | 2 +- pkg/model/water_outlet.go | 13 +++++++++-- pkg/model/zone.go | 9 ++++++++ pkg/model/zone_mode.go | 43 +++++++++++++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 pkg/model/zone_mode.go diff --git a/pkg/model/moisture_level.go b/pkg/model/moisture_level.go index 3a5f2e1..04dc674 100644 --- a/pkg/model/moisture_level.go +++ b/pkg/model/moisture_level.go @@ -4,7 +4,7 @@ type MoistureLevel struct { Percentage uint `json:"percentage"` } -func NewMoistureLevel(percentage uint) MoistureLevel { +func MakeMoistureLevel(percentage uint) MoistureLevel { return MoistureLevel{ Percentage: percentage, } diff --git a/pkg/model/moisture_reading.go b/pkg/model/moisture_reading.go index 522dfc3..7163ad8 100644 --- a/pkg/model/moisture_reading.go +++ b/pkg/model/moisture_reading.go @@ -14,13 +14,13 @@ type MoistureReading struct { } func (r *MoistureReading) CalculateMoistureLevelForSensor(sensor MoistureSensor) { - r.MoistureLevel = NewMoistureLevel( + r.MoistureLevel = MakeMoistureLevel( sensor.mapRawReadingToPercentage(r.raw), ) } // Returns the reading, the sensor ID and an error -func NewMoistureReadingFromString(line string) (MoistureReading, uint, error) { +func MakeMoistureReadingFromString(line string) (MoistureReading, uint, error) { // MS:1:700:44 # MS:ID:RAW:PERCENTAGE parts := strings.Split(line, ":") diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go index e66974e..0a9e7b7 100644 --- a/pkg/model/moisture_sensor.go +++ b/pkg/model/moisture_sensor.go @@ -19,7 +19,7 @@ func (ms MoistureSensor) mapRawReadingToPercentage(raw uint) uint { )) } -func NewMoistureSensor() MoistureSensor { +func MakeMoistureSensor() MoistureSensor { return MoistureSensor{ DryThreshold: 500, WetThreshold: 240, diff --git a/pkg/model/water_outlet.go b/pkg/model/water_outlet.go index 512a714..ad35996 100644 --- a/pkg/model/water_outlet.go +++ b/pkg/model/water_outlet.go @@ -1,6 +1,15 @@ package model type WaterOutlet struct { - Id uint - Name string + Id uint + Name string + TargetState bool + ActualState bool +} + +func NewWaterOutlet(id uint, name string) *WaterOutlet { + return &WaterOutlet{ + Id: id, + Name: name, + } } diff --git a/pkg/model/zone.go b/pkg/model/zone.go index cf7c9e1..df0b230 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -3,4 +3,13 @@ package model type Zone struct { Id uint Name string + Mode *ZoneMode +} + +func NewZone(id uint, name string) *Zone { + return &Zone{ + Id: id, + Name: name, + Mode: getDefaultZoneMode(), + } } diff --git a/pkg/model/zone_mode.go b/pkg/model/zone_mode.go new file mode 100644 index 0000000..1431fc9 --- /dev/null +++ b/pkg/model/zone_mode.go @@ -0,0 +1,43 @@ +package model + +import ( + "errors" + "log" +) + +type ZoneMode struct { + Name string + Key string +} + +var zoneModes = []*ZoneMode{ + { + Name: "Normal", + Key: "normal", + }, + + { + Name: "Boost", + Key: "boost", + }, +} + +func getDefaultZoneMode() *ZoneMode { + mode, err := getZoneModeFromKey("normal") + + if err != nil { + log.Fatal(err) + } + + return mode +} + +func getZoneModeFromKey(key string) (*ZoneMode, error) { + for _, mode := range zoneModes { + if mode.Key == key { + return mode, nil + } + } + + return &ZoneMode{}, errors.New("invalid zone mode key") +} From 7015cd1d0f803f61e5cfa91dc3efcf69ab07ed91 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 17:06:58 +0000 Subject: [PATCH 007/135] wip: configure app --- pkg/app/app.go | 53 ++++++++++++++++++++++++++++++++++-- pkg/model/moisture_sensor.go | 4 ++- pkg/model/zone.go | 18 +++++++----- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 673f067..89c9cde 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -7,13 +7,62 @@ import ( ) type App struct { - Zones []model.Zone - WaterOutlets []model.WaterOutlet + Zones []*model.Zone + WaterOutlets []*model.WaterOutlet + MoistureSensors []*model.MoistureSensor +} + +func (app *App) configure() { + + waterOutlet1 := model.NewWaterOutlet(1, "Soaker hose #1") + waterOutlet2 := model.NewWaterOutlet(1, "Soaker hose #2") + waterOutlet3 := model.NewWaterOutlet(1, "Soaker hose #3") + waterOutlet4 := model.NewWaterOutlet(1, "Soaker hose #4") + + // The only outlet which isn't tied to a zone. + app.WaterOutlets = append(app.WaterOutlets, waterOutlet4) + + moistureSensor1 := model.MakeMoistureSensor(1, "Sensor #1") + moistureSensor2 := model.MakeMoistureSensor(2, "Sensor #2") + moistureSensor3 := model.MakeMoistureSensor(3, "Sensor #3") + moistureSensor4 := model.MakeMoistureSensor(4, "Sensor #4") + moistureSensor5 := model.MakeMoistureSensor(5, "Sensor #5") + moistureSensor6 := model.MakeMoistureSensor(6, "Sensor #6") + + app.MoistureSensors = append(app.MoistureSensors, &moistureSensor1) + app.MoistureSensors = append(app.MoistureSensors, &moistureSensor2) + app.MoistureSensors = append(app.MoistureSensors, &moistureSensor3) + app.MoistureSensors = append(app.MoistureSensors, &moistureSensor4) + app.MoistureSensors = append(app.MoistureSensors, &moistureSensor5) + app.MoistureSensors = append(app.MoistureSensors, &moistureSensor6) + + app.Zones = append(app.Zones, model.NewZone( + "raised-bed-1", + "Raised Bed #1", + []*model.MoistureSensor{&moistureSensor1, &moistureSensor2}, + []*model.WaterOutlet{waterOutlet1}, + )) + + app.Zones = append(app.Zones, model.NewZone( + "raised-bed-2", + "Raised Bed #2", + []*model.MoistureSensor{&moistureSensor3, &moistureSensor4}, + []*model.WaterOutlet{waterOutlet2}, + )) + + app.Zones = append(app.Zones, model.NewZone( + "raised-bed-3", + "Raised Bed #3", + []*model.MoistureSensor{&moistureSensor5, &moistureSensor6}, + []*model.WaterOutlet{waterOutlet3}, + )) } func (app *App) Run() { fmt.Println("Bonsoir, Elliot") + app.configure() + /* Make zone configurations Connect to MQTT diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go index 0a9e7b7..a782067 100644 --- a/pkg/model/moisture_sensor.go +++ b/pkg/model/moisture_sensor.go @@ -19,8 +19,10 @@ func (ms MoistureSensor) mapRawReadingToPercentage(raw uint) uint { )) } -func MakeMoistureSensor() MoistureSensor { +func MakeMoistureSensor(id uint, name string) MoistureSensor { return MoistureSensor{ + Id: id, + Name: name, DryThreshold: 500, WetThreshold: 240, } diff --git a/pkg/model/zone.go b/pkg/model/zone.go index df0b230..84576f8 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -1,15 +1,19 @@ package model type Zone struct { - Id uint - Name string - Mode *ZoneMode + Id string + Name string + Mode *ZoneMode + MoistureSensors []*MoistureSensor + WaterOutlets []*WaterOutlet } -func NewZone(id uint, name string) *Zone { +func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []*WaterOutlet) *Zone { return &Zone{ - Id: id, - Name: name, - Mode: getDefaultZoneMode(), + Id: id, + Name: name, + Mode: getDefaultZoneMode(), + MoistureSensors: sensors, + WaterOutlets: waterOutlets, } } From dd788e1acbdfb0b27f1ebd6c162e1b93144a08b2 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 17:09:21 +0000 Subject: [PATCH 008/135] fix: ids --- pkg/app/app.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 89c9cde..b3fef8e 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1,8 +1,6 @@ package app import ( - "fmt" - "github.com/mewejo/go-watering/pkg/model" ) @@ -15,9 +13,9 @@ type App struct { func (app *App) configure() { waterOutlet1 := model.NewWaterOutlet(1, "Soaker hose #1") - waterOutlet2 := model.NewWaterOutlet(1, "Soaker hose #2") - waterOutlet3 := model.NewWaterOutlet(1, "Soaker hose #3") - waterOutlet4 := model.NewWaterOutlet(1, "Soaker hose #4") + waterOutlet2 := model.NewWaterOutlet(2, "Soaker hose #2") + waterOutlet3 := model.NewWaterOutlet(3, "Soaker hose #3") + waterOutlet4 := model.NewWaterOutlet(4, "Soaker hose #4") // The only outlet which isn't tied to a zone. app.WaterOutlets = append(app.WaterOutlets, waterOutlet4) @@ -59,8 +57,6 @@ func (app *App) configure() { } func (app *App) Run() { - fmt.Println("Bonsoir, Elliot") - app.configure() /* From 6ff4ded509f5907324ed0ac1c0c4469310cc7096 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 18:34:40 +0000 Subject: [PATCH 009/135] wip: start sending data to hass --- pkg/app/app.go | 90 +++++++++++++++---- pkg/hass/hass_client.go | 84 +++++++++++++++++ pkg/hass/mqtt_message.go | 15 ++++ pkg/model/hass_auto_discoverable.go | 6 ++ pkg/model/hass_device.go | 17 ++++ pkg/model/moisture_sensor.go | 18 +++- .../moisture_sensor_hass_configuration.go | 32 +++++++ 7 files changed, 246 insertions(+), 16 deletions(-) create mode 100644 pkg/hass/hass_client.go create mode 100644 pkg/hass/mqtt_message.go create mode 100644 pkg/model/hass_auto_discoverable.go create mode 100644 pkg/model/hass_device.go create mode 100644 pkg/model/moisture_sensor_hass_configuration.go diff --git a/pkg/app/app.go b/pkg/app/app.go index b3fef8e..1f60dfd 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1,16 +1,20 @@ package app import ( + "os" + + "github.com/mewejo/go-watering/pkg/hass" "github.com/mewejo/go-watering/pkg/model" ) type App struct { - Zones []*model.Zone - WaterOutlets []*model.WaterOutlet - MoistureSensors []*model.MoistureSensor + zones []*model.Zone + waterOutlets []*model.WaterOutlet + moistureSensors []*model.MoistureSensor + hass *hass.HassClient } -func (app *App) configure() { +func (app *App) configureHardware() { waterOutlet1 := model.NewWaterOutlet(1, "Soaker hose #1") waterOutlet2 := model.NewWaterOutlet(2, "Soaker hose #2") @@ -18,7 +22,7 @@ func (app *App) configure() { waterOutlet4 := model.NewWaterOutlet(4, "Soaker hose #4") // The only outlet which isn't tied to a zone. - app.WaterOutlets = append(app.WaterOutlets, waterOutlet4) + app.waterOutlets = append(app.waterOutlets, waterOutlet4) moistureSensor1 := model.MakeMoistureSensor(1, "Sensor #1") moistureSensor2 := model.MakeMoistureSensor(2, "Sensor #2") @@ -27,28 +31,28 @@ func (app *App) configure() { moistureSensor5 := model.MakeMoistureSensor(5, "Sensor #5") moistureSensor6 := model.MakeMoistureSensor(6, "Sensor #6") - app.MoistureSensors = append(app.MoistureSensors, &moistureSensor1) - app.MoistureSensors = append(app.MoistureSensors, &moistureSensor2) - app.MoistureSensors = append(app.MoistureSensors, &moistureSensor3) - app.MoistureSensors = append(app.MoistureSensors, &moistureSensor4) - app.MoistureSensors = append(app.MoistureSensors, &moistureSensor5) - app.MoistureSensors = append(app.MoistureSensors, &moistureSensor6) + app.moistureSensors = append(app.moistureSensors, &moistureSensor1) + app.moistureSensors = append(app.moistureSensors, &moistureSensor2) + app.moistureSensors = append(app.moistureSensors, &moistureSensor3) + app.moistureSensors = append(app.moistureSensors, &moistureSensor4) + app.moistureSensors = append(app.moistureSensors, &moistureSensor5) + app.moistureSensors = append(app.moistureSensors, &moistureSensor6) - app.Zones = append(app.Zones, model.NewZone( + app.zones = append(app.zones, model.NewZone( "raised-bed-1", "Raised Bed #1", []*model.MoistureSensor{&moistureSensor1, &moistureSensor2}, []*model.WaterOutlet{waterOutlet1}, )) - app.Zones = append(app.Zones, model.NewZone( + app.zones = append(app.zones, model.NewZone( "raised-bed-2", "Raised Bed #2", []*model.MoistureSensor{&moistureSensor3, &moistureSensor4}, []*model.WaterOutlet{waterOutlet2}, )) - app.Zones = append(app.Zones, model.NewZone( + app.zones = append(app.zones, model.NewZone( "raised-bed-3", "Raised Bed #3", []*model.MoistureSensor{&moistureSensor5, &moistureSensor6}, @@ -56,8 +60,64 @@ func (app *App) configure() { )) } +func (app *App) setupHass() error { + + hassDevice := model.NewHassDevice() + + app.hass = hass.NewClient( + os.Getenv("HOME_ASSISTANT_DISCOVERY_PREFIX")+"/", + hassDevice, + ) + + return app.hass.Connect( + hass.MakeMqttMessage("vegetable-soaker/availability", "unavailable"), // TODO veg soaker + unavail should be constants/generated/configurable + ) +} + +func (app *App) publishHassAutoDiscovery() error { + for _, moistureSensor := range app.moistureSensors { + token, err := app.hass.PublishAutoDiscovery(moistureSensor) + + if err != nil { + return err + } + + token.Wait() + } + + return nil + + /* + + for _, waterOutlet := range app.waterOutlets { + // TODO + } + + for _, zone := range app.zones { + // TODO + } + + */ +} + func (app *App) Run() { - app.configure() + app.configureHardware() + + { + err := app.setupHass() + + if err != nil { + panic(err) + } + } + + { + err := app.publishHassAutoDiscovery() + + if err != nil { + panic(err) + } + } /* Make zone configurations diff --git a/pkg/hass/hass_client.go b/pkg/hass/hass_client.go new file mode 100644 index 0000000..2152668 --- /dev/null +++ b/pkg/hass/hass_client.go @@ -0,0 +1,84 @@ +package hass + +import ( + "encoding/json" + "fmt" + "os" + "time" + + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/mewejo/go-watering/pkg/model" +) + +type HassClient struct { + client mqtt.Client + namespace string // prefix all MQTT topics with this NS + device *model.HassDevice +} + +func NewClient(namespace string, device *model.HassDevice) *HassClient { + return &HassClient{ + namespace: namespace, + device: device, + } +} + +func (c *HassClient) defaultMessageHandler(msg mqtt.Message) { + // TODO + fmt.Printf("TOPIC: %s\n", msg.Topic()) + fmt.Printf("MSG: %s\n", msg.Payload()) +} + +func (c *HassClient) PublishAutoDiscovery(entity model.HassAutoDiscoverable) (mqtt.Token, error) { + + json, err := json.Marshal(entity.AutoDiscoveryPayload(c.device)) + + if err != nil { + return nil, err + } + + fmt.Println(string(json)) + + return c.client.Publish( + c.namespace+entity.AutoDiscoveryTopic()+"/config", + 0, + false, + string(json), + ), nil +} + +func (c *HassClient) Disconnect() { + c.client.Disconnect(500) +} + +func (c *HassClient) Connect(lwt MqttMessage) error { + connectionString := fmt.Sprintf( + "tcp://%v:%v", + os.Getenv("MQTT_HOST"), + os.Getenv("MQTT_PORT"), + ) + + var defaultHandler mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) { + c.defaultMessageHandler(msg) + } + + opts := mqtt.NewClientOptions() + opts.AddBroker(connectionString) + opts.SetWill(c.namespace+lwt.topic, lwt.payload, lwt.qos, lwt.retain) + opts.SetClientID(os.Getenv("MQTT_CLIENT_ID")) + opts.SetUsername(os.Getenv("MQTT_USERNAME")) + opts.SetPassword(os.Getenv("MQTT_PASSWORD")) + opts.SetKeepAlive(2 * time.Second) + opts.SetDefaultPublishHandler(defaultHandler) + opts.SetPingTimeout(1 * time.Second) + + mqttClient := mqtt.NewClient(opts) + + if token := mqttClient.Connect(); token.Wait() && token.Error() != nil { + return token.Error() + } + + c.client = mqttClient + + return nil +} diff --git a/pkg/hass/mqtt_message.go b/pkg/hass/mqtt_message.go new file mode 100644 index 0000000..7a3cd74 --- /dev/null +++ b/pkg/hass/mqtt_message.go @@ -0,0 +1,15 @@ +package hass + +type MqttMessage struct { + topic string + payload string + retain bool + qos byte +} + +func MakeMqttMessage(topic string, payload string) MqttMessage { + return MqttMessage{ + topic: topic, + payload: payload, + } +} diff --git a/pkg/model/hass_auto_discoverable.go b/pkg/model/hass_auto_discoverable.go new file mode 100644 index 0000000..036f4fd --- /dev/null +++ b/pkg/model/hass_auto_discoverable.go @@ -0,0 +1,6 @@ +package model + +type HassAutoDiscoverable interface { + AutoDiscoveryTopic() string + AutoDiscoveryPayload(device *HassDevice) interface{} +} diff --git a/pkg/model/hass_device.go b/pkg/model/hass_device.go new file mode 100644 index 0000000..c892d08 --- /dev/null +++ b/pkg/model/hass_device.go @@ -0,0 +1,17 @@ +package model + +type HassDevice struct { + Identifier string `json:"identifiers"` + Name string `json:"name"` + Model string `json:"model"` + Manufacturer string `json:"manufacturer"` +} + +func NewHassDevice() *HassDevice { + return &HassDevice{ + Identifier: "vegetable-soaker", + Name: "Vegatable Soaker", + Model: "vegetable-soaker", + Manufacturer: "Josh Bonfield", + } +} diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go index a782067..d61d3ef 100644 --- a/pkg/model/moisture_sensor.go +++ b/pkg/model/moisture_sensor.go @@ -1,6 +1,10 @@ package model -import "github.com/mewejo/go-watering/pkg/number" +import ( + "strconv" + + "github.com/mewejo/go-watering/pkg/number" +) type MoistureSensor struct { Id uint @@ -9,6 +13,18 @@ type MoistureSensor struct { WetThreshold uint } +func (ms MoistureSensor) AutoDiscoveryTopic() string { + return "sensor/vegetable-soaker/sensor-" + ms.IdAsString() +} + +func (ms MoistureSensor) AutoDiscoveryPayload(device *HassDevice) interface{} { + return makeMoistureSensorHassConfiguration(ms, device) +} + +func (ms MoistureSensor) IdAsString() string { + return strconv.FormatUint(uint64(ms.Id), 10) +} + func (ms MoistureSensor) mapRawReadingToPercentage(raw uint) uint { return uint(number.ChangeRange( float64(raw), diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go new file mode 100644 index 0000000..b187bf7 --- /dev/null +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -0,0 +1,32 @@ +package model + +type moistureSensorHassConfiguration struct { + Name string `json:"name"` + DeviceClass string `json:"device_class"` + ObjectId string `json:"object_id"` + UniqueId string `json:"unique_id"` + StateTopic string `json:"state_topic"` + StateValueTemplate string `json:"value_template"` + AvailabilityTopic string `json:"availability_topic"` // TODO this can be set on construct..? + UnitOfMeasurement string `json:"unit_of_measurement"` + HassDevice *HassDevice `json:"device"` + PayloadAvailable string `json:"payload_available"` + PayloadNotAvailable string `json:"payload_not_available"` +} + +func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevice) moistureSensorHassConfiguration { + c := moistureSensorHassConfiguration{} + c.Name = sensor.Name + c.ObjectId = "vegetable-soaker-sensor-" + sensor.IdAsString() + c.UniqueId = c.ObjectId + c.StateTopic = "state" // TODO + c.AvailabilityTopic = "state" // TODO + c.DeviceClass = "moisture" + c.StateValueTemplate = "{{ value_json.moisture.percentage }}" + c.UnitOfMeasurement = "%" + c.PayloadAvailable = "online" + c.PayloadNotAvailable = "offline" + c.HassDevice = device + + return c +} From a81d34d88df9709299e5fd05ce36f91f0743cc79 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 18:35:42 +0000 Subject: [PATCH 010/135] cs: tidy up --- pkg/app/app.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 1f60dfd..6c2fa01 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -103,20 +103,16 @@ func (app *App) publishHassAutoDiscovery() error { func (app *App) Run() { app.configureHardware() - { - err := app.setupHass() + err := app.setupHass() - if err != nil { - panic(err) - } + if err != nil { + panic(err) } - { - err := app.publishHassAutoDiscovery() + err = app.publishHassAutoDiscovery() - if err != nil { - panic(err) - } + if err != nil { + panic(err) } /* From 4998b8b01031a2ea7740b5671f3955fa53a1fdf9 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 20:31:53 +0000 Subject: [PATCH 011/135] feat: pass around the hass device --- pkg/app/app.go | 2 +- pkg/hass/hass_client.go | 21 ++++++++++++++------- pkg/model/hass_auto_discoverable.go | 2 +- pkg/model/hass_device.go | 2 ++ pkg/model/moisture_sensor.go | 4 ++-- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 6c2fa01..dc8154c 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -65,7 +65,7 @@ func (app *App) setupHass() error { hassDevice := model.NewHassDevice() app.hass = hass.NewClient( - os.Getenv("HOME_ASSISTANT_DISCOVERY_PREFIX")+"/", + os.Getenv("HOME_ASSISTANT_DISCOVERY_PREFIX"), hassDevice, ) diff --git a/pkg/hass/hass_client.go b/pkg/hass/hass_client.go index 2152668..ea44b10 100644 --- a/pkg/hass/hass_client.go +++ b/pkg/hass/hass_client.go @@ -31,20 +31,27 @@ func (c *HassClient) defaultMessageHandler(msg mqtt.Message) { func (c *HassClient) PublishAutoDiscovery(entity model.HassAutoDiscoverable) (mqtt.Token, error) { - json, err := json.Marshal(entity.AutoDiscoveryPayload(c.device)) + json, err := json.Marshal( + entity.AutoDiscoveryPayload(c.device), + ) if err != nil { return nil, err } - fmt.Println(string(json)) + return c.Publish(MakeMqttMessage( + entity.AutoDiscoveryTopic(c.device)+"/config", + string(json), + )), nil +} +func (c *HassClient) Publish(message MqttMessage) mqtt.Token { return c.client.Publish( - c.namespace+entity.AutoDiscoveryTopic()+"/config", - 0, - false, - string(json), - ), nil + c.namespace+"/"+message.topic, + message.qos, + message.retain, + message.payload, + ) } func (c *HassClient) Disconnect() { diff --git a/pkg/model/hass_auto_discoverable.go b/pkg/model/hass_auto_discoverable.go index 036f4fd..3980f14 100644 --- a/pkg/model/hass_auto_discoverable.go +++ b/pkg/model/hass_auto_discoverable.go @@ -1,6 +1,6 @@ package model type HassAutoDiscoverable interface { - AutoDiscoveryTopic() string + AutoDiscoveryTopic(device *HassDevice) string AutoDiscoveryPayload(device *HassDevice) interface{} } diff --git a/pkg/model/hass_device.go b/pkg/model/hass_device.go index c892d08..c8579e4 100644 --- a/pkg/model/hass_device.go +++ b/pkg/model/hass_device.go @@ -5,6 +5,7 @@ type HassDevice struct { Name string `json:"name"` Model string `json:"model"` Manufacturer string `json:"manufacturer"` + Namespace string } func NewHassDevice() *HassDevice { @@ -13,5 +14,6 @@ func NewHassDevice() *HassDevice { Name: "Vegatable Soaker", Model: "vegetable-soaker", Manufacturer: "Josh Bonfield", + Namespace: "vegetable-soaker", } } diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go index d61d3ef..2f2496b 100644 --- a/pkg/model/moisture_sensor.go +++ b/pkg/model/moisture_sensor.go @@ -13,8 +13,8 @@ type MoistureSensor struct { WetThreshold uint } -func (ms MoistureSensor) AutoDiscoveryTopic() string { - return "sensor/vegetable-soaker/sensor-" + ms.IdAsString() +func (ms MoistureSensor) AutoDiscoveryTopic(device *HassDevice) string { + return "sensor/" + device.Namespace + "/sensor-" + ms.IdAsString() } func (ms MoistureSensor) AutoDiscoveryPayload(device *HassDevice) interface{} { From 438cbe82c60bf21cce52288a68e57ba1610de4c1 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 20:42:01 +0000 Subject: [PATCH 012/135] wip: topic namespacing --- pkg/app/app.go | 2 +- pkg/hass/hass_client.go | 2 +- pkg/model/hass_auto_discoverable.go | 2 +- pkg/model/hass_device.go | 24 +++++++++++-------- pkg/model/moisture_sensor.go | 2 +- .../moisture_sensor_hass_configuration.go | 6 ++--- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index dc8154c..973bac9 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -70,7 +70,7 @@ func (app *App) setupHass() error { ) return app.hass.Connect( - hass.MakeMqttMessage("vegetable-soaker/availability", "unavailable"), // TODO veg soaker + unavail should be constants/generated/configurable + hass.MakeMqttMessage(hassDevice.Namespace+"/availability", "unavailable"), // TODO veg soaker + unavail should be constants/generated/configurable ) } diff --git a/pkg/hass/hass_client.go b/pkg/hass/hass_client.go index ea44b10..87521fe 100644 --- a/pkg/hass/hass_client.go +++ b/pkg/hass/hass_client.go @@ -40,7 +40,7 @@ func (c *HassClient) PublishAutoDiscovery(entity model.HassAutoDiscoverable) (mq } return c.Publish(MakeMqttMessage( - entity.AutoDiscoveryTopic(c.device)+"/config", + entity.EntityTopic(c.device)+"/config", string(json), )), nil } diff --git a/pkg/model/hass_auto_discoverable.go b/pkg/model/hass_auto_discoverable.go index 3980f14..b876eb9 100644 --- a/pkg/model/hass_auto_discoverable.go +++ b/pkg/model/hass_auto_discoverable.go @@ -1,6 +1,6 @@ package model type HassAutoDiscoverable interface { - AutoDiscoveryTopic(device *HassDevice) string + EntityTopic(device *HassDevice) string AutoDiscoveryPayload(device *HassDevice) interface{} } diff --git a/pkg/model/hass_device.go b/pkg/model/hass_device.go index c8579e4..14e04a5 100644 --- a/pkg/model/hass_device.go +++ b/pkg/model/hass_device.go @@ -1,19 +1,23 @@ package model type HassDevice struct { - Identifier string `json:"identifiers"` - Name string `json:"name"` - Model string `json:"model"` - Manufacturer string `json:"manufacturer"` - Namespace string + Identifier string `json:"identifiers"` + Name string `json:"name"` + Model string `json:"model"` + Manufacturer string `json:"manufacturer"` + Namespace string + EntityPrefix string + AvailabilityTopic string } func NewHassDevice() *HassDevice { return &HassDevice{ - Identifier: "vegetable-soaker", - Name: "Vegatable Soaker", - Model: "vegetable-soaker", - Manufacturer: "Josh Bonfield", - Namespace: "vegetable-soaker", + Identifier: "vegetable-soaker", + Name: "Vegatable Soaker", + Model: "VegSoak 3000", + Manufacturer: "Josh Bonfield", + Namespace: "vegetable-soaker", + EntityPrefix: "vegetable-soaker-", + AvailabilityTopic: "availability", } } diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go index 2f2496b..94d39e3 100644 --- a/pkg/model/moisture_sensor.go +++ b/pkg/model/moisture_sensor.go @@ -13,7 +13,7 @@ type MoistureSensor struct { WetThreshold uint } -func (ms MoistureSensor) AutoDiscoveryTopic(device *HassDevice) string { +func (ms MoistureSensor) EntityTopic(device *HassDevice) string { return "sensor/" + device.Namespace + "/sensor-" + ms.IdAsString() } diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index b187bf7..1f92b53 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -17,10 +17,10 @@ type moistureSensorHassConfiguration struct { func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevice) moistureSensorHassConfiguration { c := moistureSensorHassConfiguration{} c.Name = sensor.Name - c.ObjectId = "vegetable-soaker-sensor-" + sensor.IdAsString() + c.ObjectId = device.EntityPrefix + "sensor-" + sensor.IdAsString() c.UniqueId = c.ObjectId - c.StateTopic = "state" // TODO - c.AvailabilityTopic = "state" // TODO + c.StateTopic = "state" + c.AvailabilityTopic = device.AvailabilityTopic c.DeviceClass = "moisture" c.StateValueTemplate = "{{ value_json.moisture.percentage }}" c.UnitOfMeasurement = "%" From 566cbc4a90c11db233c00f2558a4748b236da8be Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 20:43:15 +0000 Subject: [PATCH 013/135] tweak: change to MqttTopic --- pkg/hass/hass_client.go | 2 +- pkg/model/hass_auto_discoverable.go | 2 +- pkg/model/moisture_sensor.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/hass/hass_client.go b/pkg/hass/hass_client.go index 87521fe..2127308 100644 --- a/pkg/hass/hass_client.go +++ b/pkg/hass/hass_client.go @@ -40,7 +40,7 @@ func (c *HassClient) PublishAutoDiscovery(entity model.HassAutoDiscoverable) (mq } return c.Publish(MakeMqttMessage( - entity.EntityTopic(c.device)+"/config", + entity.MqttTopic(c.device)+"/config", string(json), )), nil } diff --git a/pkg/model/hass_auto_discoverable.go b/pkg/model/hass_auto_discoverable.go index b876eb9..049e13f 100644 --- a/pkg/model/hass_auto_discoverable.go +++ b/pkg/model/hass_auto_discoverable.go @@ -1,6 +1,6 @@ package model type HassAutoDiscoverable interface { - EntityTopic(device *HassDevice) string + MqttTopic(device *HassDevice) string AutoDiscoveryPayload(device *HassDevice) interface{} } diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go index 94d39e3..f497651 100644 --- a/pkg/model/moisture_sensor.go +++ b/pkg/model/moisture_sensor.go @@ -13,7 +13,7 @@ type MoistureSensor struct { WetThreshold uint } -func (ms MoistureSensor) EntityTopic(device *HassDevice) string { +func (ms MoistureSensor) MqttTopic(device *HassDevice) string { return "sensor/" + device.Namespace + "/sensor-" + ms.IdAsString() } From a7466958ec56baa3b59ce3481223d9b60c4b0807 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 22:55:14 +0000 Subject: [PATCH 014/135] feat: publish water outlets --- pkg/app/app.go | 14 ++++++-- pkg/model/hass_device.go | 6 ++-- pkg/model/water_outlet.go | 14 ++++++++ pkg/model/water_outlet_hass_configuration.go | 36 ++++++++++++++++++++ 4 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 pkg/model/water_outlet_hass_configuration.go diff --git a/pkg/app/app.go b/pkg/app/app.go index 973bac9..003a095 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -75,8 +75,18 @@ func (app *App) setupHass() error { } func (app *App) publishHassAutoDiscovery() error { - for _, moistureSensor := range app.moistureSensors { - token, err := app.hass.PublishAutoDiscovery(moistureSensor) + for _, entity := range app.moistureSensors { + token, err := app.hass.PublishAutoDiscovery(entity) + + if err != nil { + return err + } + + token.Wait() + } + + for _, entity := range app.waterOutlets { + token, err := app.hass.PublishAutoDiscovery(entity) if err != nil { return err diff --git a/pkg/model/hass_device.go b/pkg/model/hass_device.go index 14e04a5..0942c62 100644 --- a/pkg/model/hass_device.go +++ b/pkg/model/hass_device.go @@ -5,9 +5,9 @@ type HassDevice struct { Name string `json:"name"` Model string `json:"model"` Manufacturer string `json:"manufacturer"` - Namespace string - EntityPrefix string - AvailabilityTopic string + Namespace string `json:"-"` + EntityPrefix string `json:"-"` + AvailabilityTopic string `json:"-"` } func NewHassDevice() *HassDevice { diff --git a/pkg/model/water_outlet.go b/pkg/model/water_outlet.go index ad35996..16c4266 100644 --- a/pkg/model/water_outlet.go +++ b/pkg/model/water_outlet.go @@ -1,5 +1,7 @@ package model +import "strconv" + type WaterOutlet struct { Id uint Name string @@ -13,3 +15,15 @@ func NewWaterOutlet(id uint, name string) *WaterOutlet { Name: name, } } + +func (wo WaterOutlet) IdAsString() string { + return strconv.FormatUint(uint64(wo.Id), 10) +} + +func (wo WaterOutlet) MqttTopic(device *HassDevice) string { + return "switch/" + device.Namespace + "/outlet-" + wo.IdAsString() +} + +func (wo WaterOutlet) AutoDiscoveryPayload(device *HassDevice) interface{} { + return makeWaterOutletHassConfiguration(wo, device) +} diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go new file mode 100644 index 0000000..daa4f5e --- /dev/null +++ b/pkg/model/water_outlet_hass_configuration.go @@ -0,0 +1,36 @@ +package model + +type waterOutletHassConfiguration struct { + Name string `json:"name"` + DeviceClass string `json:"device_class"` + ObjectId string `json:"object_id"` + UniqueId string `json:"unique_id"` + StateTopic string `json:"state_topic"` + StateValueTemplate string `json:"value_template"` + AvailabilityTopic string `json:"availability_topic"` // TODO this can be set on construct..? + HassDevice *HassDevice `json:"device"` + PayloadAvailable string `json:"payload_available"` + PayloadNotAvailable string `json:"payload_not_available"` + CommandTopic string `json:"command_topic"` + StateOn string `json:"state_on"` + StateOff string `json:"state_off"` +} + +func makeWaterOutletHassConfiguration(outlet WaterOutlet, device *HassDevice) waterOutletHassConfiguration { + c := waterOutletHassConfiguration{} + c.Name = outlet.Name + c.ObjectId = device.EntityPrefix + "outlet-" + outlet.IdAsString() + c.UniqueId = c.ObjectId + c.StateTopic = "state" + c.AvailabilityTopic = device.AvailabilityTopic + c.DeviceClass = "switch" + c.StateValueTemplate = "{{ value_json.target }}" + c.PayloadAvailable = "online" + c.PayloadNotAvailable = "offline" + c.HassDevice = device + c.CommandTopic = "command" + c.StateOn = "on" + c.StateOff = "off" + + return c +} From 8e798fe8f543e5fba68f8be64a8189e3a5d78bc7 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 23:00:46 +0000 Subject: [PATCH 015/135] refactor: move methods to own files --- pkg/app/app.go | 98 --------------------------------------------- pkg/app/hardware.go | 49 +++++++++++++++++++++++ pkg/app/hass.go | 58 +++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 98 deletions(-) create mode 100644 pkg/app/hardware.go create mode 100644 pkg/app/hass.go diff --git a/pkg/app/app.go b/pkg/app/app.go index 003a095..5d2e7b7 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1,8 +1,6 @@ package app import ( - "os" - "github.com/mewejo/go-watering/pkg/hass" "github.com/mewejo/go-watering/pkg/model" ) @@ -14,102 +12,6 @@ type App struct { hass *hass.HassClient } -func (app *App) configureHardware() { - - waterOutlet1 := model.NewWaterOutlet(1, "Soaker hose #1") - waterOutlet2 := model.NewWaterOutlet(2, "Soaker hose #2") - waterOutlet3 := model.NewWaterOutlet(3, "Soaker hose #3") - waterOutlet4 := model.NewWaterOutlet(4, "Soaker hose #4") - - // The only outlet which isn't tied to a zone. - app.waterOutlets = append(app.waterOutlets, waterOutlet4) - - moistureSensor1 := model.MakeMoistureSensor(1, "Sensor #1") - moistureSensor2 := model.MakeMoistureSensor(2, "Sensor #2") - moistureSensor3 := model.MakeMoistureSensor(3, "Sensor #3") - moistureSensor4 := model.MakeMoistureSensor(4, "Sensor #4") - moistureSensor5 := model.MakeMoistureSensor(5, "Sensor #5") - moistureSensor6 := model.MakeMoistureSensor(6, "Sensor #6") - - app.moistureSensors = append(app.moistureSensors, &moistureSensor1) - app.moistureSensors = append(app.moistureSensors, &moistureSensor2) - app.moistureSensors = append(app.moistureSensors, &moistureSensor3) - app.moistureSensors = append(app.moistureSensors, &moistureSensor4) - app.moistureSensors = append(app.moistureSensors, &moistureSensor5) - app.moistureSensors = append(app.moistureSensors, &moistureSensor6) - - app.zones = append(app.zones, model.NewZone( - "raised-bed-1", - "Raised Bed #1", - []*model.MoistureSensor{&moistureSensor1, &moistureSensor2}, - []*model.WaterOutlet{waterOutlet1}, - )) - - app.zones = append(app.zones, model.NewZone( - "raised-bed-2", - "Raised Bed #2", - []*model.MoistureSensor{&moistureSensor3, &moistureSensor4}, - []*model.WaterOutlet{waterOutlet2}, - )) - - app.zones = append(app.zones, model.NewZone( - "raised-bed-3", - "Raised Bed #3", - []*model.MoistureSensor{&moistureSensor5, &moistureSensor6}, - []*model.WaterOutlet{waterOutlet3}, - )) -} - -func (app *App) setupHass() error { - - hassDevice := model.NewHassDevice() - - app.hass = hass.NewClient( - os.Getenv("HOME_ASSISTANT_DISCOVERY_PREFIX"), - hassDevice, - ) - - return app.hass.Connect( - hass.MakeMqttMessage(hassDevice.Namespace+"/availability", "unavailable"), // TODO veg soaker + unavail should be constants/generated/configurable - ) -} - -func (app *App) publishHassAutoDiscovery() error { - for _, entity := range app.moistureSensors { - token, err := app.hass.PublishAutoDiscovery(entity) - - if err != nil { - return err - } - - token.Wait() - } - - for _, entity := range app.waterOutlets { - token, err := app.hass.PublishAutoDiscovery(entity) - - if err != nil { - return err - } - - token.Wait() - } - - return nil - - /* - - for _, waterOutlet := range app.waterOutlets { - // TODO - } - - for _, zone := range app.zones { - // TODO - } - - */ -} - func (app *App) Run() { app.configureHardware() diff --git a/pkg/app/hardware.go b/pkg/app/hardware.go new file mode 100644 index 0000000..4a13f09 --- /dev/null +++ b/pkg/app/hardware.go @@ -0,0 +1,49 @@ +package app + +import "github.com/mewejo/go-watering/pkg/model" + +func (app *App) configureHardware() { + + waterOutlet1 := model.NewWaterOutlet(1, "Soaker hose #1") + waterOutlet2 := model.NewWaterOutlet(2, "Soaker hose #2") + waterOutlet3 := model.NewWaterOutlet(3, "Soaker hose #3") + waterOutlet4 := model.NewWaterOutlet(4, "Soaker hose #4") + + // The only outlet which isn't tied to a zone. + app.waterOutlets = append(app.waterOutlets, waterOutlet4) + + moistureSensor1 := model.MakeMoistureSensor(1, "Sensor #1") + moistureSensor2 := model.MakeMoistureSensor(2, "Sensor #2") + moistureSensor3 := model.MakeMoistureSensor(3, "Sensor #3") + moistureSensor4 := model.MakeMoistureSensor(4, "Sensor #4") + moistureSensor5 := model.MakeMoistureSensor(5, "Sensor #5") + moistureSensor6 := model.MakeMoistureSensor(6, "Sensor #6") + + app.moistureSensors = append(app.moistureSensors, &moistureSensor1) + app.moistureSensors = append(app.moistureSensors, &moistureSensor2) + app.moistureSensors = append(app.moistureSensors, &moistureSensor3) + app.moistureSensors = append(app.moistureSensors, &moistureSensor4) + app.moistureSensors = append(app.moistureSensors, &moistureSensor5) + app.moistureSensors = append(app.moistureSensors, &moistureSensor6) + + app.zones = append(app.zones, model.NewZone( + "raised-bed-1", + "Raised Bed #1", + []*model.MoistureSensor{&moistureSensor1, &moistureSensor2}, + []*model.WaterOutlet{waterOutlet1}, + )) + + app.zones = append(app.zones, model.NewZone( + "raised-bed-2", + "Raised Bed #2", + []*model.MoistureSensor{&moistureSensor3, &moistureSensor4}, + []*model.WaterOutlet{waterOutlet2}, + )) + + app.zones = append(app.zones, model.NewZone( + "raised-bed-3", + "Raised Bed #3", + []*model.MoistureSensor{&moistureSensor5, &moistureSensor6}, + []*model.WaterOutlet{waterOutlet3}, + )) +} diff --git a/pkg/app/hass.go b/pkg/app/hass.go new file mode 100644 index 0000000..90444d8 --- /dev/null +++ b/pkg/app/hass.go @@ -0,0 +1,58 @@ +package app + +import ( + "os" + + "github.com/mewejo/go-watering/pkg/hass" + "github.com/mewejo/go-watering/pkg/model" +) + +func (app *App) setupHass() error { + + hassDevice := model.NewHassDevice() + + app.hass = hass.NewClient( + os.Getenv("HOME_ASSISTANT_DISCOVERY_PREFIX"), + hassDevice, + ) + + return app.hass.Connect( + hass.MakeMqttMessage(hassDevice.Namespace+"/availability", "unavailable"), // TODO veg soaker + unavail should be constants/generated/configurable + ) +} + +func (app *App) publishHassAutoDiscovery() error { + for _, entity := range app.moistureSensors { + token, err := app.hass.PublishAutoDiscovery(entity) + + if err != nil { + return err + } + + token.Wait() + } + + for _, entity := range app.waterOutlets { + token, err := app.hass.PublishAutoDiscovery(entity) + + if err != nil { + return err + } + + token.Wait() + } + + return nil + + /* + + for _, waterOutlet := range app.waterOutlets { + // TODO + } + + for _, zone := range app.zones { + // TODO + } + + */ +} From 5f66b52d43cb9705cdda669fd2c1b7e0e5621d93 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 23:21:24 +0000 Subject: [PATCH 016/135] wip: send zone configs --- pkg/app/hass.go | 16 ++++++------- pkg/model/zone.go | 8 +++++++ pkg/model/zone_hass_configuration.go | 36 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 pkg/model/zone_hass_configuration.go diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 90444d8..c77224d 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -42,17 +42,15 @@ func (app *App) publishHassAutoDiscovery() error { token.Wait() } - return nil - - /* + for _, entity := range app.zones { + token, err := app.hass.PublishAutoDiscovery(entity) - for _, waterOutlet := range app.waterOutlets { - // TODO + if err != nil { + return err } - for _, zone := range app.zones { - // TODO - } + token.Wait() + } - */ + return nil } diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 84576f8..e8fd3c3 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -17,3 +17,11 @@ func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []* WaterOutlets: waterOutlets, } } + +func (zone Zone) MqttTopic(device *HassDevice) string { + return "humidifier/" + device.Namespace + "/zone-" + zone.Id +} + +func (zone Zone) AutoDiscoveryPayload(device *HassDevice) interface{} { + return makeZoneHassConfiguration(zone, device) +} diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go new file mode 100644 index 0000000..b1bf5f8 --- /dev/null +++ b/pkg/model/zone_hass_configuration.go @@ -0,0 +1,36 @@ +package model + +type zoneHassConfiguration struct { + Name string `json:"name"` + ObjectId string `json:"object_id"` + UniqueId string `json:"unique_id"` + StateTopic string `json:"state_topic"` + StateValueTemplate string `json:"value_template"` + AvailabilityTopic string `json:"availability_topic"` // TODO this can be set on construct..? + HassDevice *HassDevice `json:"device"` + PayloadAvailable string `json:"payload_available"` + PayloadNotAvailable string `json:"payload_not_available"` + CommandTopic string `json:"command_topic"` + StateOn string `json:"state_on"` + StateOff string `json:"state_off"` + TargetMoistureTopic string `json:"target_humidity_command_topic"` +} + +func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfiguration { + c := zoneHassConfiguration{} + c.Name = zone.Name + c.ObjectId = device.EntityPrefix + "zone-" + zone.Id + c.UniqueId = c.ObjectId + c.StateTopic = "humidifier" + c.TargetMoistureTopic = "target_moisture" + c.AvailabilityTopic = device.AvailabilityTopic + c.StateValueTemplate = "{{ value_json.target }}" + c.PayloadAvailable = "online" + c.PayloadNotAvailable = "offline" + c.HassDevice = device + c.CommandTopic = "command" + c.StateOn = "on" + c.StateOff = "off" + + return c +} From c4bf3bcfd5cee3b78c448d1e493402114a1f2aa3 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 23:26:12 +0000 Subject: [PATCH 017/135] wip: dynamic device availability topic --- pkg/model/hass_device.go | 4 ++++ pkg/model/moisture_sensor_hass_configuration.go | 2 +- pkg/model/water_outlet_hass_configuration.go | 2 +- pkg/model/zone_hass_configuration.go | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/model/hass_device.go b/pkg/model/hass_device.go index 0942c62..090a81a 100644 --- a/pkg/model/hass_device.go +++ b/pkg/model/hass_device.go @@ -21,3 +21,7 @@ func NewHassDevice() *HassDevice { AvailabilityTopic: "availability", } } + +func (d HassDevice) GetFqAvailabilityTopic() string { + return d.Namespace + "/availability" +} diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index 1f92b53..49b5799 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -20,7 +20,7 @@ func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevi c.ObjectId = device.EntityPrefix + "sensor-" + sensor.IdAsString() c.UniqueId = c.ObjectId c.StateTopic = "state" - c.AvailabilityTopic = device.AvailabilityTopic + c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.DeviceClass = "moisture" c.StateValueTemplate = "{{ value_json.moisture.percentage }}" c.UnitOfMeasurement = "%" diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index daa4f5e..10931d8 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -22,7 +22,7 @@ func makeWaterOutletHassConfiguration(outlet WaterOutlet, device *HassDevice) wa c.ObjectId = device.EntityPrefix + "outlet-" + outlet.IdAsString() c.UniqueId = c.ObjectId c.StateTopic = "state" - c.AvailabilityTopic = device.AvailabilityTopic + c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.DeviceClass = "switch" c.StateValueTemplate = "{{ value_json.target }}" c.PayloadAvailable = "online" diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index b1bf5f8..bd7d7a4 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -23,7 +23,7 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfigurat c.UniqueId = c.ObjectId c.StateTopic = "humidifier" c.TargetMoistureTopic = "target_moisture" - c.AvailabilityTopic = device.AvailabilityTopic + c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.StateValueTemplate = "{{ value_json.target }}" c.PayloadAvailable = "online" c.PayloadNotAvailable = "offline" From f308b6216e4cfb5d607868bfb0cd399d9b98a4a1 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Sun, 5 Feb 2023 23:59:28 +0000 Subject: [PATCH 018/135] feat: send client prefix with availability topic --- pkg/hass/hass_client.go | 4 +++- pkg/model/hass_auto_discover_payload.go | 5 +++++ pkg/model/hass_auto_discoverable.go | 2 +- pkg/model/moisture_sensor.go | 2 +- pkg/model/moisture_sensor_hass_configuration.go | 7 ++++++- pkg/model/water_outlet.go | 2 +- pkg/model/water_outlet_hass_configuration.go | 7 ++++++- pkg/model/zone.go | 2 +- pkg/model/zone_hass_configuration.go | 7 ++++++- 9 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 pkg/model/hass_auto_discover_payload.go diff --git a/pkg/hass/hass_client.go b/pkg/hass/hass_client.go index 2127308..5ee4e2c 100644 --- a/pkg/hass/hass_client.go +++ b/pkg/hass/hass_client.go @@ -31,8 +31,10 @@ func (c *HassClient) defaultMessageHandler(msg mqtt.Message) { func (c *HassClient) PublishAutoDiscovery(entity model.HassAutoDiscoverable) (mqtt.Token, error) { + payload := entity.AutoDiscoveryPayload(c.device).WithGlobalTopicPrefix(c.namespace) + json, err := json.Marshal( - entity.AutoDiscoveryPayload(c.device), + payload, ) if err != nil { diff --git a/pkg/model/hass_auto_discover_payload.go b/pkg/model/hass_auto_discover_payload.go new file mode 100644 index 0000000..78bc2ba --- /dev/null +++ b/pkg/model/hass_auto_discover_payload.go @@ -0,0 +1,5 @@ +package model + +type HassAutoDiscoverPayload interface { + WithGlobalTopicPrefix(prefix string) HassAutoDiscoverPayload +} diff --git a/pkg/model/hass_auto_discoverable.go b/pkg/model/hass_auto_discoverable.go index 049e13f..353afa8 100644 --- a/pkg/model/hass_auto_discoverable.go +++ b/pkg/model/hass_auto_discoverable.go @@ -2,5 +2,5 @@ package model type HassAutoDiscoverable interface { MqttTopic(device *HassDevice) string - AutoDiscoveryPayload(device *HassDevice) interface{} + AutoDiscoveryPayload(device *HassDevice) HassAutoDiscoverPayload } diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go index f497651..66a8ea4 100644 --- a/pkg/model/moisture_sensor.go +++ b/pkg/model/moisture_sensor.go @@ -17,7 +17,7 @@ func (ms MoistureSensor) MqttTopic(device *HassDevice) string { return "sensor/" + device.Namespace + "/sensor-" + ms.IdAsString() } -func (ms MoistureSensor) AutoDiscoveryPayload(device *HassDevice) interface{} { +func (ms MoistureSensor) AutoDiscoveryPayload(device *HassDevice) HassAutoDiscoverPayload { return makeMoistureSensorHassConfiguration(ms, device) } diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index 49b5799..1febe4c 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -14,7 +14,12 @@ type moistureSensorHassConfiguration struct { PayloadNotAvailable string `json:"payload_not_available"` } -func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevice) moistureSensorHassConfiguration { +func (c moistureSensorHassConfiguration) WithGlobalTopicPrefix(prefix string) HassAutoDiscoverPayload { + c.AvailabilityTopic = prefix + "/" + c.AvailabilityTopic + return c +} + +func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevice) HassAutoDiscoverPayload { c := moistureSensorHassConfiguration{} c.Name = sensor.Name c.ObjectId = device.EntityPrefix + "sensor-" + sensor.IdAsString() diff --git a/pkg/model/water_outlet.go b/pkg/model/water_outlet.go index 16c4266..60f1236 100644 --- a/pkg/model/water_outlet.go +++ b/pkg/model/water_outlet.go @@ -24,6 +24,6 @@ func (wo WaterOutlet) MqttTopic(device *HassDevice) string { return "switch/" + device.Namespace + "/outlet-" + wo.IdAsString() } -func (wo WaterOutlet) AutoDiscoveryPayload(device *HassDevice) interface{} { +func (wo WaterOutlet) AutoDiscoveryPayload(device *HassDevice) HassAutoDiscoverPayload { return makeWaterOutletHassConfiguration(wo, device) } diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index 10931d8..11b09b9 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -16,7 +16,12 @@ type waterOutletHassConfiguration struct { StateOff string `json:"state_off"` } -func makeWaterOutletHassConfiguration(outlet WaterOutlet, device *HassDevice) waterOutletHassConfiguration { +func (c waterOutletHassConfiguration) WithGlobalTopicPrefix(prefix string) HassAutoDiscoverPayload { + c.AvailabilityTopic = prefix + "/" + c.AvailabilityTopic + return c +} + +func makeWaterOutletHassConfiguration(outlet WaterOutlet, device *HassDevice) HassAutoDiscoverPayload { c := waterOutletHassConfiguration{} c.Name = outlet.Name c.ObjectId = device.EntityPrefix + "outlet-" + outlet.IdAsString() diff --git a/pkg/model/zone.go b/pkg/model/zone.go index e8fd3c3..7249e6e 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -22,6 +22,6 @@ func (zone Zone) MqttTopic(device *HassDevice) string { return "humidifier/" + device.Namespace + "/zone-" + zone.Id } -func (zone Zone) AutoDiscoveryPayload(device *HassDevice) interface{} { +func (zone Zone) AutoDiscoveryPayload(device *HassDevice) HassAutoDiscoverPayload { return makeZoneHassConfiguration(zone, device) } diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index bd7d7a4..adb1b24 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -16,7 +16,12 @@ type zoneHassConfiguration struct { TargetMoistureTopic string `json:"target_humidity_command_topic"` } -func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfiguration { +func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string) HassAutoDiscoverPayload { + c.AvailabilityTopic = prefix + "/" + c.AvailabilityTopic + return c +} + +func makeZoneHassConfiguration(zone Zone, device *HassDevice) HassAutoDiscoverPayload { c := zoneHassConfiguration{} c.Name = zone.Name c.ObjectId = device.EntityPrefix + "zone-" + zone.Id From d863751e6f9f2d181280367036e96530aee61fd6 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 00:00:29 +0000 Subject: [PATCH 019/135] chore: remove comments --- pkg/model/moisture_sensor_hass_configuration.go | 2 +- pkg/model/water_outlet_hass_configuration.go | 2 +- pkg/model/zone_hass_configuration.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index 1febe4c..cb7800a 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -7,7 +7,7 @@ type moistureSensorHassConfiguration struct { UniqueId string `json:"unique_id"` StateTopic string `json:"state_topic"` StateValueTemplate string `json:"value_template"` - AvailabilityTopic string `json:"availability_topic"` // TODO this can be set on construct..? + AvailabilityTopic string `json:"availability_topic"` UnitOfMeasurement string `json:"unit_of_measurement"` HassDevice *HassDevice `json:"device"` PayloadAvailable string `json:"payload_available"` diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index 11b09b9..e2c85f8 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -7,7 +7,7 @@ type waterOutletHassConfiguration struct { UniqueId string `json:"unique_id"` StateTopic string `json:"state_topic"` StateValueTemplate string `json:"value_template"` - AvailabilityTopic string `json:"availability_topic"` // TODO this can be set on construct..? + AvailabilityTopic string `json:"availability_topic"` HassDevice *HassDevice `json:"device"` PayloadAvailable string `json:"payload_available"` PayloadNotAvailable string `json:"payload_not_available"` diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index adb1b24..9c73328 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -6,7 +6,7 @@ type zoneHassConfiguration struct { UniqueId string `json:"unique_id"` StateTopic string `json:"state_topic"` StateValueTemplate string `json:"value_template"` - AvailabilityTopic string `json:"availability_topic"` // TODO this can be set on construct..? + AvailabilityTopic string `json:"availability_topic"` HassDevice *HassDevice `json:"device"` PayloadAvailable string `json:"payload_available"` PayloadNotAvailable string `json:"payload_not_available"` From acef448fa729a7b9c47e2044d57bff8847a2c69f Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 00:03:21 +0000 Subject: [PATCH 020/135] feat: use state values from the device --- pkg/model/hass_device.go | 32 +++++++++++-------- .../moisture_sensor_hass_configuration.go | 4 +-- pkg/model/water_outlet_hass_configuration.go | 4 +-- pkg/model/zone_hass_configuration.go | 4 +-- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/pkg/model/hass_device.go b/pkg/model/hass_device.go index 090a81a..37aa934 100644 --- a/pkg/model/hass_device.go +++ b/pkg/model/hass_device.go @@ -1,24 +1,28 @@ package model type HassDevice struct { - Identifier string `json:"identifiers"` - Name string `json:"name"` - Model string `json:"model"` - Manufacturer string `json:"manufacturer"` - Namespace string `json:"-"` - EntityPrefix string `json:"-"` - AvailabilityTopic string `json:"-"` + Identifier string `json:"identifiers"` + Name string `json:"name"` + Model string `json:"model"` + Manufacturer string `json:"manufacturer"` + Namespace string `json:"-"` + EntityPrefix string `json:"-"` + AvailabilityTopic string `json:"-"` + PayloadAvailable string `json:"-"` + PayloadNotAailable string `json:"-"` } func NewHassDevice() *HassDevice { return &HassDevice{ - Identifier: "vegetable-soaker", - Name: "Vegatable Soaker", - Model: "VegSoak 3000", - Manufacturer: "Josh Bonfield", - Namespace: "vegetable-soaker", - EntityPrefix: "vegetable-soaker-", - AvailabilityTopic: "availability", + Identifier: "vegetable-soaker", + Name: "Vegatable Soaker", + Model: "VegSoak 3000", + Manufacturer: "Josh Bonfield", + Namespace: "vegetable-soaker", + EntityPrefix: "vegetable-soaker-", + AvailabilityTopic: "availability", + PayloadAvailable: "online", + PayloadNotAailable: "offline", } } diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index cb7800a..ddc193d 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -29,8 +29,8 @@ func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevi c.DeviceClass = "moisture" c.StateValueTemplate = "{{ value_json.moisture.percentage }}" c.UnitOfMeasurement = "%" - c.PayloadAvailable = "online" - c.PayloadNotAvailable = "offline" + c.PayloadAvailable = device.PayloadAvailable + c.PayloadNotAvailable = device.PayloadNotAailable c.HassDevice = device return c diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index e2c85f8..1cda449 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -30,8 +30,8 @@ func makeWaterOutletHassConfiguration(outlet WaterOutlet, device *HassDevice) Ha c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.DeviceClass = "switch" c.StateValueTemplate = "{{ value_json.target }}" - c.PayloadAvailable = "online" - c.PayloadNotAvailable = "offline" + c.PayloadAvailable = device.PayloadAvailable + c.PayloadNotAvailable = device.PayloadNotAailable c.HassDevice = device c.CommandTopic = "command" c.StateOn = "on" diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 9c73328..be86364 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -30,8 +30,8 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) HassAutoDiscoverPa c.TargetMoistureTopic = "target_moisture" c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.StateValueTemplate = "{{ value_json.target }}" - c.PayloadAvailable = "online" - c.PayloadNotAvailable = "offline" + c.PayloadAvailable = device.PayloadAvailable + c.PayloadNotAvailable = device.PayloadNotAailable c.HassDevice = device c.CommandTopic = "command" c.StateOn = "on" From 1ab5c05abcda2485f16e8a337c2f2cce92418017 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 00:06:59 +0000 Subject: [PATCH 021/135] fix: use the device fqn --- pkg/model/moisture_sensor_hass_configuration.go | 2 +- pkg/model/water_outlet_hass_configuration.go | 2 +- pkg/model/zone_hass_configuration.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index ddc193d..667f1b7 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -15,7 +15,7 @@ type moistureSensorHassConfiguration struct { } func (c moistureSensorHassConfiguration) WithGlobalTopicPrefix(prefix string) HassAutoDiscoverPayload { - c.AvailabilityTopic = prefix + "/" + c.AvailabilityTopic + c.AvailabilityTopic = prefix + "/" + c.HassDevice.GetFqAvailabilityTopic() return c } diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index 1cda449..a607145 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -17,7 +17,7 @@ type waterOutletHassConfiguration struct { } func (c waterOutletHassConfiguration) WithGlobalTopicPrefix(prefix string) HassAutoDiscoverPayload { - c.AvailabilityTopic = prefix + "/" + c.AvailabilityTopic + c.AvailabilityTopic = prefix + "/" + c.HassDevice.GetFqAvailabilityTopic() return c } diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index be86364..77f64e2 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -17,7 +17,7 @@ type zoneHassConfiguration struct { } func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string) HassAutoDiscoverPayload { - c.AvailabilityTopic = prefix + "/" + c.AvailabilityTopic + c.AvailabilityTopic = prefix + "/" + c.HassDevice.GetFqAvailabilityTopic() return c } From 7c9166d4b9be94cea6543f61f9570b594586413e Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 00:57:42 +0000 Subject: [PATCH 022/135] feat: hass availability --- pkg/app/app.go | 29 +++++++++++++ pkg/app/hass.go | 42 +++++++++++++++++-- pkg/hass/hass_client.go | 2 +- pkg/model/hass_device.go | 36 ++++++++-------- .../moisture_sensor_hass_configuration.go | 2 +- pkg/model/water_outlet_hass_configuration.go | 2 +- pkg/model/zone_hass_configuration.go | 2 +- 7 files changed, 90 insertions(+), 25 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 5d2e7b7..fd68e28 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1,6 +1,10 @@ package app import ( + "os" + "os/signal" + "syscall" + "github.com/mewejo/go-watering/pkg/hass" "github.com/mewejo/go-watering/pkg/model" ) @@ -10,6 +14,22 @@ type App struct { waterOutlets []*model.WaterOutlet moistureSensors []*model.MoistureSensor hass *hass.HassClient + hassDevice *model.HassDevice +} + +func (app *App) setupCloseHandler() chan bool { + exitChan := make(chan bool) + sigChan := make(chan os.Signal) + + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + go func() { + <-sigChan + app.markHassNotAvailable() + exitChan <- true + }() + + return exitChan } func (app *App) Run() { @@ -27,6 +47,15 @@ func (app *App) Run() { panic(err) } + app.startAvailabilityTimer() + + osExit := app.setupCloseHandler() + + { + <-osExit + os.Exit(0) + } + /* Make zone configurations Connect to MQTT diff --git a/pkg/app/hass.go b/pkg/app/hass.go index c77224d..1ddfc9c 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -2,22 +2,58 @@ package app import ( "os" + "time" "github.com/mewejo/go-watering/pkg/hass" "github.com/mewejo/go-watering/pkg/model" ) +func (app *App) markHassNotAvailable() { + app.hass.Publish( + hass.MakeMqttMessage( + app.hassDevice.GetFqAvailabilityTopic(), + app.hassDevice.PayloadNotAvailable, + ), + ).Wait() +} + +func (app *App) startAvailabilityTimer() chan bool { + ticker := time.NewTicker(5 * time.Second) + + quit := make(chan bool) + + go func() { + for { + select { + case <-ticker.C: + app.hass.Publish( + hass.MakeMqttMessage( + app.hassDevice.GetFqAvailabilityTopic(), + app.hassDevice.PayloadAvailable, + ), + ) + + case <-quit: + ticker.Stop() + return + } + } + }() + + return quit +} + func (app *App) setupHass() error { - hassDevice := model.NewHassDevice() + app.hassDevice = model.NewHassDevice() app.hass = hass.NewClient( os.Getenv("HOME_ASSISTANT_DISCOVERY_PREFIX"), - hassDevice, + app.hassDevice, ) return app.hass.Connect( - hass.MakeMqttMessage(hassDevice.Namespace+"/availability", "unavailable"), // TODO veg soaker + unavail should be constants/generated/configurable + hass.MakeMqttMessage(app.hassDevice.GetFqAvailabilityTopic(), app.hassDevice.PayloadNotAvailable), ) } diff --git a/pkg/hass/hass_client.go b/pkg/hass/hass_client.go index 5ee4e2c..0236837 100644 --- a/pkg/hass/hass_client.go +++ b/pkg/hass/hass_client.go @@ -73,7 +73,7 @@ func (c *HassClient) Connect(lwt MqttMessage) error { opts := mqtt.NewClientOptions() opts.AddBroker(connectionString) - opts.SetWill(c.namespace+lwt.topic, lwt.payload, lwt.qos, lwt.retain) + opts.SetWill(c.namespace+"/"+lwt.topic, lwt.payload, lwt.qos, lwt.retain) opts.SetClientID(os.Getenv("MQTT_CLIENT_ID")) opts.SetUsername(os.Getenv("MQTT_USERNAME")) opts.SetPassword(os.Getenv("MQTT_PASSWORD")) diff --git a/pkg/model/hass_device.go b/pkg/model/hass_device.go index 37aa934..cd6209a 100644 --- a/pkg/model/hass_device.go +++ b/pkg/model/hass_device.go @@ -1,28 +1,28 @@ package model type HassDevice struct { - Identifier string `json:"identifiers"` - Name string `json:"name"` - Model string `json:"model"` - Manufacturer string `json:"manufacturer"` - Namespace string `json:"-"` - EntityPrefix string `json:"-"` - AvailabilityTopic string `json:"-"` - PayloadAvailable string `json:"-"` - PayloadNotAailable string `json:"-"` + Identifier string `json:"identifiers"` + Name string `json:"name"` + Model string `json:"model"` + Manufacturer string `json:"manufacturer"` + Namespace string `json:"-"` + EntityPrefix string `json:"-"` + AvailabilityTopic string `json:"-"` + PayloadAvailable string `json:"-"` + PayloadNotAvailable string `json:"-"` } func NewHassDevice() *HassDevice { return &HassDevice{ - Identifier: "vegetable-soaker", - Name: "Vegatable Soaker", - Model: "VegSoak 3000", - Manufacturer: "Josh Bonfield", - Namespace: "vegetable-soaker", - EntityPrefix: "vegetable-soaker-", - AvailabilityTopic: "availability", - PayloadAvailable: "online", - PayloadNotAailable: "offline", + Identifier: "vegetable-soaker", + Name: "Vegatable Soaker", + Model: "VegSoak 3000", + Manufacturer: "Josh Bonfield", + Namespace: "vegetable-soaker", + EntityPrefix: "vegetable-soaker-", + AvailabilityTopic: "availability", + PayloadAvailable: "online", + PayloadNotAvailable: "offline", } } diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index 667f1b7..de5e4e3 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -30,7 +30,7 @@ func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevi c.StateValueTemplate = "{{ value_json.moisture.percentage }}" c.UnitOfMeasurement = "%" c.PayloadAvailable = device.PayloadAvailable - c.PayloadNotAvailable = device.PayloadNotAailable + c.PayloadNotAvailable = device.PayloadNotAvailable c.HassDevice = device return c diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index a607145..5048350 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -31,7 +31,7 @@ func makeWaterOutletHassConfiguration(outlet WaterOutlet, device *HassDevice) Ha c.DeviceClass = "switch" c.StateValueTemplate = "{{ value_json.target }}" c.PayloadAvailable = device.PayloadAvailable - c.PayloadNotAvailable = device.PayloadNotAailable + c.PayloadNotAvailable = device.PayloadNotAvailable c.HassDevice = device c.CommandTopic = "command" c.StateOn = "on" diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 77f64e2..5fd75ec 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -31,7 +31,7 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) HassAutoDiscoverPa c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.StateValueTemplate = "{{ value_json.target }}" c.PayloadAvailable = device.PayloadAvailable - c.PayloadNotAvailable = device.PayloadNotAailable + c.PayloadNotAvailable = device.PayloadNotAvailable c.HassDevice = device c.CommandTopic = "command" c.StateOn = "on" From 3457f12bb395a3d6348f0f1b0e6ea819fd91c51d Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 01:08:38 +0000 Subject: [PATCH 023/135] tweak: naming etc --- pkg/app/app.go | 2 +- pkg/app/hass.go | 2 +- pkg/persistence/moisture_reading_store.go | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index fd68e28..481d5d0 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -47,7 +47,7 @@ func (app *App) Run() { panic(err) } - app.startAvailabilityTimer() + app.startHassAvailabilityTimer() osExit := app.setupCloseHandler() diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 1ddfc9c..856246b 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -17,7 +17,7 @@ func (app *App) markHassNotAvailable() { ).Wait() } -func (app *App) startAvailabilityTimer() chan bool { +func (app *App) startHassAvailabilityTimer() chan bool { ticker := time.NewTicker(5 * time.Second) quit := make(chan bool) diff --git a/pkg/persistence/moisture_reading_store.go b/pkg/persistence/moisture_reading_store.go index 2318cdd..c0d198e 100644 --- a/pkg/persistence/moisture_reading_store.go +++ b/pkg/persistence/moisture_reading_store.go @@ -11,7 +11,7 @@ type moistureReadingStore struct { func (s *moistureReadingStore) recordReading(r model.MoistureReading) { s.readings = append(s.readings, r) - limitReadings(&s.readings, 100) + limitReadings(&s.readings, 1000) } func limitReadings(s *[]model.MoistureReading, length int) { @@ -22,14 +22,14 @@ func limitReadings(s *[]model.MoistureReading, length int) { *s = (*s)[len(*s)-length:] } -var stores []moistureReadingStore +var moistureReadingStores []moistureReadingStore -func RecordReading(sensor model.MoistureSensor, reading model.MoistureReading) { +func RecordMoistureReading(sensor model.MoistureSensor, reading model.MoistureReading) { getOrMakeStore(sensor).recordReading(reading) } func getOrMakeStore(sensor model.MoistureSensor) *moistureReadingStore { - for _, store := range stores { + for _, store := range moistureReadingStores { if store.sensor == sensor { return &store } @@ -39,7 +39,7 @@ func getOrMakeStore(sensor model.MoistureSensor) *moistureReadingStore { sensor: sensor, } - stores = append(stores, store) + moistureReadingStores = append(moistureReadingStores, store) return &store } From e4809ad84ba1dd292e10d89b18e4d47a20426f0a Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 01:47:19 +0000 Subject: [PATCH 024/135] feat: subscribe to topic --- pkg/app/app.go | 2 ++ pkg/hass/hass_client.go | 34 ++++++++++++------- pkg/hass/message_handler.go | 5 +++ pkg/model/hass_auto_discover_payload.go | 2 +- .../moisture_sensor_hass_configuration.go | 2 +- pkg/model/water_outlet_hass_configuration.go | 3 +- pkg/model/zone_hass_configuration.go | 2 +- 7 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 pkg/hass/message_handler.go diff --git a/pkg/app/app.go b/pkg/app/app.go index 481d5d0..0f71077 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -51,6 +51,8 @@ func (app *App) Run() { osExit := app.setupCloseHandler() + //app.hass.Subscribe("") + { <-osExit os.Exit(0) diff --git a/pkg/hass/hass_client.go b/pkg/hass/hass_client.go index 0236837..c120839 100644 --- a/pkg/hass/hass_client.go +++ b/pkg/hass/hass_client.go @@ -12,14 +12,14 @@ import ( type HassClient struct { client mqtt.Client - namespace string // prefix all MQTT topics with this NS - device *model.HassDevice + Namespace string // prefix all MQTT topics with this NS + Device *model.HassDevice } func NewClient(namespace string, device *model.HassDevice) *HassClient { return &HassClient{ - namespace: namespace, - device: device, + Namespace: namespace, + Device: device, } } @@ -31,7 +31,7 @@ func (c *HassClient) defaultMessageHandler(msg mqtt.Message) { func (c *HassClient) PublishAutoDiscovery(entity model.HassAutoDiscoverable) (mqtt.Token, error) { - payload := entity.AutoDiscoveryPayload(c.device).WithGlobalTopicPrefix(c.namespace) + payload := entity.AutoDiscoveryPayload(c.Device).WithGlobalTopicPrefix(c.Namespace, c.Device, entity) json, err := json.Marshal( payload, @@ -42,20 +42,33 @@ func (c *HassClient) PublishAutoDiscovery(entity model.HassAutoDiscoverable) (mq } return c.Publish(MakeMqttMessage( - entity.MqttTopic(c.device)+"/config", + entity.MqttTopic(c.Device)+"/config", string(json), )), nil } func (c *HassClient) Publish(message MqttMessage) mqtt.Token { return c.client.Publish( - c.namespace+"/"+message.topic, + c.Namespace+"/"+message.topic, message.qos, message.retain, message.payload, ) } +func (c *HassClient) Subscribe(topic string, handler MessageHandler) { + + var passHandler = func(client mqtt.Client, message mqtt.Message) { + handler(message) + } + + c.client.Subscribe( + c.Namespace+"/"+topic, + 2, + passHandler, + ) +} + func (c *HassClient) Disconnect() { c.client.Disconnect(500) } @@ -67,18 +80,13 @@ func (c *HassClient) Connect(lwt MqttMessage) error { os.Getenv("MQTT_PORT"), ) - var defaultHandler mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) { - c.defaultMessageHandler(msg) - } - opts := mqtt.NewClientOptions() opts.AddBroker(connectionString) - opts.SetWill(c.namespace+"/"+lwt.topic, lwt.payload, lwt.qos, lwt.retain) + opts.SetWill(c.Namespace+"/"+lwt.topic, lwt.payload, lwt.qos, lwt.retain) opts.SetClientID(os.Getenv("MQTT_CLIENT_ID")) opts.SetUsername(os.Getenv("MQTT_USERNAME")) opts.SetPassword(os.Getenv("MQTT_PASSWORD")) opts.SetKeepAlive(2 * time.Second) - opts.SetDefaultPublishHandler(defaultHandler) opts.SetPingTimeout(1 * time.Second) mqttClient := mqtt.NewClient(opts) diff --git a/pkg/hass/message_handler.go b/pkg/hass/message_handler.go new file mode 100644 index 0000000..1cd3119 --- /dev/null +++ b/pkg/hass/message_handler.go @@ -0,0 +1,5 @@ +package hass + +import mqtt "github.com/eclipse/paho.mqtt.golang" + +type MessageHandler func(mqtt.Message) diff --git a/pkg/model/hass_auto_discover_payload.go b/pkg/model/hass_auto_discover_payload.go index 78bc2ba..c0b2723 100644 --- a/pkg/model/hass_auto_discover_payload.go +++ b/pkg/model/hass_auto_discover_payload.go @@ -1,5 +1,5 @@ package model type HassAutoDiscoverPayload interface { - WithGlobalTopicPrefix(prefix string) HassAutoDiscoverPayload + WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload } diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index de5e4e3..78c374a 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -14,7 +14,7 @@ type moistureSensorHassConfiguration struct { PayloadNotAvailable string `json:"payload_not_available"` } -func (c moistureSensorHassConfiguration) WithGlobalTopicPrefix(prefix string) HassAutoDiscoverPayload { +func (c moistureSensorHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { c.AvailabilityTopic = prefix + "/" + c.HassDevice.GetFqAvailabilityTopic() return c } diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index 5048350..a8ce745 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -16,8 +16,9 @@ type waterOutletHassConfiguration struct { StateOff string `json:"state_off"` } -func (c waterOutletHassConfiguration) WithGlobalTopicPrefix(prefix string) HassAutoDiscoverPayload { +func (c waterOutletHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { c.AvailabilityTopic = prefix + "/" + c.HassDevice.GetFqAvailabilityTopic() + c.CommandTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.CommandTopic return c } diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 5fd75ec..455e63c 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -16,7 +16,7 @@ type zoneHassConfiguration struct { TargetMoistureTopic string `json:"target_humidity_command_topic"` } -func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string) HassAutoDiscoverPayload { +func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { c.AvailabilityTopic = prefix + "/" + c.HassDevice.GetFqAvailabilityTopic() return c } From a8bdd3861a62e024439f20e12a3f2ee161e42f47 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 01:51:43 +0000 Subject: [PATCH 025/135] wip: temp sub to a topic --- pkg/app/app.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 0f71077..97859ac 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1,10 +1,12 @@ package app import ( + "fmt" "os" "os/signal" "syscall" + mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/mewejo/go-watering/pkg/hass" "github.com/mewejo/go-watering/pkg/model" ) @@ -51,7 +53,9 @@ func (app *App) Run() { osExit := app.setupCloseHandler() - //app.hass.Subscribe("") + app.hass.Subscribe("switch/vegetable-soaker/outlet-4/command", func(m mqtt.Message) { + fmt.Println(string(m.Payload())) + }) { <-osExit From 5f8ce3ef7fdf8604734bbe636e9415e60066918c Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 11:21:58 +0000 Subject: [PATCH 026/135] fix: compare against IDs --- pkg/persistence/moisture_reading_store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/persistence/moisture_reading_store.go b/pkg/persistence/moisture_reading_store.go index c0d198e..f4d15f3 100644 --- a/pkg/persistence/moisture_reading_store.go +++ b/pkg/persistence/moisture_reading_store.go @@ -30,7 +30,7 @@ func RecordMoistureReading(sensor model.MoistureSensor, reading model.MoistureRe func getOrMakeStore(sensor model.MoistureSensor) *moistureReadingStore { for _, store := range moistureReadingStores { - if store.sensor == sensor { + if store.sensor.Id == sensor.Id { return &store } } From 3363642439e61389282b606beedd6bfb09cde866 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 11:37:43 +0000 Subject: [PATCH 027/135] wip: does this get data from arduino? --- pkg/app/app.go | 19 ++++++++++--------- pkg/app/arduino.go | 40 ++++++++++++++++++++++++++++++++++++++++ pkg/arduino/arduino.go | 4 ++-- 3 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 pkg/app/arduino.go diff --git a/pkg/app/app.go b/pkg/app/app.go index 97859ac..00c8d71 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -7,6 +7,7 @@ import ( "syscall" mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/mewejo/go-watering/pkg/arduino" "github.com/mewejo/go-watering/pkg/hass" "github.com/mewejo/go-watering/pkg/model" ) @@ -17,21 +18,15 @@ type App struct { moistureSensors []*model.MoistureSensor hass *hass.HassClient hassDevice *model.HassDevice + arduino *arduino.Arduino } -func (app *App) setupCloseHandler() chan bool { - exitChan := make(chan bool) +func (app *App) setupCloseHandler() <-chan os.Signal { sigChan := make(chan os.Signal) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - go func() { - <-sigChan - app.markHassNotAvailable() - exitChan <- true - }() - - return exitChan + return sigChan } func (app *App) Run() { @@ -53,12 +48,18 @@ func (app *App) Run() { osExit := app.setupCloseHandler() + closeArduinoChan, arduinoInputChan := app.initialiseArduino() + + go app.handleArduinoDataInput(arduinoInputChan) + app.hass.Subscribe("switch/vegetable-soaker/outlet-4/command", func(m mqtt.Message) { fmt.Println(string(m.Payload())) }) { <-osExit + app.markHassNotAvailable() + close(closeArduinoChan) os.Exit(0) } diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go new file mode 100644 index 0000000..3771a76 --- /dev/null +++ b/pkg/app/arduino.go @@ -0,0 +1,40 @@ +package app + +import ( + "fmt" + + "github.com/mewejo/go-watering/pkg/arduino" +) + +func (app *App) initialiseArduino() (chan bool, <-chan string) { + app.arduino = arduino.NewArduino() + + if app.arduino.FindAndOpenPort() != nil { + panic("could not find or open Arduino port") + } + + closeChan := make(chan bool) + dataChan := make(chan string, 500) + + go func() { + { + <-closeChan + app.arduino.ClosePort() + return + } + }() + + go func() { + for { + dataChan <- app.arduino.ReadLine() + } + }() + + return closeChan, dataChan +} + +func (app *App) handleArduinoDataInput(dataChan <-chan string) { + for line := range dataChan { + fmt.Println(line) + } +} diff --git a/pkg/arduino/arduino.go b/pkg/arduino/arduino.go index cdcea87..0c74bc2 100644 --- a/pkg/arduino/arduino.go +++ b/pkg/arduino/arduino.go @@ -40,11 +40,11 @@ func findArduinoPort() (string, error) { ports, err := serial.GetPortsList() if err != nil { - log.Fatal(err) + return "", err } if len(ports) == 0 { - log.Fatal("no serial ports found!") + return "", errors.New("no serial ports found!") } for _, port := range ports { From 9d364a0f2ba72e8d1f50c4172185870a0ec85f90 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 11:41:46 +0000 Subject: [PATCH 028/135] fix: trim returns from the line --- pkg/arduino/arduino.go | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/pkg/arduino/arduino.go b/pkg/arduino/arduino.go index 0c74bc2..c8df1de 100644 --- a/pkg/arduino/arduino.go +++ b/pkg/arduino/arduino.go @@ -20,22 +20,6 @@ func (a Arduino) ReadData(buffer []byte) (int, error) { return a.port.Read(buffer) } -func (a Arduino) ReadLines(until string) []string { - lines := []string{} - - for { - line := a.ReadLine() - - lines = append(lines, line) - - if strings.Contains(line, until) { - break - } - } - - return lines -} - func findArduinoPort() (string, error) { ports, err := serial.GetPortsList() @@ -80,6 +64,9 @@ func (a Arduino) ReadLine() string { } } + data = strings.TrimSuffix(data, "\n") + data = strings.TrimSuffix(data, "\r") + return data } From 0f533a06887abfb6bb5abf5041c020fe66396b62 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 12:02:19 +0000 Subject: [PATCH 029/135] wip: readinf rom arduino --- pkg/app/app.go | 1 + pkg/app/arduino.go | 13 ++++++++++++- pkg/arduino/arduino.go | 16 ++++++++++------ pkg/model/water_outlet.go | 38 +++++++++++++++++++++++++++++++++++++- 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 00c8d71..2224e06 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -30,6 +30,7 @@ func (app *App) setupCloseHandler() <-chan os.Signal { } func (app *App) Run() { + app.configureHardware() err := app.setupHass() diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 3771a76..7922745 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -26,7 +26,18 @@ func (app *App) initialiseArduino() (chan bool, <-chan string) { go func() { for { - dataChan <- app.arduino.ReadLine() + select { + case <-closeChan: + return + default: + line, err := app.arduino.ReadLine() + + if err != nil { + continue + } + + dataChan <- line + } } }() diff --git a/pkg/arduino/arduino.go b/pkg/arduino/arduino.go index c8df1de..c9004c6 100644 --- a/pkg/arduino/arduino.go +++ b/pkg/arduino/arduino.go @@ -2,14 +2,16 @@ package arduino import ( "errors" - "log" "strings" + "time" "go.bug.st/serial" ) type Arduino struct { - port serial.Port + port serial.Port + heartbeatPayload string + LastHeartbeat time.Time } func (a Arduino) SendCommand(command Command) (int, error) { @@ -42,7 +44,7 @@ func findArduinoPort() (string, error) { return "", errors.New("no devices found which look like an Arduino") } -func (a Arduino) ReadLine() string { +func (a Arduino) ReadLine() (string, error) { buff := make([]byte, 1) data := "" @@ -50,7 +52,7 @@ func (a Arduino) ReadLine() string { n, err := a.ReadData(buff) if err != nil { - log.Fatal(err) + return "", err } if n == 0 { @@ -67,7 +69,7 @@ func (a Arduino) ReadLine() string { data = strings.TrimSuffix(data, "\n") data = strings.TrimSuffix(data, "\r") - return data + return data, nil } func (a *Arduino) ClosePort() error { @@ -98,5 +100,7 @@ func (a *Arduino) FindAndOpenPort() error { func NewArduino() *Arduino { - return &Arduino{} + return &Arduino{ + heartbeatPayload: "HEARTBEAT", + } } diff --git a/pkg/model/water_outlet.go b/pkg/model/water_outlet.go index 60f1236..60791a8 100644 --- a/pkg/model/water_outlet.go +++ b/pkg/model/water_outlet.go @@ -1,6 +1,10 @@ package model -import "strconv" +import ( + "errors" + "strconv" + "strings" +) type WaterOutlet struct { Id uint @@ -27,3 +31,35 @@ func (wo WaterOutlet) MqttTopic(device *HassDevice) string { func (wo WaterOutlet) AutoDiscoveryPayload(device *HassDevice) HassAutoDiscoverPayload { return makeWaterOutletHassConfiguration(wo, device) } + +func DecodeWaterOutletStateFromString(line string) (uint, bool, bool, error) { + // WO:1:0:0 # WO:ID:REAL_STATE:SET_STATE (1 = on, 0 = off) + parts := strings.Split(line, ":") + + if parts[0] != "WO" { + return 0, false, false, errors.New("line was not a water outlet state") + } + + outletId, err := strconv.Atoi(parts[1]) + + if err != nil { + return 0, false, false, err + } + + realState, err := strconv.Atoi(parts[2]) + + if err != nil { + return 0, false, false, err + } + + setState, err := strconv.Atoi(parts[3]) + + if err != nil { + return 0, false, false, err + } + + return uint(outletId), + setState == 1, + realState == 1, + nil +} From dd74991e340cb7a916e6ea0620d93e07e04db10f Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 12:12:45 +0000 Subject: [PATCH 030/135] wip: handle incoming data --- pkg/app/arduino.go | 37 +++++++++++++++++++++++++++++++--- pkg/arduino/arduino.go | 11 ++++------ pkg/model/arduino_heartbeat.go | 20 ++++++++++++++++++ pkg/model/water_outlet.go | 2 +- 4 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 pkg/model/arduino_heartbeat.go diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 7922745..258baed 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -1,9 +1,8 @@ package app import ( - "fmt" - "github.com/mewejo/go-watering/pkg/arduino" + "github.com/mewejo/go-watering/pkg/model" ) func (app *App) initialiseArduino() (chan bool, <-chan string) { @@ -45,7 +44,39 @@ func (app *App) initialiseArduino() (chan bool, <-chan string) { } func (app *App) handleArduinoDataInput(dataChan <-chan string) { + + handleHeartbeat := func(hb model.ArduinoHeartbeat) { + app.arduino.LastHeartbeat = &hb + } + + handleMoistureReading := func(reading model.MoistureReading, sensorId uint) { + + } + + handleWaterOutletState := func(outletId uint, realState bool, setState bool) { + + } + for line := range dataChan { - fmt.Println(line) + heartbeat, err := model.MakeArduinoHeartbeatFromString(line) + + if err == nil { + go handleHeartbeat(heartbeat) + continue + } + + moistureReading, sensorId, err := model.MakeMoistureReadingFromString(line) + + if err == nil { + go handleMoistureReading(moistureReading, sensorId) + continue + } + + outletId, realState, setState, err := model.DecodeWaterOutletStateFromString(line) + + if err == nil { + go handleWaterOutletState(outletId, realState, setState) + continue + } } } diff --git a/pkg/arduino/arduino.go b/pkg/arduino/arduino.go index c9004c6..4ba66a7 100644 --- a/pkg/arduino/arduino.go +++ b/pkg/arduino/arduino.go @@ -3,15 +3,14 @@ package arduino import ( "errors" "strings" - "time" + "github.com/mewejo/go-watering/pkg/model" "go.bug.st/serial" ) type Arduino struct { - port serial.Port - heartbeatPayload string - LastHeartbeat time.Time + port serial.Port + LastHeartbeat *model.ArduinoHeartbeat } func (a Arduino) SendCommand(command Command) (int, error) { @@ -100,7 +99,5 @@ func (a *Arduino) FindAndOpenPort() error { func NewArduino() *Arduino { - return &Arduino{ - heartbeatPayload: "HEARTBEAT", - } + return &Arduino{} } diff --git a/pkg/model/arduino_heartbeat.go b/pkg/model/arduino_heartbeat.go new file mode 100644 index 0000000..460ea06 --- /dev/null +++ b/pkg/model/arduino_heartbeat.go @@ -0,0 +1,20 @@ +package model + +import ( + "errors" + "time" +) + +type ArduinoHeartbeat struct { + Time time.Time +} + +func MakeArduinoHeartbeatFromString(line string) (ArduinoHeartbeat, error) { + if line != "HEARTBEAT" { + return ArduinoHeartbeat{}, errors.New("line was not an Arduino heartbeat") + } + + return ArduinoHeartbeat{ + Time: time.Now(), + }, nil +} diff --git a/pkg/model/water_outlet.go b/pkg/model/water_outlet.go index 60791a8..be81145 100644 --- a/pkg/model/water_outlet.go +++ b/pkg/model/water_outlet.go @@ -59,7 +59,7 @@ func DecodeWaterOutletStateFromString(line string) (uint, bool, bool, error) { } return uint(outletId), - setState == 1, realState == 1, + setState == 1, nil } From e3050082938be8cf0ce641fa2c7e704980f2f330 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 12:15:41 +0000 Subject: [PATCH 031/135] wip: handle moisture readings + heartbeat --- pkg/app/arduino.go | 5 +++-- pkg/persistence/moisture_reading_store.go | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 258baed..b84675c 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -3,6 +3,7 @@ package app import ( "github.com/mewejo/go-watering/pkg/arduino" "github.com/mewejo/go-watering/pkg/model" + "github.com/mewejo/go-watering/pkg/persistence" ) func (app *App) initialiseArduino() (chan bool, <-chan string) { @@ -50,11 +51,11 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { } handleMoistureReading := func(reading model.MoistureReading, sensorId uint) { - + persistence.RecordMoistureReading(sensorId, reading) } handleWaterOutletState := func(outletId uint, realState bool, setState bool) { - + // TODO } for line := range dataChan { diff --git a/pkg/persistence/moisture_reading_store.go b/pkg/persistence/moisture_reading_store.go index f4d15f3..ba9c26b 100644 --- a/pkg/persistence/moisture_reading_store.go +++ b/pkg/persistence/moisture_reading_store.go @@ -5,7 +5,7 @@ import ( ) type moistureReadingStore struct { - sensor model.MoistureSensor + sensorId uint readings []model.MoistureReading } @@ -24,19 +24,19 @@ func limitReadings(s *[]model.MoistureReading, length int) { var moistureReadingStores []moistureReadingStore -func RecordMoistureReading(sensor model.MoistureSensor, reading model.MoistureReading) { - getOrMakeStore(sensor).recordReading(reading) +func RecordMoistureReading(sensorId uint, reading model.MoistureReading) { + getOrMakeStore(sensorId).recordReading(reading) } -func getOrMakeStore(sensor model.MoistureSensor) *moistureReadingStore { +func getOrMakeStore(sensorId uint) *moistureReadingStore { for _, store := range moistureReadingStores { - if store.sensor.Id == sensor.Id { + if store.sensorId == sensorId { return &store } } store := moistureReadingStore{ - sensor: sensor, + sensorId: sensorId, } moistureReadingStores = append(moistureReadingStores, store) From 865098c16da52dc70d31e7e1377ad693250db421 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 12:26:31 +0000 Subject: [PATCH 032/135] feat: update outlet states --- pkg/app/arduino.go | 14 ++++++++++---- pkg/app/hardware.go | 14 ++++++++------ pkg/app/hass.go | 5 +++++ pkg/model/water_outlet.go | 24 +++++++++++++----------- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index b84675c..747125c 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -54,8 +54,14 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { persistence.RecordMoistureReading(sensorId, reading) } - handleWaterOutletState := func(outletId uint, realState bool, setState bool) { - // TODO + handleWaterOutletState := func(outletId uint, actualState bool, targetState bool) { + for _, outlet := range app.waterOutlets { + if outlet.Id != outletId { + continue + } + + outlet.ActualState = actualState + } } for line := range dataChan { @@ -73,10 +79,10 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { continue } - outletId, realState, setState, err := model.DecodeWaterOutletStateFromString(line) + outletId, actualState, targetState, err := model.DecodeWaterOutletStateFromString(line) if err == nil { - go handleWaterOutletState(outletId, realState, setState) + go handleWaterOutletState(outletId, actualState, targetState) continue } } diff --git a/pkg/app/hardware.go b/pkg/app/hardware.go index 4a13f09..d8c77ee 100644 --- a/pkg/app/hardware.go +++ b/pkg/app/hardware.go @@ -4,12 +4,14 @@ import "github.com/mewejo/go-watering/pkg/model" func (app *App) configureHardware() { - waterOutlet1 := model.NewWaterOutlet(1, "Soaker hose #1") - waterOutlet2 := model.NewWaterOutlet(2, "Soaker hose #2") - waterOutlet3 := model.NewWaterOutlet(3, "Soaker hose #3") - waterOutlet4 := model.NewWaterOutlet(4, "Soaker hose #4") - - // The only outlet which isn't tied to a zone. + waterOutlet1 := model.NewWaterOutlet(1, "Soaker hose #1", false) + waterOutlet2 := model.NewWaterOutlet(2, "Soaker hose #2", false) + waterOutlet3 := model.NewWaterOutlet(3, "Soaker hose #3", false) + waterOutlet4 := model.NewWaterOutlet(4, "Soaker hose #4", true) + + app.waterOutlets = append(app.waterOutlets, waterOutlet1) + app.waterOutlets = append(app.waterOutlets, waterOutlet2) + app.waterOutlets = append(app.waterOutlets, waterOutlet3) app.waterOutlets = append(app.waterOutlets, waterOutlet4) moistureSensor1 := model.MakeMoistureSensor(1, "Sensor #1") diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 856246b..85e6eaf 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -69,6 +69,11 @@ func (app *App) publishHassAutoDiscovery() error { } for _, entity := range app.waterOutlets { + + if !entity.IndependentlyControlled { + continue + } + token, err := app.hass.PublishAutoDiscovery(entity) if err != nil { diff --git a/pkg/model/water_outlet.go b/pkg/model/water_outlet.go index be81145..b640972 100644 --- a/pkg/model/water_outlet.go +++ b/pkg/model/water_outlet.go @@ -7,16 +7,18 @@ import ( ) type WaterOutlet struct { - Id uint - Name string - TargetState bool - ActualState bool + Id uint + Name string + TargetState bool + ActualState bool + IndependentlyControlled bool } -func NewWaterOutlet(id uint, name string) *WaterOutlet { +func NewWaterOutlet(id uint, name string, independentlyControlled bool) *WaterOutlet { return &WaterOutlet{ - Id: id, - Name: name, + Id: id, + Name: name, + IndependentlyControlled: independentlyControlled, } } @@ -46,20 +48,20 @@ func DecodeWaterOutletStateFromString(line string) (uint, bool, bool, error) { return 0, false, false, err } - realState, err := strconv.Atoi(parts[2]) + actualState, err := strconv.Atoi(parts[2]) if err != nil { return 0, false, false, err } - setState, err := strconv.Atoi(parts[3]) + targetState, err := strconv.Atoi(parts[3]) if err != nil { return 0, false, false, err } return uint(outletId), - realState == 1, - setState == 1, + actualState == 1, + targetState == 1, nil } From 5ed387b88f8c5af5aafb5ec4187e9b6286fe9446 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 13:02:41 +0000 Subject: [PATCH 033/135] feat: send water outlet state --- pkg/app/arduino.go | 1 + pkg/app/hass.go | 19 +++++++++++++++++++ pkg/model/water_outlet.go | 14 +++++++++----- pkg/model/water_outlet_hass_configuration.go | 4 ++-- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 747125c..cbfb4b7 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -61,6 +61,7 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { } outlet.ActualState = actualState + app.publishWaterOutletState(outlet) } } diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 85e6eaf..931ea8a 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -1,6 +1,7 @@ package app import ( + "encoding/json" "os" "time" @@ -8,6 +9,24 @@ import ( "github.com/mewejo/go-watering/pkg/model" ) +func (app *App) publishWaterOutletState(outlet *model.WaterOutlet) error { + + payload, err := json.Marshal(outlet) + + if err != nil { + return err + } + + app.hass.Publish( + hass.MakeMqttMessage( + outlet.MqttStateTopic(app.hassDevice), + string(payload), + ), + ) + + return nil +} + func (app *App) markHassNotAvailable() { app.hass.Publish( hass.MakeMqttMessage( diff --git a/pkg/model/water_outlet.go b/pkg/model/water_outlet.go index b640972..05a1f78 100644 --- a/pkg/model/water_outlet.go +++ b/pkg/model/water_outlet.go @@ -7,11 +7,11 @@ import ( ) type WaterOutlet struct { - Id uint - Name string - TargetState bool - ActualState bool - IndependentlyControlled bool + Id uint `json:"id"` + Name string `json:"name"` + TargetState bool `json:"target"` + ActualState bool `json:"actual"` + IndependentlyControlled bool `json:"independently_controlled"` } func NewWaterOutlet(id uint, name string, independentlyControlled bool) *WaterOutlet { @@ -30,6 +30,10 @@ func (wo WaterOutlet) MqttTopic(device *HassDevice) string { return "switch/" + device.Namespace + "/outlet-" + wo.IdAsString() } +func (wo WaterOutlet) MqttStateTopic(device *HassDevice) string { + return wo.MqttTopic(device) + "/" + makeWaterOutletHassConfiguration(wo, device).StateTopic +} + func (wo WaterOutlet) AutoDiscoveryPayload(device *HassDevice) HassAutoDiscoverPayload { return makeWaterOutletHassConfiguration(wo, device) } diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index a8ce745..f4733f8 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -22,7 +22,7 @@ func (c waterOutletHassConfiguration) WithGlobalTopicPrefix(prefix string, devic return c } -func makeWaterOutletHassConfiguration(outlet WaterOutlet, device *HassDevice) HassAutoDiscoverPayload { +func makeWaterOutletHassConfiguration(outlet WaterOutlet, device *HassDevice) waterOutletHassConfiguration { c := waterOutletHassConfiguration{} c.Name = outlet.Name c.ObjectId = device.EntityPrefix + "outlet-" + outlet.IdAsString() @@ -30,7 +30,7 @@ func makeWaterOutletHassConfiguration(outlet WaterOutlet, device *HassDevice) Ha c.StateTopic = "state" c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.DeviceClass = "switch" - c.StateValueTemplate = "{{ value_json.target }}" + c.StateValueTemplate = "{{ value_json.actual ? 'on' : 'off' }}" c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable c.HassDevice = device From 4565b17a8edec039b0478f0e8631793b72fe42d0 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 13:11:26 +0000 Subject: [PATCH 034/135] feat: request outlet states --- pkg/app/app.go | 2 ++ pkg/app/arduino.go | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/pkg/app/app.go b/pkg/app/app.go index 2224e06..bd22f73 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -50,6 +50,7 @@ func (app *App) Run() { osExit := app.setupCloseHandler() closeArduinoChan, arduinoInputChan := app.initialiseArduino() + stopRestingOutletStatesChan := app.startRequestingWaterOutletStates() go app.handleArduinoDataInput(arduinoInputChan) @@ -60,6 +61,7 @@ func (app *App) Run() { { <-osExit app.markHassNotAvailable() + close(stopRestingOutletStatesChan) close(closeArduinoChan) os.Exit(0) } diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index cbfb4b7..ef4f12e 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -1,6 +1,8 @@ package app import ( + "time" + "github.com/mewejo/go-watering/pkg/arduino" "github.com/mewejo/go-watering/pkg/model" "github.com/mewejo/go-watering/pkg/persistence" @@ -44,6 +46,26 @@ func (app *App) initialiseArduino() (chan bool, <-chan string) { return closeChan, dataChan } +func (app *App) startRequestingWaterOutletStates() chan bool { + ticker := time.NewTicker(2 * time.Second) + + quit := make(chan bool) + + go func() { + for { + select { + case <-ticker.C: + app.arduino.SendCommand(arduino.REQUEST_OUTLETS) + case <-quit: + ticker.Stop() + return + } + } + }() + + return quit +} + func (app *App) handleArduinoDataInput(dataChan <-chan string) { handleHeartbeat := func(hb model.ArduinoHeartbeat) { From b075ebe01e9a0f61e4f29d5a46d92be9109ada49 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 13:11:58 +0000 Subject: [PATCH 035/135] debug: print ln --- pkg/app/arduino.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index ef4f12e..821d0d3 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -1,6 +1,7 @@ package app import ( + "fmt" "time" "github.com/mewejo/go-watering/pkg/arduino" @@ -88,6 +89,8 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { } for line := range dataChan { + fmt.Println(line) + heartbeat, err := model.MakeArduinoHeartbeatFromString(line) if err == nil { From 543604f32a2f129f672b8be69b344ffe44f238af Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 13:15:29 +0000 Subject: [PATCH 036/135] feat: send the correct state topic --- pkg/app/arduino.go | 2 +- pkg/model/water_outlet_hass_configuration.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 821d0d3..3045a6f 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -89,7 +89,7 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { } for line := range dataChan { - fmt.Println(line) + fmt.Println(line) // TODO remove this heartbeat, err := model.MakeArduinoHeartbeatFromString(line) diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index f4733f8..c960c2d 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -19,6 +19,7 @@ type waterOutletHassConfiguration struct { func (c waterOutletHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { c.AvailabilityTopic = prefix + "/" + c.HassDevice.GetFqAvailabilityTopic() c.CommandTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.CommandTopic + c.StateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.StateTopic return c } From 4cae75c2bcd20a9606dd3a07dba63e1b1f940955 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 13:22:43 +0000 Subject: [PATCH 037/135] fix: use state tpl --- pkg/model/water_outlet_hass_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index c960c2d..8bb45dc 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -31,7 +31,7 @@ func makeWaterOutletHassConfiguration(outlet WaterOutlet, device *HassDevice) wa c.StateTopic = "state" c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.DeviceClass = "switch" - c.StateValueTemplate = "{{ value_json.actual ? 'on' : 'off' }}" + c.StateValueTemplate = "{% if value_json.actual -%}on{%- else -%}off{%- endif %}" c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable c.HassDevice = device From 5f54b6a418f07d6620056d35209611a8762b8167 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 13:39:34 +0000 Subject: [PATCH 038/135] wip: listen to state changes --- pkg/app/app.go | 10 +++------- pkg/app/hass.go | 21 ++++++++++++++++++++ pkg/constants/hass_state.go | 4 ++++ pkg/model/water_outlet.go | 4 ++++ pkg/model/water_outlet_hass_configuration.go | 12 ++++++----- 5 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 pkg/constants/hass_state.go diff --git a/pkg/app/app.go b/pkg/app/app.go index bd22f73..375e713 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1,12 +1,10 @@ package app import ( - "fmt" "os" "os/signal" "syscall" - mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/mewejo/go-watering/pkg/arduino" "github.com/mewejo/go-watering/pkg/hass" "github.com/mewejo/go-watering/pkg/model" @@ -51,18 +49,16 @@ func (app *App) Run() { closeArduinoChan, arduinoInputChan := app.initialiseArduino() stopRestingOutletStatesChan := app.startRequestingWaterOutletStates() + app.listenForWaterOutletCommands() go app.handleArduinoDataInput(arduinoInputChan) - app.hass.Subscribe("switch/vegetable-soaker/outlet-4/command", func(m mqtt.Message) { - fmt.Println(string(m.Payload())) - }) - { <-osExit - app.markHassNotAvailable() close(stopRestingOutletStatesChan) close(closeArduinoChan) + app.markHassNotAvailable() + app.hass.Disconnect() os.Exit(0) } diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 931ea8a..e8c8054 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -5,10 +5,31 @@ import ( "os" "time" + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/mewejo/go-watering/pkg/constants" "github.com/mewejo/go-watering/pkg/hass" "github.com/mewejo/go-watering/pkg/model" ) +func (app *App) listenForWaterOutletCommands() { + for _, outlet := range app.waterOutlets { + if !outlet.IndependentlyControlled { + continue + } + + app.hass.Subscribe( + outlet.MqttCommandTopic(app.hassDevice), + func(message mqtt.Message) { + if string(message.Payload()) == constants.HASS_STATE_ON { + outlet.TargetState = true + } else if string(message.Payload()) == constants.HASS_STATE_OFF { + outlet.TargetState = false + } + }, + ) + } +} + func (app *App) publishWaterOutletState(outlet *model.WaterOutlet) error { payload, err := json.Marshal(outlet) diff --git a/pkg/constants/hass_state.go b/pkg/constants/hass_state.go new file mode 100644 index 0000000..ca1dd83 --- /dev/null +++ b/pkg/constants/hass_state.go @@ -0,0 +1,4 @@ +package constants + +const HASS_STATE_ON = "on" +const HASS_STATE_OFF = "on" diff --git a/pkg/model/water_outlet.go b/pkg/model/water_outlet.go index 05a1f78..7c7e8c6 100644 --- a/pkg/model/water_outlet.go +++ b/pkg/model/water_outlet.go @@ -34,6 +34,10 @@ func (wo WaterOutlet) MqttStateTopic(device *HassDevice) string { return wo.MqttTopic(device) + "/" + makeWaterOutletHassConfiguration(wo, device).StateTopic } +func (wo WaterOutlet) MqttCommandTopic(device *HassDevice) string { + return wo.MqttTopic(device) + "/" + makeWaterOutletHassConfiguration(wo, device).CommandTopic +} + func (wo WaterOutlet) AutoDiscoveryPayload(device *HassDevice) HassAutoDiscoverPayload { return makeWaterOutletHassConfiguration(wo, device) } diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index 8bb45dc..0081c96 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -1,5 +1,7 @@ package model +import "github.com/mewejo/go-watering/pkg/constants" + type waterOutletHassConfiguration struct { Name string `json:"name"` DeviceClass string `json:"device_class"` @@ -12,8 +14,8 @@ type waterOutletHassConfiguration struct { PayloadAvailable string `json:"payload_available"` PayloadNotAvailable string `json:"payload_not_available"` CommandTopic string `json:"command_topic"` - StateOn string `json:"state_on"` - StateOff string `json:"state_off"` + StateOn string `json:"payload_on"` + StateOff string `json:"payload_off"` } func (c waterOutletHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { @@ -26,18 +28,18 @@ func (c waterOutletHassConfiguration) WithGlobalTopicPrefix(prefix string, devic func makeWaterOutletHassConfiguration(outlet WaterOutlet, device *HassDevice) waterOutletHassConfiguration { c := waterOutletHassConfiguration{} c.Name = outlet.Name + c.StateOn = constants.HASS_STATE_ON + c.StateOff = constants.HASS_STATE_OFF c.ObjectId = device.EntityPrefix + "outlet-" + outlet.IdAsString() c.UniqueId = c.ObjectId c.StateTopic = "state" c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.DeviceClass = "switch" - c.StateValueTemplate = "{% if value_json.actual -%}on{%- else -%}off{%- endif %}" + c.StateValueTemplate = "{% if value_json.actual -%}" + c.StateOn + "{%- else -%}" + c.StateOff + "{%- endif %}" c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable c.HassDevice = device c.CommandTopic = "command" - c.StateOn = "on" - c.StateOff = "off" return c } From 6c294b4548ae66e4890c3078ada9dcc31d3a3dd2 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 13:40:35 +0000 Subject: [PATCH 039/135] feat: use constants on zone too --- pkg/model/zone_hass_configuration.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 455e63c..203ff0a 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -1,5 +1,7 @@ package model +import "github.com/mewejo/go-watering/pkg/constants" + type zoneHassConfiguration struct { Name string `json:"name"` ObjectId string `json:"object_id"` @@ -29,13 +31,13 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) HassAutoDiscoverPa c.StateTopic = "humidifier" c.TargetMoistureTopic = "target_moisture" c.AvailabilityTopic = device.GetFqAvailabilityTopic() - c.StateValueTemplate = "{{ value_json.target }}" + c.StateValueTemplate = "{{ value_json.target }}" // TODO c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable c.HassDevice = device c.CommandTopic = "command" - c.StateOn = "on" - c.StateOff = "off" + c.StateOn = constants.HASS_STATE_ON + c.StateOff = constants.HASS_STATE_OFF return c } From d61871d5d0a850de2c6185cd1b8eb3c7df264792 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 13:41:24 +0000 Subject: [PATCH 040/135] fix: plonker --- pkg/constants/hass_state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/constants/hass_state.go b/pkg/constants/hass_state.go index ca1dd83..23dad3e 100644 --- a/pkg/constants/hass_state.go +++ b/pkg/constants/hass_state.go @@ -1,4 +1,4 @@ package constants const HASS_STATE_ON = "on" -const HASS_STATE_OFF = "on" +const HASS_STATE_OFF = "off" From 665df36ed4d627357a5234e809c419088f939474 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 13:49:01 +0000 Subject: [PATCH 041/135] feat: will it turn on..? --- pkg/app/hass.go | 2 ++ pkg/arduino/arduino.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index e8c8054..469d2e4 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -25,6 +25,8 @@ func (app *App) listenForWaterOutletCommands() { } else if string(message.Payload()) == constants.HASS_STATE_OFF { outlet.TargetState = false } + + app.arduino.SetWaterOutletState(outlet) }, ) } diff --git a/pkg/arduino/arduino.go b/pkg/arduino/arduino.go index 4ba66a7..bbb4bda 100644 --- a/pkg/arduino/arduino.go +++ b/pkg/arduino/arduino.go @@ -13,6 +13,36 @@ type Arduino struct { LastHeartbeat *model.ArduinoHeartbeat } +func (a *Arduino) SetWaterOutletState(outlet *model.WaterOutlet) { + var command Command + + if outlet.TargetState { + switch outlet.Id { + case 1: + command = WATER_1_ON + case 2: + command = WATER_2_ON + case 3: + command = WATER_3_ON + case 4: + command = WATER_4_ON + } + } else { + switch outlet.Id { + case 1: + command = WATER_1_OFF + case 2: + command = WATER_2_OFF + case 3: + command = WATER_3_OFF + case 4: + command = WATER_4_OFF + } + } + + a.SendCommand(command) +} + func (a Arduino) SendCommand(command Command) (int, error) { return a.port.Write([]byte(command)) } From 21425a807174b444f66129e42a3d778412dcd004 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 14:21:20 +0000 Subject: [PATCH 042/135] feat: keep sending the water state to arduino --- pkg/app/app.go | 6 ++++-- pkg/app/arduino.go | 22 ++++++++++++++++++++++ pkg/model/hass_device.go | 2 +- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 375e713..5ecf41e 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -48,15 +48,17 @@ func (app *App) Run() { osExit := app.setupCloseHandler() closeArduinoChan, arduinoInputChan := app.initialiseArduino() - stopRestingOutletStatesChan := app.startRequestingWaterOutletStates() + stopRequestingOutletStatesChan := app.startRequestingWaterOutletStates() + stopSendingOutletStatesToArduinoChan := app.startSendingWaterStatesToArduino() app.listenForWaterOutletCommands() go app.handleArduinoDataInput(arduinoInputChan) { <-osExit - close(stopRestingOutletStatesChan) + close(stopRequestingOutletStatesChan) close(closeArduinoChan) + close(stopSendingOutletStatesToArduinoChan) app.markHassNotAvailable() app.hass.Disconnect() os.Exit(0) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 3045a6f..15a0fcc 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -47,6 +47,28 @@ func (app *App) initialiseArduino() (chan bool, <-chan string) { return closeChan, dataChan } +func (app *App) startSendingWaterStatesToArduino() chan bool { + ticker := time.NewTicker(1 * time.Second) + + quit := make(chan bool) + + go func() { + for { + select { + case <-ticker.C: + for _, outlet := range app.waterOutlets { + go app.arduino.SetWaterOutletState(outlet) + } + case <-quit: + ticker.Stop() + return + } + } + }() + + return quit +} + func (app *App) startRequestingWaterOutletStates() chan bool { ticker := time.NewTicker(2 * time.Second) diff --git a/pkg/model/hass_device.go b/pkg/model/hass_device.go index cd6209a..ab02da4 100644 --- a/pkg/model/hass_device.go +++ b/pkg/model/hass_device.go @@ -15,7 +15,7 @@ type HassDevice struct { func NewHassDevice() *HassDevice { return &HassDevice{ Identifier: "vegetable-soaker", - Name: "Vegatable Soaker", + Name: "Vegetable Soaker", Model: "VegSoak 3000", Manufacturer: "Josh Bonfield", Namespace: "vegetable-soaker", From 95f9b98fe18fdf3279a27579b329a4d47b46cb25 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 14:23:45 +0000 Subject: [PATCH 043/135] debug: send dms --- pkg/app/hass.go | 18 ++++++++++++------ pkg/arduino/arduino.go | 4 ++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 469d2e4..5013202 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -64,16 +64,22 @@ func (app *App) startHassAvailabilityTimer() chan bool { quit := make(chan bool) + sendAvailableMessage := func() { + app.hass.Publish( + hass.MakeMqttMessage( + app.hassDevice.GetFqAvailabilityTopic(), + app.hassDevice.PayloadAvailable, + ), + ) + } + + sendAvailableMessage() + go func() { for { select { case <-ticker.C: - app.hass.Publish( - hass.MakeMqttMessage( - app.hassDevice.GetFqAvailabilityTopic(), - app.hassDevice.PayloadAvailable, - ), - ) + sendAvailableMessage() case <-quit: ticker.Stop() diff --git a/pkg/arduino/arduino.go b/pkg/arduino/arduino.go index bbb4bda..9d354c6 100644 --- a/pkg/arduino/arduino.go +++ b/pkg/arduino/arduino.go @@ -2,6 +2,7 @@ package arduino import ( "errors" + "fmt" "strings" "github.com/mewejo/go-watering/pkg/model" @@ -44,6 +45,9 @@ func (a *Arduino) SetWaterOutletState(outlet *model.WaterOutlet) { } func (a Arduino) SendCommand(command Command) (int, error) { + fmt.Print("Sending command: ") + fmt.Println(command) + return a.port.Write([]byte(command)) } From d64c8947859b16cf93254af5d2b30382113c8181 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 14:31:41 +0000 Subject: [PATCH 044/135] feat: update states more frequently --- pkg/app/arduino.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 15a0fcc..692b602 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -70,7 +70,7 @@ func (app *App) startSendingWaterStatesToArduino() chan bool { } func (app *App) startRequestingWaterOutletStates() chan bool { - ticker := time.NewTicker(2 * time.Second) + ticker := time.NewTicker(500 * time.Millisecond) quit := make(chan bool) From 41cd9abd83833bb222261a78ae2372636c2a990d Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 14:34:56 +0000 Subject: [PATCH 045/135] feat: start requesting readings --- pkg/app/app.go | 4 +++- pkg/app/arduino.go | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 5ecf41e..be5cae2 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -49,6 +49,7 @@ func (app *App) Run() { closeArduinoChan, arduinoInputChan := app.initialiseArduino() stopRequestingOutletStatesChan := app.startRequestingWaterOutletStates() + stopRequestingMoistureSensorReadingsChan := app.startRequestingMoistureSensorReadings() stopSendingOutletStatesToArduinoChan := app.startSendingWaterStatesToArduino() app.listenForWaterOutletCommands() @@ -57,8 +58,9 @@ func (app *App) Run() { { <-osExit close(stopRequestingOutletStatesChan) - close(closeArduinoChan) close(stopSendingOutletStatesToArduinoChan) + close(stopRequestingMoistureSensorReadingsChan) + close(closeArduinoChan) app.markHassNotAvailable() app.hass.Disconnect() os.Exit(0) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 692b602..8c8b0a4 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -89,6 +89,26 @@ func (app *App) startRequestingWaterOutletStates() chan bool { return quit } +func (app *App) startRequestingMoistureSensorReadings() chan bool { + ticker := time.NewTicker(1 * time.Second) + + quit := make(chan bool) + + go func() { + for { + select { + case <-ticker.C: + app.arduino.SendCommand(arduino.REQUEST_READINGS) + case <-quit: + ticker.Stop() + return + } + } + }() + + return quit +} + func (app *App) handleArduinoDataInput(dataChan <-chan string) { handleHeartbeat := func(hb model.ArduinoHeartbeat) { From b5af58394dddf81a2effe7946636569715bc9e76 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 14:56:11 +0000 Subject: [PATCH 046/135] feat: send moisture sensor readings --- pkg/app/app.go | 2 + pkg/app/arduino.go | 22 ++++++- pkg/app/hass.go | 58 +++++++++++++++++++ pkg/model/moisture_reading.go | 6 +- pkg/model/moisture_sensor.go | 8 ++- .../moisture_sensor_hass_configuration.go | 2 +- pkg/model/moisture_sensor_hass_state.go | 6 ++ pkg/persistence/moisture_reading_store.go | 14 +++++ 8 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 pkg/model/moisture_sensor_hass_state.go diff --git a/pkg/app/app.go b/pkg/app/app.go index be5cae2..09cbd78 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -51,6 +51,7 @@ func (app *App) Run() { stopRequestingOutletStatesChan := app.startRequestingWaterOutletStates() stopRequestingMoistureSensorReadingsChan := app.startRequestingMoistureSensorReadings() stopSendingOutletStatesToArduinoChan := app.startSendingWaterStatesToArduino() + stopSendingMoistureSensorReadingsChan := app.startSendingMoistureSensorReadings() app.listenForWaterOutletCommands() go app.handleArduinoDataInput(arduinoInputChan) @@ -60,6 +61,7 @@ func (app *App) Run() { close(stopRequestingOutletStatesChan) close(stopSendingOutletStatesToArduinoChan) close(stopRequestingMoistureSensorReadingsChan) + close(stopSendingMoistureSensorReadingsChan) close(closeArduinoChan) app.markHassNotAvailable() app.hass.Disconnect() diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 8c8b0a4..e318c34 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -1,6 +1,7 @@ package app import ( + "errors" "fmt" "time" @@ -109,6 +110,18 @@ func (app *App) startRequestingMoistureSensorReadings() chan bool { return quit } +func (app *App) findMoistureSensorById(id uint) (*model.MoistureSensor, error) { + for _, sensor := range app.moistureSensors { + if sensor.Id != id { + continue + } + + return sensor, nil + } + + return &model.MoistureSensor{}, errors.New("could not find sensor by ID") +} + func (app *App) handleArduinoDataInput(dataChan <-chan string) { handleHeartbeat := func(hb model.ArduinoHeartbeat) { @@ -143,7 +156,14 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { moistureReading, sensorId, err := model.MakeMoistureReadingFromString(line) if err == nil { - go handleMoistureReading(moistureReading, sensorId) + + sensor, err := app.findMoistureSensorById(sensorId) + + if err != nil { + moistureReading.CalculateMoistureLevelForSensor(sensor) + go handleMoistureReading(moistureReading, sensorId) + } + continue } diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 5013202..ace82cb 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -9,6 +9,7 @@ import ( "github.com/mewejo/go-watering/pkg/constants" "github.com/mewejo/go-watering/pkg/hass" "github.com/mewejo/go-watering/pkg/model" + "github.com/mewejo/go-watering/pkg/persistence" ) func (app *App) listenForWaterOutletCommands() { @@ -50,6 +51,63 @@ func (app *App) publishWaterOutletState(outlet *model.WaterOutlet) error { return nil } +func (app *App) startSendingMoistureSensorReadings() chan bool { + + ticker := time.NewTicker(5 * time.Second) + + quit := make(chan bool) + + sendSensorStates := func() { + for _, sensor := range app.moistureSensors { + go app.publishMoistureSensorState(sensor) + } + } + + go func() { + for { + select { + case <-ticker.C: + go sendSensorStates() + + case <-quit: + ticker.Stop() + return + } + } + }() + + return quit +} + +func (app *App) publishMoistureSensorState(sensor *model.MoistureSensor) error { + + reading, err := persistence.GetLatestReadingForMoistureSensorId(sensor.Id) + + if err != nil { + return err + } + + state := model.MoistureSensorHassState{ + Sensor: sensor, + Reading: reading, + } + + payload, err := json.Marshal(state) + + if err != nil { + return err + } + + app.hass.Publish( + hass.MakeMqttMessage( + sensor.MqttStateTopic(app.hassDevice), + string(payload), + ), + ) + + return nil +} + func (app *App) markHassNotAvailable() { app.hass.Publish( hass.MakeMqttMessage( diff --git a/pkg/model/moisture_reading.go b/pkg/model/moisture_reading.go index 7163ad8..c26b2f5 100644 --- a/pkg/model/moisture_reading.go +++ b/pkg/model/moisture_reading.go @@ -8,12 +8,12 @@ import ( ) type MoistureReading struct { - Time time.Time + Time time.Time `json:"recorded_at"` raw uint - MoistureLevel MoistureLevel + MoistureLevel MoistureLevel `json:"percentage"` } -func (r *MoistureReading) CalculateMoistureLevelForSensor(sensor MoistureSensor) { +func (r *MoistureReading) CalculateMoistureLevelForSensor(sensor *MoistureSensor) { r.MoistureLevel = MakeMoistureLevel( sensor.mapRawReadingToPercentage(r.raw), ) diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go index 66a8ea4..be113f3 100644 --- a/pkg/model/moisture_sensor.go +++ b/pkg/model/moisture_sensor.go @@ -9,14 +9,18 @@ import ( type MoistureSensor struct { Id uint Name string - DryThreshold uint - WetThreshold uint + DryThreshold uint `json:"dry_threshold"` + WetThreshold uint `json:"wet_threshold"` } func (ms MoistureSensor) MqttTopic(device *HassDevice) string { return "sensor/" + device.Namespace + "/sensor-" + ms.IdAsString() } +func (ms MoistureSensor) MqttStateTopic(device *HassDevice) string { + return ms.MqttTopic(device) + "/" + makeMoistureSensorHassConfiguration(ms, device).StateTopic +} + func (ms MoistureSensor) AutoDiscoveryPayload(device *HassDevice) HassAutoDiscoverPayload { return makeMoistureSensorHassConfiguration(ms, device) } diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index 78c374a..c0e0108 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -19,7 +19,7 @@ func (c moistureSensorHassConfiguration) WithGlobalTopicPrefix(prefix string, de return c } -func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevice) HassAutoDiscoverPayload { +func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevice) moistureSensorHassConfiguration { c := moistureSensorHassConfiguration{} c.Name = sensor.Name c.ObjectId = device.EntityPrefix + "sensor-" + sensor.IdAsString() diff --git a/pkg/model/moisture_sensor_hass_state.go b/pkg/model/moisture_sensor_hass_state.go new file mode 100644 index 0000000..d8e57f2 --- /dev/null +++ b/pkg/model/moisture_sensor_hass_state.go @@ -0,0 +1,6 @@ +package model + +type MoistureSensorHassState struct { + Sensor *MoistureSensor `json:"sensor"` + Reading *MoistureReading `json:"reading"` +} diff --git a/pkg/persistence/moisture_reading_store.go b/pkg/persistence/moisture_reading_store.go index ba9c26b..1502cff 100644 --- a/pkg/persistence/moisture_reading_store.go +++ b/pkg/persistence/moisture_reading_store.go @@ -1,6 +1,8 @@ package persistence import ( + "errors" + "github.com/mewejo/go-watering/pkg/model" ) @@ -14,6 +16,14 @@ func (s *moistureReadingStore) recordReading(r model.MoistureReading) { limitReadings(&s.readings, 1000) } +func (s *moistureReadingStore) getLatest() (*model.MoistureReading, error) { + if len(s.readings) < 1 { + return &model.MoistureReading{}, errors.New("no readings available") + } + + return &s.readings[len(s.readings)-1], nil +} + func limitReadings(s *[]model.MoistureReading, length int) { if len(*s) <= length { return @@ -24,6 +34,10 @@ func limitReadings(s *[]model.MoistureReading, length int) { var moistureReadingStores []moistureReadingStore +func GetLatestReadingForMoistureSensorId(sensorId uint) (*model.MoistureReading, error) { + return getOrMakeStore(sensorId).getLatest() +} + func RecordMoistureReading(sensorId uint, reading model.MoistureReading) { getOrMakeStore(sensorId).recordReading(reading) } From 0684ee6b480e58704eae236df37119032485a5a9 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 14:58:06 +0000 Subject: [PATCH 047/135] fix: store ref --- pkg/persistence/moisture_reading_store.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/persistence/moisture_reading_store.go b/pkg/persistence/moisture_reading_store.go index 1502cff..db184cb 100644 --- a/pkg/persistence/moisture_reading_store.go +++ b/pkg/persistence/moisture_reading_store.go @@ -32,7 +32,7 @@ func limitReadings(s *[]model.MoistureReading, length int) { *s = (*s)[len(*s)-length:] } -var moistureReadingStores []moistureReadingStore +var moistureReadingStores []*moistureReadingStore func GetLatestReadingForMoistureSensorId(sensorId uint) (*model.MoistureReading, error) { return getOrMakeStore(sensorId).getLatest() @@ -45,7 +45,7 @@ func RecordMoistureReading(sensorId uint, reading model.MoistureReading) { func getOrMakeStore(sensorId uint) *moistureReadingStore { for _, store := range moistureReadingStores { if store.sensorId == sensorId { - return &store + return store } } @@ -53,7 +53,7 @@ func getOrMakeStore(sensorId uint) *moistureReadingStore { sensorId: sensorId, } - moistureReadingStores = append(moistureReadingStores, store) + moistureReadingStores = append(moistureReadingStores, &store) return &store } From cb4927d262bac310ebb7be1f0c43ac8233460625 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:00:05 +0000 Subject: [PATCH 048/135] debug: print error --- pkg/app/hass.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index ace82cb..3f138e5 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -2,6 +2,7 @@ package app import ( "encoding/json" + "fmt" "os" "time" @@ -59,7 +60,13 @@ func (app *App) startSendingMoistureSensorReadings() chan bool { sendSensorStates := func() { for _, sensor := range app.moistureSensors { - go app.publishMoistureSensorState(sensor) + go func(sensor *model.MoistureSensor) { + err := app.publishMoistureSensorState(sensor) + + if err != nil { + fmt.Println("Error: " + err.Error()) + } + }(sensor) } } From ca7b4699accd79f8380fbbc3fcc5e3c8c2eb244c Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:04:24 +0000 Subject: [PATCH 049/135] debug: print latest reading --- pkg/persistence/moisture_reading_store.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/persistence/moisture_reading_store.go b/pkg/persistence/moisture_reading_store.go index db184cb..2ac6f7d 100644 --- a/pkg/persistence/moisture_reading_store.go +++ b/pkg/persistence/moisture_reading_store.go @@ -2,6 +2,7 @@ package persistence import ( "errors" + "fmt" "github.com/mewejo/go-watering/pkg/model" ) @@ -40,6 +41,7 @@ func GetLatestReadingForMoistureSensorId(sensorId uint) (*model.MoistureReading, func RecordMoistureReading(sensorId uint, reading model.MoistureReading) { getOrMakeStore(sensorId).recordReading(reading) + fmt.Println(GetLatestReadingForMoistureSensorId(sensorId)) } func getOrMakeStore(sensorId uint) *moistureReadingStore { From 34e93e5ca1654e1d867e19f60e8f98a62f9fb087 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:05:12 +0000 Subject: [PATCH 050/135] debug: dont' limit --- pkg/persistence/moisture_reading_store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/persistence/moisture_reading_store.go b/pkg/persistence/moisture_reading_store.go index 2ac6f7d..d47da3e 100644 --- a/pkg/persistence/moisture_reading_store.go +++ b/pkg/persistence/moisture_reading_store.go @@ -14,7 +14,7 @@ type moistureReadingStore struct { func (s *moistureReadingStore) recordReading(r model.MoistureReading) { s.readings = append(s.readings, r) - limitReadings(&s.readings, 1000) + //limitReadings(&s.readings, 1000) } func (s *moistureReadingStore) getLatest() (*model.MoistureReading, error) { From 6ef22fbbd9ee399ed37c0e5425a9253a648847d8 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:06:04 +0000 Subject: [PATCH 051/135] debug: try without normalising --- pkg/app/arduino.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index e318c34..3d1422a 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -157,6 +157,8 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { if err == nil { + go handleMoistureReading(moistureReading, sensorId) + sensor, err := app.findMoistureSensorById(sensorId) if err != nil { From 771b5c2069b729d31093a0a1d833f63ae273deed Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:08:55 +0000 Subject: [PATCH 052/135] fix: not looking for an error --- pkg/app/arduino.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 3d1422a..f8d195f 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -157,11 +157,9 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { if err == nil { - go handleMoistureReading(moistureReading, sensorId) - sensor, err := app.findMoistureSensorById(sensorId) - if err != nil { + if err == nil { moistureReading.CalculateMoistureLevelForSensor(sensor) go handleMoistureReading(moistureReading, sensorId) } From 672e5f6767b4987b2859eb30d6f3e85bdb3791d6 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:09:46 +0000 Subject: [PATCH 053/135] feat: use correct value tpl --- pkg/model/moisture_sensor_hass_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index c0e0108..ef0e786 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -27,7 +27,7 @@ func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevi c.StateTopic = "state" c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.DeviceClass = "moisture" - c.StateValueTemplate = "{{ value_json.moisture.percentage }}" + c.StateValueTemplate = "{{ value_json.reading.percentage.percentage }}" c.UnitOfMeasurement = "%" c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable From c762219d8f0fb8f7e362ed0da1ae7c195ecef6d8 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:10:58 +0000 Subject: [PATCH 054/135] fix: use a real state --- pkg/model/moisture_sensor_hass_configuration.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index ef0e786..ee1ab82 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -16,6 +16,7 @@ type moistureSensorHassConfiguration struct { func (c moistureSensorHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { c.AvailabilityTopic = prefix + "/" + c.HassDevice.GetFqAvailabilityTopic() + c.StateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.StateTopic return c } From 3bab7e9afcc1ce33816f571fb3eb78b414412366 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:36:19 +0000 Subject: [PATCH 055/135] feat: use the avg for sensor --- pkg/app/hass.go | 6 ++-- .../moisture_sensor_hass_configuration.go | 2 +- pkg/model/moisture_sensor_hass_state.go | 4 +-- pkg/persistence/moisture_reading_store.go | 35 ++++++++++++++++++- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 3f138e5..ada88b5 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -88,15 +88,15 @@ func (app *App) startSendingMoistureSensorReadings() chan bool { func (app *App) publishMoistureSensorState(sensor *model.MoistureSensor) error { - reading, err := persistence.GetLatestReadingForMoistureSensorId(sensor.Id) + moistureLevel, err := persistence.GetAverageReadingForSince(sensor.Id, 2*time.Minute) if err != nil { return err } state := model.MoistureSensorHassState{ - Sensor: sensor, - Reading: reading, + Sensor: sensor, + MoistureLevel: moistureLevel, } payload, err := json.Marshal(state) diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index ee1ab82..6aa87ae 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -28,7 +28,7 @@ func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevi c.StateTopic = "state" c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.DeviceClass = "moisture" - c.StateValueTemplate = "{{ value_json.reading.percentage.percentage }}" + c.StateValueTemplate = "{{ value_json.moisture_level.percentage }}" c.UnitOfMeasurement = "%" c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable diff --git a/pkg/model/moisture_sensor_hass_state.go b/pkg/model/moisture_sensor_hass_state.go index d8e57f2..fddbfb5 100644 --- a/pkg/model/moisture_sensor_hass_state.go +++ b/pkg/model/moisture_sensor_hass_state.go @@ -1,6 +1,6 @@ package model type MoistureSensorHassState struct { - Sensor *MoistureSensor `json:"sensor"` - Reading *MoistureReading `json:"reading"` + Sensor *MoistureSensor `json:"sensor"` + MoistureLevel *MoistureLevel `json:"moisture_level"` } diff --git a/pkg/persistence/moisture_reading_store.go b/pkg/persistence/moisture_reading_store.go index d47da3e..29fd2e7 100644 --- a/pkg/persistence/moisture_reading_store.go +++ b/pkg/persistence/moisture_reading_store.go @@ -3,6 +3,7 @@ package persistence import ( "errors" "fmt" + "time" "github.com/mewejo/go-watering/pkg/model" ) @@ -12,9 +13,37 @@ type moistureReadingStore struct { readings []model.MoistureReading } +func (s *moistureReadingStore) getAverageSince(since time.Duration) (model.MoistureLevel, error) { + readings := []model.MoistureLevel{} + + cutOffTime := time.Now().Add(-since) + + for _, reading := range s.readings { + if reading.Time.Before(cutOffTime) { + continue + } + + readings = append(readings, reading.MoistureLevel) + } + + if len(readings) < 1 { + return model.MoistureLevel{}, errors.New("no readings to calculate average from") + } + + var totalPercentage uint + + for _, reading := range readings { + totalPercentage += reading.Percentage + } + + return model.MakeMoistureLevel( + uint(totalPercentage / uint(len(readings))), + ), nil +} + func (s *moistureReadingStore) recordReading(r model.MoistureReading) { s.readings = append(s.readings, r) - //limitReadings(&s.readings, 1000) + limitReadings(&s.readings, 1000) } func (s *moistureReadingStore) getLatest() (*model.MoistureReading, error) { @@ -39,6 +68,10 @@ func GetLatestReadingForMoistureSensorId(sensorId uint) (*model.MoistureReading, return getOrMakeStore(sensorId).getLatest() } +func GetAverageReadingForSince(sensorId uint, since time.Duration) (model.MoistureLevel, error) { + return getOrMakeStore(sensorId).getAverageSince(since) +} + func RecordMoistureReading(sensorId uint, reading model.MoistureReading) { getOrMakeStore(sensorId).recordReading(reading) fmt.Println(GetLatestReadingForMoistureSensorId(sensorId)) From 32044af1b74aee5befb748b8032efc7aa39fa159 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:36:52 +0000 Subject: [PATCH 056/135] fix: pointers --- pkg/model/moisture_sensor_hass_state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/moisture_sensor_hass_state.go b/pkg/model/moisture_sensor_hass_state.go index fddbfb5..a814b54 100644 --- a/pkg/model/moisture_sensor_hass_state.go +++ b/pkg/model/moisture_sensor_hass_state.go @@ -2,5 +2,5 @@ package model type MoistureSensorHassState struct { Sensor *MoistureSensor `json:"sensor"` - MoistureLevel *MoistureLevel `json:"moisture_level"` + MoistureLevel MoistureLevel `json:"moisture_level"` } From d1128dfd37c042fadaef62ccf43135c6300fdc89 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:39:10 +0000 Subject: [PATCH 057/135] chore: remove some debugging --- pkg/app/hass.go | 9 +-------- pkg/persistence/moisture_reading_store.go | 2 -- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index ada88b5..8409f2e 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -2,7 +2,6 @@ package app import ( "encoding/json" - "fmt" "os" "time" @@ -60,13 +59,7 @@ func (app *App) startSendingMoistureSensorReadings() chan bool { sendSensorStates := func() { for _, sensor := range app.moistureSensors { - go func(sensor *model.MoistureSensor) { - err := app.publishMoistureSensorState(sensor) - - if err != nil { - fmt.Println("Error: " + err.Error()) - } - }(sensor) + go app.publishMoistureSensorState(sensor) } } diff --git a/pkg/persistence/moisture_reading_store.go b/pkg/persistence/moisture_reading_store.go index 29fd2e7..6bf5578 100644 --- a/pkg/persistence/moisture_reading_store.go +++ b/pkg/persistence/moisture_reading_store.go @@ -2,7 +2,6 @@ package persistence import ( "errors" - "fmt" "time" "github.com/mewejo/go-watering/pkg/model" @@ -74,7 +73,6 @@ func GetAverageReadingForSince(sensorId uint, since time.Duration) (model.Moistu func RecordMoistureReading(sensorId uint, reading model.MoistureReading) { getOrMakeStore(sensorId).recordReading(reading) - fmt.Println(GetLatestReadingForMoistureSensorId(sensorId)) } func getOrMakeStore(sensorId uint) *moistureReadingStore { From a5285422fdbe7e706f0ccbfb732f43482bf79612 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:39:53 +0000 Subject: [PATCH 058/135] chore: remove debug --- pkg/arduino/arduino.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/arduino/arduino.go b/pkg/arduino/arduino.go index 9d354c6..bbb4bda 100644 --- a/pkg/arduino/arduino.go +++ b/pkg/arduino/arduino.go @@ -2,7 +2,6 @@ package arduino import ( "errors" - "fmt" "strings" "github.com/mewejo/go-watering/pkg/model" @@ -45,9 +44,6 @@ func (a *Arduino) SetWaterOutletState(outlet *model.WaterOutlet) { } func (a Arduino) SendCommand(command Command) (int, error) { - fmt.Print("Sending command: ") - fmt.Println(command) - return a.port.Write([]byte(command)) } From a6386dedcbb5c953f7971ee846a07623305e334d Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:47:00 +0000 Subject: [PATCH 059/135] feat: send modes --- pkg/model/zone_hass_configuration.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 203ff0a..dd8f031 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -16,6 +16,7 @@ type zoneHassConfiguration struct { StateOn string `json:"state_on"` StateOff string `json:"state_off"` TargetMoistureTopic string `json:"target_humidity_command_topic"` + Modes []string `json:"modes"` } func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { @@ -39,5 +40,9 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) HassAutoDiscoverPa c.StateOn = constants.HASS_STATE_ON c.StateOff = constants.HASS_STATE_OFF + for _, mode := range zoneModes { + c.Modes = append(c.Modes, mode.Key) + } + return c } From 951a64f309640f1188889012b768322630943473 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:50:40 +0000 Subject: [PATCH 060/135] feat: cmd topics for zones --- pkg/model/zone_hass_configuration.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index dd8f031..28e986e 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -17,10 +17,15 @@ type zoneHassConfiguration struct { StateOff string `json:"state_off"` TargetMoistureTopic string `json:"target_humidity_command_topic"` Modes []string `json:"modes"` + ModeCommandTopic string `json:"mode_command_topic"` + ModeStateTopic string `json:"mode_state_topic"` } func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { c.AvailabilityTopic = prefix + "/" + c.HassDevice.GetFqAvailabilityTopic() + c.CommandTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.CommandTopic + c.ModeCommandTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.ModeCommandTopic + c.ModeStateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.ModeStateTopic return c } @@ -37,6 +42,8 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) HassAutoDiscoverPa c.PayloadNotAvailable = device.PayloadNotAvailable c.HassDevice = device c.CommandTopic = "command" + c.ModeCommandTopic = "mode_command" + c.ModeStateTopic = "mode" c.StateOn = constants.HASS_STATE_ON c.StateOff = constants.HASS_STATE_OFF From 37c3df13fe6c1b0ec3681109ad79457ccaef6f17 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 15:56:54 +0000 Subject: [PATCH 061/135] wip: sending zone states to hass --- pkg/app/app.go | 7 +++++-- pkg/app/hass.go | 32 +++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 09cbd78..9e9e959 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -51,7 +51,9 @@ func (app *App) Run() { stopRequestingOutletStatesChan := app.startRequestingWaterOutletStates() stopRequestingMoistureSensorReadingsChan := app.startRequestingMoistureSensorReadings() stopSendingOutletStatesToArduinoChan := app.startSendingWaterStatesToArduino() - stopSendingMoistureSensorReadingsChan := app.startSendingMoistureSensorReadings() + stopSendingMoistureSensorReadingsToHassChan := app.startSendingMoistureSensorReadingsToHass() + stopSendingZoneStatesToHassChan := app.startSendingZoneStateToHass() + app.listenForWaterOutletCommands() go app.handleArduinoDataInput(arduinoInputChan) @@ -61,8 +63,9 @@ func (app *App) Run() { close(stopRequestingOutletStatesChan) close(stopSendingOutletStatesToArduinoChan) close(stopRequestingMoistureSensorReadingsChan) - close(stopSendingMoistureSensorReadingsChan) + close(stopSendingMoistureSensorReadingsToHassChan) close(closeArduinoChan) + close(stopSendingZoneStatesToHassChan) app.markHassNotAvailable() app.hass.Disconnect() os.Exit(0) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 8409f2e..d089849 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -51,7 +51,33 @@ func (app *App) publishWaterOutletState(outlet *model.WaterOutlet) error { return nil } -func (app *App) startSendingMoistureSensorReadings() chan bool { +func (app *App) sendZoneStateToHas(zone *model.Zone) { + // TODO +} + +func (app *App) startSendingZoneStateToHass() chan bool { + ticker := time.NewTicker(1 * time.Second) + + quit := make(chan bool) + + go func() { + for { + select { + case <-ticker.C: + for _, zone := range app.zones { + go app.sendZoneStateToHas(zone) + } + case <-quit: + ticker.Stop() + return + } + } + }() + + return quit +} + +func (app *App) startSendingMoistureSensorReadingsToHass() chan bool { ticker := time.NewTicker(5 * time.Second) @@ -59,7 +85,7 @@ func (app *App) startSendingMoistureSensorReadings() chan bool { sendSensorStates := func() { for _, sensor := range app.moistureSensors { - go app.publishMoistureSensorState(sensor) + go app.publishMoistureSensorStateToHass(sensor) } } @@ -79,7 +105,7 @@ func (app *App) startSendingMoistureSensorReadings() chan bool { return quit } -func (app *App) publishMoistureSensorState(sensor *model.MoistureSensor) error { +func (app *App) publishMoistureSensorStateToHass(sensor *model.MoistureSensor) error { moistureLevel, err := persistence.GetAverageReadingForSince(sensor.Id, 2*time.Minute) From a87cae5afbfeb620db3279953180165071063860 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:09:24 +0000 Subject: [PATCH 062/135] feat; send zone state to hass --- pkg/app/hass.go | 32 ++++++++++++++++++++--- pkg/model/zone.go | 4 +++ pkg/model/zone_hass_configuration.go | 2 +- pkg/model/zone_hass_state.go | 13 +++++++++ pkg/model/zone_mode.go | 4 +-- pkg/persistence/moisture_reading_store.go | 24 ++++++++++++++++- 6 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 pkg/model/zone_hass_state.go diff --git a/pkg/app/hass.go b/pkg/app/hass.go index d089849..5cdaf9f 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -51,8 +51,34 @@ func (app *App) publishWaterOutletState(outlet *model.WaterOutlet) error { return nil } -func (app *App) sendZoneStateToHas(zone *model.Zone) { - // TODO +func (app *App) sendZoneStateToHas(zone *model.Zone) error { + + average, err := persistence.GetAverageReadingForSensorsSince(zone.MoistureSensors, 2*time.Minute) + + if err != nil { + return err + } + + state := model.MakeZoneHassState( + zone.Mode, + average, + ) + + payload, err := json.Marshal(state) + + if err != nil { + return err + } + + app.hass.Publish( + hass.MakeMqttMessage( + zone.MqttStateTopic(app.hassDevice), + string(payload), + ), + ) + + return nil + } func (app *App) startSendingZoneStateToHass() chan bool { @@ -107,7 +133,7 @@ func (app *App) startSendingMoistureSensorReadingsToHass() chan bool { func (app *App) publishMoistureSensorStateToHass(sensor *model.MoistureSensor) error { - moistureLevel, err := persistence.GetAverageReadingForSince(sensor.Id, 2*time.Minute) + moistureLevel, err := persistence.GetAverageReadingForSensorIdSince(sensor.Id, 2*time.Minute) if err != nil { return err diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 7249e6e..5e5ba0f 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -22,6 +22,10 @@ func (zone Zone) MqttTopic(device *HassDevice) string { return "humidifier/" + device.Namespace + "/zone-" + zone.Id } +func (zone Zone) MqttStateTopic(device *HassDevice) string { + return zone.MqttTopic(device) + "/" + makeZoneHassConfiguration(zone, device).StateTopic +} + func (zone Zone) AutoDiscoveryPayload(device *HassDevice) HassAutoDiscoverPayload { return makeZoneHassConfiguration(zone, device) } diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 28e986e..2bf4a69 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -29,7 +29,7 @@ func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string, device *Hass return c } -func makeZoneHassConfiguration(zone Zone, device *HassDevice) HassAutoDiscoverPayload { +func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfiguration { c := zoneHassConfiguration{} c.Name = zone.Name c.ObjectId = device.EntityPrefix + "zone-" + zone.Id diff --git a/pkg/model/zone_hass_state.go b/pkg/model/zone_hass_state.go new file mode 100644 index 0000000..42653dd --- /dev/null +++ b/pkg/model/zone_hass_state.go @@ -0,0 +1,13 @@ +package model + +type ZoneHassState struct { + Mode *ZoneMode `json:"mode"` + AverageMoisture MoistureLevel `json:"average_moisture"` +} + +func MakeZoneHassState(mode *ZoneMode, averageMoisture MoistureLevel) ZoneHassState { + return ZoneHassState{ + Mode: mode, + AverageMoisture: averageMoisture, + } +} diff --git a/pkg/model/zone_mode.go b/pkg/model/zone_mode.go index 1431fc9..fbfab16 100644 --- a/pkg/model/zone_mode.go +++ b/pkg/model/zone_mode.go @@ -6,8 +6,8 @@ import ( ) type ZoneMode struct { - Name string - Key string + Name string `json:"name"` + Key string `json:"key"` } var zoneModes = []*ZoneMode{ diff --git a/pkg/persistence/moisture_reading_store.go b/pkg/persistence/moisture_reading_store.go index 6bf5578..773c780 100644 --- a/pkg/persistence/moisture_reading_store.go +++ b/pkg/persistence/moisture_reading_store.go @@ -67,10 +67,32 @@ func GetLatestReadingForMoistureSensorId(sensorId uint) (*model.MoistureReading, return getOrMakeStore(sensorId).getLatest() } -func GetAverageReadingForSince(sensorId uint, since time.Duration) (model.MoistureLevel, error) { +func GetAverageReadingForSensorIdSince(sensorId uint, since time.Duration) (model.MoistureLevel, error) { return getOrMakeStore(sensorId).getAverageSince(since) } +func GetAverageReadingForSensorsSince(sensors []*model.MoistureSensor, since time.Duration) (model.MoistureLevel, error) { + if len(sensors) < 1 { + return model.MoistureLevel{}, errors.New("no sensors provided") + } + + var totalPercentage uint + + for _, sensor := range sensors { + level, err := getOrMakeStore(sensor.Id).getAverageSince(since) + + if err != nil { + return model.MoistureLevel{}, err + } + + totalPercentage += level.Percentage + } + + return model.MakeMoistureLevel( + uint(totalPercentage / uint(len(sensors))), + ), nil +} + func RecordMoistureReading(sensorId uint, reading model.MoistureReading) { getOrMakeStore(sensorId).recordReading(reading) } From 9da7bc3c67a2e4c422dc7d94391ad9bc405dd2cf Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:10:12 +0000 Subject: [PATCH 063/135] fix: topic name --- pkg/model/zone_hass_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 2bf4a69..c4785a0 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -34,7 +34,7 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfigurat c.Name = zone.Name c.ObjectId = device.EntityPrefix + "zone-" + zone.Id c.UniqueId = c.ObjectId - c.StateTopic = "humidifier" + c.StateTopic = "state" c.TargetMoistureTopic = "target_moisture" c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.StateValueTemplate = "{{ value_json.target }}" // TODO From 11001607e958ba1b17923ed09f09c5dc9f916ecf Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:11:52 +0000 Subject: [PATCH 064/135] feat: send enabled --- pkg/app/hass.go | 2 +- pkg/model/zone.go | 2 ++ pkg/model/zone_hass_state.go | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 5cdaf9f..4bb9737 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -60,7 +60,7 @@ func (app *App) sendZoneStateToHas(zone *model.Zone) error { } state := model.MakeZoneHassState( - zone.Mode, + zone, average, ) diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 5e5ba0f..395a068 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -6,6 +6,7 @@ type Zone struct { Mode *ZoneMode MoistureSensors []*MoistureSensor WaterOutlets []*WaterOutlet + Enabled bool } func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []*WaterOutlet) *Zone { @@ -15,6 +16,7 @@ func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []* Mode: getDefaultZoneMode(), MoistureSensors: sensors, WaterOutlets: waterOutlets, + Enabled: true, } } diff --git a/pkg/model/zone_hass_state.go b/pkg/model/zone_hass_state.go index 42653dd..19752ac 100644 --- a/pkg/model/zone_hass_state.go +++ b/pkg/model/zone_hass_state.go @@ -3,11 +3,13 @@ package model type ZoneHassState struct { Mode *ZoneMode `json:"mode"` AverageMoisture MoistureLevel `json:"average_moisture"` + Enabled bool `json:"enabled"` } -func MakeZoneHassState(mode *ZoneMode, averageMoisture MoistureLevel) ZoneHassState { +func MakeZoneHassState(zone *Zone, averageMoisture MoistureLevel) ZoneHassState { return ZoneHassState{ - Mode: mode, + Mode: zone.Mode, AverageMoisture: averageMoisture, + Enabled: zone.Enabled, } } From 02c422c58a18f058e1d2546090041d08be8d0511 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:12:21 +0000 Subject: [PATCH 065/135] feat: use state --- pkg/model/zone_hass_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index c4785a0..04af72a 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -37,7 +37,7 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfigurat c.StateTopic = "state" c.TargetMoistureTopic = "target_moisture" c.AvailabilityTopic = device.GetFqAvailabilityTopic() - c.StateValueTemplate = "{{ value_json.target }}" // TODO + c.StateValueTemplate = "{% if value_json.enabled -%}" + c.StateOn + "{%- else -%}" + c.StateOff + "{%- endif %}" c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable c.HassDevice = device From 4a930af100ffe6066da6291652c115e22b449a72 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:13:55 +0000 Subject: [PATCH 066/135] fix: StateTopic --- pkg/model/zone_hass_configuration.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 04af72a..7de065f 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -26,6 +26,7 @@ func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string, device *Hass c.CommandTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.CommandTopic c.ModeCommandTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.ModeCommandTopic c.ModeStateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.ModeStateTopic + c.StateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.StateTopic return c } From 85ae0e0bfb79078a589d62620f23d7c0d41879ca Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:15:04 +0000 Subject: [PATCH 067/135] fix: state value template --- pkg/model/zone_hass_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 7de065f..3c69159 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -7,7 +7,7 @@ type zoneHassConfiguration struct { ObjectId string `json:"object_id"` UniqueId string `json:"unique_id"` StateTopic string `json:"state_topic"` - StateValueTemplate string `json:"value_template"` + StateValueTemplate string `json:"state_value_template"` AvailabilityTopic string `json:"availability_topic"` HassDevice *HassDevice `json:"device"` PayloadAvailable string `json:"payload_available"` From 2a2c3cbfeea0fd2d0177d55249fb48026a299bd6 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:16:23 +0000 Subject: [PATCH 068/135] fix: payload --- pkg/model/zone_hass_configuration.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 3c69159..0b88d2a 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -13,8 +13,8 @@ type zoneHassConfiguration struct { PayloadAvailable string `json:"payload_available"` PayloadNotAvailable string `json:"payload_not_available"` CommandTopic string `json:"command_topic"` - StateOn string `json:"state_on"` - StateOff string `json:"state_off"` + StateOn string `json:"payload_on"` + StateOff string `json:"payload_off"` TargetMoistureTopic string `json:"target_humidity_command_topic"` Modes []string `json:"modes"` ModeCommandTopic string `json:"mode_command_topic"` From ff1ebaf29327f3e4b58c2bca84f247befb71b42d Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:18:09 +0000 Subject: [PATCH 069/135] fix: duh. --- pkg/model/zone_hass_configuration.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 0b88d2a..98055e9 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -33,6 +33,8 @@ func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string, device *Hass func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfiguration { c := zoneHassConfiguration{} c.Name = zone.Name + c.StateOn = constants.HASS_STATE_ON + c.StateOff = constants.HASS_STATE_OFF c.ObjectId = device.EntityPrefix + "zone-" + zone.Id c.UniqueId = c.ObjectId c.StateTopic = "state" @@ -45,8 +47,6 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfigurat c.CommandTopic = "command" c.ModeCommandTopic = "mode_command" c.ModeStateTopic = "mode" - c.StateOn = constants.HASS_STATE_ON - c.StateOff = constants.HASS_STATE_OFF for _, mode := range zoneModes { c.Modes = append(c.Modes, mode.Key) From a36098603477a57d2d0cfda084c7c49646ccb8a7 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:23:16 +0000 Subject: [PATCH 070/135] feat: target moisture level --- pkg/model/zone.go | 2 ++ pkg/model/zone_hass_configuration.go | 37 ++++++++++++++++------------ pkg/model/zone_hass_state.go | 1 + 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 395a068..614ceb2 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -4,6 +4,7 @@ type Zone struct { Id string Name string Mode *ZoneMode + TargetMoisture MoistureLevel MoistureSensors []*MoistureSensor WaterOutlets []*WaterOutlet Enabled bool @@ -17,6 +18,7 @@ func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []* MoistureSensors: sensors, WaterOutlets: waterOutlets, Enabled: true, + TargetMoisture: MakeMoistureLevel(50), // TODO set to zero on boot? } } diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 98055e9..dc08521 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -3,22 +3,24 @@ package model import "github.com/mewejo/go-watering/pkg/constants" type zoneHassConfiguration struct { - Name string `json:"name"` - ObjectId string `json:"object_id"` - UniqueId string `json:"unique_id"` - StateTopic string `json:"state_topic"` - StateValueTemplate string `json:"state_value_template"` - AvailabilityTopic string `json:"availability_topic"` - HassDevice *HassDevice `json:"device"` - PayloadAvailable string `json:"payload_available"` - PayloadNotAvailable string `json:"payload_not_available"` - CommandTopic string `json:"command_topic"` - StateOn string `json:"payload_on"` - StateOff string `json:"payload_off"` - TargetMoistureTopic string `json:"target_humidity_command_topic"` - Modes []string `json:"modes"` - ModeCommandTopic string `json:"mode_command_topic"` - ModeStateTopic string `json:"mode_state_topic"` + Name string `json:"name"` + ObjectId string `json:"object_id"` + UniqueId string `json:"unique_id"` + StateTopic string `json:"state_topic"` + StateValueTemplate string `json:"state_value_template"` + AvailabilityTopic string `json:"availability_topic"` + HassDevice *HassDevice `json:"device"` + PayloadAvailable string `json:"payload_available"` + PayloadNotAvailable string `json:"payload_not_available"` + CommandTopic string `json:"command_topic"` + StateOn string `json:"payload_on"` + StateOff string `json:"payload_off"` + TargetMoistureTopic string `json:"target_humidity_command_topic"` + TargetMoistureStateTopic string `json:"target_humidity_state_topic"` + TargetMoistureStateValueTemplate string `json:"target_humidity_state_template"` + Modes []string `json:"modes"` + ModeCommandTopic string `json:"mode_command_topic"` + ModeStateTopic string `json:"mode_state_topic"` } func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { @@ -27,6 +29,7 @@ func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string, device *Hass c.ModeCommandTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.ModeCommandTopic c.ModeStateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.ModeStateTopic c.StateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.StateTopic + c.TargetMoistureStateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.TargetMoistureStateTopic return c } @@ -38,8 +41,10 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfigurat c.ObjectId = device.EntityPrefix + "zone-" + zone.Id c.UniqueId = c.ObjectId c.StateTopic = "state" + c.TargetMoistureStateTopic = "state" c.TargetMoistureTopic = "target_moisture" c.AvailabilityTopic = device.GetFqAvailabilityTopic() + c.TargetMoistureStateValueTemplate = "{{ value_json.target_moisture }}" c.StateValueTemplate = "{% if value_json.enabled -%}" + c.StateOn + "{%- else -%}" + c.StateOff + "{%- endif %}" c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable diff --git a/pkg/model/zone_hass_state.go b/pkg/model/zone_hass_state.go index 19752ac..245022f 100644 --- a/pkg/model/zone_hass_state.go +++ b/pkg/model/zone_hass_state.go @@ -3,6 +3,7 @@ package model type ZoneHassState struct { Mode *ZoneMode `json:"mode"` AverageMoisture MoistureLevel `json:"average_moisture"` + TargetMoisture MoistureLevel `json:"target_moisture"` Enabled bool `json:"enabled"` } From d41218fdf230aacc5677993a9f38068482af95e3 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:23:45 +0000 Subject: [PATCH 071/135] fix: percentage --- pkg/model/zone_hass_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index dc08521..8a5d90d 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -44,7 +44,7 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfigurat c.TargetMoistureStateTopic = "state" c.TargetMoistureTopic = "target_moisture" c.AvailabilityTopic = device.GetFqAvailabilityTopic() - c.TargetMoistureStateValueTemplate = "{{ value_json.target_moisture }}" + c.TargetMoistureStateValueTemplate = "{{ value_json.target_moisture.percentage }}" c.StateValueTemplate = "{% if value_json.enabled -%}" + c.StateOn + "{%- else -%}" + c.StateOff + "{%- endif %}" c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable From f59d0ac8d82881670cb8ba47aa9791a5554026ca Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:24:19 +0000 Subject: [PATCH 072/135] fix: actually send it --- pkg/model/zone_hass_state.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/model/zone_hass_state.go b/pkg/model/zone_hass_state.go index 245022f..68c66de 100644 --- a/pkg/model/zone_hass_state.go +++ b/pkg/model/zone_hass_state.go @@ -12,5 +12,6 @@ func MakeZoneHassState(zone *Zone, averageMoisture MoistureLevel) ZoneHassState Mode: zone.Mode, AverageMoisture: averageMoisture, Enabled: zone.Enabled, + TargetMoisture: zone.TargetMoisture, } } From 8ccecd721a59a7e0084f4f862c459f1fa71dc409 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:25:53 +0000 Subject: [PATCH 073/135] feat: set mode --- pkg/model/zone_hass_configuration.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 8a5d90d..33f8b75 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -18,6 +18,7 @@ type zoneHassConfiguration struct { TargetMoistureTopic string `json:"target_humidity_command_topic"` TargetMoistureStateTopic string `json:"target_humidity_state_topic"` TargetMoistureStateValueTemplate string `json:"target_humidity_state_template"` + ModeStateTemplate string `json:"mode_state_template"` Modes []string `json:"modes"` ModeCommandTopic string `json:"mode_command_topic"` ModeStateTopic string `json:"mode_state_topic"` @@ -44,6 +45,7 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfigurat c.TargetMoistureStateTopic = "state" c.TargetMoistureTopic = "target_moisture" c.AvailabilityTopic = device.GetFqAvailabilityTopic() + c.ModeStateTopic = "{{ value_json.mode.key }}" c.TargetMoistureStateValueTemplate = "{{ value_json.target_moisture.percentage }}" c.StateValueTemplate = "{% if value_json.enabled -%}" + c.StateOn + "{%- else -%}" + c.StateOff + "{%- endif %}" c.PayloadAvailable = device.PayloadAvailable From 27c0d4324d43522b28702a04a636de1dbe7d18bc Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:26:32 +0000 Subject: [PATCH 074/135] fix: plonker --- pkg/model/zone_hass_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 33f8b75..08a0a53 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -45,7 +45,7 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfigurat c.TargetMoistureStateTopic = "state" c.TargetMoistureTopic = "target_moisture" c.AvailabilityTopic = device.GetFqAvailabilityTopic() - c.ModeStateTopic = "{{ value_json.mode.key }}" + c.ModeStateTemplate = "{{ value_json.mode.key }}" c.TargetMoistureStateValueTemplate = "{{ value_json.target_moisture.percentage }}" c.StateValueTemplate = "{% if value_json.enabled -%}" + c.StateOn + "{%- else -%}" + c.StateOff + "{%- endif %}" c.PayloadAvailable = device.PayloadAvailable From 18843169c7558ca12e41c1d8571bc58b7a26e65f Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 16:27:32 +0000 Subject: [PATCH 075/135] fix: from the right topic --- pkg/model/zone_hass_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 08a0a53..235ccb1 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -53,7 +53,7 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfigurat c.HassDevice = device c.CommandTopic = "command" c.ModeCommandTopic = "mode_command" - c.ModeStateTopic = "mode" + c.ModeStateTopic = "state" for _, mode := range zoneModes { c.Modes = append(c.Modes, mode.Key) From a55513ef188a2457136fe3c40c4cd3b62edfd760 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:17:23 +0000 Subject: [PATCH 076/135] feat: average moisture level for zones --- pkg/app/hass.go | 10 +++++++ pkg/model/hass_auto_discoverable.go | 1 + pkg/model/moisture_sensor.go | 4 +++ .../moisture_sensor_hass_configuration.go | 30 ++++++++++++++++--- pkg/model/water_outlet.go | 4 +++ pkg/model/water_outlet_hass_configuration.go | 7 +++++ pkg/model/zone.go | 25 +++++++++++----- pkg/model/zone_average_moisture_sensor.go | 27 +++++++++++++++++ pkg/model/zone_hass_configuration.go | 7 +++++ 9 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 pkg/model/zone_average_moisture_sensor.go diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 4bb9737..52b06be 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -251,5 +251,15 @@ func (app *App) publishHassAutoDiscovery() error { token.Wait() } + for _, entity := range app.zones { + token, err := app.hass.PublishAutoDiscovery(entity.AverageMoistureSensor) + + if err != nil { + return err + } + + token.Wait() + } + return nil } diff --git a/pkg/model/hass_auto_discoverable.go b/pkg/model/hass_auto_discoverable.go index 353afa8..7530d68 100644 --- a/pkg/model/hass_auto_discoverable.go +++ b/pkg/model/hass_auto_discoverable.go @@ -2,5 +2,6 @@ package model type HassAutoDiscoverable interface { MqttTopic(device *HassDevice) string + OverriddenMqttStateTopic(device *HassDevice) string AutoDiscoveryPayload(device *HassDevice) HassAutoDiscoverPayload } diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go index be113f3..a72207f 100644 --- a/pkg/model/moisture_sensor.go +++ b/pkg/model/moisture_sensor.go @@ -17,6 +17,10 @@ func (ms MoistureSensor) MqttTopic(device *HassDevice) string { return "sensor/" + device.Namespace + "/sensor-" + ms.IdAsString() } +func (ms MoistureSensor) OverriddenMqttStateTopic(device *HassDevice) string { + return "" +} + func (ms MoistureSensor) MqttStateTopic(device *HassDevice) string { return ms.MqttTopic(device) + "/" + makeMoistureSensorHassConfiguration(ms, device).StateTopic } diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index 6aa87ae..24af9b8 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -17,18 +17,31 @@ type moistureSensorHassConfiguration struct { func (c moistureSensorHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { c.AvailabilityTopic = prefix + "/" + c.HassDevice.GetFqAvailabilityTopic() c.StateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.StateTopic + + stateOverride := entity.OverriddenMqttStateTopic(device) + + if stateOverride != "" { + c.StateTopic = prefix + "/" + stateOverride + } + return c } -func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevice) moistureSensorHassConfiguration { +func makeMoistureSensorForZoneAverageMoistureSensor(averageMoistureSensor ZoneAverageMoistureSensor, device *HassDevice) moistureSensorHassConfiguration { + c := makeMoistureSensorConfiugurationForDevice(device) + c.Name = averageMoistureSensor.Name + c.ObjectId = device.EntityPrefix + "sensor-" + averageMoistureSensor.Id + c.StateValueTemplate = "{{ value_json.average_moisture.percentage }}" + + return c +} + +func makeMoistureSensorConfiugurationForDevice(device *HassDevice) moistureSensorHassConfiguration { c := moistureSensorHassConfiguration{} - c.Name = sensor.Name - c.ObjectId = device.EntityPrefix + "sensor-" + sensor.IdAsString() c.UniqueId = c.ObjectId c.StateTopic = "state" c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.DeviceClass = "moisture" - c.StateValueTemplate = "{{ value_json.moisture_level.percentage }}" c.UnitOfMeasurement = "%" c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable @@ -36,3 +49,12 @@ func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevi return c } + +func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevice) moistureSensorHassConfiguration { + c := makeMoistureSensorConfiugurationForDevice(device) + c.Name = sensor.Name + c.ObjectId = device.EntityPrefix + "sensor-" + sensor.IdAsString() + c.StateValueTemplate = "{{ value_json.moisture_level.percentage }}" + + return c +} diff --git a/pkg/model/water_outlet.go b/pkg/model/water_outlet.go index 7c7e8c6..a4c423f 100644 --- a/pkg/model/water_outlet.go +++ b/pkg/model/water_outlet.go @@ -30,6 +30,10 @@ func (wo WaterOutlet) MqttTopic(device *HassDevice) string { return "switch/" + device.Namespace + "/outlet-" + wo.IdAsString() } +func (wo WaterOutlet) OverriddenMqttStateTopic(device *HassDevice) string { + return "" +} + func (wo WaterOutlet) MqttStateTopic(device *HassDevice) string { return wo.MqttTopic(device) + "/" + makeWaterOutletHassConfiguration(wo, device).StateTopic } diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index 0081c96..2158464 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -22,6 +22,13 @@ func (c waterOutletHassConfiguration) WithGlobalTopicPrefix(prefix string, devic c.AvailabilityTopic = prefix + "/" + c.HassDevice.GetFqAvailabilityTopic() c.CommandTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.CommandTopic c.StateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.StateTopic + + stateOverride := entity.OverriddenMqttStateTopic(device) + + if stateOverride != "" { + c.StateTopic = prefix + "/" + stateOverride + } + return c } diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 614ceb2..77e37b8 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -1,17 +1,18 @@ package model type Zone struct { - Id string - Name string - Mode *ZoneMode - TargetMoisture MoistureLevel - MoistureSensors []*MoistureSensor - WaterOutlets []*WaterOutlet - Enabled bool + Id string + Name string + Mode *ZoneMode + TargetMoisture MoistureLevel + MoistureSensors []*MoistureSensor + WaterOutlets []*WaterOutlet + Enabled bool + AverageMoistureSensor *ZoneAverageMoistureSensor } func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []*WaterOutlet) *Zone { - return &Zone{ + zone := &Zone{ Id: id, Name: name, Mode: getDefaultZoneMode(), @@ -20,12 +21,20 @@ func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []* Enabled: true, TargetMoisture: MakeMoistureLevel(50), // TODO set to zero on boot? } + + zone.AverageMoistureSensor = newZoneAverageMoistureSensor(zone) + + return zone } func (zone Zone) MqttTopic(device *HassDevice) string { return "humidifier/" + device.Namespace + "/zone-" + zone.Id } +func (zone Zone) OverriddenMqttStateTopic(device *HassDevice) string { + return "" +} + func (zone Zone) MqttStateTopic(device *HassDevice) string { return zone.MqttTopic(device) + "/" + makeZoneHassConfiguration(zone, device).StateTopic } diff --git a/pkg/model/zone_average_moisture_sensor.go b/pkg/model/zone_average_moisture_sensor.go new file mode 100644 index 0000000..1551369 --- /dev/null +++ b/pkg/model/zone_average_moisture_sensor.go @@ -0,0 +1,27 @@ +package model + +type ZoneAverageMoistureSensor struct { + Id string + Name string + Zone *Zone +} + +func newZoneAverageMoistureSensor(zone *Zone) *ZoneAverageMoistureSensor { + return &ZoneAverageMoistureSensor{ + Id: "zone-" + zone.Id + "-average-moisture", + Name: zone.Name + " Average Moisture", + Zone: zone, + } +} + +func (sensor ZoneAverageMoistureSensor) OverriddenMqttStateTopic(device *HassDevice) string { + return sensor.Zone.MqttTopic(device) +} + +func (sensor ZoneAverageMoistureSensor) MqttTopic(device *HassDevice) string { + return "sensor/" + device.Namespace + "/zone-" + sensor.Zone.Id + "-average-moisture-sensor" +} + +func (sensor ZoneAverageMoistureSensor) AutoDiscoveryPayload(device *HassDevice) HassAutoDiscoverPayload { + return makeMoistureSensorForZoneAverageMoistureSensor(sensor, device) +} diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 235ccb1..cab19f1 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -31,6 +31,13 @@ func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string, device *Hass c.ModeStateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.ModeStateTopic c.StateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.StateTopic c.TargetMoistureStateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.TargetMoistureStateTopic + + stateOverride := entity.OverriddenMqttStateTopic(device) + + if stateOverride != "" { + c.StateTopic = prefix + "/" + stateOverride + } + return c } From 82bd06d562691e9ebc180c25bc18827f46e39ad4 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:26:22 +0000 Subject: [PATCH 077/135] fix: fix unique ids --- pkg/model/moisture_sensor_hass_configuration.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index 24af9b8..23debbd 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -30,7 +30,8 @@ func (c moistureSensorHassConfiguration) WithGlobalTopicPrefix(prefix string, de func makeMoistureSensorForZoneAverageMoistureSensor(averageMoistureSensor ZoneAverageMoistureSensor, device *HassDevice) moistureSensorHassConfiguration { c := makeMoistureSensorConfiugurationForDevice(device) c.Name = averageMoistureSensor.Name - c.ObjectId = device.EntityPrefix + "sensor-" + averageMoistureSensor.Id + c.ObjectId = device.EntityPrefix + "zone-average-sensor-" + averageMoistureSensor.Id + c.UniqueId = c.ObjectId c.StateValueTemplate = "{{ value_json.average_moisture.percentage }}" return c @@ -38,7 +39,6 @@ func makeMoistureSensorForZoneAverageMoistureSensor(averageMoistureSensor ZoneAv func makeMoistureSensorConfiugurationForDevice(device *HassDevice) moistureSensorHassConfiguration { c := moistureSensorHassConfiguration{} - c.UniqueId = c.ObjectId c.StateTopic = "state" c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.DeviceClass = "moisture" @@ -54,6 +54,7 @@ func makeMoistureSensorHassConfiguration(sensor MoistureSensor, device *HassDevi c := makeMoistureSensorConfiugurationForDevice(device) c.Name = sensor.Name c.ObjectId = device.EntityPrefix + "sensor-" + sensor.IdAsString() + c.UniqueId = c.ObjectId c.StateValueTemplate = "{{ value_json.moisture_level.percentage }}" return c From 20cafb480edfc980754a1e17255913bbd8149d50 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:28:07 +0000 Subject: [PATCH 078/135] fix: zone state topic --- pkg/model/zone_average_moisture_sensor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/zone_average_moisture_sensor.go b/pkg/model/zone_average_moisture_sensor.go index 1551369..64c17c7 100644 --- a/pkg/model/zone_average_moisture_sensor.go +++ b/pkg/model/zone_average_moisture_sensor.go @@ -15,7 +15,7 @@ func newZoneAverageMoistureSensor(zone *Zone) *ZoneAverageMoistureSensor { } func (sensor ZoneAverageMoistureSensor) OverriddenMqttStateTopic(device *HassDevice) string { - return sensor.Zone.MqttTopic(device) + return sensor.Zone.MqttStateTopic(device) } func (sensor ZoneAverageMoistureSensor) MqttTopic(device *HassDevice) string { From c619816d190d41ea00462fc3c69b832e0124512b Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:31:33 +0000 Subject: [PATCH 079/135] feat: set the zone state --- pkg/app/hass.go | 15 +++++++++++++++ pkg/model/zone.go | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 52b06be..aa02d4a 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -33,6 +33,21 @@ func (app *App) listenForWaterOutletCommands() { } } +func (app *App) listenForZoneCommands() { + for _, zone := range app.zones { + app.hass.Subscribe( + zone.MqttCommandTopic(app.hassDevice), + func(message mqtt.Message) { + if string(message.Payload()) == constants.HASS_STATE_ON { + zone.Enabled = true + } else if string(message.Payload()) == constants.HASS_STATE_OFF { + zone.Enabled = false + } + }, + ) + } +} + func (app *App) publishWaterOutletState(outlet *model.WaterOutlet) error { payload, err := json.Marshal(outlet) diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 77e37b8..4d37947 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -35,6 +35,10 @@ func (zone Zone) OverriddenMqttStateTopic(device *HassDevice) string { return "" } +func (zone Zone) MqttCommandTopic(device *HassDevice) string { + return zone.MqttTopic(device) + "/" + makeZoneHassConfiguration(zone, device).CommandTopic +} + func (zone Zone) MqttStateTopic(device *HassDevice) string { return zone.MqttTopic(device) + "/" + makeZoneHassConfiguration(zone, device).StateTopic } From dd69a335b6a5fe529222b6a2c9e978a47e928928 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:32:31 +0000 Subject: [PATCH 080/135] feat: actually listen to zone cmss --- pkg/app/app.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/app/app.go b/pkg/app/app.go index 9e9e959..1b8b9eb 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -55,6 +55,7 @@ func (app *App) Run() { stopSendingZoneStatesToHassChan := app.startSendingZoneStateToHass() app.listenForWaterOutletCommands() + app.listenForZoneCommands() go app.handleArduinoDataInput(arduinoInputChan) From dac584bc3805502ce86164e6bff5faf1e9649132 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:34:18 +0000 Subject: [PATCH 081/135] fix: by ref? --- pkg/app/hass.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index aa02d4a..1726d7e 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -34,7 +34,8 @@ func (app *App) listenForWaterOutletCommands() { } func (app *App) listenForZoneCommands() { - for _, zone := range app.zones { + + subscribe := func(zone *model.Zone) { app.hass.Subscribe( zone.MqttCommandTopic(app.hassDevice), func(message mqtt.Message) { @@ -46,6 +47,10 @@ func (app *App) listenForZoneCommands() { }, ) } + + for _, zone := range app.zones { + subscribe(zone) + } } func (app *App) publishWaterOutletState(outlet *model.WaterOutlet) error { From c0816067f046ca6e2ac0044481fd21783428b8a6 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:35:24 +0000 Subject: [PATCH 082/135] fix: fix by ref issue for outlets too --- pkg/app/hass.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 1726d7e..ae8e910 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -13,11 +13,8 @@ import ( ) func (app *App) listenForWaterOutletCommands() { - for _, outlet := range app.waterOutlets { - if !outlet.IndependentlyControlled { - continue - } + subscribe := func(outlet *model.WaterOutlet) { app.hass.Subscribe( outlet.MqttCommandTopic(app.hassDevice), func(message mqtt.Message) { @@ -31,6 +28,14 @@ func (app *App) listenForWaterOutletCommands() { }, ) } + + for _, outlet := range app.waterOutlets { + if !outlet.IndependentlyControlled { + continue + } + + subscribe(outlet) + } } func (app *App) listenForZoneCommands() { From f5cc11272ce46a40d75d1d0842fe30a606b678ac Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:40:33 +0000 Subject: [PATCH 083/135] feat: accept tatget moisture --- pkg/app/hass.go | 14 ++++++++++++++ pkg/model/zone.go | 4 ++++ pkg/model/zone_hass_configuration.go | 4 ++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index ae8e910..770e90f 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -3,6 +3,7 @@ package app import ( "encoding/json" "os" + "strconv" "time" mqtt "github.com/eclipse/paho.mqtt.golang" @@ -51,6 +52,19 @@ func (app *App) listenForZoneCommands() { } }, ) + + app.hass.Subscribe( + zone.MqttTargetMoistureCommandTopic(app.hassDevice), + func(message mqtt.Message) { + moisturePercent, err := strconv.Atoi(string(message.Payload())) + + if err != nil { + return + } + + zone.TargetMoisture = model.MakeMoistureLevel(uint(moisturePercent)) + }, + ) } for _, zone := range app.zones { diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 4d37947..cf4283d 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -39,6 +39,10 @@ func (zone Zone) MqttCommandTopic(device *HassDevice) string { return zone.MqttTopic(device) + "/" + makeZoneHassConfiguration(zone, device).CommandTopic } +func (zone Zone) MqttTargetMoistureCommandTopic(device *HassDevice) string { + return zone.MqttTopic(device) + "/" + makeZoneHassConfiguration(zone, device).TargetMoistureCommandTopic +} + func (zone Zone) MqttStateTopic(device *HassDevice) string { return zone.MqttTopic(device) + "/" + makeZoneHassConfiguration(zone, device).StateTopic } diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index cab19f1..4367b15 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -15,7 +15,7 @@ type zoneHassConfiguration struct { CommandTopic string `json:"command_topic"` StateOn string `json:"payload_on"` StateOff string `json:"payload_off"` - TargetMoistureTopic string `json:"target_humidity_command_topic"` + TargetMoistureCommandTopic string `json:"target_humidity_command_topic"` TargetMoistureStateTopic string `json:"target_humidity_state_topic"` TargetMoistureStateValueTemplate string `json:"target_humidity_state_template"` ModeStateTemplate string `json:"mode_state_template"` @@ -50,7 +50,7 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfigurat c.UniqueId = c.ObjectId c.StateTopic = "state" c.TargetMoistureStateTopic = "state" - c.TargetMoistureTopic = "target_moisture" + c.TargetMoistureCommandTopic = "target_moisture" c.AvailabilityTopic = device.GetFqAvailabilityTopic() c.ModeStateTemplate = "{{ value_json.mode.key }}" c.TargetMoistureStateValueTemplate = "{{ value_json.target_moisture.percentage }}" From 7bacc00f34904faeb649dfd13e59cec0d93532f8 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:41:43 +0000 Subject: [PATCH 084/135] fix: correct topic --- pkg/model/zone_hass_configuration.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index 4367b15..c0e60cd 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -31,6 +31,7 @@ func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string, device *Hass c.ModeStateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.ModeStateTopic c.StateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.StateTopic c.TargetMoistureStateTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.TargetMoistureStateTopic + c.TargetMoistureCommandTopic = prefix + "/" + entity.MqttTopic(device) + "/" + c.TargetMoistureCommandTopic stateOverride := entity.OverriddenMqttStateTopic(device) From 4ce0ea8ba6523b04815056751a0b38807cb50670 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:43:23 +0000 Subject: [PATCH 085/135] feat: retain cmds --- pkg/model/zone_hass_configuration.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/model/zone_hass_configuration.go b/pkg/model/zone_hass_configuration.go index c0e60cd..b26855e 100644 --- a/pkg/model/zone_hass_configuration.go +++ b/pkg/model/zone_hass_configuration.go @@ -22,6 +22,7 @@ type zoneHassConfiguration struct { Modes []string `json:"modes"` ModeCommandTopic string `json:"mode_command_topic"` ModeStateTopic string `json:"mode_state_topic"` + MqttRetain bool `json:"retain"` } func (c zoneHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { @@ -62,6 +63,7 @@ func makeZoneHassConfiguration(zone Zone, device *HassDevice) zoneHassConfigurat c.CommandTopic = "command" c.ModeCommandTopic = "mode_command" c.ModeStateTopic = "state" + c.MqttRetain = true for _, mode := range zoneModes { c.Modes = append(c.Modes, mode.Key) From 08c9c340d0357d679f816cd9181612597dbb6378 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:45:22 +0000 Subject: [PATCH 086/135] feat: retain for others --- pkg/model/moisture_sensor_hass_configuration.go | 2 ++ pkg/model/water_outlet_hass_configuration.go | 2 ++ pkg/model/zone.go | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index 23debbd..1c41972 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -12,6 +12,7 @@ type moistureSensorHassConfiguration struct { HassDevice *HassDevice `json:"device"` PayloadAvailable string `json:"payload_available"` PayloadNotAvailable string `json:"payload_not_available"` + MqttRetain bool `json:"retain"` } func (c moistureSensorHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { @@ -46,6 +47,7 @@ func makeMoistureSensorConfiugurationForDevice(device *HassDevice) moistureSenso c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable c.HassDevice = device + c.MqttRetain = false return c } diff --git a/pkg/model/water_outlet_hass_configuration.go b/pkg/model/water_outlet_hass_configuration.go index 2158464..978de4d 100644 --- a/pkg/model/water_outlet_hass_configuration.go +++ b/pkg/model/water_outlet_hass_configuration.go @@ -16,6 +16,7 @@ type waterOutletHassConfiguration struct { CommandTopic string `json:"command_topic"` StateOn string `json:"payload_on"` StateOff string `json:"payload_off"` + MqttRetain bool `json:"retain"` } func (c waterOutletHassConfiguration) WithGlobalTopicPrefix(prefix string, device *HassDevice, entity HassAutoDiscoverable) HassAutoDiscoverPayload { @@ -47,6 +48,7 @@ func makeWaterOutletHassConfiguration(outlet WaterOutlet, device *HassDevice) wa c.PayloadNotAvailable = device.PayloadNotAvailable c.HassDevice = device c.CommandTopic = "command" + c.MqttRetain = true return c } diff --git a/pkg/model/zone.go b/pkg/model/zone.go index cf4283d..8e0fc52 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -18,8 +18,8 @@ func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []* Mode: getDefaultZoneMode(), MoistureSensors: sensors, WaterOutlets: waterOutlets, - Enabled: true, - TargetMoisture: MakeMoistureLevel(50), // TODO set to zero on boot? + Enabled: false, + TargetMoisture: MakeMoistureLevel(0), } zone.AverageMoistureSensor = newZoneAverageMoistureSensor(zone) From 46b41d6136c89c32dc19eec64f007cc064660b60 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:47:58 +0000 Subject: [PATCH 087/135] feat: sync the states back to hass immediately --- pkg/app/arduino.go | 2 +- pkg/app/hass.go | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index f8d195f..ec1864d 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -139,7 +139,7 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { } outlet.ActualState = actualState - app.publishWaterOutletState(outlet) + app.sendWaterOutletStateToHass(outlet) } } diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 770e90f..9a045a9 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -26,6 +26,7 @@ func (app *App) listenForWaterOutletCommands() { } app.arduino.SetWaterOutletState(outlet) + app.sendWaterOutletStateToHass(outlet) }, ) } @@ -50,6 +51,8 @@ func (app *App) listenForZoneCommands() { } else if string(message.Payload()) == constants.HASS_STATE_OFF { zone.Enabled = false } + + app.sendZoneStateToHass(zone) }, ) @@ -63,6 +66,7 @@ func (app *App) listenForZoneCommands() { } zone.TargetMoisture = model.MakeMoistureLevel(uint(moisturePercent)) + app.sendZoneStateToHass(zone) }, ) } @@ -72,7 +76,7 @@ func (app *App) listenForZoneCommands() { } } -func (app *App) publishWaterOutletState(outlet *model.WaterOutlet) error { +func (app *App) sendWaterOutletStateToHass(outlet *model.WaterOutlet) error { payload, err := json.Marshal(outlet) @@ -90,7 +94,7 @@ func (app *App) publishWaterOutletState(outlet *model.WaterOutlet) error { return nil } -func (app *App) sendZoneStateToHas(zone *model.Zone) error { +func (app *App) sendZoneStateToHass(zone *model.Zone) error { average, err := persistence.GetAverageReadingForSensorsSince(zone.MoistureSensors, 2*time.Minute) @@ -130,7 +134,7 @@ func (app *App) startSendingZoneStateToHass() chan bool { select { case <-ticker.C: for _, zone := range app.zones { - go app.sendZoneStateToHas(zone) + go app.sendZoneStateToHass(zone) } case <-quit: ticker.Stop() From 22ac70b0bdab113eb5e9c422d41ad8ab84457e24 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:51:07 +0000 Subject: [PATCH 088/135] feat: listen for zone modes --- pkg/app/hass.go | 16 ++++++++++++++++ pkg/model/zone.go | 4 ++++ pkg/model/zone_mode.go | 4 ++-- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 9a045a9..0a81e39 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -69,6 +69,22 @@ func (app *App) listenForZoneCommands() { app.sendZoneStateToHass(zone) }, ) + + app.hass.Subscribe( + zone.MqttModeCommandTopic(app.hassDevice), + func(message mqtt.Message) { + mode, err := model.GetZoneModeFromKey( + string(message.Payload()), + ) + + if err != nil { + return + } + + zone.Mode = mode + app.sendZoneStateToHass(zone) + }, + ) } for _, zone := range app.zones { diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 8e0fc52..33fcc56 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -43,6 +43,10 @@ func (zone Zone) MqttTargetMoistureCommandTopic(device *HassDevice) string { return zone.MqttTopic(device) + "/" + makeZoneHassConfiguration(zone, device).TargetMoistureCommandTopic } +func (zone Zone) MqttModeCommandTopic(device *HassDevice) string { + return zone.MqttTopic(device) + "/" + makeZoneHassConfiguration(zone, device).ModeCommandTopic +} + func (zone Zone) MqttStateTopic(device *HassDevice) string { return zone.MqttTopic(device) + "/" + makeZoneHassConfiguration(zone, device).StateTopic } diff --git a/pkg/model/zone_mode.go b/pkg/model/zone_mode.go index fbfab16..1fcb435 100644 --- a/pkg/model/zone_mode.go +++ b/pkg/model/zone_mode.go @@ -23,7 +23,7 @@ var zoneModes = []*ZoneMode{ } func getDefaultZoneMode() *ZoneMode { - mode, err := getZoneModeFromKey("normal") + mode, err := GetZoneModeFromKey("normal") if err != nil { log.Fatal(err) @@ -32,7 +32,7 @@ func getDefaultZoneMode() *ZoneMode { return mode } -func getZoneModeFromKey(key string) (*ZoneMode, error) { +func GetZoneModeFromKey(key string) (*ZoneMode, error) { for _, mode := range zoneModes { if mode.Key == key { return mode, nil From a5764c9ef4722585eba774b2f3cf2b00c7138d93 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 17:51:51 +0000 Subject: [PATCH 089/135] chore: remove debug --- pkg/app/arduino.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index ec1864d..8b270cf 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -2,7 +2,6 @@ package app import ( "errors" - "fmt" "time" "github.com/mewejo/go-watering/pkg/arduino" @@ -144,8 +143,6 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { } for line := range dataChan { - fmt.Println(line) // TODO remove this - heartbeat, err := model.MakeArduinoHeartbeatFromString(line) if err == nil { From ab01ffa4bcb6776f033f554140bfb1e4f3f217bc Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 18:04:40 +0000 Subject: [PATCH 090/135] feat: make things snappier --- pkg/app/arduino.go | 6 +++--- pkg/app/hass.go | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 8b270cf..052dde2 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -48,7 +48,7 @@ func (app *App) initialiseArduino() (chan bool, <-chan string) { } func (app *App) startSendingWaterStatesToArduino() chan bool { - ticker := time.NewTicker(1 * time.Second) + ticker := time.NewTicker(250 * time.Millisecond) quit := make(chan bool) @@ -70,7 +70,7 @@ func (app *App) startSendingWaterStatesToArduino() chan bool { } func (app *App) startRequestingWaterOutletStates() chan bool { - ticker := time.NewTicker(500 * time.Millisecond) + ticker := time.NewTicker(250 * time.Millisecond) quit := make(chan bool) @@ -90,7 +90,7 @@ func (app *App) startRequestingWaterOutletStates() chan bool { } func (app *App) startRequestingMoistureSensorReadings() chan bool { - ticker := time.NewTicker(1 * time.Second) + ticker := time.NewTicker(250 * time.Millisecond) quit := make(chan bool) diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 0a81e39..7a2caa8 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -26,7 +26,6 @@ func (app *App) listenForWaterOutletCommands() { } app.arduino.SetWaterOutletState(outlet) - app.sendWaterOutletStateToHass(outlet) }, ) } From ba448b0f61b910d7ea42f7d5fed483f0c0275da2 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 19:49:30 +0000 Subject: [PATCH 091/135] wip: basic poc for zone management --- pkg/app/app.go | 2 ++ pkg/app/manage_zones.go | 40 ++++++++++++++++++++++++++++++++++++++++ pkg/model/zone.go | 6 ++++++ 3 files changed, 48 insertions(+) create mode 100644 pkg/app/manage_zones.go diff --git a/pkg/app/app.go b/pkg/app/app.go index 1b8b9eb..aad8ac2 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -53,6 +53,7 @@ func (app *App) Run() { stopSendingOutletStatesToArduinoChan := app.startSendingWaterStatesToArduino() stopSendingMoistureSensorReadingsToHassChan := app.startSendingMoistureSensorReadingsToHass() stopSendingZoneStatesToHassChan := app.startSendingZoneStateToHass() + stopRegulatingZonesChan := app.regulateZones() app.listenForWaterOutletCommands() app.listenForZoneCommands() @@ -67,6 +68,7 @@ func (app *App) Run() { close(stopSendingMoistureSensorReadingsToHassChan) close(closeArduinoChan) close(stopSendingZoneStatesToHassChan) + close(stopRegulatingZonesChan) app.markHassNotAvailable() app.hass.Disconnect() os.Exit(0) diff --git a/pkg/app/manage_zones.go b/pkg/app/manage_zones.go new file mode 100644 index 0000000..f1476f4 --- /dev/null +++ b/pkg/app/manage_zones.go @@ -0,0 +1,40 @@ +package app + +import ( + "time" + + "github.com/mewejo/go-watering/pkg/model" +) + +func (app *App) regulateZones() chan bool { + ticker := time.NewTicker(1 * time.Second) + + quit := make(chan bool) + + go func() { + for { + select { + case <-ticker.C: + for _, zone := range app.zones { + go app.regulateZone(zone) + } + case <-quit: + ticker.Stop() + return + } + } + }() + + return quit +} + +func (app *App) regulateZone(zone *model.Zone) error { + if !zone.Enabled { + zone.SetWaterOutletState(false) + return nil + } + + zone.SetWaterOutletState(true) + + return nil +} diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 33fcc56..71fc983 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -27,6 +27,12 @@ func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []* return zone } +func (zone *Zone) SetWaterOutletState(state bool) { + for _, outlet := range zone.WaterOutlets { + outlet.TargetState = state + } +} + func (zone Zone) MqttTopic(device *HassDevice) string { return "humidifier/" + device.Namespace + "/zone-" + zone.Id } From 9d23086de7be2ff8b400fe92060c7348facba543 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 19:55:22 +0000 Subject: [PATCH 092/135] refactor: outlet state --- pkg/app/manage_zones.go | 17 +++++++++++++---- pkg/model/zone.go | 7 +------ pkg/model/zone_hass_state.go | 18 ++++++++++-------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/pkg/app/manage_zones.go b/pkg/app/manage_zones.go index f1476f4..547c869 100644 --- a/pkg/app/manage_zones.go +++ b/pkg/app/manage_zones.go @@ -11,12 +11,17 @@ func (app *App) regulateZones() chan bool { quit := make(chan bool) + handleZone := func(zone *model.Zone) { + app.regulateZone(zone) + app.ensureZoneWaterOutletState(zone) + } + go func() { for { select { case <-ticker.C: for _, zone := range app.zones { - go app.regulateZone(zone) + go handleZone(zone) } case <-quit: ticker.Stop() @@ -28,13 +33,17 @@ func (app *App) regulateZones() chan bool { return quit } +func (app *App) ensureZoneWaterOutletState(zone *model.Zone) { + for _, outlet := range zone.WaterOutlets { + outlet.TargetState = zone.WaterOutletsOpen + } +} + func (app *App) regulateZone(zone *model.Zone) error { if !zone.Enabled { - zone.SetWaterOutletState(false) + zone.WaterOutletsOpen = false return nil } - zone.SetWaterOutletState(true) - return nil } diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 71fc983..d7c8af6 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -9,6 +9,7 @@ type Zone struct { WaterOutlets []*WaterOutlet Enabled bool AverageMoistureSensor *ZoneAverageMoistureSensor + WaterOutletsOpen bool } func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []*WaterOutlet) *Zone { @@ -27,12 +28,6 @@ func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []* return zone } -func (zone *Zone) SetWaterOutletState(state bool) { - for _, outlet := range zone.WaterOutlets { - outlet.TargetState = state - } -} - func (zone Zone) MqttTopic(device *HassDevice) string { return "humidifier/" + device.Namespace + "/zone-" + zone.Id } diff --git a/pkg/model/zone_hass_state.go b/pkg/model/zone_hass_state.go index 68c66de..2c2e34e 100644 --- a/pkg/model/zone_hass_state.go +++ b/pkg/model/zone_hass_state.go @@ -1,17 +1,19 @@ package model type ZoneHassState struct { - Mode *ZoneMode `json:"mode"` - AverageMoisture MoistureLevel `json:"average_moisture"` - TargetMoisture MoistureLevel `json:"target_moisture"` - Enabled bool `json:"enabled"` + Mode *ZoneMode `json:"mode"` + AverageMoisture MoistureLevel `json:"average_moisture"` + TargetMoisture MoistureLevel `json:"target_moisture"` + Enabled bool `json:"enabled"` + WaterOutletsOpen bool `json:"water_outlets_open"` } func MakeZoneHassState(zone *Zone, averageMoisture MoistureLevel) ZoneHassState { return ZoneHassState{ - Mode: zone.Mode, - AverageMoisture: averageMoisture, - Enabled: zone.Enabled, - TargetMoisture: zone.TargetMoisture, + Mode: zone.Mode, + AverageMoisture: averageMoisture, + Enabled: zone.Enabled, + TargetMoisture: zone.TargetMoisture, + WaterOutletsOpen: zone.WaterOutletsOpen, } } From 93af06161d7559a8d0f1a2450af01c4122371534 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 19:58:32 +0000 Subject: [PATCH 093/135] feat: record when zone outlet state changed --- pkg/app/manage_zones.go | 4 ++-- pkg/model/zone.go | 26 +++++++++++++++++--------- pkg/model/zone_hass_state.go | 20 ++++++++++---------- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/pkg/app/manage_zones.go b/pkg/app/manage_zones.go index 547c869..9706f52 100644 --- a/pkg/app/manage_zones.go +++ b/pkg/app/manage_zones.go @@ -35,13 +35,13 @@ func (app *App) regulateZones() chan bool { func (app *App) ensureZoneWaterOutletState(zone *model.Zone) { for _, outlet := range zone.WaterOutlets { - outlet.TargetState = zone.WaterOutletsOpen + outlet.TargetState = zone.WaterOutletsState } } func (app *App) regulateZone(zone *model.Zone) error { if !zone.Enabled { - zone.WaterOutletsOpen = false + zone.SetWaterOutletsState(false) return nil } diff --git a/pkg/model/zone.go b/pkg/model/zone.go index d7c8af6..8d3a33a 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -1,15 +1,18 @@ package model +import "time" + type Zone struct { - Id string - Name string - Mode *ZoneMode - TargetMoisture MoistureLevel - MoistureSensors []*MoistureSensor - WaterOutlets []*WaterOutlet - Enabled bool - AverageMoistureSensor *ZoneAverageMoistureSensor - WaterOutletsOpen bool + Id string + Name string + Mode *ZoneMode + TargetMoisture MoistureLevel + MoistureSensors []*MoistureSensor + WaterOutlets []*WaterOutlet + Enabled bool + AverageMoistureSensor *ZoneAverageMoistureSensor + WaterOutletsState bool + WaterOutletsStateChangedAt time.Time } func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []*WaterOutlet) *Zone { @@ -28,6 +31,11 @@ func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []* return zone } +func (zone *Zone) SetWaterOutletsState(state bool) { + zone.WaterOutletsState = state + zone.WaterOutletsStateChangedAt = time.Now() +} + func (zone Zone) MqttTopic(device *HassDevice) string { return "humidifier/" + device.Namespace + "/zone-" + zone.Id } diff --git a/pkg/model/zone_hass_state.go b/pkg/model/zone_hass_state.go index 2c2e34e..fd895a0 100644 --- a/pkg/model/zone_hass_state.go +++ b/pkg/model/zone_hass_state.go @@ -1,19 +1,19 @@ package model type ZoneHassState struct { - Mode *ZoneMode `json:"mode"` - AverageMoisture MoistureLevel `json:"average_moisture"` - TargetMoisture MoistureLevel `json:"target_moisture"` - Enabled bool `json:"enabled"` - WaterOutletsOpen bool `json:"water_outlets_open"` + Mode *ZoneMode `json:"mode"` + AverageMoisture MoistureLevel `json:"average_moisture"` + TargetMoisture MoistureLevel `json:"target_moisture"` + Enabled bool `json:"enabled"` + WaterOutletsState bool `json:"water_outlets_open"` } func MakeZoneHassState(zone *Zone, averageMoisture MoistureLevel) ZoneHassState { return ZoneHassState{ - Mode: zone.Mode, - AverageMoisture: averageMoisture, - Enabled: zone.Enabled, - TargetMoisture: zone.TargetMoisture, - WaterOutletsOpen: zone.WaterOutletsOpen, + Mode: zone.Mode, + AverageMoisture: averageMoisture, + Enabled: zone.Enabled, + TargetMoisture: zone.TargetMoisture, + WaterOutletsState: zone.WaterOutletsState, } } From 8fd53fba38f3759f397deeca47e0b505c28deb39 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 20:00:05 +0000 Subject: [PATCH 094/135] feat: record only when changed --- pkg/model/zone.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 8d3a33a..57bee4e 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -17,13 +17,15 @@ type Zone struct { func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []*WaterOutlet) *Zone { zone := &Zone{ - Id: id, - Name: name, - Mode: getDefaultZoneMode(), - MoistureSensors: sensors, - WaterOutlets: waterOutlets, - Enabled: false, - TargetMoisture: MakeMoistureLevel(0), + Id: id, + Name: name, + Mode: getDefaultZoneMode(), + MoistureSensors: sensors, + WaterOutlets: waterOutlets, + Enabled: false, + TargetMoisture: MakeMoistureLevel(0), + WaterOutletsState: false, + WaterOutletsStateChangedAt: time.Now(), } zone.AverageMoistureSensor = newZoneAverageMoistureSensor(zone) @@ -32,8 +34,14 @@ func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []* } func (zone *Zone) SetWaterOutletsState(state bool) { + + changing := zone.WaterOutletsState != state + zone.WaterOutletsState = state - zone.WaterOutletsStateChangedAt = time.Now() + + if changing { + zone.WaterOutletsStateChangedAt = time.Now() + } } func (zone Zone) MqttTopic(device *HassDevice) string { From e37d046c2c2d3944710faa8678d95b559b5dbf9d Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 20:02:07 +0000 Subject: [PATCH 095/135] wip: basic implementation without hysteresis --- pkg/app/manage_zones.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/app/manage_zones.go b/pkg/app/manage_zones.go index 9706f52..4ff5608 100644 --- a/pkg/app/manage_zones.go +++ b/pkg/app/manage_zones.go @@ -4,6 +4,7 @@ import ( "time" "github.com/mewejo/go-watering/pkg/model" + "github.com/mewejo/go-watering/pkg/persistence" ) func (app *App) regulateZones() chan bool { @@ -45,5 +46,19 @@ func (app *App) regulateZone(zone *model.Zone) error { return nil } + averageMoisture, err := persistence.GetAverageReadingForSensorsSince(zone.MoistureSensors, 2*time.Minute) + + if err != nil { + return err + } + + if averageMoisture.Percentage < zone.TargetMoisture.Percentage { + zone.SetWaterOutletsState(true) + return nil + } else if averageMoisture.Percentage > zone.TargetMoisture.Percentage { + zone.SetWaterOutletsState(false) + return nil + } + return nil } From bdd335d7c9d1ce4225d719690a22166223bc78a0 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 20:06:09 +0000 Subject: [PATCH 096/135] feat: regulate zone moisture level --- pkg/app/manage_zones.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pkg/app/manage_zones.go b/pkg/app/manage_zones.go index 4ff5608..0c696ac 100644 --- a/pkg/app/manage_zones.go +++ b/pkg/app/manage_zones.go @@ -46,18 +46,22 @@ func (app *App) regulateZone(zone *model.Zone) error { return nil } - averageMoisture, err := persistence.GetAverageReadingForSensorsSince(zone.MoistureSensors, 2*time.Minute) + if zone.Mode.Key == "normal" { + averageMoisture, err := persistence.GetAverageReadingForSensorsSince(zone.MoistureSensors, 2*time.Minute) - if err != nil { - return err - } + if err != nil { + return err + } - if averageMoisture.Percentage < zone.TargetMoisture.Percentage { - zone.SetWaterOutletsState(true) - return nil - } else if averageMoisture.Percentage > zone.TargetMoisture.Percentage { - zone.SetWaterOutletsState(false) - return nil + var hysteresis uint = 5 + + if averageMoisture.Percentage < (zone.TargetMoisture.Percentage - hysteresis) { + zone.SetWaterOutletsState(true) + return nil + } else if averageMoisture.Percentage > (zone.TargetMoisture.Percentage + hysteresis) { + zone.SetWaterOutletsState(false) + return nil + } } return nil From e57c7a1b85c215cc562d943f3bbbc485e7e5a5a9 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 20:08:37 +0000 Subject: [PATCH 097/135] feat: log err --- pkg/app/manage_zones.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/app/manage_zones.go b/pkg/app/manage_zones.go index 0c696ac..01400c9 100644 --- a/pkg/app/manage_zones.go +++ b/pkg/app/manage_zones.go @@ -1,6 +1,7 @@ package app import ( + "log" "time" "github.com/mewejo/go-watering/pkg/model" @@ -13,7 +14,10 @@ func (app *App) regulateZones() chan bool { quit := make(chan bool) handleZone := func(zone *model.Zone) { - app.regulateZone(zone) + if err := app.regulateZone(zone); err != nil { + log.Println(err) + } + app.ensureZoneWaterOutletState(zone) } From 9dc5de9d4966da46878827dc14263cf3513c32b9 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 20:12:30 +0000 Subject: [PATCH 098/135] feat: boost mode + prevent flooding --- pkg/app/manage_zones.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pkg/app/manage_zones.go b/pkg/app/manage_zones.go index 01400c9..3118d1a 100644 --- a/pkg/app/manage_zones.go +++ b/pkg/app/manage_zones.go @@ -15,9 +15,10 @@ func (app *App) regulateZones() chan bool { handleZone := func(zone *model.Zone) { if err := app.regulateZone(zone); err != nil { - log.Println(err) + log.Println("regulating zone " + zone.Name + ": " + err.Error()) } + app.preventZoneFlooding(zone) app.ensureZoneWaterOutletState(zone) } @@ -44,6 +45,18 @@ func (app *App) ensureZoneWaterOutletState(zone *model.Zone) { } } +func (app *App) preventZoneFlooding(zone *model.Zone) { + if !zone.WaterOutletsState { + return + } + + cutoff := time.Now().Add(-(time.Minute * 30)) + + if zone.WaterOutletsStateChangedAt.Before(cutoff) { + zone.SetWaterOutletsState(false) + } +} + func (app *App) regulateZone(zone *model.Zone) error { if !zone.Enabled { zone.SetWaterOutletsState(false) @@ -66,6 +79,8 @@ func (app *App) regulateZone(zone *model.Zone) error { zone.SetWaterOutletsState(false) return nil } + } else if zone.Mode.Key == "boost" { + zone.SetWaterOutletsState(true) } return nil From e91b805505e5ca0474ef61b172e408c72d01cfdb Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 21:34:38 +0000 Subject: [PATCH 099/135] fix: set zone mode to normal when turning off, else boost will turn it on again --- pkg/app/manage_zones.go | 1 + pkg/model/zone.go | 2 +- pkg/model/zone_mode.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/app/manage_zones.go b/pkg/app/manage_zones.go index 3118d1a..7bed47c 100644 --- a/pkg/app/manage_zones.go +++ b/pkg/app/manage_zones.go @@ -53,6 +53,7 @@ func (app *App) preventZoneFlooding(zone *model.Zone) { cutoff := time.Now().Add(-(time.Minute * 30)) if zone.WaterOutletsStateChangedAt.Before(cutoff) { + zone.Mode = model.GetDefaultZoneMode() zone.SetWaterOutletsState(false) } } diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 57bee4e..6ad374b 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -19,7 +19,7 @@ func NewZone(id string, name string, sensors []*MoistureSensor, waterOutlets []* zone := &Zone{ Id: id, Name: name, - Mode: getDefaultZoneMode(), + Mode: GetDefaultZoneMode(), MoistureSensors: sensors, WaterOutlets: waterOutlets, Enabled: false, diff --git a/pkg/model/zone_mode.go b/pkg/model/zone_mode.go index 1fcb435..71327ee 100644 --- a/pkg/model/zone_mode.go +++ b/pkg/model/zone_mode.go @@ -22,7 +22,7 @@ var zoneModes = []*ZoneMode{ }, } -func getDefaultZoneMode() *ZoneMode { +func GetDefaultZoneMode() *ZoneMode { mode, err := GetZoneModeFromKey("normal") if err != nil { From f4b87a6fac1ec8387c445823cf4fc99733bf1bed Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 21:36:17 +0000 Subject: [PATCH 100/135] debug: print water change states --- pkg/model/zone.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/model/zone.go b/pkg/model/zone.go index 6ad374b..ceef6ca 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -1,6 +1,9 @@ package model -import "time" +import ( + "fmt" + "time" +) type Zone struct { Id string @@ -40,6 +43,12 @@ func (zone *Zone) SetWaterOutletsState(state bool) { zone.WaterOutletsState = state if changing { + if state { + fmt.Println("Turning ON water for zone: " + zone.Name) + } else { + fmt.Println("Turning OFF water for zone: " + zone.Name) + } + zone.WaterOutletsStateChangedAt = time.Now() } } From d6b75a0e221da5a729717cdd1c0c8f42c78f6794 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 21:44:25 +0000 Subject: [PATCH 101/135] fix: uint underflow --- pkg/app/manage_zones.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/app/manage_zones.go b/pkg/app/manage_zones.go index 7bed47c..78740e7 100644 --- a/pkg/app/manage_zones.go +++ b/pkg/app/manage_zones.go @@ -71,12 +71,15 @@ func (app *App) regulateZone(zone *model.Zone) error { return err } - var hysteresis uint = 5 + var hysteresis int = 5 - if averageMoisture.Percentage < (zone.TargetMoisture.Percentage - hysteresis) { + averageMoisturePercent := int(averageMoisture.Percentage) + targetMoisturePercent := int(zone.TargetMoisture.Percentage) + + if averageMoisturePercent < (targetMoisturePercent - hysteresis) { zone.SetWaterOutletsState(true) return nil - } else if averageMoisture.Percentage > (zone.TargetMoisture.Percentage + hysteresis) { + } else if averageMoisturePercent > (targetMoisturePercent + hysteresis) { zone.SetWaterOutletsState(false) return nil } From 94ab1eb9efa898b14bb679812183069400c53ff8 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 21:45:14 +0000 Subject: [PATCH 102/135] debug: turn off after 3 sec? --- pkg/app/manage_zones.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/app/manage_zones.go b/pkg/app/manage_zones.go index 78740e7..10ca901 100644 --- a/pkg/app/manage_zones.go +++ b/pkg/app/manage_zones.go @@ -50,7 +50,7 @@ func (app *App) preventZoneFlooding(zone *model.Zone) { return } - cutoff := time.Now().Add(-(time.Minute * 30)) + cutoff := time.Now().Add(-(time.Second * 3)) if zone.WaterOutletsStateChangedAt.Before(cutoff) { zone.Mode = model.GetDefaultZoneMode() From cd993977714fe22fc30d04cc6264881ee18cdc17 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 21:48:20 +0000 Subject: [PATCH 103/135] feat: boost 30 min --- pkg/app/manage_zones.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/app/manage_zones.go b/pkg/app/manage_zones.go index 10ca901..7ba8b31 100644 --- a/pkg/app/manage_zones.go +++ b/pkg/app/manage_zones.go @@ -50,7 +50,16 @@ func (app *App) preventZoneFlooding(zone *model.Zone) { return } - cutoff := time.Now().Add(-(time.Second * 3)) + var cutoffDuration time.Duration + + if zone.Mode.Key == "boost" { + cutoffDuration = time.Minute * 30 + } else { + // Assume normal mode. Forces a recalculation after the water being on continuously for an hour. + cutoffDuration = time.Hour + } + + cutoff := time.Now().Add(-cutoffDuration) if zone.WaterOutletsStateChangedAt.Before(cutoff) { zone.Mode = model.GetDefaultZoneMode() From 8071fc45b7eeba737e942608ca7791109ee4156a Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 21:50:08 +0000 Subject: [PATCH 104/135] debug: does hass send it all back --- pkg/model/zone_mode.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/model/zone_mode.go b/pkg/model/zone_mode.go index 71327ee..e4ba062 100644 --- a/pkg/model/zone_mode.go +++ b/pkg/model/zone_mode.go @@ -20,6 +20,11 @@ var zoneModes = []*ZoneMode{ Name: "Boost", Key: "boost", }, + + { + Name: "Boost 1 hour", + Key: "Boost 1 hour", + }, } func GetDefaultZoneMode() *ZoneMode { From 6317b936a003484ed4aaa3b581ff4118c362a518 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 21:57:22 +0000 Subject: [PATCH 105/135] feat: smarten up zone modes --- pkg/app/manage_zones.go | 15 +++------------ pkg/model/zone_mode.go | 26 +++++++++++++++++--------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/pkg/app/manage_zones.go b/pkg/app/manage_zones.go index 7ba8b31..ec7292c 100644 --- a/pkg/app/manage_zones.go +++ b/pkg/app/manage_zones.go @@ -50,16 +50,7 @@ func (app *App) preventZoneFlooding(zone *model.Zone) { return } - var cutoffDuration time.Duration - - if zone.Mode.Key == "boost" { - cutoffDuration = time.Minute * 30 - } else { - // Assume normal mode. Forces a recalculation after the water being on continuously for an hour. - cutoffDuration = time.Hour - } - - cutoff := time.Now().Add(-cutoffDuration) + cutoff := time.Now().Add(-zone.Mode.CutOffDuration) if zone.WaterOutletsStateChangedAt.Before(cutoff) { zone.Mode = model.GetDefaultZoneMode() @@ -73,7 +64,7 @@ func (app *App) regulateZone(zone *model.Zone) error { return nil } - if zone.Mode.Key == "normal" { + if zone.Mode == model.GetDefaultZoneMode() { averageMoisture, err := persistence.GetAverageReadingForSensorsSince(zone.MoistureSensors, 2*time.Minute) if err != nil { @@ -92,7 +83,7 @@ func (app *App) regulateZone(zone *model.Zone) error { zone.SetWaterOutletsState(false) return nil } - } else if zone.Mode.Key == "boost" { + } else { zone.SetWaterOutletsState(true) } diff --git a/pkg/model/zone_mode.go b/pkg/model/zone_mode.go index e4ba062..4366538 100644 --- a/pkg/model/zone_mode.go +++ b/pkg/model/zone_mode.go @@ -3,32 +3,40 @@ package model import ( "errors" "log" + "time" ) type ZoneMode struct { - Name string `json:"name"` - Key string `json:"key"` + Key string `json:"key"` + CutOffDuration time.Duration } +const normalMode = "normal" + var zoneModes = []*ZoneMode{ { - Name: "Normal", - Key: "normal", + Key: normalMode, + CutOffDuration: time.Minute * 30, + }, + + { + Key: "15 Minute Boost", + CutOffDuration: time.Minute * 15, }, { - Name: "Boost", - Key: "boost", + Key: "30 Minute Boost", + CutOffDuration: time.Minute * 30, }, { - Name: "Boost 1 hour", - Key: "Boost 1 hour", + Key: "1 hour Boost", + CutOffDuration: time.Hour, }, } func GetDefaultZoneMode() *ZoneMode { - mode, err := GetZoneModeFromKey("normal") + mode, err := GetZoneModeFromKey(normalMode) if err != nil { log.Fatal(err) From e480dcd6bdca27e390d70859fae10ddfae4400b7 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 21:58:26 +0000 Subject: [PATCH 106/135] debug: testing times --- pkg/model/zone_mode.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/model/zone_mode.go b/pkg/model/zone_mode.go index 4366538..184db27 100644 --- a/pkg/model/zone_mode.go +++ b/pkg/model/zone_mode.go @@ -21,17 +21,17 @@ var zoneModes = []*ZoneMode{ { Key: "15 Minute Boost", - CutOffDuration: time.Minute * 15, + CutOffDuration: time.Second * 5, }, { Key: "30 Minute Boost", - CutOffDuration: time.Minute * 30, + CutOffDuration: time.Second * 10, }, { Key: "1 hour Boost", - CutOffDuration: time.Hour, + CutOffDuration: time.Second * 15, }, } From 46f60c935be5acb3ef4d470bd17cf6f76918fc1f Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 22:01:43 +0000 Subject: [PATCH 107/135] fix: do not include duration in json --- pkg/model/zone_mode.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/model/zone_mode.go b/pkg/model/zone_mode.go index 184db27..064a26f 100644 --- a/pkg/model/zone_mode.go +++ b/pkg/model/zone_mode.go @@ -7,8 +7,8 @@ import ( ) type ZoneMode struct { - Key string `json:"key"` - CutOffDuration time.Duration + Key string `json:"key"` + CutOffDuration time.Duration `json:"-"` } const normalMode = "normal" From b503f7fb2020b5ac70a9268379dd8146e1fa08ef Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 22:04:01 +0000 Subject: [PATCH 108/135] debug: put values back to true --- pkg/model/zone_mode.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/model/zone_mode.go b/pkg/model/zone_mode.go index 064a26f..edcea7d 100644 --- a/pkg/model/zone_mode.go +++ b/pkg/model/zone_mode.go @@ -21,17 +21,17 @@ var zoneModes = []*ZoneMode{ { Key: "15 Minute Boost", - CutOffDuration: time.Second * 5, + CutOffDuration: time.Minute * 15, }, { Key: "30 Minute Boost", - CutOffDuration: time.Second * 10, + CutOffDuration: time.Minute * 30, }, { Key: "1 hour Boost", - CutOffDuration: time.Second * 15, + CutOffDuration: time.Hour, }, } From aff7aa392c1848cb8d98fe4fdc4df4510da2ba57 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 22:11:23 +0000 Subject: [PATCH 109/135] feat: send default modes --- pkg/app/app.go | 1 + pkg/app/hass.go | 15 +++++++++++++++ pkg/hass/mqtt_message.go | 10 ++++++++++ 3 files changed, 26 insertions(+) diff --git a/pkg/app/app.go b/pkg/app/app.go index aad8ac2..0609dd2 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -44,6 +44,7 @@ func (app *App) Run() { } app.startHassAvailabilityTimer() + app.sendInitialZoneModesToHass() osExit := app.setupCloseHandler() diff --git a/pkg/app/hass.go b/pkg/app/hass.go index 7a2caa8..7689136 100644 --- a/pkg/app/hass.go +++ b/pkg/app/hass.go @@ -109,6 +109,21 @@ func (app *App) sendWaterOutletStateToHass(outlet *model.WaterOutlet) error { return nil } +func (app *App) sendInitialZoneModesToHass() { + for _, zone := range app.zones { + app.sendZoneModeCommandToHass(zone, model.GetDefaultZoneMode()) + } +} + +func (app *App) sendZoneModeCommandToHass(zone *model.Zone, mode *model.ZoneMode) { + app.hass.Publish( + hass.MakeMqttMessage( + zone.MqttModeCommandTopic(app.hassDevice), + mode.Key, + ).Retained().Qos(2), + ) +} + func (app *App) sendZoneStateToHass(zone *model.Zone) error { average, err := persistence.GetAverageReadingForSensorsSince(zone.MoistureSensors, 2*time.Minute) diff --git a/pkg/hass/mqtt_message.go b/pkg/hass/mqtt_message.go index 7a3cd74..68aeab1 100644 --- a/pkg/hass/mqtt_message.go +++ b/pkg/hass/mqtt_message.go @@ -13,3 +13,13 @@ func MakeMqttMessage(topic string, payload string) MqttMessage { payload: payload, } } + +func (m MqttMessage) Retained() MqttMessage { + m.retain = true + return m +} + +func (m MqttMessage) Qos(qos byte) MqttMessage { + m.qos = qos + return m +} From 671b0b83591cabc4892a17cd1e65ebe7c891e98a Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 22:15:38 +0000 Subject: [PATCH 110/135] feat: new sensors --- pkg/app/hardware.go | 32 ++++++++++++++++---------------- pkg/model/moisture_sensor.go | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pkg/app/hardware.go b/pkg/app/hardware.go index d8c77ee..e0c1885 100644 --- a/pkg/app/hardware.go +++ b/pkg/app/hardware.go @@ -14,38 +14,38 @@ func (app *App) configureHardware() { app.waterOutlets = append(app.waterOutlets, waterOutlet3) app.waterOutlets = append(app.waterOutlets, waterOutlet4) - moistureSensor1 := model.MakeMoistureSensor(1, "Sensor #1") - moistureSensor2 := model.MakeMoistureSensor(2, "Sensor #2") - moistureSensor3 := model.MakeMoistureSensor(3, "Sensor #3") - moistureSensor4 := model.MakeMoistureSensor(4, "Sensor #4") - moistureSensor5 := model.MakeMoistureSensor(5, "Sensor #5") - moistureSensor6 := model.MakeMoistureSensor(6, "Sensor #6") - - app.moistureSensors = append(app.moistureSensors, &moistureSensor1) - app.moistureSensors = append(app.moistureSensors, &moistureSensor2) - app.moistureSensors = append(app.moistureSensors, &moistureSensor3) - app.moistureSensors = append(app.moistureSensors, &moistureSensor4) - app.moistureSensors = append(app.moistureSensors, &moistureSensor5) - app.moistureSensors = append(app.moistureSensors, &moistureSensor6) + moistureSensor1 := model.NewMoistureSensor(1, "Sensor #1") + moistureSensor2 := model.NewMoistureSensor(2, "Sensor #2") + moistureSensor3 := model.NewMoistureSensor(3, "Sensor #3") + moistureSensor4 := model.NewMoistureSensor(4, "Sensor #4") + moistureSensor5 := model.NewMoistureSensor(5, "Sensor #5") + moistureSensor6 := model.NewMoistureSensor(6, "Sensor #6") + + app.moistureSensors = append(app.moistureSensors, moistureSensor1) + app.moistureSensors = append(app.moistureSensors, moistureSensor2) + app.moistureSensors = append(app.moistureSensors, moistureSensor3) + app.moistureSensors = append(app.moistureSensors, moistureSensor4) + app.moistureSensors = append(app.moistureSensors, moistureSensor5) + app.moistureSensors = append(app.moistureSensors, moistureSensor6) app.zones = append(app.zones, model.NewZone( "raised-bed-1", "Raised Bed #1", - []*model.MoistureSensor{&moistureSensor1, &moistureSensor2}, + []*model.MoistureSensor{moistureSensor1, moistureSensor2}, []*model.WaterOutlet{waterOutlet1}, )) app.zones = append(app.zones, model.NewZone( "raised-bed-2", "Raised Bed #2", - []*model.MoistureSensor{&moistureSensor3, &moistureSensor4}, + []*model.MoistureSensor{moistureSensor3, moistureSensor4}, []*model.WaterOutlet{waterOutlet2}, )) app.zones = append(app.zones, model.NewZone( "raised-bed-3", "Raised Bed #3", - []*model.MoistureSensor{&moistureSensor5, &moistureSensor6}, + []*model.MoistureSensor{moistureSensor5, moistureSensor6}, []*model.WaterOutlet{waterOutlet3}, )) } diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go index a72207f..8599aef 100644 --- a/pkg/model/moisture_sensor.go +++ b/pkg/model/moisture_sensor.go @@ -43,8 +43,8 @@ func (ms MoistureSensor) mapRawReadingToPercentage(raw uint) uint { )) } -func MakeMoistureSensor(id uint, name string) MoistureSensor { - return MoistureSensor{ +func NewMoistureSensor(id uint, name string) *MoistureSensor { + return &MoistureSensor{ Id: id, Name: name, DryThreshold: 500, From a78a9f56deddd414d1c92d5e12d631f77433b1ea Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 22:24:59 +0000 Subject: [PATCH 111/135] feat: allow overriding the moisture thresholds --- .env.example | 15 +++++++++++++++ pkg/app/hardware.go | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 74da9c7..8767119 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,18 @@ MQTT_USERNAME= MQTT_PASSWORD= MQTT_CLIENT_ID= HOME_ASSISTANT_DISCOVERY_PREFIX=homeassistant + +# 500/240 are defaults in the code, override here +#MOISTURE_SENSOR_1_DRY=500 +#MOISTURE_SENSOR_1_WET=240 +#MOISTURE_SENSOR_2_DRY=500 +#MOISTURE_SENSOR_3_WET=240 +#MOISTURE_SENSOR_3_DRY=500 +#MOISTURE_SENSOR_3_WET=240 +#MOISTURE_SENSOR_4_DRY=500 +#MOISTURE_SENSOR_4_WET=240 +#MOISTURE_SENSOR_5_DRY=500 +#MOISTURE_SENSOR_5_WET=240 +#MOISTURE_SENSOR_6_DRY=500 +#MOISTURE_SENSOR_6_WET=240 + diff --git a/pkg/app/hardware.go b/pkg/app/hardware.go index e0c1885..9214bc4 100644 --- a/pkg/app/hardware.go +++ b/pkg/app/hardware.go @@ -1,6 +1,12 @@ package app -import "github.com/mewejo/go-watering/pkg/model" +import ( + "fmt" + "os" + "strconv" + + "github.com/mewejo/go-watering/pkg/model" +) func (app *App) configureHardware() { @@ -28,6 +34,33 @@ func (app *App) configureHardware() { app.moistureSensors = append(app.moistureSensors, moistureSensor5) app.moistureSensors = append(app.moistureSensors, moistureSensor6) + for _, moistureSensor := range app.moistureSensors { + dryThreshold := os.Getenv("MOISTURE_SENSOR_" + moistureSensor.IdAsString() + "_DRY") + wetThreshold := os.Getenv("MOISTURE_SENSOR_" + moistureSensor.IdAsString() + "_WET") + + if dryThreshold != "" { + threshold, err := strconv.Atoi(dryThreshold) + + if err != nil { + panic("could not get dry threshold for sensor ID " + moistureSensor.IdAsString() + ": " + err.Error()) + } + + moistureSensor.DryThreshold = uint(threshold) + } + + if wetThreshold != "" { + threshold, err := strconv.Atoi(wetThreshold) + + if err != nil { + panic("could not get wet threshold for sensor ID " + moistureSensor.IdAsString() + ": " + err.Error()) + } + + moistureSensor.WetThreshold = uint(threshold) + } + + fmt.Println(moistureSensor) + } + app.zones = append(app.zones, model.NewZone( "raised-bed-1", "Raised Bed #1", From 6b8aec201f0dd343b08b03773469954a7a26a617 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 22:26:47 +0000 Subject: [PATCH 112/135] fix: another underflow --- pkg/model/moisture_sensor.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/model/moisture_sensor.go b/pkg/model/moisture_sensor.go index 8599aef..e6279d1 100644 --- a/pkg/model/moisture_sensor.go +++ b/pkg/model/moisture_sensor.go @@ -34,13 +34,19 @@ func (ms MoistureSensor) IdAsString() string { } func (ms MoistureSensor) mapRawReadingToPercentage(raw uint) uint { - return uint(number.ChangeRange( + value := number.ChangeRange( float64(raw), float64(ms.DryThreshold), float64(ms.WetThreshold), 0, 100, - )) + ) + + if value < 0 { + value = 0 + } + + return uint(value) } func NewMoistureSensor(id uint, name string) *MoistureSensor { From d20419a7f9332f5d561d57654085e67f1d381dd5 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 22:31:43 +0000 Subject: [PATCH 113/135] feat: debugging --- .env.example | 1 + pkg/app/app.go | 3 +++ pkg/app/arduino.go | 9 +++++++++ pkg/model/moisture_reading.go | 6 +++--- pkg/model/zone.go | 7 ------- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 8767119..3da7f58 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ MQTT_USERNAME= MQTT_PASSWORD= MQTT_CLIENT_ID= HOME_ASSISTANT_DISCOVERY_PREFIX=homeassistant +DEBUG=true # 500/240 are defaults in the code, override here #MOISTURE_SENSOR_1_DRY=500 diff --git a/pkg/app/app.go b/pkg/app/app.go index 0609dd2..8090d8b 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -17,6 +17,7 @@ type App struct { hass *hass.HassClient hassDevice *model.HassDevice arduino *arduino.Arduino + debug bool } func (app *App) setupCloseHandler() <-chan os.Signal { @@ -29,6 +30,8 @@ func (app *App) setupCloseHandler() <-chan os.Signal { func (app *App) Run() { + app.debug = os.Getenv("debug") == "true" + app.configureHardware() err := app.setupHass() diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 052dde2..67d53c0 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -2,6 +2,7 @@ package app import ( "errors" + "log" "time" "github.com/mewejo/go-watering/pkg/arduino" @@ -128,6 +129,14 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { } handleMoistureReading := func(reading model.MoistureReading, sensorId uint) { + if app.debug { + log.Printf( + "Sensor ID %d - Raw value: %d", + sensorId, + reading.Raw, + ) + } + persistence.RecordMoistureReading(sensorId, reading) } diff --git a/pkg/model/moisture_reading.go b/pkg/model/moisture_reading.go index c26b2f5..250ab4b 100644 --- a/pkg/model/moisture_reading.go +++ b/pkg/model/moisture_reading.go @@ -9,13 +9,13 @@ import ( type MoistureReading struct { Time time.Time `json:"recorded_at"` - raw uint + Raw uint MoistureLevel MoistureLevel `json:"percentage"` } func (r *MoistureReading) CalculateMoistureLevelForSensor(sensor *MoistureSensor) { r.MoistureLevel = MakeMoistureLevel( - sensor.mapRawReadingToPercentage(r.raw), + sensor.mapRawReadingToPercentage(r.Raw), ) } @@ -42,6 +42,6 @@ func MakeMoistureReadingFromString(line string) (MoistureReading, uint, error) { return MoistureReading{ Time: time.Now(), - raw: uint(rawValue), + Raw: uint(rawValue), }, uint(sensorId), nil } diff --git a/pkg/model/zone.go b/pkg/model/zone.go index ceef6ca..a608f5e 100644 --- a/pkg/model/zone.go +++ b/pkg/model/zone.go @@ -1,7 +1,6 @@ package model import ( - "fmt" "time" ) @@ -43,12 +42,6 @@ func (zone *Zone) SetWaterOutletsState(state bool) { zone.WaterOutletsState = state if changing { - if state { - fmt.Println("Turning ON water for zone: " + zone.Name) - } else { - fmt.Println("Turning OFF water for zone: " + zone.Name) - } - zone.WaterOutletsStateChangedAt = time.Now() } } From 60b8ea1da722c08803c4f16692f7c1a0aec3c2e3 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 22:32:20 +0000 Subject: [PATCH 114/135] chore: remove debug --- pkg/app/hardware.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/app/hardware.go b/pkg/app/hardware.go index 9214bc4..ee9c699 100644 --- a/pkg/app/hardware.go +++ b/pkg/app/hardware.go @@ -1,7 +1,6 @@ package app import ( - "fmt" "os" "strconv" @@ -57,8 +56,6 @@ func (app *App) configureHardware() { moistureSensor.WetThreshold = uint(threshold) } - - fmt.Println(moistureSensor) } app.zones = append(app.zones, model.NewZone( From 340a5913b571c7cfa927c9e67e00e9a439461fca Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 22:33:33 +0000 Subject: [PATCH 115/135] fix: APP_DEBUG --- .env.example | 2 +- pkg/app/app.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 3da7f58..482d92e 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ MQTT_USERNAME= MQTT_PASSWORD= MQTT_CLIENT_ID= HOME_ASSISTANT_DISCOVERY_PREFIX=homeassistant -DEBUG=true +APP_DEBUG=true # 500/240 are defaults in the code, override here #MOISTURE_SENSOR_1_DRY=500 diff --git a/pkg/app/app.go b/pkg/app/app.go index 8090d8b..842f7eb 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -30,7 +30,7 @@ func (app *App) setupCloseHandler() <-chan os.Signal { func (app *App) Run() { - app.debug = os.Getenv("debug") == "true" + app.debug = os.Getenv("APP_DEBUG") == "true" app.configureHardware() From 62484e68393dfe596083e359c51ad4943c66ad04 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 22:51:27 +0000 Subject: [PATCH 116/135] feat: turn off all outlets on close --- pkg/app/app.go | 1 + pkg/app/hardware.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/pkg/app/app.go b/pkg/app/app.go index 842f7eb..3115ed5 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -67,6 +67,7 @@ func (app *App) Run() { { <-osExit close(stopRequestingOutletStatesChan) + app.forceSetAllWaterOutletStates(false) close(stopSendingOutletStatesToArduinoChan) close(stopRequestingMoistureSensorReadingsChan) close(stopSendingMoistureSensorReadingsToHassChan) diff --git a/pkg/app/hardware.go b/pkg/app/hardware.go index ee9c699..b31fe70 100644 --- a/pkg/app/hardware.go +++ b/pkg/app/hardware.go @@ -4,9 +4,25 @@ import ( "os" "strconv" + "github.com/mewejo/go-watering/pkg/arduino" "github.com/mewejo/go-watering/pkg/model" ) +// Set the states and immediately call the Arduino to action it +func (app *App) forceSetAllWaterOutletStates(state bool) { + for _, outlet := range app.waterOutlets { + outlet.TargetState = state // So they don't somehow get changed, as we're using the bulk command + + // Using the bulk command to do it quickly. + // If this method is used, it's likely on shut down + if state { + app.arduino.SendCommand(arduino.WATER_ON) + } else { + app.arduino.SendCommand(arduino.WATER_OFF) + } + } +} + func (app *App) configureHardware() { waterOutlet1 := model.NewWaterOutlet(1, "Soaker hose #1", false) From ed4a8204fdbbfef3d9207de7b72e696b09da0504 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 22:59:02 +0000 Subject: [PATCH 117/135] feat: check ard's heartbeat --- pkg/app/app.go | 14 +++++++++++--- pkg/app/arduino.go | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 3115ed5..ac1489f 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -52,6 +52,7 @@ func (app *App) Run() { osExit := app.setupCloseHandler() closeArduinoChan, arduinoInputChan := app.initialiseArduino() + arduinoHeartbeatStoppedChan, stopMonitoringArduinoHeartbeatChan := app.monitorArduinoHeartbeat() stopRequestingOutletStatesChan := app.startRequestingWaterOutletStates() stopRequestingMoistureSensorReadingsChan := app.startRequestingMoistureSensorReadings() stopSendingOutletStatesToArduinoChan := app.startSendingWaterStatesToArduino() @@ -64,8 +65,8 @@ func (app *App) Run() { go app.handleArduinoDataInput(arduinoInputChan) - { - <-osExit + doExit := func(code int) { + close(stopMonitoringArduinoHeartbeatChan) close(stopRequestingOutletStatesChan) app.forceSetAllWaterOutletStates(false) close(stopSendingOutletStatesToArduinoChan) @@ -76,7 +77,14 @@ func (app *App) Run() { close(stopRegulatingZonesChan) app.markHassNotAvailable() app.hass.Disconnect() - os.Exit(0) + os.Exit(code) + } + + { + <-osExit + doExit(0) + <-arduinoHeartbeatStoppedChan + doExit(1) } /* diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 67d53c0..2fc8926 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -10,6 +10,31 @@ import ( "github.com/mewejo/go-watering/pkg/persistence" ) +func (app *App) monitorArduinoHeartbeat() (<-chan bool, chan bool) { + ticker := time.NewTicker(1 * time.Second) + deadArduino := make(chan bool) + closeTimer := make(chan bool) + + go func() { + for { + select { + case <-ticker.C: + cutOff := time.Now().Add(-time.Minute) + if app.arduino.LastHeartbeat.Time.Before(cutOff) { + deadArduino <- true + return + } + + case <-closeTimer: + ticker.Stop() + return + } + } + }() + + return deadArduino, closeTimer +} + func (app *App) initialiseArduino() (chan bool, <-chan string) { app.arduino = arduino.NewArduino() From fb5d00cf4e757cef0f91d5efe5289a3caf9973d5 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 22:59:50 +0000 Subject: [PATCH 118/135] debug: ard heartbeat --- pkg/app/app.go | 2 ++ pkg/app/arduino.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index ac1489f..dd33133 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1,6 +1,7 @@ package app import ( + "log" "os" "os/signal" "syscall" @@ -84,6 +85,7 @@ func (app *App) Run() { <-osExit doExit(0) <-arduinoHeartbeatStoppedChan + log.Println("did not receive heartbeat from Arduino in time") doExit(1) } diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 2fc8926..2d0ce0e 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -19,7 +19,7 @@ func (app *App) monitorArduinoHeartbeat() (<-chan bool, chan bool) { for { select { case <-ticker.C: - cutOff := time.Now().Add(-time.Minute) + cutOff := time.Now().Add(-time.Millisecond) if app.arduino.LastHeartbeat.Time.Before(cutOff) { deadArduino <- true return From 33761e92619c534dca43a4de8d168ba7e9cdc7c6 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:01:23 +0000 Subject: [PATCH 119/135] fix: nil pointer ref --- pkg/arduino/arduino.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/arduino/arduino.go b/pkg/arduino/arduino.go index bbb4bda..2d35094 100644 --- a/pkg/arduino/arduino.go +++ b/pkg/arduino/arduino.go @@ -129,5 +129,7 @@ func (a *Arduino) FindAndOpenPort() error { func NewArduino() *Arduino { - return &Arduino{} + return &Arduino{ + LastHeartbeat: &model.ArduinoHeartbeat{}, + } } From de3afbb9e9ccd81e2fd12ec6ae94b1da3115ae29 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:02:04 +0000 Subject: [PATCH 120/135] fix: only check if not zero too --- pkg/app/arduino.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 2d0ce0e..cbaeedd 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -19,7 +19,12 @@ func (app *App) monitorArduinoHeartbeat() (<-chan bool, chan bool) { for { select { case <-ticker.C: + if app.arduino.LastHeartbeat.Time.IsZero() { + continue + } + cutOff := time.Now().Add(-time.Millisecond) + if app.arduino.LastHeartbeat.Time.Before(cutOff) { deadArduino <- true return From b5caea27b15f4a52440327bf9b42cbbcc2f9ef02 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:02:50 +0000 Subject: [PATCH 121/135] debug: dead? --- pkg/app/arduino.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index cbaeedd..06b2379 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -2,6 +2,7 @@ package app import ( "errors" + "fmt" "log" "time" @@ -26,6 +27,7 @@ func (app *App) monitorArduinoHeartbeat() (<-chan bool, chan bool) { cutOff := time.Now().Add(-time.Millisecond) if app.arduino.LastHeartbeat.Time.Before(cutOff) { + fmt.Println("dead ard") deadArduino <- true return } From 146b7fba501c03b7eb676a9c68fb20d94666512a Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:04:01 +0000 Subject: [PATCH 122/135] debug: receiving? --- pkg/app/arduino.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 06b2379..d7e8f0f 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -2,7 +2,6 @@ package app import ( "errors" - "fmt" "log" "time" @@ -27,7 +26,6 @@ func (app *App) monitorArduinoHeartbeat() (<-chan bool, chan bool) { cutOff := time.Now().Add(-time.Millisecond) if app.arduino.LastHeartbeat.Time.Before(cutOff) { - fmt.Println("dead ard") deadArduino <- true return } @@ -157,6 +155,7 @@ func (app *App) findMoistureSensorById(id uint) (*model.MoistureSensor, error) { func (app *App) handleArduinoDataInput(dataChan <-chan string) { handleHeartbeat := func(hb model.ArduinoHeartbeat) { + log.Println("recording heartbeat") app.arduino.LastHeartbeat = &hb } From f27d745c8072bc740d45571817ba3f08f35d5777 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:05:49 +0000 Subject: [PATCH 123/135] debug: heartbeat error --- pkg/app/arduino.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index d7e8f0f..bca931c 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -185,6 +185,8 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { for line := range dataChan { heartbeat, err := model.MakeArduinoHeartbeatFromString(line) + log.Println(err) + if err == nil { go handleHeartbeat(heartbeat) continue From ab366d2a5f0e5e7c807576ebf6127994b7a31183 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:06:26 +0000 Subject: [PATCH 124/135] debug: make it useful --- pkg/app/arduino.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index bca931c..cdd1979 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -185,7 +185,7 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { for line := range dataChan { heartbeat, err := model.MakeArduinoHeartbeatFromString(line) - log.Println(err) + log.Println(err.Error() + ": " + line) if err == nil { go handleHeartbeat(heartbeat) From c32717d54b4e419071e10985a761870347466cf4 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:08:17 +0000 Subject: [PATCH 125/135] debug: whole line --- pkg/app/arduino.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index cdd1979..1516ccb 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -183,10 +183,9 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { } for line := range dataChan { + log.Println(line) heartbeat, err := model.MakeArduinoHeartbeatFromString(line) - log.Println(err.Error() + ": " + line) - if err == nil { go handleHeartbeat(heartbeat) continue From e6f1a115e0e962e78ec7d0bcb0d9321fb272ce12 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:15:11 +0000 Subject: [PATCH 126/135] tweak: slow data comms down --- pkg/app/arduino.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 1516ccb..a957356 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -79,7 +79,7 @@ func (app *App) initialiseArduino() (chan bool, <-chan string) { } func (app *App) startSendingWaterStatesToArduino() chan bool { - ticker := time.NewTicker(250 * time.Millisecond) + ticker := time.NewTicker(1 * time.Second) quit := make(chan bool) @@ -101,7 +101,7 @@ func (app *App) startSendingWaterStatesToArduino() chan bool { } func (app *App) startRequestingWaterOutletStates() chan bool { - ticker := time.NewTicker(250 * time.Millisecond) + ticker := time.NewTicker(1 * time.Second) quit := make(chan bool) @@ -121,7 +121,7 @@ func (app *App) startRequestingWaterOutletStates() chan bool { } func (app *App) startRequestingMoistureSensorReadings() chan bool { - ticker := time.NewTicker(250 * time.Millisecond) + ticker := time.NewTicker(1 * time.Second) quit := make(chan bool) From 893e0822ed328ff2d96fbfb3c7f5326f381b749d Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:17:26 +0000 Subject: [PATCH 127/135] tweak: do not ref the hb --- pkg/app/arduino.go | 4 +--- pkg/arduino/arduino.go | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index a957356..1ae3f7a 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -155,8 +155,7 @@ func (app *App) findMoistureSensorById(id uint) (*model.MoistureSensor, error) { func (app *App) handleArduinoDataInput(dataChan <-chan string) { handleHeartbeat := func(hb model.ArduinoHeartbeat) { - log.Println("recording heartbeat") - app.arduino.LastHeartbeat = &hb + app.arduino.LastHeartbeat = hb } handleMoistureReading := func(reading model.MoistureReading, sensorId uint) { @@ -183,7 +182,6 @@ func (app *App) handleArduinoDataInput(dataChan <-chan string) { } for line := range dataChan { - log.Println(line) heartbeat, err := model.MakeArduinoHeartbeatFromString(line) if err == nil { diff --git a/pkg/arduino/arduino.go b/pkg/arduino/arduino.go index 2d35094..852b744 100644 --- a/pkg/arduino/arduino.go +++ b/pkg/arduino/arduino.go @@ -10,7 +10,7 @@ import ( type Arduino struct { port serial.Port - LastHeartbeat *model.ArduinoHeartbeat + LastHeartbeat model.ArduinoHeartbeat } func (a *Arduino) SetWaterOutletState(outlet *model.WaterOutlet) { @@ -130,6 +130,6 @@ func (a *Arduino) FindAndOpenPort() error { func NewArduino() *Arduino { return &Arduino{ - LastHeartbeat: &model.ArduinoHeartbeat{}, + LastHeartbeat: model.ArduinoHeartbeat{}, } } From 1854d00a413148bc2bdc3197cb38655f271a8cb0 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:18:42 +0000 Subject: [PATCH 128/135] debug: avail hb? --- pkg/app/arduino.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 1ae3f7a..dd3a793 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -20,6 +20,7 @@ func (app *App) monitorArduinoHeartbeat() (<-chan bool, chan bool) { select { case <-ticker.C: if app.arduino.LastHeartbeat.Time.IsZero() { + log.Println("no hb available..") continue } From f9d82747b762ea646d400a0f738e4b6cf699b99a Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:19:20 +0000 Subject: [PATCH 129/135] debug: dead --- pkg/app/arduino.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index dd3a793..9d2076d 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -27,6 +27,7 @@ func (app *App) monitorArduinoHeartbeat() (<-chan bool, chan bool) { cutOff := time.Now().Add(-time.Millisecond) if app.arduino.LastHeartbeat.Time.Before(cutOff) { + log.Println("dead") deadArduino <- true return } From 45e8c26e013bc3c51bdb80f92bd4fc3774957266 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:20:54 +0000 Subject: [PATCH 130/135] tweak: clearer code --- pkg/app/app.go | 6 +++--- pkg/app/arduino.go | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index dd33133..65ab1e3 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -81,10 +81,10 @@ func (app *App) Run() { os.Exit(code) } - { - <-osExit + select { + case <-osExit: doExit(0) - <-arduinoHeartbeatStoppedChan + case <-arduinoHeartbeatStoppedChan: log.Println("did not receive heartbeat from Arduino in time") doExit(1) } diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 9d2076d..1ae3f7a 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -20,14 +20,12 @@ func (app *App) monitorArduinoHeartbeat() (<-chan bool, chan bool) { select { case <-ticker.C: if app.arduino.LastHeartbeat.Time.IsZero() { - log.Println("no hb available..") continue } cutOff := time.Now().Add(-time.Millisecond) if app.arduino.LastHeartbeat.Time.Before(cutOff) { - log.Println("dead") deadArduino <- true return } From 2873ecea30f1d0fd51aeb10af352839769fcf8c3 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:21:39 +0000 Subject: [PATCH 131/135] tweak: check for heartbeat over a min --- pkg/app/arduino.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/app/arduino.go b/pkg/app/arduino.go index 1ae3f7a..e25fb09 100644 --- a/pkg/app/arduino.go +++ b/pkg/app/arduino.go @@ -23,7 +23,7 @@ func (app *App) monitorArduinoHeartbeat() (<-chan bool, chan bool) { continue } - cutOff := time.Now().Add(-time.Millisecond) + cutOff := time.Now().Add(-time.Minute) if app.arduino.LastHeartbeat.Time.Before(cutOff) { deadArduino <- true From c136e66902e9b778d2302fdbd2657fa29d37a788 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:22:04 +0000 Subject: [PATCH 132/135] chore: remove comments --- pkg/app/app.go | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 65ab1e3..36924dd 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -88,34 +88,6 @@ func (app *App) Run() { log.Println("did not receive heartbeat from Arduino in time") doExit(1) } - - /* - Make zone configurations - Connect to MQTT - Set LWT for availability topic (shared by all entities). Support modes normal/boost - Publish MQTT auto discovery for Zones (climate), moisture sensors, and outlets not attached to zones (or zones with no sensors?) - Find Arduino port - Open serial connection - Wait for heartbeat - Wait for above to be ready - Loop - Read & process sensors and water states - Publish zone states (freq as below) - Publish sensor states (every 5 min in prod, 2 sec in testing) - Publish outlets without zones states - Check for heartbeat - if none after X: - Close Arduino serial connection - Attempt to establish a new connection - Wait for a heartbeat - Loop as above - Program exit - Send water off command - Send unavailable topic to MQTT - Exit - - - */ - } func NewApp() *App { From 4fc1a3a95631c15b5f61d4c3151bac98772083fe Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:34:16 +0000 Subject: [PATCH 133/135] tweak: humidity to satisfy HomeKit --- pkg/model/moisture_sensor_hass_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index 1c41972..8ea90d3 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -42,7 +42,7 @@ func makeMoistureSensorConfiugurationForDevice(device *HassDevice) moistureSenso c := moistureSensorHassConfiguration{} c.StateTopic = "state" c.AvailabilityTopic = device.GetFqAvailabilityTopic() - c.DeviceClass = "moisture" + c.DeviceClass = "humidity" c.UnitOfMeasurement = "%" c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable From 055a27d893c59692bb131fb59fac5a78f1dd0970 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:39:36 +0000 Subject: [PATCH 134/135] Revert "tweak: humidity to satisfy HomeKit" This reverts commit 4fc1a3a95631c15b5f61d4c3151bac98772083fe. --- pkg/model/moisture_sensor_hass_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index 8ea90d3..1c41972 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -42,7 +42,7 @@ func makeMoistureSensorConfiugurationForDevice(device *HassDevice) moistureSenso c := moistureSensorHassConfiguration{} c.StateTopic = "state" c.AvailabilityTopic = device.GetFqAvailabilityTopic() - c.DeviceClass = "humidity" + c.DeviceClass = "moisture" c.UnitOfMeasurement = "%" c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable From bdc33cdabe94f513d1b12542648a9862c2f6ba56 Mon Sep 17 00:00:00 2001 From: Josh Bonfield Date: Mon, 6 Feb 2023 23:41:20 +0000 Subject: [PATCH 135/135] Revert "Revert "tweak: humidity to satisfy HomeKit"" This reverts commit 055a27d893c59692bb131fb59fac5a78f1dd0970. --- pkg/model/moisture_sensor_hass_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/model/moisture_sensor_hass_configuration.go b/pkg/model/moisture_sensor_hass_configuration.go index 1c41972..8ea90d3 100644 --- a/pkg/model/moisture_sensor_hass_configuration.go +++ b/pkg/model/moisture_sensor_hass_configuration.go @@ -42,7 +42,7 @@ func makeMoistureSensorConfiugurationForDevice(device *HassDevice) moistureSenso c := moistureSensorHassConfiguration{} c.StateTopic = "state" c.AvailabilityTopic = device.GetFqAvailabilityTopic() - c.DeviceClass = "moisture" + c.DeviceClass = "humidity" c.UnitOfMeasurement = "%" c.PayloadAvailable = device.PayloadAvailable c.PayloadNotAvailable = device.PayloadNotAvailable