Skip to content
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
4 changes: 3 additions & 1 deletion entsoe/api_client.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package entsoe provides a client for the ENTSO-E Transparency Platform API.
package entsoe

import (
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions entsoe/api_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}))
Expand All @@ -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("<invalid><xml></invalid>"))
Expand All @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package main provides the Energy Management System (EMS) entry point and CLI interface.
package main

import (
Expand Down
2 changes: 1 addition & 1 deletion meteo/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}))
Expand Down
1 change: 1 addition & 0 deletions meteo/example/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package main provides an example of using the meteo client to fetch weather forecasts.
package main

import (
Expand Down
1 change: 1 addition & 0 deletions meteo/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
18 changes: 14 additions & 4 deletions miners/avalon.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package miners provides functionality for discovering and controlling cryptocurrency miners.
package miners

import (
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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,
Expand All @@ -388,14 +397,15 @@ 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 {
return writeCommand("litestats", conn)
},
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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
4 changes: 2 additions & 2 deletions miners/avalon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion miners/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"`
Expand All @@ -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"`
Expand All @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down
3 changes: 2 additions & 1 deletion mpc/example/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package main provides an example of using the MPC (Model Predictive Control) optimization.
package main

import (
Expand All @@ -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)
Expand Down
31 changes: 16 additions & 15 deletions mpc/mpc.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package mpc provides Model Predictive Control optimization for energy management systems.
package mpc

import (
Expand Down Expand Up @@ -48,24 +49,24 @@ 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,
}
}

// 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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down
Loading