Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add power control #2

Merged
merged 20 commits into from
Mar 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions control/control.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package control

import (
"time"
"math"
"context"

"gijs.eu/vonkje/modbus"
"gijs.eu/vonkje/metrics"
"gijs.eu/vonkje/packages/victoria_metrics"

"github.com/spf13/viper"
"github.com/sirupsen/logrus"
)

type Config struct {
Run bool `mapstructure:"run"`
MinimumSolarOverProduction int `mapstructure:"minimum-solar-over-production"`
}

type Control struct {
config Config
errChannel chan error
ctx context.Context
logger *logrus.Logger
victoriaMetrics *victoria_metrics.VictoriaMetrics
modbus *modbus.Modbus
}

func New(
config Config,
errChannel chan error,
ctx context.Context,
logger *logrus.Logger,
victoriaMetrics *victoria_metrics.VictoriaMetrics,
modbus *modbus.Modbus,
) *Control {
return &Control{
config: config,
errChannel: errChannel,
ctx: ctx,
logger: logger,
victoriaMetrics: victoriaMetrics,
modbus: modbus,
}
}

type batteryState struct {
inverter string
battery string
capacity float64
}

func (c *Control) Start() {
if !c.config.Run {
c.logger.Warn("Control loop is disabled")
return
}

c.logger.Infof("Waiting %d seconds before starting control loop to collect metrics", viper.GetInt("modbus.read-metrics-interval"))
time.Sleep(time.Duration(viper.GetInt("modbus.read-metrics-interval")) * time.Second)

c.logger.Info("Starting control loop")

ticker := time.NewTicker(time.Duration(viper.GetInt("modbus.read-metrics-interval")) * time.Second)
defer ticker.Stop()

for {
select {
case <-c.ctx.Done():
c.logger.Info("Stopping control loop")
return
case <-ticker.C:
c.logger.Debug("Control loop tick")
metrics.SetMetricValue("control", "action", map[string]string{"action": "charge_batteries"}, 0)
metrics.SetMetricValue("control", "action", map[string]string{"action": "discharge_battery"}, 0)
metrics.SetMetricValue("control", "action", map[string]string{"action": "pull_from_grid"}, 0)

// 1. Get current home energy consumption
avgMeterPhaseVoltage, err := metrics.GetMetricLastEntryAverage("power_meter", "phase_voltage")
if err != nil {
c.errChannel <- err
continue
}

avgMeterPhaseCurrent, err := metrics.GetMetricLastEntryAverage("power_meter", "phase_current")
if err != nil {
c.errChannel <- err
continue
}

avgInverterPhaseVoltage, err := metrics.GetMetricLastEntryAverage("sun2000", "phase_voltage")
if err != nil {
c.errChannel <- err
continue
}

avgInverterPhaseCurrent, err := metrics.GetMetricLastEntryAverage("sun2000", "phase_current")
if err != nil {
c.errChannel <- err
continue
}

avgHomeLoad := math.Ceil((avgMeterPhaseVoltage * avgMeterPhaseCurrent) - (avgInverterPhaseVoltage * avgInverterPhaseCurrent))
if avgHomeLoad < 0 {
avgHomeLoad = 0
}

// 2. Get current solar production
avgSolarIn, err := metrics.GetMetricLastEntryAverage("sun2000", "input_power")
if err != nil {
c.errChannel <- err
continue
}
avgSolarIn = math.Floor(avgSolarIn * 1000)
c.logger.WithFields(logrus.Fields{"avgSolarIn": avgSolarIn, "avgHomeLoad": avgHomeLoad}).Info("Solar production and home load")

// 3. Get current battery capacities
batteryMetricValues, err := metrics.GetMetricValues("luna2000", "battery_capacity")
if err != nil {
c.errChannel <- err
continue
}
batteries := []batteryState{}
for _, batteryMetricValue := range batteryMetricValues {
batteries = append(batteries, batteryState{
inverter: batteryMetricValue.Fields["inverter"],
battery: batteryMetricValue.Fields["battery"],
capacity: batteryMetricValue.Values[len(batteryMetricValue.Values) - 1],
})
}

// Get over production in percentage
var overProduction float64
var overProductionWatts int
if avgSolarIn > avgHomeLoad {
overProduction = math.Ceil((avgSolarIn - avgHomeLoad) / avgSolarIn * 100)
overProductionWatts = int(math.Floor(avgSolarIn - avgHomeLoad))
} else {
overProduction = 0
overProductionWatts = 0
}
metrics.SetMetricValue("control", "over_production", map[string]string{}, overProduction)
c.logger.WithFields(logrus.Fields{"percentage": overProduction, "watts": overProductionWatts}).Info("Over production")

// 4. if solar over production is more than x%, charge battery
if overProduction > float64(c.config.MinimumSolarOverProduction) {
metrics.SetMetricValue("control", "action", map[string]string{"action": "charge_batteries"}, 1)

// charge batteries with 20% less than over production
batteryChargeWatts := uint(math.Floor(float64(overProductionWatts) * 0.80))

for _, battery := range batteries {
if battery.capacity < 100 {
c.logger.WithFields(logrus.Fields{"inverter": battery.inverter, "battery": battery.battery, "capacity": battery.capacity, "watts": batteryChargeWatts}).Info("Battery is not fully charged, starting charge")
err := c.modbus.ChangeBatteryForceCharge(battery.inverter, battery.battery, modbus.MODBUS_STATE_BATTERY_1_FORCIBLE_CHARGE_DISCHARGE_CHARGE, batteryChargeWatts)
if err != nil {
c.errChannel <- err
continue
}
} else {
c.logger.WithFields(logrus.Fields{"inverter": battery.inverter, "battery": battery.battery, "watts": batteryChargeWatts}).Info("Battery is fully charged, stopping charge")
err := c.modbus.ChangeBatteryForceCharge(battery.inverter, battery.battery, modbus.MODBUS_STATE_BATTERY_1_FORCIBLE_CHARGE_DISCHARGE_STOP, batteryChargeWatts)
if err != nil {
c.errChannel <- err
continue
}
}
}

continue
} else {
metrics.SetMetricValue("control", "action", map[string]string{"action": "charge_batteries"}, 0)
}

// 5. if solar production < home energy consumption && battery capacity > 5%, discharge battery
wattsRequired := math.Ceil(avgHomeLoad - avgSolarIn)
batteriesRequired := math.Ceil(wattsRequired / 5000)
if avgSolarIn < avgHomeLoad {
if batteriesRequired > 0 {
metrics.SetMetricValue("control", "action", map[string]string{"action": "discharge_battery"}, 1)
} else {
metrics.SetMetricValue("control", "action", map[string]string{"action": "discharge_battery"}, 0)
}

for _, battery := range batteries {
if batteriesRequired > 0 {
if battery.capacity > 5 {
batteriesRequired--
wattsRequired -= 5000

c.logger.WithFields(logrus.Fields{"inverter": battery.inverter, "battery": battery.battery, "capacity": battery.capacity}).Info("Discharging battery")

err := c.modbus.ChangeBatteryForceCharge(battery.inverter, battery.battery, modbus.MODBUS_STATE_BATTERY_1_FORCIBLE_CHARGE_DISCHARGE_DISCHARGE, 5000)
if err != nil {
c.errChannel <- err
}
}
} else {
c.logger.WithFields(logrus.Fields{"inverter": battery.inverter, "battery": battery.battery}).Info("Battery is not required, stopping discharge")

err := c.modbus.ChangeBatteryForceCharge(battery.inverter, battery.battery, modbus.MODBUS_STATE_BATTERY_1_FORCIBLE_CHARGE_DISCHARGE_STOP, 5000)
if err != nil {
c.errChannel <- err
}
}
}

if wattsRequired > 0 {
metrics.SetMetricValue("control", "action", map[string]string{"action": "pull_from_grid"}, 1)
c.logger.WithFields(logrus.Fields{"wattsRequired": wattsRequired}).Info("Pulling watts from grid")
} else {
metrics.SetMetricValue("control", "action", map[string]string{"action": "pull_from_grid"}, 0)
}
}
}
}
}
3 changes: 3 additions & 0 deletions docs/control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Control
The control module is responsible for optimizing where power comes from. For example we don't want to use the grid when we have solar power available.

5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"gijs.eu/vonkje/http"
"gijs.eu/vonkje/modbus"
"gijs.eu/vonkje/control"
"gijs.eu/vonkje/power_prices"
"gijs.eu/vonkje/packages/victoria_metrics"

Expand All @@ -22,6 +23,7 @@ type Config struct {
Modbus modbus.Config `mapstructure:"modbus"`
VictoriaMetrics victoria_metrics.Config `mapstructure:"victoria-metrics"`
PowerPrices power_prices.Config `mapstructure:"power-prices"`
Control control.Config `mapstructure:"control"`
}

var (
Expand Down Expand Up @@ -99,6 +101,9 @@ func main() {
powerPricesClient := power_prices.New(config.PowerPrices, errChannel, stopCtx, logger, victoriaMetricsClient)
go powerPricesClient.Start()

controlClient := control.New(config.Control, errChannel, stopCtx, logger, victoriaMetricsClient, modbusClient)
go controlClient.Start()

<-stopCtx.Done()

modbusClient.Close()
Expand Down
18 changes: 18 additions & 0 deletions metrics/control.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package metrics

var controlMetrics = []Metric{
{
Namespace: "control",
Name: "action",
Help: "The what action is currently being executed",
Fields: []string{
"action",
},
},
{
Namespace: "control",
Name: "over_production",
Help: "The a percentage of solar over production",
Fields: []string{},
},
}
Loading
Loading