diff --git a/app/stats.go b/app/pq.go similarity index 100% rename from app/stats.go rename to app/pq.go diff --git a/app/trading.go b/app/trading.go new file mode 100644 index 0000000..90682d4 --- /dev/null +++ b/app/trading.go @@ -0,0 +1,102 @@ +package app + +import ( + "fmt" + "math" + + "git.fuyu.moe/5GPowerQuality/api/app/internal/model" + "git.ultraware.nl/NiseVoid/qb" + "git.ultraware.nl/NiseVoid/qb/qc" +) + +// GetScores returns the scores for the trading page +func GetScores(meterID int) []Scores { + m := model.Measurement() + q := m.Select( + m.UGem1, m.UGem2, m.UGem3, + m.IMax1, m.IMax2, m.IMax3, + m.CGem1, m.CGem2, m.CGem3, + ). + Where(qc.Eq(m.MeterID, meterID)). + OrderBy(qb.Desc(m.Time)) + + data := [3]powerData{} + err := db.QueryRow(q).Scan( + &data[0].Voltage, &data[1].Voltage, &data[2].Voltage, + &data[0].MaxAmpere, &data[1].MaxAmpere, &data[2].MaxAmpere, + &data[0].CosPhi, &data[1].CosPhi, &data[2].CosPhi, + ) + if err != nil { + panic(err) + } + + scores := make([]Scores, 3) + for k, v := range data { + scores[k] = Scores{ + Phase: k + 1, + Producation: productionScore(v), + Usage: usageScore(v), + Storage: storageScore(v), + } + } + + return scores +} + +// Scores contains all scores for a phase +type Scores struct { + Phase int `json:"phase"` + Producation float64 `json:"production"` + Usage float64 `json:"usage"` + Storage float64 `json:"storage"` +} + +type powerData struct { + Voltage float64 + MaxAmpere float64 + CosPhi float64 +} + +func (data powerData) String() string { + return fmt.Sprintf(`%3.0fV, %3.0fA, %.1fφ`, data.Voltage, data.MaxAmpere, data.CosPhi) +} + +func getScoreModifier(expected, value, step float64) float64 { + var neg bool + if value-expected < 0 { + neg = true + } + r := math.Pow((value-expected)/step, 2) + if neg { + r = r * -1 + } + return r +} + +func productionScore(data powerData) float64 { + vMod := getScoreModifier(230, data.Voltage, 5) * -1 + aMod := getScoreModifier(100, data.MaxAmpere, 25) * -1 + if aMod > 0 { + aMod = 0 + } + cpMod := getScoreModifier(0.8, math.Abs(data.CosPhi), 0.1) + + return vMod + aMod + cpMod +} + +func usageScore(data powerData) float64 { + vMod := getScoreModifier(230, data.Voltage, 5) + aMod := getScoreModifier(100, data.MaxAmpere, 25) * -1 + if aMod > 0 { + aMod = 0 + } + + return vMod + aMod +} + +func storageScore(data powerData) float64 { + vMod := getScoreModifier(230, data.Voltage, 5) + cpMod := getScoreModifier(0.8, math.Abs(data.CosPhi), 0.1) * -1 + + return vMod + cpMod +} diff --git a/app/trading_test.go b/app/trading_test.go new file mode 100644 index 0000000..2aff783 --- /dev/null +++ b/app/trading_test.go @@ -0,0 +1,115 @@ +package app + +import ( + "fmt" + "testing" + + "github.com/kortschak/ct" +) + +type testcase struct { + Data powerData + Result resultType +} + +type resultType uint8 + +func (r resultType) String() string { + return []string{``, `positive`, `negative`, ` neutral`}[r] +} + +const ( + resultPositive = iota + 1 + resultNegative + resultNeutral +) + +func resultTypeFromScore(s float64) resultType { + switch { + case s < 1 && s > -1: + return resultNeutral + case s >= 1: + return resultPositive + case s <= -1: + return resultNegative + } + panic(`Unreachable`) +} + +var ( + passColor = ct.Fg(ct.BoldGreen).Paint + infoColor = ct.Fg(ct.Yellow).Paint +) + +func TestProductionScore(t *testing.T) { + cases := []testcase{ + {powerData{Voltage: 240, MaxAmpere: 10, CosPhi: 0.8}, resultNegative}, + {powerData{Voltage: 230, MaxAmpere: 10, CosPhi: 0.4}, resultNegative}, + {powerData{Voltage: 230, MaxAmpere: 200, CosPhi: 0.8}, resultNegative}, + + {powerData{Voltage: 220, MaxAmpere: 10, CosPhi: 0.8}, resultPositive}, + {powerData{Voltage: 230, MaxAmpere: 10, CosPhi: 1}, resultPositive}, + {powerData{Voltage: 230, MaxAmpere: 0, CosPhi: 0.8}, resultNeutral}, + + {powerData{Voltage: 234, MaxAmpere: 10, CosPhi: 0.8}, resultNeutral}, + {powerData{Voltage: 226, MaxAmpere: 10, CosPhi: 0.8}, resultNeutral}, + + {powerData{Voltage: 230, MaxAmpere: 10, CosPhi: 0.71}, resultNeutral}, + {powerData{Voltage: 230, MaxAmpere: 10, CosPhi: 0.89}, resultNeutral}, + + {powerData{Voltage: 230, MaxAmpere: 120, CosPhi: 0.8}, resultNeutral}, + } + + testScore(t, cases, productionScore) +} + +func TestUsageScore(t *testing.T) { + cases := []testcase{ + {powerData{Voltage: 240, MaxAmpere: 10, CosPhi: 0.8}, resultPositive}, + {powerData{Voltage: 230, MaxAmpere: 200, CosPhi: 0.8}, resultNegative}, + + {powerData{Voltage: 220, MaxAmpere: 10, CosPhi: 0.8}, resultNegative}, + {powerData{Voltage: 230, MaxAmpere: 0, CosPhi: 0.8}, resultNeutral}, + + {powerData{Voltage: 234, MaxAmpere: 10, CosPhi: 0.8}, resultNeutral}, + {powerData{Voltage: 226, MaxAmpere: 10, CosPhi: 0.8}, resultNeutral}, + + {powerData{Voltage: 230, MaxAmpere: 120, CosPhi: 0.8}, resultNeutral}, + } + + testScore(t, cases, usageScore) +} + +func TestStorageScore(t *testing.T) { + cases := []testcase{ + {powerData{Voltage: 240, MaxAmpere: 10, CosPhi: 0.8}, resultPositive}, + {powerData{Voltage: 230, MaxAmpere: 10, CosPhi: 0.4}, resultPositive}, + + {powerData{Voltage: 220, MaxAmpere: 10, CosPhi: 0.8}, resultNegative}, + {powerData{Voltage: 230, MaxAmpere: 10, CosPhi: 1}, resultNegative}, + + {powerData{Voltage: 234, MaxAmpere: 10, CosPhi: 0.8}, resultNeutral}, + {powerData{Voltage: 226, MaxAmpere: 10, CosPhi: 0.8}, resultNeutral}, + + {powerData{Voltage: 230, MaxAmpere: 10, CosPhi: 0.71}, resultNeutral}, + {powerData{Voltage: 230, MaxAmpere: 10, CosPhi: 0.89}, resultNeutral}, + } + + testScore(t, cases, storageScore) +} + +func testScore(t *testing.T, cases []testcase, f func(powerData) float64) { + t.Helper() + + for _, v := range cases { + s := f(v.Data) + r := resultTypeFromScore(s) + if r != v.Result { + t.Error(`Expected a`, v.Result, `score, but got`, s) + continue + } + + score := fmt.Sprintf(`%6.2f`, s) + t.Log(passColor(`PASS`), infoColor(v.Data), `->`, infoColor(r, `(`+score+`)`)) + } +} diff --git a/server/main.go b/server/main.go index 54bf3a9..f53c8a7 100644 --- a/server/main.go +++ b/server/main.go @@ -40,6 +40,9 @@ func main() { attrs = append(attrs, strings.ToLower(v.Name)) } + t := e.Group(`/trading`) + t.GET(`/:meter/scores`, getScores) + panic(e.Start(`localhost:33333`)) } @@ -72,3 +75,12 @@ func getAttr(index int) echo.HandlerFunc { return c.JSON(200, app.GetAttribute(index, meter, date)) } } + +func getScores(c echo.Context) error { + meter, err := strconv.Atoi(c.Param(`meter`)) + if err != nil { + return c.NoContent(400) + } + + return c.JSON(200, app.GetScores(meter)) +}