diff --git a/entsoe/api_client.go b/entsoe/api_client.go index d7d641e..7705b70 100644 --- a/entsoe/api_client.go +++ b/entsoe/api_client.go @@ -1,3 +1,4 @@ +// Package entsoe provides a client for the ENTSO-E Transparency Platform API. package entsoe import ( @@ -37,12 +38,13 @@ func (c *APIClient) DownloadPublicationMarketData(ctx context.Context, apiURL st return DownloadPublicationMarketDataWithOptions(ctx, apiURL, opts) } -// DownloadPublicationMarketDataWithOptions downloads and decodes a PublicationMarketData with additional options +// DownloadOptions contains options for downloading publication market data with additional options. type DownloadOptions struct { UserAgent string Headers map[string]string } +// DownloadPublicationMarketData downloads and decodes publication market data for the current and next day if needed. func DownloadPublicationMarketData(ctx context.Context, securityToken string, urlFormat string, location *time.Location) (*PublicationMarketData, error) { now := time.Now().In(location) diff --git a/entsoe/api_client_test.go b/entsoe/api_client_test.go index e99b22c..bcb2afe 100644 --- a/entsoe/api_client_test.go +++ b/entsoe/api_client_test.go @@ -143,7 +143,7 @@ func TestDownloadPublicationMarketData_EmptyURL(t *testing.T) { func TestDownloadPublicationMarketData_HTTPError(t *testing.T) { // Create a test server that returns HTTP 500 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal Server Error")) })) @@ -166,7 +166,7 @@ func TestDownloadPublicationMarketData_HTTPError(t *testing.T) { func TestDownloadPublicationMarketData_InvalidXML(t *testing.T) { // Create a test server that returns invalid XML - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/xml") w.WriteHeader(http.StatusOK) w.Write([]byte("")) @@ -190,7 +190,7 @@ func TestDownloadPublicationMarketData_InvalidXML(t *testing.T) { func TestDownloadPublicationMarketData_ContextCancellation(t *testing.T) { // Create a test server with a delay - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { time.Sleep(100 * time.Millisecond) w.Header().Set("Content-Type", "application/xml") w.WriteHeader(http.StatusOK) @@ -410,7 +410,7 @@ func TestDownloadPublicationMarketData_CustomUserAgent(t *testing.T) { // Benchmark tests func BenchmarkDownloadPublicationMarketData(b *testing.B) { // Create a test server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/xml") w.WriteHeader(http.StatusOK) w.Write([]byte(sampleXMLResponse)) diff --git a/main.go b/main.go index 336e8b5..7ed03cc 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,4 @@ +// Package main provides the Energy Management System (EMS) entry point and CLI interface. package main import ( diff --git a/meteo/client_test.go b/meteo/client_test.go index 3cd88e7..1723d79 100644 --- a/meteo/client_test.go +++ b/meteo/client_test.go @@ -279,7 +279,7 @@ func TestGetCompact(t *testing.T) { func TestAPIError(t *testing.T) { // Create test server that returns an error - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("Bad Request: Invalid parameters")) })) diff --git a/meteo/example/main.go b/meteo/example/main.go index e190106..c75345f 100644 --- a/meteo/example/main.go +++ b/meteo/example/main.go @@ -1,3 +1,4 @@ +// Package main provides an example of using the meteo client to fetch weather forecasts. package main import ( diff --git a/meteo/types.go b/meteo/types.go index 894aadc..c80dbfb 100644 --- a/meteo/types.go +++ b/meteo/types.go @@ -5,6 +5,7 @@ import "time" // WeatherSymbol represents weather condition identifiers type WeatherSymbol string +// Weather symbol constants represent different weather conditions. const ( ClearSkyDay WeatherSymbol = "clearsky_day" ClearSkyNight WeatherSymbol = "clearsky_night" diff --git a/miners/avalon.go b/miners/avalon.go index c97af5d..c554653 100644 --- a/miners/avalon.go +++ b/miners/avalon.go @@ -1,3 +1,4 @@ +// Package miners provides functionality for discovering and controlling cryptocurrency miners. package miners import ( @@ -15,9 +16,13 @@ import ( "time" ) +// Sender is a function type for sending data over a network connection. type Sender func(conn net.Conn) error + +// Receiver is a generic function type for receiving data over a network connection. type Receiver[T any] func(conn net.Conn) (T, error) +// UnmarshalJSON implements custom JSON unmarshaling for StatsItem. func (s *StatsItem) UnmarshalJSON(data []byte) error { var raw map[string]string if err := json.Unmarshal(data, &raw); err != nil { @@ -196,7 +201,7 @@ func (s *StatsItem) UnmarshalJSON(data []byte) error { } case "PCOMM_E": if i, err := strconv.Atoi(value); err == nil { - stats.PCOMM_E = i + stats.PCommE = i } case "GHSspd": if f, err := strconv.ParseFloat(value, 64); err == nil { @@ -315,6 +320,7 @@ func (s *StatsItem) UnmarshalJSON(data []byte) error { return nil } +// Discover searches for Avalon miners on the specified network and returns a list of discovered hosts. func Discover(ctx context.Context, network string) []*AvalonQHost { hosts := make([]*AvalonQHost, 0) var wg sync.WaitGroup @@ -351,6 +357,7 @@ func getAddresses(network string) iter.Seq[netip.Addr] { } } +// Standby puts the Avalon miner into standby mode. func (h *AvalonQHost) Standby(ctx context.Context) (string, error) { if _, err := h.SetWorkMode(ctx, AvalonEcoMode, true); err != nil { return "", err @@ -364,6 +371,7 @@ func (h *AvalonQHost) Standby(ctx context.Context) (string, error) { ) } +// SetWorkMode sets the work mode of the Avalon miner. func (h *AvalonQHost) SetWorkMode(ctx context.Context, mode AvalonWorkMode, resetHistory bool) (string, error) { if resetHistory { h.ResetLiteStats() @@ -377,6 +385,7 @@ func (h *AvalonQHost) SetWorkMode(ctx context.Context, mode AvalonWorkMode, rese ) } +// WakeUp wakes up the Avalon miner from standby mode. func (h *AvalonQHost) WakeUp(ctx context.Context) (string, error) { h.ResetLiteStats() return send(ctx, h.Address, h.Port, @@ -388,6 +397,7 @@ func (h *AvalonQHost) WakeUp(ctx context.Context) (string, error) { ) } +// RefreshLiteStats refreshes the lite statistics for the Avalon miner. func (h *AvalonQHost) RefreshLiteStats(ctx context.Context) { stats, err := send(ctx, h.Address, h.Port, func(conn net.Conn) error { @@ -395,7 +405,7 @@ func (h *AvalonQHost) RefreshLiteStats(ctx context.Context) { }, func(conn net.Conn) (*AvalonQLiteStats, error) { stats := &AvalonQLiteStats{} - if err := readJsonResponse(conn, stats); err != nil { + if err := readJSONResponse(conn, stats); err != nil { return nil, err } return stats, nil @@ -417,7 +427,7 @@ func version(ctx context.Context, address string, port int) (*AvalonQVersion, er }, func(conn net.Conn) (*AvalonQVersion, error) { v := &AvalonQVersion{} - if err := readJsonResponse(conn, v); err != nil { + if err := readJSONResponse(conn, v); err != nil { return nil, err } return v, nil @@ -465,7 +475,7 @@ func readStringResponse(conn net.Conn) (string, error) { return string(r), nil } -func readJsonResponse(conn net.Conn, response any) error { +func readJSONResponse(conn net.Conn, response any) error { dec := json.NewDecoder(conn) return dec.Decode(response) } diff --git a/miners/avalon_test.go b/miners/avalon_test.go index c13cdbd..887a9f0 100644 --- a/miners/avalon_test.go +++ b/miners/avalon_test.go @@ -209,8 +209,8 @@ func TestAvalonQLiteStatParsing(t *testing.T) { } // Test communication error - if stats.PCOMM_E != 0 { - t.Errorf("Expected PCOMM_E 0, got %d", stats.PCOMM_E) + if stats.PCommE != 0 { + t.Errorf("Expected PCommE 0, got %d", stats.PCommE) } // Test hash rate fields diff --git a/miners/models.go b/miners/models.go index 97270c1..961770e 100644 --- a/miners/models.go +++ b/miners/models.go @@ -6,12 +6,14 @@ type AvalonState int // AvalonWorkMode represents the work mode of an Avalon miner type AvalonWorkMode int +// Avalon miner state constants const ( AvalonStateRunning AvalonState = 0 // Running AvalonStateMining AvalonState = 1 // Mining AvalonStateStandBy AvalonState = 2 // StandBy ) +// Avalon miner work mode constants const ( AvalonEcoMode AvalonWorkMode = 0 // Eco AvalonStandardMode AvalonWorkMode = 1 // Standard @@ -56,10 +58,12 @@ func (w AvalonWorkMode) String() string { } } +// AvalonQCommand represents a command to send to an Avalon miner. type AvalonQCommand struct { Command string `json:"command"` } +// AvalonQHost represents an Avalon miner host with its connection details and statistics. type AvalonQHost struct { Address string Port int @@ -89,12 +93,14 @@ func (h *AvalonQHost) ResetLiteStats() { } } +// AvalonQVersion represents version information from an Avalon miner. type AvalonQVersion struct { Status []StatusItem `json:"STATUS"` Version []VersionItem `json:"VERSION"` ID int `json:"id"` } +// StatusItem represents a status response item from an Avalon miner. type StatusItem struct { Status string `json:"STATUS"` When int64 `json:"When"` @@ -103,6 +109,7 @@ type StatusItem struct { Description string `json:"Description"` } +// VersionItem represents version details from an Avalon miner. type VersionItem struct { CGMiner string `json:"CGMiner"` API string `json:"API"` @@ -119,16 +126,19 @@ type VersionItem struct { MAC string `json:"MAC"` } +// AvalonQLiteStats represents lite statistics response from an Avalon miner. type AvalonQLiteStats struct { Status []StatusItem `json:"STATUS"` Stats []StatsItem `json:"STATS"` ID int `json:"id"` } +// StatsItem represents a statistics item from an Avalon miner. type StatsItem struct { MMIDSummary *AvalonLiteStats `json:"-"` } +// AvalonLiteStats represents detailed statistics from an Avalon miner. type AvalonLiteStats struct { Ver string `json:"ver"` LVer string `json:"lver"` @@ -169,7 +179,7 @@ type AvalonLiteStats struct { FanErr int `json:"fan_err"` SoloAllowed int `json:"solo_allowed"` PS []int `json:"ps"` - PCOMM_E int `json:"pcomm_e"` + PCommE int `json:"pcomm_e"` GHSspd float64 `json:"ghs_spd"` DHspd string `json:"dh_spd"` GHSmm float64 `json:"ghs_mm"` diff --git a/mpc/example/main.go b/mpc/example/main.go index 288eaa2..a3539f1 100644 --- a/mpc/example/main.go +++ b/mpc/example/main.go @@ -1,3 +1,4 @@ +// Package main provides an example of using the MPC (Model Predictive Control) optimization. package main import ( @@ -24,7 +25,7 @@ func main() { } // Create MPC controller with 24-hour horizon - mpcController := mpc.NewMPCController(config, 24, 0.5) // Start at 50% SOC + mpcController := mpc.NewController(config, 24, 0.5) // Start at 50% SOC // Create 24-hour forecast forecast := make([]mpc.TimeSlot, 24) diff --git a/mpc/mpc.go b/mpc/mpc.go index 7da3cce..e702ab3 100644 --- a/mpc/mpc.go +++ b/mpc/mpc.go @@ -1,3 +1,4 @@ +// Package mpc provides Model Predictive Control optimization for energy management systems. package mpc import ( @@ -48,16 +49,16 @@ type ControlDecision struct { WeatherSymbol string // weather condition symbol } -// MPCController implements Model Predictive Control -type MPCController struct { +// Controller implements Model Predictive Control +type Controller struct { Config SystemConfig Horizon int // hours to look ahead CurrentSOC float64 } -// NewMPCController creates a new MPC controller -func NewMPCController(config SystemConfig, horizon int, initialSOC float64) *MPCController { - return &MPCController{ +// NewController creates a new MPC controller +func NewController(config SystemConfig, horizon int, initialSOC float64) *Controller { + return &Controller{ Config: config, Horizon: horizon, CurrentSOC: initialSOC, @@ -65,7 +66,7 @@ func NewMPCController(config SystemConfig, horizon int, initialSOC float64) *MPC } // Optimize finds the optimal control strategy using dynamic programming -func (mpc *MPCController) Optimize(forecast []TimeSlot) []ControlDecision { +func (mpc *Controller) Optimize(forecast []TimeSlot) []ControlDecision { if len(forecast) == 0 { return nil } @@ -162,7 +163,7 @@ func (mpc *MPCController) Optimize(forecast []TimeSlot) []ControlDecision { } // generateFeasibleDecisions creates a set of feasible control decisions -func (mpc *MPCController) generateFeasibleDecisions(currentSOC float64, slot TimeSlot) []ControlDecision { +func (mpc *Controller) generateFeasibleDecisions(currentSOC float64, slot TimeSlot) []ControlDecision { decisions := []ControlDecision{} // Always include idle option @@ -241,7 +242,7 @@ func (mpc *MPCController) generateFeasibleDecisions(currentSOC float64, slot Tim // The power balance equation ensures: Solar + GridImport + BatteryDischarge*eff = Load + GridExport + BatteryCharge/eff // Therefore, GridImport and GridExport already reflect the effect of battery operations. // Profit is simply: revenue from exports - cost of imports - degradation cost -func (mpc *MPCController) calculateProfit(dec ControlDecision, slot TimeSlot) float64 { +func (mpc *Controller) calculateProfit(dec ControlDecision, slot TimeSlot) float64 { // Revenue from exporting to grid revenue := dec.GridExport * slot.ExportPrice @@ -267,32 +268,32 @@ func (mpc *MPCController) calculateProfit(dec ControlDecision, slot TimeSlot) fl } // Helper functions -func (mpc *MPCController) canCharge(soc, charge float64) bool { +func (mpc *Controller) canCharge(soc, charge float64) bool { newSOC := soc + (charge / mpc.Config.BatteryCapacity) return newSOC <= mpc.Config.BatteryMaxSOC } -func (mpc *MPCController) canDischarge(soc, discharge float64) bool { +func (mpc *Controller) canDischarge(soc, discharge float64) bool { newSOC := soc - (discharge / mpc.Config.BatteryCapacity) return newSOC >= mpc.Config.BatteryMinSOC } -func (mpc *MPCController) calculateNewSOC(currentSOC, charge, discharge float64) float64 { +func (mpc *Controller) calculateNewSOC(currentSOC, charge, discharge float64) float64 { chargeEnergy := charge * mpc.Config.BatteryEfficiency socChange := (chargeEnergy - discharge) / mpc.Config.BatteryCapacity newSOC := currentSOC + socChange return math.Max(mpc.Config.BatteryMinSOC, math.Min(mpc.Config.BatteryMaxSOC, newSOC)) } -func (mpc *MPCController) socToIndex(soc float64, socStep float64) int { +func (mpc *Controller) socToIndex(soc float64, socStep float64) int { return int(math.Round((soc - mpc.Config.BatteryMinSOC) / socStep)) } -func (mpc *MPCController) indexToSOC(index int, socStep float64) float64 { +func (mpc *Controller) indexToSOC(index int, socStep float64) float64 { return mpc.Config.BatteryMinSOC + float64(index)*socStep } -func (mpc *MPCController) isFeasible(dec ControlDecision) bool { +func (mpc *Controller) isFeasible(dec ControlDecision) bool { // Check all constraints are satisfied if dec.BatteryCharge > mpc.Config.BatteryMaxCharge { return false @@ -310,7 +311,7 @@ func (mpc *MPCController) isFeasible(dec ControlDecision) bool { } // ExecuteControl applies the first decision and returns it -func (mpc *MPCController) ExecuteControl(forecast []TimeSlot) *ControlDecision { +func (mpc *Controller) ExecuteControl(forecast []TimeSlot) *ControlDecision { decisions := mpc.Optimize(forecast) if len(decisions) == 0 { return nil diff --git a/mpc/mpc_test.go b/mpc/mpc_test.go index 6c4c2fb..3a29a64 100644 --- a/mpc/mpc_test.go +++ b/mpc/mpc_test.go @@ -254,7 +254,7 @@ func TestCalculateProfit(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mpc := &MPCController{ + mpc := &Controller{ Config: tt.config, } @@ -286,7 +286,7 @@ func TestCalculateProfitNoDegradation(t *testing.T) { BatteryDegradationCost: 0.0, // No degradation } - mpc := &MPCController{ + mpc := &Controller{ Config: config, } @@ -326,7 +326,7 @@ func TestCalculateProfitArbitrage(t *testing.T) { BatteryDegradationCost: 0.01, } - mpc := &MPCController{ + mpc := &Controller{ Config: config, } @@ -458,7 +458,7 @@ func TestOptimize(t *testing.T) { } // Create MPC controller with 25% initial SOC - mpc := NewMPCController(config, len(hourlyPrices), 0.25) + mpc := NewController(config, len(hourlyPrices), 0.25) // Run optimization decisions := mpc.Optimize(forecast) @@ -666,7 +666,7 @@ func TestOptimizeEmptyForecast(t *testing.T) { BatteryEfficiency: 0.9, } - mpc := NewMPCController(config, 24, 0.5) + mpc := NewController(config, 24, 0.5) decisions := mpc.Optimize([]TimeSlot{}) if decisions != nil { @@ -707,7 +707,7 @@ func TestOptimizeShortHorizon(t *testing.T) { }, } - mpc := NewMPCController(config, 2, 0.5) + mpc := NewController(config, 2, 0.5) decisions := mpc.Optimize(forecast) if len(decisions) != 2 { @@ -745,7 +745,7 @@ func TestOptimizeHighLoad(t *testing.T) { }, } - mpc := NewMPCController(config, 1, 0.9) // Start at high SOC + mpc := NewController(config, 1, 0.9) // Start at high SOC decisions := mpc.Optimize(forecast) if len(decisions) != 1 { @@ -785,7 +785,7 @@ func TestOptimizeHighSolar(t *testing.T) { }, } - mpc := NewMPCController(config, 1, 0.1) // Start at low SOC + mpc := NewController(config, 1, 0.1) // Start at low SOC decisions := mpc.Optimize(forecast) if len(decisions) != 1 { diff --git a/scheduler/config.go b/scheduler/config.go index a2accc4..faf9d27 100644 --- a/scheduler/config.go +++ b/scheduler/config.go @@ -1,3 +1,4 @@ +// Package scheduler provides energy management scheduling and optimization functionality. package scheduler import ( @@ -22,7 +23,7 @@ type Config struct { // API settings SecurityToken string `json:"security_token"` // ENTSO-E API token APITimeout time.Duration `json:"api_timeout"` // Timeout for API calls - UrlFormat string `json:"url_format"` // ENTSO-E API URL format string + URLFormat string `json:"url_format"` // ENTSO-E API URL format string // Logging settings LogLevel string `json:"log_level"` // Log level: debug, info, warn, error @@ -102,7 +103,7 @@ func DefaultConfig() *Config { PVPollInterval: 10 * time.Second, PVIntegrationPeriod: 15 * time.Minute, PostgresConnString: "", - UrlFormat: "https://web-api.tp.entsoe.eu/api?documentType=A44&out_Domain=10YLV-1001A00074&in_Domain=10YLV-1001A00074&periodStart=%s&periodEnd=%s&securityToken=%s", + URLFormat: "https://web-api.tp.entsoe.eu/api?documentType=A44&out_Domain=10YLV-1001A00074&in_Domain=10YLV-1001A00074&periodStart=%s&periodEnd=%s&securityToken=%s", PlantModbusAddress: "", Latitude: 56.9496, // Riga, Latvia Longitude: 24.1052, // Riga, Latvia @@ -216,7 +217,7 @@ func (c *Config) Validate() error { return fmt.Errorf("api_timeout must be greater than 0, got: %s", c.APITimeout) } - if c.UrlFormat == "" { + if c.URLFormat == "" { return fmt.Errorf("url_format cannot be empty") } @@ -393,7 +394,7 @@ func (c *Config) UnmarshalJSON(data []byte) error { MPCExecutionInterval string `json:"mpc_execution_interval"` APITimeout string `json:"api_timeout"` MinerTimeout string `json:"miner_timeout"` - UrlFormat string `json:"url_format"` + URLFormat string `json:"url_format"` PVPollInterval string `json:"pv_poll_interval"` PVIntegrationPeriod string `json:"pv_integration_period"` WeatherUpdateInterval string `json:"weather_update_interval"` @@ -458,8 +459,8 @@ func (c *Config) UnmarshalJSON(data []byte) error { return fmt.Errorf("invalid pv_integration_period: %w", err) } } - if aux.UrlFormat != "" { - c.UrlFormat = aux.UrlFormat + if aux.URLFormat != "" { + c.URLFormat = aux.URLFormat } return nil diff --git a/scheduler/data.go b/scheduler/data.go index f5c569f..ded1aae 100644 --- a/scheduler/data.go +++ b/scheduler/data.go @@ -9,6 +9,7 @@ import ( "github.com/devskill-org/ems/sigenergy" ) +// WeatherForecastCache caches weather forecast data with expiration. type WeatherForecastCache struct { mu sync.RWMutex forecast *meteo.METJSONForecast @@ -16,6 +17,7 @@ type WeatherForecastCache struct { cacheDuration time.Duration } +// Get retrieves the cached weather forecast if it's still valid. func (w *WeatherForecastCache) Get() (*meteo.METJSONForecast, bool) { w.mu.RLock() defer w.mu.RUnlock() @@ -31,6 +33,7 @@ func (w *WeatherForecastCache) Get() (*meteo.METJSONForecast, bool) { return w.forecast, true } +// Set updates the cached weather forecast with a new value. func (w *WeatherForecastCache) Set(forecast *meteo.METJSONForecast) { w.mu.Lock() defer w.mu.Unlock() @@ -39,6 +42,7 @@ func (w *WeatherForecastCache) Set(forecast *meteo.METJSONForecast) { w.fetchedAt = time.Now() } +// DataSample represents a single measurement of power and battery data. type DataSample struct { pvPower float64 gridPower float64 // positive = import, negative = export @@ -48,11 +52,13 @@ type DataSample struct { ts time.Time } +// DataSamples is a thread-safe collection of power measurement samples. type DataSamples struct { mu sync.Mutex samples []DataSample } +// AddSample adds a new power measurement sample to the collection. func (d *DataSamples) AddSample(pvPower, gridPower, batteryPower, evdcPower, batterySoc float64, ts time.Time) { d.mu.Lock() defer d.mu.Unlock() @@ -66,6 +72,7 @@ func (d *DataSamples) AddSample(pvPower, gridPower, batteryPower, evdcPower, bat }) } +// IntegratedData represents aggregated power measurements over a period. type IntegratedData struct { pvTotalPower float64 gridExportPower float64 @@ -77,6 +84,7 @@ type IntegratedData struct { batterySoc float64 } +// IntegrateSamples computes integrated power values from collected samples. func (d *DataSamples) IntegrateSamples(pollInterval time.Duration) IntegratedData { d.mu.Lock() defer d.mu.Unlock() @@ -117,6 +125,7 @@ func (d *DataSamples) IntegrateSamples(pollInterval time.Duration) IntegratedDat return result } +// IsEmpty returns true if there are no samples collected. func (d *DataSamples) IsEmpty() bool { d.mu.Lock() defer d.mu.Unlock() diff --git a/scheduler/mpc.go b/scheduler/mpc.go index 5f808fe..231cb66 100644 --- a/scheduler/mpc.go +++ b/scheduler/mpc.go @@ -63,7 +63,7 @@ func (s *MinerScheduler) RunMPCOptimize(ctx context.Context) { } horizon := len(forecast) - controller := mpc.NewMPCController(systemConfig, horizon, initialSOC) + controller := mpc.NewController(systemConfig, horizon, initialSOC) // Step 4: Run optimization decisions := controller.Optimize(forecast) diff --git a/scheduler/pricing.go b/scheduler/pricing.go index 81ea536..8f3f9f3 100644 --- a/scheduler/pricing.go +++ b/scheduler/pricing.go @@ -45,7 +45,7 @@ func (s *MinerScheduler) GetMarketData(ctx context.Context) (*entsoe.Publication s.mu.Lock() defer s.mu.Unlock() - newDoc, err := entsoe.DownloadPublicationMarketData(ctx, s.config.SecurityToken, s.config.UrlFormat, location) + newDoc, err := entsoe.DownloadPublicationMarketData(ctx, s.config.SecurityToken, s.config.URLFormat, location) if err != nil { return nil, fmt.Errorf("failed to download PublicationMarketData: %w", err) } diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index de991e8..8856672 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -11,7 +11,7 @@ import ( "github.com/devskill-org/ems/entsoe" "github.com/devskill-org/ems/miners" "github.com/devskill-org/ems/mpc" - _ "github.com/lib/pq" + _ "github.com/lib/pq" // PostgreSQL driver ) // PeriodicTask represents a task that runs periodically with an optional initial delay @@ -65,6 +65,7 @@ func (pt *PeriodicTask) run(ctx context.Context, stopChan <-chan struct{}, logge } } +// MinerScheduler manages energy system optimization, miner control, and scheduling tasks. type MinerScheduler struct { // Configuration config *Config @@ -335,11 +336,11 @@ func (s *MinerScheduler) IsRunning() bool { } // GetStatus returns the current status of the scheduler -func (s *MinerScheduler) GetStatus() SchedulerStatus { +func (s *MinerScheduler) GetStatus() Status { s.mu.RLock() defer s.mu.RUnlock() - return SchedulerStatus{ + return Status{ IsRunning: s.isRunning, MinersCount: len(s.discoveredMiners), HasMarketData: s.pricesMarketData != nil, @@ -361,8 +362,8 @@ func (s *MinerScheduler) GetMPCDecisions() []mpc.ControlDecision { return decisionsCopy } -// SchedulerStatus represents the current status of the scheduler -type SchedulerStatus struct { +// Status represents the current status of the scheduler +type Status struct { IsRunning bool `json:"is_running"` MinersCount int `json:"miners_count"` HasMarketData bool `json:"has_latest_document"` diff --git a/scheduler/scheduler_test.go b/scheduler/scheduler_test.go index 816cc76..4946beb 100644 --- a/scheduler/scheduler_test.go +++ b/scheduler/scheduler_test.go @@ -16,7 +16,7 @@ import ( // mockEnergyPricesServer creates a mock HTTP server that returns valid energy prices XML func mockEnergyPricesServer() *httptest.Server { - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { // Return a valid XML response with prices now := time.Now() hourStart := now.Truncate(time.Hour) @@ -77,7 +77,7 @@ func mockEnergyPricesServer() *httptest.Server { } // mockMinerDiscovery returns an empty list of miners to avoid real network requests -func mockMinerDiscovery(ctx context.Context, network string) []*miners.AvalonQHost { +func mockMinerDiscovery(_ context.Context, _ string) []*miners.AvalonQHost { return []*miners.AvalonQHost{} } @@ -106,7 +106,7 @@ func testConfigWithServer(server *httptest.Server) *Config { MPCExecutionInterval: time.Minute, Location: "Europe/Riga", SecurityToken: "test-token", - UrlFormat: urlFormat, + URLFormat: urlFormat, } } @@ -705,7 +705,7 @@ func TestMockMinerDiscovery(t *testing.T) { // Create a custom mock that tracks if it was called mockCalled := false - customMock := func(ctx context.Context, network string) []*miners.AvalonQHost { + customMock := func(_ context.Context, _ string) []*miners.AvalonQHost { mockCalled = true // Return some test miners return []*miners.AvalonQHost{ diff --git a/scheduler/server.go b/scheduler/server.go index f41cc72..f23f9e7 100644 --- a/scheduler/server.go +++ b/scheduler/server.go @@ -27,17 +27,17 @@ type WebServer struct { // StatusResponse represents the health check response type StatusResponse struct { - Status string `json:"status"` - Timestamp string `json:"timestamp"` - Version string `json:"version,omitempty"` - Scheduler SchedulerHealth `json:"scheduler"` - System SystemHealth `json:"system"` - EMS EMSHealth `json:"ems"` - Sun SunInfo `json:"sun"` + Status string `json:"status"` + Timestamp string `json:"timestamp"` + Version string `json:"version,omitempty"` + Scheduler Health `json:"scheduler"` + System SystemHealth `json:"system"` + EMS EMSHealth `json:"ems"` + Sun SunInfo `json:"sun"` } -// SchedulerHealth represents scheduler-specific health information -type SchedulerHealth struct { +// Health represents scheduler-specific health information +type Health struct { IsRunning bool `json:"is_running"` MinersCount int `json:"miners_count"` LastCheck *time.Time `json:"last_check,omitempty"` @@ -118,7 +118,7 @@ func NewWebServer(scheduler *MinerScheduler, port int) *WebServer { upgrader: websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, - CheckOrigin: func(r *http.Request) bool { + CheckOrigin: func(_ *http.Request) bool { return true // Allow all origins in development }, }, @@ -178,7 +178,7 @@ func (hs *WebServer) Stop(ctx context.Context) error { close(hs.done) // Close all WebSocket connections - hs.clients.Range(func(key, value any) bool { + hs.clients.Range(func(key, _ any) bool { if conn, ok := key.(*websocket.Conn); ok { conn.Close() //nolint:gosec } @@ -223,7 +223,7 @@ func (hs *WebServer) healthHandler(w http.ResponseWriter, r *http.Request) { Status: "healthy", Timestamp: time.Now().UTC().Format(time.RFC3339), Version: "1.0.0", - Scheduler: SchedulerHealth{ + Scheduler: Health{ IsRunning: status.IsRunning, MinersCount: status.MinersCount, HasMarketData: status.HasMarketData, @@ -361,7 +361,7 @@ func (hs *WebServer) wsHandler(w http.ResponseWriter, r *http.Request) { hs.clients.Store(conn, true) clientCount := 0 - hs.clients.Range(func(key, value any) bool { + hs.clients.Range(func(_, _ any) bool { clientCount++ return true }) @@ -376,7 +376,7 @@ func (hs *WebServer) wsHandler(w http.ResponseWriter, r *http.Request) { conn.Close() //nolint:gosec clientCount := 0 - hs.clients.Range(func(key, value any) bool { + hs.clients.Range(func(_, _ any) bool { clientCount++ return true }) @@ -400,7 +400,7 @@ func (hs *WebServer) handleBroadcasts() { for { select { case message := <-hs.broadcast: - hs.clients.Range(func(key, value any) bool { + hs.clients.Range(func(key, _ any) bool { conn, ok := key.(*websocket.Conn) if !ok { return true @@ -429,7 +429,7 @@ func (hs *WebServer) broadcastStatus() { select { case <-ticker.C: hasClients := false - hs.clients.Range(func(key, value any) bool { + hs.clients.Range(func(_, _ any) bool { hasClients = true return false // Stop after finding first client }) @@ -519,7 +519,7 @@ func (hs *WebServer) buildStatusData() map[string]any { Status: overallStatus, Timestamp: time.Now().UTC().Format(time.RFC3339), Version: "1.0.0", - Scheduler: SchedulerHealth{ + Scheduler: Health{ IsRunning: status.IsRunning, MinersCount: status.MinersCount, HasMarketData: status.HasMarketData, diff --git a/sigenergy/info.go b/sigenergy/info.go index 3480201..c5b8775 100644 --- a/sigenergy/info.go +++ b/sigenergy/info.go @@ -1,3 +1,4 @@ +// Package sigenergy provides Modbus client functionality for Sigenergy inverter systems. package sigenergy import ( diff --git a/sigenergy/modbus_client.go b/sigenergy/modbus_client.go index f56a52b..3401306 100644 --- a/sigenergy/modbus_client.go +++ b/sigenergy/modbus_client.go @@ -23,9 +23,7 @@ type SigenModbusClient struct { tcpHandler *modbus.TCPClientHandler } -// NewSigenModbusClient creates a new Sigenergy Modbus client -// For TCP: use NewTCPClient -// For RTU: use NewRTUClient +// NewRTUClient creates a new Sigenergy Modbus RTU client func NewRTUClient(device string, baudRate int, slaveID byte) (*SigenModbusClient, error) { handler := modbus.NewRTUClientHandler(device) handler.BaudRate = baudRate @@ -46,6 +44,7 @@ func NewRTUClient(device string, baudRate int, slaveID byte) (*SigenModbusClient }, nil } +// NewTCPClient creates a new Sigenergy Modbus TCP client func NewTCPClient(address string, slaveID byte) (*SigenModbusClient, error) { handler := modbus.NewTCPClientHandler(address) handler.SlaveId = slaveID @@ -117,7 +116,7 @@ func s32ToBytes(val int32) []byte { return buf } -// Plant Running Information Structures (Section 5.1) +// PlantRunningInfo represents the plant running information (Section 5.1) type PlantRunningInfo struct { SystemTime uint32 // Epoch seconds SystemTimeZone int16 // minutes @@ -221,7 +220,7 @@ func (c *SigenModbusClient) ReadPlantRunningInfo() (*PlantRunningInfo, error) { return info, nil } -// Plant Parameter Settings (Section 5.2) +// PlantParameters represents the plant parameter settings (Section 5.2) type PlantParameters struct { ActivePowerFixedTarget float64 // kW ReactivePowerFixedTarget float64 // kVar @@ -332,7 +331,7 @@ func (c *SigenModbusClient) SetPVMaxPowerLimit(powerKW float64) error { return err } -// Hybrid Inverter Running Information (Section 5.3) +// HybridInverterInfo represents the hybrid inverter running information (Section 5.3) type HybridInverterInfo struct { ModelType string SerialNumber string @@ -348,8 +347,8 @@ type HybridInverterInfo struct { ActivePower float64 // kW ReactivePower float64 // kVar ESSChargeOrDischargePower float64 // kW - ESS_SOC float64 // % - ESS_SOH float64 // % + ESSSOC float64 // % + ESSSOH float64 // % ESSAvgCellTemperature float64 // °C ESSAvgCellVoltage float64 // V Alarm1 uint16 @@ -406,8 +405,8 @@ func (c *SigenModbusClient) ReadHybridInverterInfo(slaveID byte) (*HybridInverte info.ActivePower = float64(bytesToS32(data2[18:22])) / 1000.0 info.ReactivePower = float64(bytesToS32(data2[22:26])) / 1000.0 info.ESSChargeOrDischargePower = float64(bytesToS32(data2[42:46])) / 1000.0 - info.ESS_SOC = float64(bytesToU16(data2[46:48])) / 10.0 - info.ESS_SOH = float64(bytesToU16(data2[48:50])) / 10.0 + info.ESSSOC = float64(bytesToU16(data2[46:48])) / 10.0 + info.ESSSOH = float64(bytesToU16(data2[48:50])) / 10.0 info.ESSAvgCellTemperature = float64(bytesToS16(data2[50:52])) / 10.0 info.ESSAvgCellVoltage = float64(bytesToU16(data2[52:54])) / 1000.0 info.Alarm1 = bytesToU16(data2[54:56]) @@ -460,7 +459,7 @@ func (c *SigenModbusClient) StopInverter(slaveID byte) error { return err } -// AC-Charger Information (Section 5.5) +// ACChargerInfo represents the AC-Charger information (Section 5.5) type ACChargerInfo struct { SystemState uint16 // System state according to IEC61851-1 TotalEnergyConsumed float64 // kWh diff --git a/sun/example/main.go b/sun/example/main.go index 90c701d..04030b7 100644 --- a/sun/example/main.go +++ b/sun/example/main.go @@ -1,3 +1,4 @@ +// Package main provides an example of using sun calculations for sunrise/sunset times. package main import ( diff --git a/utils/time.go b/utils/time.go index b2d4e3e..03f3d78 100644 --- a/utils/time.go +++ b/utils/time.go @@ -1,8 +1,9 @@ -package utils +// Package utils provides utility functions for the EMS application. +package utils //nolint:revive // utils is a common and acceptable package name import "time" -// GetUTC formats a time.Time to the ENTSO-E API format YYYYMMDDHHmm +// GetUTCString formats a time.Time to the ENTSO-E API format YYYYMMDDHHmm. func GetUTCString(t time.Time) string { return t.UTC().Format("200601021504") }