improve wall box integration
Signed-off-by: Thomas Klaehn <tkl@blackfinn.de>
This commit is contained in:
@@ -50,6 +50,7 @@ export interface ChargerStatus {
|
|||||||
session_wh: number
|
session_wh: number
|
||||||
amp: number // current charging current setting
|
amp: number // current charging current setting
|
||||||
soc: number // car battery %, 0 = not reported
|
soc: number // car battery %, 0 = not reported
|
||||||
|
alw: boolean // charging allowed; false = RFID/access control blocking
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChargingMode = 'off' | 'grid' | 'solar' | 'solar_battery'
|
export type ChargingMode = 'off' | 'grid' | 'solar' | 'solar_battery'
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ watch(selectedRange, refreshLive)
|
|||||||
<span class="ch-info">{{ (chargerSt.session_wh / 1000).toFixed(2) }} kWh</span>
|
<span class="ch-info">{{ (chargerSt.session_wh / 1000).toFixed(2) }} kWh</span>
|
||||||
<span v-if="chargerSt.amp" class="ch-info">{{ chargerSt.amp }}A</span>
|
<span v-if="chargerSt.amp" class="ch-info">{{ chargerSt.amp }}A</span>
|
||||||
<span v-if="chargerSt.soc" class="ch-info">car {{ chargerSt.soc }}%</span>
|
<span v-if="chargerSt.soc" class="ch-info">car {{ chargerSt.soc }}%</span>
|
||||||
|
<span v-if="!chargerSt.alw && chargerSt.car !== 1" class="ch-blocked">access control</span>
|
||||||
<span v-if="chargerCtrl" class="ch-ctrl-status">{{ chargerCtrl.status }}</span>
|
<span v-if="chargerCtrl" class="ch-ctrl-status">{{ chargerCtrl.status }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -501,6 +502,12 @@ watch(selectedRange, refreshLive)
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ch-blocked {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #ef5350;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.ch-ctrl-status {
|
.ch-ctrl-status {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|||||||
@@ -80,17 +80,40 @@ func (c *ChargerController) SetParams(p ChargingParams) error {
|
|||||||
|
|
||||||
switch p.Mode {
|
switch p.Mode {
|
||||||
case ModeOff:
|
case ModeOff:
|
||||||
|
// Stop charging immediately, then restore RFID-gated access so the
|
||||||
|
// car does not start charging automatically on next plug-in.
|
||||||
|
// frc=0 (neutral after stopping) lets the RFID card still work at
|
||||||
|
// the wallbox; acs=1 + nmo=false ensures RFID is mandatory.
|
||||||
|
if err := setChargerFrc(c.host, 1); err != nil {
|
||||||
|
log.Printf("charger ctrl: off frc: %v", err)
|
||||||
|
}
|
||||||
|
if err := setChargerNmo(c.host, false); err != nil {
|
||||||
|
log.Printf("charger ctrl: off nmo: %v", err)
|
||||||
|
}
|
||||||
|
if err := setChargerAcs(c.host, 1); err != nil {
|
||||||
|
log.Printf("charger ctrl: off acs: %v", err)
|
||||||
|
}
|
||||||
|
if err := setChargerFrc(c.host, 0); err != nil {
|
||||||
|
log.Printf("charger ctrl: off frc0: %v", err)
|
||||||
|
}
|
||||||
c.setStatus("off")
|
c.setStatus("off")
|
||||||
return setChargerFrc(c.host, 1)
|
return nil
|
||||||
|
|
||||||
case ModeGrid:
|
case ModeGrid:
|
||||||
if err := setChargerAmp(c.host, p.MaxAmp); err != nil {
|
if err := c.enableCharging(p.MaxAmp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.setStatus(fmt.Sprintf("grid %dA", p.MaxAmp))
|
c.setStatus(fmt.Sprintf("grid %dA", p.MaxAmp))
|
||||||
return setChargerFrc(c.host, 2)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
c.mu.Lock()
|
||||||
|
c.cancel = cancel
|
||||||
|
c.mu.Unlock()
|
||||||
|
go c.run(ctx)
|
||||||
|
return nil
|
||||||
|
|
||||||
case ModeSolar, ModeSolarBattery:
|
case ModeSolar, ModeSolarBattery:
|
||||||
|
// enableCharging is called by the first adjust() tick so the goroutine
|
||||||
|
// can restart the session asynchronously if car=4.
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
c.cancel = cancel
|
c.cancel = cancel
|
||||||
@@ -100,6 +123,41 @@ func (c *ChargerController) SetParams(p ChargingParams) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enableCharging prepares the charger for a new session and starts it.
|
||||||
|
// If the charger is in car=4 (complete) state the old session cannot be
|
||||||
|
// re-used; a soft reset is required to clear it before starting fresh.
|
||||||
|
func (c *ChargerController) enableCharging(amps int) error {
|
||||||
|
st, err := fetchChargerStatus(c.host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("charger unreachable: %w", err)
|
||||||
|
}
|
||||||
|
if st.Car == 4 {
|
||||||
|
c.setStatus("resetting — previous session complete")
|
||||||
|
if err := resetCharger(c.host); err != nil {
|
||||||
|
return fmt.Errorf("reset: %w", err)
|
||||||
|
}
|
||||||
|
// Reset clears currentPhases so phase-switch logic starts fresh
|
||||||
|
c.mu.Lock()
|
||||||
|
c.currentPhases = 0
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
if err := setChargerNmo(c.host, true); err != nil {
|
||||||
|
log.Printf("charger ctrl: nmo: %v", err)
|
||||||
|
}
|
||||||
|
if err := setChargerAcs(c.host, 0); err != nil {
|
||||||
|
log.Printf("charger ctrl: acs: %v", err)
|
||||||
|
}
|
||||||
|
if amps > 0 {
|
||||||
|
if err := setChargerAmp(c.host, amps); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := setChargerTrx(c.host); err != nil {
|
||||||
|
log.Printf("charger ctrl: trx: %v", err)
|
||||||
|
}
|
||||||
|
return setChargerFrc(c.host, 2)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ChargerController) run(ctx context.Context) {
|
func (c *ChargerController) run(ctx context.Context) {
|
||||||
c.adjust(ctx)
|
c.adjust(ctx)
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
@@ -119,32 +177,17 @@ func (c *ChargerController) adjust(ctx context.Context) {
|
|||||||
params := c.params
|
params := c.params
|
||||||
c.mu.RUnlock()
|
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 ─────────────────────────────────────────────────
|
// ── Query real-time power ─────────────────────────────────────────────────
|
||||||
|
// Runs unconditionally before charger-status checks so that battPaused is
|
||||||
|
// always current. Without this ordering, a car=4 early-return would prevent
|
||||||
|
// the SOC recovery logic from ever running, leaving battPaused=true
|
||||||
|
// permanently even after the battery fully recovers.
|
||||||
|
|
||||||
var pvPower, batterySoc float64
|
var pvPower, batterySoc float64
|
||||||
err := c.pool.QueryRow(ctx,
|
if err := c.pool.QueryRow(ctx,
|
||||||
`SELECT COALESCE(AVG(pv1_power + pv2_power), 0), COALESCE(AVG(battery_soc), 0)
|
`SELECT COALESCE(AVG(pv1_power + pv2_power), 0), COALESCE(AVG(battery_soc), 0)
|
||||||
FROM inverter WHERE time > NOW() - '30 seconds'::interval`,
|
FROM inverter WHERE time > NOW() - '30 seconds'::interval`,
|
||||||
).Scan(&pvPower, &batterySoc)
|
).Scan(&pvPower, &batterySoc); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Printf("charger ctrl: inverter: %v", err)
|
log.Printf("charger ctrl: inverter: %v", err)
|
||||||
c.setStatus("db error")
|
c.setStatus("db error")
|
||||||
return
|
return
|
||||||
@@ -175,6 +218,72 @@ func (c *ChargerController) adjust(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update battPaused before checking car state so that battery recovery
|
||||||
|
// while the charger reports car=4 is detected and charging can resume.
|
||||||
|
if params.Mode == 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
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fetch charger status ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
st, chargerErr := fetchChargerStatus(c.host)
|
||||||
|
if chargerErr != nil {
|
||||||
|
c.setStatus("charger unreachable — " + chargerErr.Error())
|
||||||
|
// fall through: mode logic still runs; commands will be retried next tick
|
||||||
|
} else {
|
||||||
|
// car=1: no car connected — restore RFID-gated state and stop the loop.
|
||||||
|
if st.Car == 1 {
|
||||||
|
c.stopOnDisconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// car=4: previous session ended; a soft reset is the only way to clear
|
||||||
|
// the transaction before a new session can start.
|
||||||
|
// Exception: if a battery-SOC pause is still active (battery hasn't
|
||||||
|
// recovered yet), do NOT restart — that would cause charge/stop toggling.
|
||||||
|
if st.Car == 4 {
|
||||||
|
c.mu.RLock()
|
||||||
|
paused := c.battPaused
|
||||||
|
c.mu.RUnlock()
|
||||||
|
if !paused {
|
||||||
|
c.setStatus("restarting — session complete")
|
||||||
|
if err := c.enableCharging(0); err != nil {
|
||||||
|
log.Printf("charger ctrl: session restart: %v", err)
|
||||||
|
c.setStatus("session restart failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.TargetKwh > 0 || params.TargetSoc > 0 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid mode: current is fixed and already applied by enableCharging(); the
|
||||||
|
// only job of the monitoring goroutine is car=1/4 detection above.
|
||||||
|
if params.Mode == ModeGrid {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ── Compute desired charging watts ────────────────────────────────────────
|
// ── Compute desired charging watts ────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// solar: track PV surplus (PV minus all home loads); auto-switch
|
// solar: track PV surplus (PV minus all home loads); auto-switch
|
||||||
@@ -191,25 +300,19 @@ func (c *ChargerController) adjust(ctx context.Context) {
|
|||||||
desiredW = pvPower - housePower - barnPower
|
desiredW = pvPower - housePower - barnPower
|
||||||
|
|
||||||
case ModeSolarBattery:
|
case ModeSolarBattery:
|
||||||
hysteresis := params.Hysteresis
|
c.mu.RLock()
|
||||||
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
|
paused := c.battPaused
|
||||||
c.mu.Unlock()
|
c.mu.RUnlock()
|
||||||
|
|
||||||
if paused {
|
if paused {
|
||||||
if err := setChargerFrc(c.host, 1); err != nil {
|
if err := setChargerFrc(c.host, 1); err != nil {
|
||||||
log.Printf("charger ctrl: batt pause: %v", err)
|
log.Printf("charger ctrl: batt pause: %v", err)
|
||||||
}
|
}
|
||||||
|
hysteresis := params.Hysteresis
|
||||||
|
if hysteresis == 0 {
|
||||||
|
hysteresis = 5
|
||||||
|
}
|
||||||
|
resumeAt := float64(params.MinBatterySoc + hysteresis)
|
||||||
c.setStatus(fmt.Sprintf("paused — bat %.0f%% ≤ %d%% (resume at %.0f%%)", batterySoc, params.MinBatterySoc, resumeAt))
|
c.setStatus(fmt.Sprintf("paused — bat %.0f%% ≤ %d%% (resume at %.0f%%)", batterySoc, params.MinBatterySoc, resumeAt))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -281,6 +384,32 @@ func (c *ChargerController) adjust(ctx context.Context) {
|
|||||||
c.setStatus(fmt.Sprintf("%dA/%dph · %.0fW surplus · bat %.0f%%", amps, targetPhases, desiredW, batterySoc))
|
c.setStatus(fmt.Sprintf("%dA/%dph · %.0fW surplus · bat %.0f%%", amps, targetPhases, desiredW, batterySoc))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stopOnDisconnect restores RFID-gated idle state and cancels the goroutine.
|
||||||
|
// Called when car=1 (no car connected) is detected during a monitoring tick.
|
||||||
|
func (c *ChargerController) stopOnDisconnect() {
|
||||||
|
if err := setChargerFrc(c.host, 1); err != nil {
|
||||||
|
log.Printf("charger ctrl: disconnect frc1: %v", err)
|
||||||
|
}
|
||||||
|
if err := setChargerNmo(c.host, false); err != nil {
|
||||||
|
log.Printf("charger ctrl: disconnect nmo: %v", err)
|
||||||
|
}
|
||||||
|
if err := setChargerAcs(c.host, 1); err != nil {
|
||||||
|
log.Printf("charger ctrl: disconnect acs: %v", err)
|
||||||
|
}
|
||||||
|
if err := setChargerFrc(c.host, 0); err != nil {
|
||||||
|
log.Printf("charger ctrl: disconnect frc0: %v", err)
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
c.params.Mode = ModeOff
|
||||||
|
c.battPaused = false
|
||||||
|
if c.cancel != nil {
|
||||||
|
c.cancel()
|
||||||
|
c.cancel = nil
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
c.setStatus("off — car disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ChargerController) stopForTarget(reason string) {
|
func (c *ChargerController) stopForTarget(reason string) {
|
||||||
if err := setChargerFrc(c.host, 1); err != nil {
|
if err := setChargerFrc(c.host, 1); err != nil {
|
||||||
log.Printf("charger ctrl: stop: %v", err)
|
log.Printf("charger ctrl: stop: %v", err)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type ChargerStatus struct {
|
|||||||
SessionWh float64 `json:"session_wh"`
|
SessionWh float64 `json:"session_wh"`
|
||||||
Amp int `json:"amp"`
|
Amp int `json:"amp"`
|
||||||
Soc int `json:"soc"` // car battery %, 0 = not reported by car
|
Soc int `json:"soc"` // car battery %, 0 = not reported by car
|
||||||
|
Alw bool `json:"alw"` // charging allowed (false = access control blocking)
|
||||||
}
|
}
|
||||||
|
|
||||||
type goeStatus struct {
|
type goeStatus struct {
|
||||||
@@ -22,12 +23,13 @@ type goeStatus struct {
|
|||||||
Wh float64 `json:"wh"`
|
Wh float64 `json:"wh"`
|
||||||
Amp int `json:"amp"`
|
Amp int `json:"amp"`
|
||||||
Soc int `json:"soc"`
|
Soc int `json:"soc"`
|
||||||
|
Alw bool `json:"alw"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var chargerHTTP = &http.Client{Timeout: 5 * time.Second}
|
var chargerHTTP = &http.Client{Timeout: 15 * time.Second}
|
||||||
|
|
||||||
func fetchChargerStatus(host string) (ChargerStatus, error) {
|
func fetchChargerStatus(host string) (ChargerStatus, error) {
|
||||||
url := "http://" + host + "/api/status?filter=car,frc,wh,amp,soc"
|
url := "http://" + host + "/api/status?filter=car,frc,wh,amp,soc,alw"
|
||||||
resp, err := chargerHTTP.Get(url)
|
resp, err := chargerHTTP.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ChargerStatus{}, fmt.Errorf("charger unreachable: %w", err)
|
return ChargerStatus{}, fmt.Errorf("charger unreachable: %w", err)
|
||||||
@@ -41,7 +43,46 @@ func fetchChargerStatus(host string) (ChargerStatus, error) {
|
|||||||
if err := json.Unmarshal(body, &s); err != nil {
|
if err := json.Unmarshal(body, &s); err != nil {
|
||||||
return ChargerStatus{}, err
|
return ChargerStatus{}, err
|
||||||
}
|
}
|
||||||
return ChargerStatus{Car: s.Car, Frc: s.Frc, SessionWh: s.Wh, Amp: s.Amp, Soc: s.Soc}, nil
|
return ChargerStatus{Car: s.Car, Frc: s.Frc, SessionWh: s.Wh, Amp: s.Amp, Soc: s.Soc, Alw: s.Alw}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setChargerAcs sets access control: 0 = open (app controls), 1 = RFID required.
|
||||||
|
func setChargerAcs(host string, acs int) error {
|
||||||
|
url := fmt.Sprintf("http://%s/api/set?acs=%d", host, acs)
|
||||||
|
resp, err := chargerHTTP.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("charger unreachable: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setChargerNmo sets Norwegian mode: true = charge without RFID, false = normal.
|
||||||
|
func setChargerNmo(host string, enabled bool) error {
|
||||||
|
val := "false"
|
||||||
|
if enabled {
|
||||||
|
val = "true"
|
||||||
|
}
|
||||||
|
url := "http://" + host + "/api/set?nmo=" + val
|
||||||
|
resp, err := chargerHTTP.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("charger unreachable: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setChargerTrx starts a charging session (trx=0). With acs=0 this bypasses
|
||||||
|
// RFID authentication but still initiates the IEC 61851 handshake — without
|
||||||
|
// it the charger stays in modelStatus "paused" and never closes its relay.
|
||||||
|
func setChargerTrx(host string) error {
|
||||||
|
url := "http://" + host + "/api/set?trx=0"
|
||||||
|
resp, err := chargerHTTP.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("charger unreachable: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setChargerFrc(host string, frc int) error {
|
func setChargerFrc(host string, frc int) error {
|
||||||
@@ -65,14 +106,14 @@ func setChargerAmp(host string, amps int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resetCharger sends rst=1 and waits for the charger to reboot.
|
// 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.
|
// frc and trx are cleared by the firmware on reset; callers must re-apply mode after this.
|
||||||
func resetCharger(host string) error {
|
func resetCharger(host string) error {
|
||||||
url := "http://" + host + "/api/set?rst=1"
|
url := "http://" + host + "/api/set?rst=1"
|
||||||
resp, _ := chargerHTTP.Get(url) // charger may close connection before responding
|
resp, _ := chargerHTTP.Get(url) // charger may close connection before responding
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
}
|
}
|
||||||
time.Sleep(8 * time.Second)
|
time.Sleep(12 * time.Second) // v59.x firmware takes longer to reboot
|
||||||
if _, err := fetchChargerStatus(host); err != nil {
|
if _, err := fetchChargerStatus(host); err != nil {
|
||||||
return fmt.Errorf("charger did not recover from reset: %w", err)
|
return fmt.Errorf("charger did not recover from reset: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user