@@ -0,0 +1,302 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type ChargingMode string
|
||||
|
||||
const (
|
||||
ModeOff ChargingMode = "off"
|
||||
ModeGrid ChargingMode = "grid"
|
||||
ModeSolar ChargingMode = "solar"
|
||||
ModeSolarBattery ChargingMode = "solar_battery"
|
||||
)
|
||||
|
||||
type ChargingParams struct {
|
||||
Mode ChargingMode `json:"mode"`
|
||||
MaxAmp int `json:"max_amp"` // 6–32, 0 → 16
|
||||
MinBatterySoc int `json:"min_battery_soc"` // solar_battery: stop below this %
|
||||
Hysteresis int `json:"hysteresis"` // solar_battery: resume only above min+hysteresis %, 0 → 5
|
||||
TargetKwh float64 `json:"target_kwh"` // stop after X kWh this session, 0 = no limit
|
||||
TargetSoc int `json:"target_soc"` // stop when car SOC reaches X%, 0 = no limit
|
||||
Phases int `json:"phases"` // 1 or 3, 0 → 3
|
||||
}
|
||||
|
||||
type ControllerState struct {
|
||||
Params ChargingParams `json:"params"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type ChargerController struct {
|
||||
mu sync.RWMutex
|
||||
params ChargingParams
|
||||
status string
|
||||
host string
|
||||
pool *pgxpool.Pool
|
||||
cancel context.CancelFunc
|
||||
currentPhases int // active phase count set on charger; 0 = unknown
|
||||
battPaused bool // true while waiting for battery to recover above min+hysteresis
|
||||
}
|
||||
|
||||
func NewChargerController(host string, pool *pgxpool.Pool) *ChargerController {
|
||||
return &ChargerController{
|
||||
host: host,
|
||||
pool: pool,
|
||||
params: ChargingParams{Mode: ModeOff},
|
||||
status: "idle",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ChargerController) State() ControllerState {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return ControllerState{Params: c.params, Status: c.status}
|
||||
}
|
||||
|
||||
func (c *ChargerController) SetParams(p ChargingParams) error {
|
||||
if p.MaxAmp == 0 {
|
||||
p.MaxAmp = 16
|
||||
}
|
||||
if p.Phases == 0 {
|
||||
p.Phases = 3
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
c.cancel = nil
|
||||
}
|
||||
c.params = p
|
||||
c.currentPhases = 0
|
||||
c.battPaused = false
|
||||
c.mu.Unlock()
|
||||
|
||||
switch p.Mode {
|
||||
case ModeOff:
|
||||
c.setStatus("off")
|
||||
return setChargerFrc(c.host, 1)
|
||||
|
||||
case ModeGrid:
|
||||
if err := setChargerAmp(c.host, p.MaxAmp); err != nil {
|
||||
return err
|
||||
}
|
||||
c.setStatus(fmt.Sprintf("grid %dA", p.MaxAmp))
|
||||
return setChargerFrc(c.host, 2)
|
||||
|
||||
case ModeSolar, ModeSolarBattery:
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c.mu.Lock()
|
||||
c.cancel = cancel
|
||||
c.mu.Unlock()
|
||||
go c.run(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChargerController) run(ctx context.Context) {
|
||||
c.adjust(ctx)
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
c.adjust(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ChargerController) adjust(ctx context.Context) {
|
||||
c.mu.RLock()
|
||||
params := c.params
|
||||
c.mu.RUnlock()
|
||||
|
||||
// ── Check stop targets ────────────────────────────────────────────────────
|
||||
|
||||
if params.TargetKwh > 0 || params.TargetSoc > 0 {
|
||||
st, err := fetchChargerStatus(c.host)
|
||||
if err != nil {
|
||||
c.setStatus("charger unreachable")
|
||||
return
|
||||
}
|
||||
if params.TargetSoc > 0 && st.Soc > 0 && st.Soc >= params.TargetSoc {
|
||||
c.stopForTarget(fmt.Sprintf("car SOC %d%% reached", params.TargetSoc))
|
||||
return
|
||||
}
|
||||
if params.TargetKwh > 0 && st.SessionWh/1000.0 >= params.TargetKwh {
|
||||
c.stopForTarget(fmt.Sprintf("%.1f kWh target reached", params.TargetKwh))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ── Query real-time power ─────────────────────────────────────────────────
|
||||
|
||||
var pvPower, batterySoc float64
|
||||
err := c.pool.QueryRow(ctx,
|
||||
`SELECT COALESCE(AVG(pv1_power + pv2_power), 0), COALESCE(AVG(battery_soc), 0)
|
||||
FROM inverter WHERE time > NOW() - '30 seconds'::interval`,
|
||||
).Scan(&pvPower, &batterySoc)
|
||||
if err != nil {
|
||||
log.Printf("charger ctrl: inverter: %v", err)
|
||||
c.setStatus("db error")
|
||||
return
|
||||
}
|
||||
|
||||
housePower, barnPower := 0.0, 0.0
|
||||
rows, err := c.pool.Query(ctx,
|
||||
`SELECT device, COALESCE(AVG(l1_power + l2_power + l3_power), 0)
|
||||
FROM power_meter WHERE time > NOW() - '30 seconds'::interval
|
||||
GROUP BY device`)
|
||||
if err != nil {
|
||||
log.Printf("charger ctrl: meter: %v", err)
|
||||
c.setStatus("db error")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var dev string
|
||||
var pwr float64
|
||||
if err := rows.Scan(&dev, &pwr); err != nil {
|
||||
continue
|
||||
}
|
||||
switch dev {
|
||||
case "house":
|
||||
housePower = pwr
|
||||
case "barn":
|
||||
barnPower = pwr
|
||||
}
|
||||
}
|
||||
|
||||
// ── Compute desired charging watts ────────────────────────────────────────
|
||||
//
|
||||
// solar: track PV surplus (PV minus all home loads); auto-switch
|
||||
// phases to stay above the 6 A minimum where possible.
|
||||
//
|
||||
// solar_battery: charge at full configured current regardless of surplus;
|
||||
// the battery absorbs the delta. Stop when battery SOC hits
|
||||
// MinBatterySoc; resume only after it recovers to
|
||||
// MinBatterySoc+Hysteresis (prevents rapid cycling).
|
||||
|
||||
var desiredW float64
|
||||
switch params.Mode {
|
||||
case ModeSolar:
|
||||
desiredW = pvPower - housePower - barnPower
|
||||
|
||||
case ModeSolarBattery:
|
||||
hysteresis := params.Hysteresis
|
||||
if hysteresis == 0 {
|
||||
hysteresis = 5
|
||||
}
|
||||
resumeAt := float64(params.MinBatterySoc + hysteresis)
|
||||
|
||||
c.mu.Lock()
|
||||
if !c.battPaused && batterySoc <= float64(params.MinBatterySoc) {
|
||||
c.battPaused = true
|
||||
} else if c.battPaused && batterySoc >= resumeAt {
|
||||
c.battPaused = false
|
||||
}
|
||||
paused := c.battPaused
|
||||
c.mu.Unlock()
|
||||
|
||||
if paused {
|
||||
if err := setChargerFrc(c.host, 1); err != nil {
|
||||
log.Printf("charger ctrl: batt pause: %v", err)
|
||||
}
|
||||
c.setStatus(fmt.Sprintf("paused — bat %.0f%% ≤ %d%% (resume at %.0f%%)", batterySoc, params.MinBatterySoc, resumeAt))
|
||||
return
|
||||
}
|
||||
|
||||
// Battery above threshold — charge at max
|
||||
desiredW = float64(params.MaxAmp*params.Phases) * 230.0
|
||||
}
|
||||
|
||||
// ── Auto phase selection ──────────────────────────────────────────────────
|
||||
//
|
||||
// Prefer max phases when surplus is sufficient; drop to 1-phase to keep
|
||||
// charging on partly cloudy days rather than stopping entirely.
|
||||
// Minimum current per phase is 6 A (IEC 61851 floor).
|
||||
|
||||
const minAmps = 6
|
||||
const voltageV = 230.0
|
||||
maxPh := params.Phases // user-configured ceiling (1 or 3)
|
||||
|
||||
var targetPhases int
|
||||
if desiredW >= float64(minAmps*maxPh)*voltageV {
|
||||
targetPhases = maxPh
|
||||
} else if desiredW >= float64(minAmps)*voltageV {
|
||||
targetPhases = 1
|
||||
} else {
|
||||
// Not enough even for 1-phase — pause
|
||||
if err := setChargerFrc(c.host, 1); err != nil {
|
||||
log.Printf("charger ctrl: pause: %v", err)
|
||||
}
|
||||
c.setStatus(fmt.Sprintf("waiting — %.0fW surplus (need ≥%.0fW for 1-phase)", desiredW, float64(minAmps)*voltageV))
|
||||
return
|
||||
}
|
||||
|
||||
// Switch phases only when necessary to avoid interrupting the session
|
||||
c.mu.Lock()
|
||||
needSwitch := c.currentPhases != targetPhases
|
||||
if needSwitch {
|
||||
c.currentPhases = targetPhases
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if needSwitch {
|
||||
if err := setChargerPhases(c.host, targetPhases); err != nil {
|
||||
log.Printf("charger ctrl: phase switch: %v", err)
|
||||
c.setStatus("phase switch failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ── Apply current ─────────────────────────────────────────────────────────
|
||||
|
||||
amps := int(desiredW / float64(targetPhases) / voltageV)
|
||||
if amps > params.MaxAmp {
|
||||
amps = params.MaxAmp
|
||||
}
|
||||
if amps < minAmps {
|
||||
amps = minAmps
|
||||
}
|
||||
|
||||
if err := setChargerAmp(c.host, amps); err != nil {
|
||||
log.Printf("charger ctrl: amp: %v", err)
|
||||
c.setStatus("amp set failed")
|
||||
return
|
||||
}
|
||||
if err := setChargerFrc(c.host, 2); err != nil {
|
||||
log.Printf("charger ctrl: enable: %v", err)
|
||||
c.setStatus("enable failed")
|
||||
return
|
||||
}
|
||||
c.setStatus(fmt.Sprintf("%dA/%dph · %.0fW surplus · bat %.0f%%", amps, targetPhases, desiredW, batterySoc))
|
||||
}
|
||||
|
||||
func (c *ChargerController) stopForTarget(reason string) {
|
||||
if err := setChargerFrc(c.host, 1); err != nil {
|
||||
log.Printf("charger ctrl: stop: %v", err)
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.params.Mode = ModeOff
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
c.cancel = nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.setStatus(reason)
|
||||
}
|
||||
|
||||
func (c *ChargerController) setStatus(s string) {
|
||||
c.mu.Lock()
|
||||
c.status = s
|
||||
c.mu.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ChargerStatus struct {
|
||||
Car int `json:"car"`
|
||||
Frc int `json:"frc"`
|
||||
SessionWh float64 `json:"session_wh"`
|
||||
Amp int `json:"amp"`
|
||||
Soc int `json:"soc"` // car battery %, 0 = not reported by car
|
||||
}
|
||||
|
||||
type goeStatus struct {
|
||||
Car int `json:"car"`
|
||||
Frc int `json:"frc"`
|
||||
Wh float64 `json:"wh"`
|
||||
Amp int `json:"amp"`
|
||||
Soc int `json:"soc"`
|
||||
}
|
||||
|
||||
var chargerHTTP = &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
func fetchChargerStatus(host string) (ChargerStatus, error) {
|
||||
url := "http://" + host + "/api/status?filter=car,frc,wh,amp,soc"
|
||||
resp, err := chargerHTTP.Get(url)
|
||||
if err != nil {
|
||||
return ChargerStatus{}, fmt.Errorf("charger unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return ChargerStatus{}, err
|
||||
}
|
||||
var s goeStatus
|
||||
if err := json.Unmarshal(body, &s); err != nil {
|
||||
return ChargerStatus{}, err
|
||||
}
|
||||
return ChargerStatus{Car: s.Car, Frc: s.Frc, SessionWh: s.Wh, Amp: s.Amp, Soc: s.Soc}, nil
|
||||
}
|
||||
|
||||
func setChargerFrc(host string, frc int) error {
|
||||
url := fmt.Sprintf("http://%s/api/set?frc=%d", host, frc)
|
||||
resp, err := chargerHTTP.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("charger unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func setChargerAmp(host string, amps int) error {
|
||||
url := fmt.Sprintf("http://%s/api/set?amp=%d", host, amps)
|
||||
resp, err := chargerHTTP.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("charger unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// resetCharger sends rst=1 and waits for the charger to reboot.
|
||||
// frc is reset to 0 by the charger firmware, so callers must re-apply mode after this.
|
||||
func resetCharger(host string) error {
|
||||
url := "http://" + host + "/api/set?rst=1"
|
||||
resp, _ := chargerHTTP.Get(url) // charger may close connection before responding
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
time.Sleep(8 * time.Second)
|
||||
if _, err := fetchChargerStatus(host); err != nil {
|
||||
return fmt.Errorf("charger did not recover from reset: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setChargerPhases switches between 1-phase (psm=1) and 3-phase (psm=2).
|
||||
// Requires phase-switching hardware in the charger.
|
||||
func setChargerPhases(host string, phases int) error {
|
||||
psm := 2
|
||||
if phases == 1 {
|
||||
psm = 1
|
||||
}
|
||||
url := fmt.Sprintf("http://%s/api/set?psm=%d", host, psm)
|
||||
resp, err := chargerHTTP.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("charger unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
+41
-16
@@ -38,10 +38,11 @@ var periodDefaults = map[string]int{
|
||||
}
|
||||
|
||||
// viewForRange picks the right continuous aggregate views for the given time range.
|
||||
// Returns empty strings for invView when the range should use raw tables (see getPower).
|
||||
func viewForRange(timeRange string) (invView, meterView, chargerView, interval string) {
|
||||
switch timeRange {
|
||||
case "1h":
|
||||
return "inverter_10m", "power_meter_10m", "charger_10m", "1 hour"
|
||||
return "", "", "", "1 hour" // raw tables, 30s buckets
|
||||
case "6h":
|
||||
return "inverter_10m", "power_meter_10m", "charger_10m", "6 hours"
|
||||
case "7d":
|
||||
@@ -90,18 +91,35 @@ func getPower(ctx context.Context, pool *pgxpool.Pool, timeRange string) ([]Powe
|
||||
return m, rows.Err()
|
||||
}
|
||||
|
||||
pvSQL := fmt.Sprintf(
|
||||
`SELECT bucket, COALESCE(pv_power, 0) FROM %s WHERE bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||
invView, interval)
|
||||
houseSQL := fmt.Sprintf(
|
||||
`SELECT bucket, COALESCE(total_power, 0) FROM %s WHERE device = 'house' AND bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||
meterView, interval)
|
||||
barnSQL := fmt.Sprintf(
|
||||
`SELECT bucket, COALESCE(total_power, 0) FROM %s WHERE device = 'barn' AND bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||
meterView, interval)
|
||||
chargerSQL := fmt.Sprintf(
|
||||
`SELECT bucket, COALESCE(power, 0) FROM %s WHERE bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||
chargerView, interval)
|
||||
var pvSQL, houseSQL, barnSQL, chargerSQL string
|
||||
if invView == "" {
|
||||
// Raw tables with 30-second buckets for short live range
|
||||
pvSQL = fmt.Sprintf(
|
||||
`SELECT time_bucket('30 seconds', time) AS bucket, COALESCE(AVG(pv1_power + pv2_power), 0) FROM inverter WHERE time >= NOW() - '%s'::interval GROUP BY bucket ORDER BY bucket`,
|
||||
interval)
|
||||
houseSQL = fmt.Sprintf(
|
||||
`SELECT time_bucket('30 seconds', time) AS bucket, COALESCE(AVG(l1_power + l2_power + l3_power), 0) FROM power_meter WHERE device = 'house' AND time >= NOW() - '%s'::interval GROUP BY bucket ORDER BY bucket`,
|
||||
interval)
|
||||
barnSQL = fmt.Sprintf(
|
||||
`SELECT time_bucket('30 seconds', time) AS bucket, COALESCE(AVG(l1_power + l2_power + l3_power), 0) FROM power_meter WHERE device = 'barn' AND time >= NOW() - '%s'::interval GROUP BY bucket ORDER BY bucket`,
|
||||
interval)
|
||||
chargerSQL = fmt.Sprintf(
|
||||
`SELECT time_bucket('30 seconds', time) AS bucket, COALESCE(AVG(power), 0) FROM charger WHERE time >= NOW() - '%s'::interval GROUP BY bucket ORDER BY bucket`,
|
||||
interval)
|
||||
} else {
|
||||
pvSQL = fmt.Sprintf(
|
||||
`SELECT bucket, COALESCE(pv_power, 0) FROM %s WHERE bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||
invView, interval)
|
||||
houseSQL = fmt.Sprintf(
|
||||
`SELECT bucket, COALESCE(total_power, 0) FROM %s WHERE device = 'house' AND bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||
meterView, interval)
|
||||
barnSQL = fmt.Sprintf(
|
||||
`SELECT bucket, COALESCE(total_power, 0) FROM %s WHERE device = 'barn' AND bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||
meterView, interval)
|
||||
chargerSQL = fmt.Sprintf(
|
||||
`SELECT bucket, COALESCE(power, 0) FROM %s WHERE bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||
chargerView, interval)
|
||||
}
|
||||
|
||||
type result struct {
|
||||
m map[time.Time]float64
|
||||
@@ -152,9 +170,16 @@ func getPower(ctx context.Context, pool *pgxpool.Pool, timeRange string) ([]Powe
|
||||
func getBattery(ctx context.Context, pool *pgxpool.Pool, timeRange string) ([]BatteryPoint, error) {
|
||||
invView, _, _, interval := viewForRange(timeRange)
|
||||
|
||||
sql := fmt.Sprintf(
|
||||
`SELECT bucket, COALESCE(battery_soc, 0) FROM %s WHERE bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||
invView, interval)
|
||||
var sql string
|
||||
if invView == "" {
|
||||
sql = fmt.Sprintf(
|
||||
`SELECT time_bucket('30 seconds', time) AS bucket, COALESCE(AVG(battery_soc), 0) FROM inverter WHERE time >= NOW() - '%s'::interval GROUP BY bucket ORDER BY bucket`,
|
||||
interval)
|
||||
} else {
|
||||
sql = fmt.Sprintf(
|
||||
`SELECT bucket, COALESCE(battery_soc, 0) FROM %s WHERE bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||
invView, interval)
|
||||
}
|
||||
|
||||
rows, err := pool.Query(ctx, sql)
|
||||
if err != nil {
|
||||
|
||||
@@ -69,6 +69,65 @@ func NewRouter(cfg config.Config, frontendFS fs.FS) http.Handler {
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"points": pts})
|
||||
})
|
||||
|
||||
var controller *ChargerController
|
||||
if cfg.Charger.Host != "" {
|
||||
controller = NewChargerController(cfg.Charger.Host, pool)
|
||||
}
|
||||
|
||||
protected.HandleFunc("GET /api/charger", func(w http.ResponseWriter, r *http.Request) {
|
||||
if controller == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "charger not configured"})
|
||||
return
|
||||
}
|
||||
status, err := fetchChargerStatus(cfg.Charger.Host)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, status)
|
||||
})
|
||||
protected.HandleFunc("GET /api/charger/mode", func(w http.ResponseWriter, r *http.Request) {
|
||||
if controller == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "charger not configured"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, controller.State())
|
||||
})
|
||||
protected.HandleFunc("POST /api/charger/reset", func(w http.ResponseWriter, r *http.Request) {
|
||||
if controller == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "charger not configured"})
|
||||
return
|
||||
}
|
||||
if err := resetCharger(cfg.Charger.Host); err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// Re-apply current mode since reset clears frc to 0
|
||||
_ = controller.SetParams(controller.State().Params)
|
||||
status, err := fetchChargerStatus(cfg.Charger.Host)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, status)
|
||||
})
|
||||
protected.HandleFunc("POST /api/charger/mode", func(w http.ResponseWriter, r *http.Request) {
|
||||
if controller == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "charger not configured"})
|
||||
return
|
||||
}
|
||||
var p ChargingParams
|
||||
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid body"})
|
||||
return
|
||||
}
|
||||
if err := controller.SetParams(p); err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, controller.State())
|
||||
})
|
||||
|
||||
mux.Handle("/api/", auth.middleware(protected))
|
||||
|
||||
if frontendFS != nil {
|
||||
|
||||
Reference in New Issue
Block a user