Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
mtrossbach committed Jul 29, 2024
1 parent a545b42 commit 6b466cd
Show file tree
Hide file tree
Showing 13 changed files with 374 additions and 290 deletions.
46 changes: 44 additions & 2 deletions cmd/noah-mqtt/main.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package main

import (
"fmt"
mqtt "github.com/eclipse/paho.mqtt.golang"
"log/slog"
"noah-mqtt/internal/config"
"noah-mqtt/internal/growatt"
"noah-mqtt/internal/homeassistant"
"noah-mqtt/internal/logging"
"noah-mqtt/internal/service"
"noah-mqtt/internal/polling"
"os"
"os/signal"
"os/user"
Expand All @@ -24,10 +28,48 @@ func main() {
slog.Info("running as", slog.String("username", currentUser.Username), slog.String("uid", currentUser.Uid))
}

service.Start()
connectMqtt(cfg.Mqtt, func(client mqtt.Client) {
growattClient := growatt.NewClient(cfg.Growatt.Username, cfg.Growatt.Password)
haService := homeassistant.NewService(homeassistant.Options{
MqttClient: client,
TopicPrefix: cfg.HomeAssistant.TopicPrefix,
})
pollingService := polling.NewService(polling.Options{
GrowattClient: growattClient,
HaClient: haService,
MqttClient: client,
PollingInterval: cfg.PollingInterval,
TopicPrefix: cfg.Mqtt.TopicPrefix,
})
pollingService.Start()
})

cancelChan := make(chan os.Signal, 1)
signal.Notify(cancelChan, syscall.SIGTERM, syscall.SIGINT)
sig := <-cancelChan
slog.Info("Caught signal", slog.Any("signal", sig))
}

func connectMqtt(mqttCfg config.Mqtt, onConnected func(client mqtt.Client)) {
opts := mqtt.NewClientOptions().
AddBroker(fmt.Sprintf("tcp://%s:%d", mqttCfg.Host, mqttCfg.Port)).
SetClientID(mqttCfg.ClientId).
SetUsername(mqttCfg.Username).
SetUsername(mqttCfg.Password)

opts.OnConnect = func(client mqtt.Client) {
slog.Info("connected to mqtt broker")
onConnected(client)
}

opts.OnConnectionLost = func(client mqtt.Client, err error) {
slog.Error("lost connection to mqtt broker", slog.String("error", err.Error()))
panic(err)
}

c := mqtt.NewClient(opts)
slog.Info("connecting to mqtt broker", slog.String("host", mqttCfg.Host), slog.Int("port", mqttCfg.Port), slog.String("clientId", mqttCfg.ClientId), slog.String("username", mqttCfg.Username))
if token := c.Connect(); token.Wait() && token.Error() != nil {
panic(token.Error())
}
}
8 changes: 3 additions & 5 deletions internal/growatt/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,7 @@ func (h *Client) Login() error {
}

if resp.StatusCode != 200 || !data.Back.Success {
slog.Error("login failed", slog.String("data", string(b)))
slog.Info("waiting before exiting")
<-time.After(60 * time.Second)
panic("login failed")
return fmt.Errorf("login failed: %s", string(b))
}

h.userId = fmt.Sprintf("%d", data.Back.User.ID)
Expand Down Expand Up @@ -161,7 +158,8 @@ func (h *Client) GetNoahStatus(serialNumber string) (*NoahStatus, error) {
if err := json.Unmarshal(b, &data); err != nil {
if strings.Contains(err.Error(), "invalid character '<' looking for beginning of value") {
if err := h.Login(); err != nil {
return nil, err
<-time.After(60 * time.Second)
panic(err)
}
return h.GetNoahStatus(serialNumber)
} else {
Expand Down
34 changes: 0 additions & 34 deletions internal/growatt/models.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package growatt

import (
"noah-mqtt/pkg/models"
"strconv"
)

type LoginResult struct {
Back struct {
Msg string `json:"msg"`
Expand Down Expand Up @@ -58,32 +53,3 @@ type NoahStatus struct {
Status string `json:"status"`
} `json:"obj"`
}

func (n *NoahStatus) ToPayload() models.Payload {
return models.Payload{
OutputPower: parseFloat(n.Obj.Pac),
SolarPower: parseFloat(n.Obj.Ppv),
Soc: parseFloat(n.Obj.Soc),
ChargePower: parseFloat(n.Obj.ChargePower),
DischargePower: parseFloat(n.Obj.DisChargePower),
BatteryNum: int(parseFloat(n.Obj.BatteryNum)),
GenerationTotalEnergy: parseFloat(n.Obj.EacTotal),
GenerationTodayEnergy: parseFloat(n.Obj.EacToday),
WorkMode: workModeFromString(n.Obj.WorkMode),
}
}

func workModeFromString(s string) models.WorkMode {
if s == "0" {
return models.WorkModeLoadFirst
}
return models.WorkModeBatteryFirst
}

func parseFloat(s string) float64 {
if s, err := strconv.ParseFloat(s, 64); err == nil {
return s
} else {
return 0
}
}
52 changes: 52 additions & 0 deletions internal/homeassistant/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package homeassistant

type DeviceClass string

const (
DeviceClassEnergy DeviceClass = "energy"
DeviceClassBattery DeviceClass = "battery"
DeviceClassPower DeviceClass = "power"
)

type StateClass string

const (
StateClassMeasurement StateClass = "measurement"
StateClassTotalIncreasing StateClass = "total_increasing"
)

type Unit string

const (
UnitKilowattHours Unit = "kWh"
UnitWatt Unit = "W"
UnitPercent Unit = "%"
)

type Icon string

const (
IconSolarPower Icon = "mdi:solar-power"
IconBatteryPlus Icon = "mdi:battery-plus"
IconBatteryMinus Icon = "mdi:battery-minus"
)

type Sensor struct {
Name string `json:"name"`
Icon Icon `json:"icon,omitempty"`
DeviceClass DeviceClass `json:"device_class,omitempty"`
StateTopic string `json:"state_topic"`
StateClass StateClass `json:"state_class,omitempty"`
UnitOfMeasurement Unit `json:"unit_of_measurement,omitempty"`
ValueTemplate string `json:"value_template,omitempty"`
UniqueId string `json:"unique_id,omitempty"`
Device Device `json:"device,omitempty"`
}

type Device struct {
Identifiers []string `json:"identifiers,omitempty"`
Name string `json:"name,omitempty"`
Manufacturer string `json:"manufacturer,omitempty"`
Model string `json:"model,omitempty"`
SerialNumber string `json:"serial_number,omitempty"`
}
39 changes: 39 additions & 0 deletions internal/homeassistant/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package homeassistant

import (
"encoding/json"
"fmt"
mqtt "github.com/eclipse/paho.mqtt.golang"
"log/slog"
"strings"
)

type Options struct {
MqttClient mqtt.Client
TopicPrefix string
}

type Service struct {
options Options
}

func NewService(opts Options) *Service {
return &Service{
options: opts,
}
}

func (s *Service) SendSensorDiscoveryPayload(sensors []Sensor) {
for _, sensor := range sensors {
if b, err := json.Marshal(sensor); err != nil {
slog.Error("could not marshal sensor discovery payload", slog.Any("sensor", sensor))
} else {
topic := s.sensorTopic(sensor)
s.options.MqttClient.Publish(topic, 1, false, string(b))
}
}
}

func (s *Service) sensorTopic(sensor Sensor) string {
return fmt.Sprintf("%s/sensor/%s/%s/config", s.options.TopicPrefix, fmt.Sprintf("noah_%s", sensor.Device.SerialNumber), strings.ReplaceAll(sensor.Name, " ", ""))
}
89 changes: 89 additions & 0 deletions internal/homeassistant/service_discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package homeassistant

import "fmt"

func (s *Service) GenerateSensorDiscoveryPayload(deviceName string, serialNumber string, stateTopic string) []Sensor {
device := Device{
Identifiers: []string{fmt.Sprintf("noah_%s", serialNumber)},
Name: deviceName,
Manufacturer: "Growatt",
Model: "Noah",
SerialNumber: serialNumber,
}

return []Sensor{
{
Name: "Output Power",
DeviceClass: DeviceClassPower,
StateClass: StateClassMeasurement,
StateTopic: stateTopic,
UnitOfMeasurement: UnitWatt,
ValueTemplate: "{{ value_json.output_w }}",
UniqueId: fmt.Sprintf("%s_%s", serialNumber, "output_power"),
Device: device,
},
{
Name: "Solar Power",
Icon: IconSolarPower,
DeviceClass: DeviceClassPower,
StateClass: StateClassMeasurement,
StateTopic: stateTopic,
UnitOfMeasurement: UnitWatt,
ValueTemplate: "{{ value_json.solar_w }}",
UniqueId: fmt.Sprintf("%s_%s", serialNumber, "solar_power"),
Device: device,
},
{
Name: "Charging Power",
Icon: IconBatteryPlus,
DeviceClass: DeviceClassPower,
StateClass: StateClassMeasurement,
StateTopic: stateTopic,
UnitOfMeasurement: UnitWatt,
ValueTemplate: "{{ value_json.charge_w }}",
UniqueId: fmt.Sprintf("%s_%s", serialNumber, "charging_power"),
Device: device,
},
{
Name: "Discharge Power",
Icon: IconBatteryMinus,
DeviceClass: DeviceClassPower,
StateClass: StateClassMeasurement,
StateTopic: stateTopic,
UnitOfMeasurement: UnitWatt,
ValueTemplate: "{{ value_json.discharge_w }}",
UniqueId: fmt.Sprintf("%s_%s", serialNumber, "discharge_power"),
Device: device,
},
{
Name: "Generation Total",
DeviceClass: DeviceClassEnergy,
StateClass: StateClassTotalIncreasing,
StateTopic: stateTopic,
UnitOfMeasurement: UnitKilowattHours,
ValueTemplate: "{{ value_json.generation_total_kwh }}",
UniqueId: fmt.Sprintf("%s_%s", serialNumber, "generation_total"),
Device: device,
},
{
Name: "Generation Today",
DeviceClass: DeviceClassEnergy,
StateClass: StateClassTotalIncreasing,
StateTopic: stateTopic,
UnitOfMeasurement: UnitKilowattHours,
ValueTemplate: "{{ value_json.generation_today_kwh }}",
UniqueId: fmt.Sprintf("%s_%s", serialNumber, "generation_today"),
Device: device,
},
{
Name: "SoC",
DeviceClass: DeviceClassBattery,
StateClass: StateClassMeasurement,
StateTopic: stateTopic,
UnitOfMeasurement: UnitPercent,
ValueTemplate: "{{ value_json.soc }}",
UniqueId: fmt.Sprintf("%s_%s", serialNumber, "soc"),
Device: device,
},
}
}
36 changes: 36 additions & 0 deletions internal/polling/payload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package polling

import (
"noah-mqtt/internal/growatt"
"noah-mqtt/pkg/models"
"strconv"
)

func noahStatusToPayload(n *growatt.NoahStatus) models.Payload {
return models.Payload{
OutputPower: parseFloat(n.Obj.Pac),
SolarPower: parseFloat(n.Obj.Ppv),
Soc: parseFloat(n.Obj.Soc),
ChargePower: parseFloat(n.Obj.ChargePower),
DischargePower: parseFloat(n.Obj.DisChargePower),
BatteryNum: int(parseFloat(n.Obj.BatteryNum)),
GenerationTotalEnergy: parseFloat(n.Obj.EacTotal),
GenerationTodayEnergy: parseFloat(n.Obj.EacToday),
WorkMode: workModeFromString(n.Obj.WorkMode),
}
}

func workModeFromString(s string) models.WorkMode {
if s == "0" {
return models.WorkModeLoadFirst
}
return models.WorkModeBatteryFirst
}

func parseFloat(s string) float64 {
if s, err := strconv.ParseFloat(s, 64); err == nil {
return s
} else {
return 0
}
}
Loading

0 comments on commit 6b466cd

Please sign in to comment.