evcc-io/charger/easee.go

903 lines
23 KiB
Go

package charger
// LICENSE
// Copyright (c) evcc.io (andig, naltatis, premultiply)
// This module is NOT covered by the MIT license. All rights reserved.
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"sync"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/charger/easee"
"github.com/evcc-io/evcc/core/loadpoint"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/sponsor"
"github.com/philippseith/signalr"
"github.com/samber/lo"
"golang.org/x/oauth2"
)
// observationTimeout is the maximum age of the most recently received charger
// observation before power and current readings are considered stale and zeroed.
// The Easee charger sends a thermal heartbeat every ~15 minutes when idle, so
// 20 minutes gives a comfortable margin above that while staying well below the
// time a user would expect stale data to linger.
const observationTimeout = 20 * time.Minute
// Easee charger implementation
type Easee struct {
*request.Helper
charger string
site, circuit int
log *util.Logger
mux sync.RWMutex
maxChargerCurrent float64
dynamicChargerCurrent float64
dynamicCircuitCurrent [3]float64
current float64
chargerEnabled bool
smartCharging bool
authorize bool
enabled bool
opMode int
pilotMode string
reasonForNoCurrent int
phaseMode int
currentPower, sessionEnergy, totalEnergy,
currentL1, currentL2, currentL3 float64
currentSessionID int
rfid string
lp loadpoint.API
dispatcher *easee.CommandDispatcher
obsC chan easee.Observation
obsTime map[easee.ObservationID]time.Time
lastObsReceived time.Time
startDone func()
}
func init() {
registry.AddCtx("easee", NewEaseeFromConfig)
}
// NewEaseeFromConfig creates a Easee charger from generic config
func NewEaseeFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) {
cc := struct {
User string
Password string
Charger string
Timeout time.Duration
Authorize bool
}{
Timeout: request.Timeout,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if cc.User == "" || cc.Password == "" {
return nil, api.ErrMissingCredentials
}
return NewEasee(ctx, cc.User, cc.Password, cc.Charger, cc.Timeout, cc.Authorize)
}
// NewEasee creates Easee charger
func NewEasee(ctx context.Context, user, password, charger string, timeout time.Duration, authorize bool) (*Easee, error) {
log := util.NewLogger("easee").Redact(user, password)
if !sponsor.IsAuthorized() {
return nil, api.ErrSponsorRequired
}
done := make(chan struct{})
c := &Easee{
Helper: request.NewHelper(log),
charger: charger,
authorize: authorize,
log: log,
current: 6, // default current
startDone: sync.OnceFunc(func() { close(done) }),
obsC: make(chan easee.Observation),
obsTime: make(map[easee.ObservationID]time.Time),
}
c.Client.Timeout = timeout
c.dispatcher = easee.NewCommandDispatcher(c.Helper, log, timeout)
ts, err := easee.TokenSource(log, user, password)
if err != nil {
return nil, err
}
// replace client transport with authenticated transport
c.Client.Transport = &oauth2.Transport{
Source: ts,
Base: c.Client.Transport,
}
// find charger
if charger == "" {
chargers, err := c.chargers()
if err != nil {
return nil, err
}
if len(chargers) != 1 {
return nil, fmt.Errorf("cannot determine charger id, found: %v", lo.Map(chargers, func(c easee.Charger, _ int) string { return c.ID }))
}
c.charger = chargers[0].ID
}
// find site
site, err := c.chargerSite(c.charger)
if err != nil {
return nil, err
}
if err := c.determineCircuit(site); err != nil {
return nil, err
}
client, err := signalr.NewClient(ctx,
signalr.WithConnector(c.connect(ts)),
signalr.WithBackoff(func() backoff.BackOff {
return backoff.NewExponentialBackOff(backoff.WithMaxElapsedTime(0)) // prevents SignalR stack to silently give up after 15 mins
}),
signalr.WithReceiver(c),
signalr.Logger(easee.SignalrLogger(c.log.TRACE), false),
)
if err == nil {
c.subscribe(client)
client.Start()
ctx, cancel := context.WithTimeout(ctx, request.Timeout)
defer cancel()
err = <-client.WaitForState(ctx, signalr.ClientConnected)
}
// wait for first update
select {
case <-done:
case <-ctx.Done():
err = ctx.Err()
case <-time.After(request.Timeout):
err = os.ErrDeadlineExceeded
}
if err == nil {
c.waitForOptionalState()
}
return c, err
}
func (c *Easee) waitForOptionalState() {
for range 30 {
if c.optionalStatePresent() {
return
}
time.Sleep(100 * time.Millisecond)
}
c.log.WARN.Println("did not receive full state from cloud")
}
// check c.obsTime for presence of ALL of the following keys: easee.SESSION_ENERGY, easee.LIFETIME_ENERGY
func (c *Easee) optionalStatePresent() bool {
c.mux.Lock()
defer c.mux.Unlock()
wanted := []easee.ObservationID{easee.SESSION_ENERGY, easee.LIFETIME_ENERGY, easee.TOTAL_POWER}
return len(wanted) == len(lo.Intersect(wanted, lo.Keys(c.obsTime)))
}
func (c *Easee) chargerSite(charger string) (easee.Site, error) {
var res easee.Site
uri := fmt.Sprintf("%s/chargers/%s/site", easee.API, charger)
err := c.GetJSON(uri, &res)
return res, err
}
func isTNGrid(gridType int) bool {
switch gridType {
case easee.PowerGridTN3Phase, easee.PowerGridTN2PhasePin234, easee.PowerGridTN1Phase:
return true
}
return false
}
func (c *Easee) chargerConfig(charger string) (res easee.ChargerConfig, err error) {
uri := fmt.Sprintf("%s/chargers/%s/config", easee.API, charger)
err = c.GetJSON(uri, &res)
return res, err
}
func (c *Easee) determineCircuit(site easee.Site) error {
config, err := c.chargerConfig(c.charger)
if err != nil {
return fmt.Errorf("charger config unavailable: %w", err)
}
if !isTNGrid(config.DetectedPowerGridType) {
return nil
}
for _, circuit := range site.Circuits {
if len(circuit.Chargers) > 1 {
continue
}
for _, charger := range circuit.Chargers {
if charger.ID == c.charger {
c.site = site.ID
c.circuit = circuit.ID
return nil
}
}
}
return nil
}
// connect creates an HTTP connection to the signalR hub
func (c *Easee) connect(ts oauth2.TokenSource) func() (signalr.Connection, error) {
bo := backoff.NewExponentialBackOff(backoff.WithMaxInterval(time.Minute), backoff.WithMaxElapsedTime(0))
return func() (conn signalr.Connection, err error) {
defer func() {
if err != nil {
time.Sleep(bo.NextBackOff())
} else {
bo.Reset()
}
}()
tok, err := ts.Token()
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), request.Timeout)
defer cancel()
return signalr.NewHTTPConnection(ctx, "https://streams.easee.com/hubs/chargers",
signalr.WithHTTPClient(c.Client),
signalr.WithHTTPHeaders(func() http.Header {
return http.Header{
"Authorization": []string{"Bearer " + tok.AccessToken},
}
}),
)
}
}
// subscribe listen to state changes and sends subscription requests when connection is established
func (c *Easee) subscribe(client signalr.Client) {
stateC := make(chan signalr.ClientState, 1)
_ = client.ObserveStateChanged(stateC)
go func() {
for state := range stateC {
if state == signalr.ClientConnected {
if err := <-client.Send("SubscribeWithCurrentState", c.charger, true); err != nil {
c.log.ERROR.Printf("SubscribeWithCurrentState: %v", err)
}
}
}
}()
}
// ProductUpdate implements the signalr receiver
func (c *Easee) ProductUpdate(i json.RawMessage) {
var res easee.Observation
if err := json.Unmarshal(i, &res); err != nil {
c.log.ERROR.Printf("invalid message: %s %v", i, err)
return
}
value, err := res.TypedValue()
if err != nil {
c.log.ERROR.Println(err)
return
}
// https://github.com/evcc-io/evcc/issues/8009
// logging might be slow or block, execute outside lock
c.log.TRACE.Printf("ProductUpdate %s: (%v) %s %v", res.Mid, res.Timestamp, res.ID, value)
c.mux.Lock()
defer c.mux.Unlock()
if prevTime, ok := c.obsTime[res.ID]; ok && prevTime.After(res.Timestamp) {
// received observation is outdated, ignoring
return
}
c.obsTime[res.ID] = res.Timestamp
// Update liveness timestamp for observations with a fresh charger-side timestamp.
// Stale cloud replay on restart has old timestamps and must not refresh this.
if time.Since(res.Timestamp) < observationTimeout {
c.lastObsReceived = time.Now()
}
switch res.ID {
case easee.USER_IDTOKEN:
c.rfid = res.Value
case easee.IS_ENABLED:
c.chargerEnabled = value.(bool)
case easee.SMART_CHARGING:
c.smartCharging = value.(bool)
case easee.TOTAL_POWER:
c.currentPower = 1e3 * value.(float64)
case easee.SESSION_ENERGY:
// SESSION_ENERGY must not be set to 0 by Productupdates, they occur erratic
// Reset to 0 is done in case CHARGER_OP_MODE
if value.(float64) != 0 {
c.sessionEnergy = value.(float64)
}
case easee.LIFETIME_ENERGY:
if v := value.(float64); v >= c.totalEnergy {
c.totalEnergy = v
}
case easee.INT_CURRENT_T3:
c.currentL1 = value.(float64)
case easee.INT_CURRENT_T4:
c.currentL2 = value.(float64)
case easee.INT_CURRENT_T5:
c.currentL3 = value.(float64)
case easee.PHASE_MODE:
c.phaseMode = value.(int)
case easee.DYNAMIC_CIRCUIT_CURRENT_P1:
c.dynamicCircuitCurrent[0] = value.(float64)
case easee.DYNAMIC_CIRCUIT_CURRENT_P2:
c.dynamicCircuitCurrent[1] = value.(float64)
case easee.DYNAMIC_CIRCUIT_CURRENT_P3:
c.dynamicCircuitCurrent[2] = value.(float64)
case easee.MAX_CHARGER_CURRENT:
c.maxChargerCurrent = value.(float64)
case easee.DYNAMIC_CHARGER_CURRENT:
c.dynamicChargerCurrent = value.(float64)
case easee.CHARGE_SESSION_START:
var data easee.ChargingSessionStartData
if err := json.Unmarshal([]byte(res.Value), &data); err != nil {
c.log.ERROR.Printf("CHARGE_SESSION_START: %v", err)
break
}
c.currentSessionID = data.ID
if data.MeterValue >= c.totalEnergy {
c.totalEnergy = data.MeterValue
}
case easee.CHARGING_SESSION:
var data easee.ChargingSessionData
if err := json.Unmarshal([]byte(res.Value), &data); err != nil {
c.log.ERROR.Printf("CHARGING_SESSION: %v", err)
break
}
if data.MeterValueStop >= c.totalEnergy {
c.totalEnergy = data.MeterValueStop
}
if data.ID != c.currentSessionID {
break
}
c.sessionEnergy = data.EnergyKwh
case easee.CHARGER_OP_MODE:
opMode := value.(int)
// New charging session pending, reset internal value of SESSION_ENERGY to 0, and its observation timestamp to "now".
// This should be done in a proper way by the api, but it's not.
if c.opMode <= easee.ModeDisconnected && opMode >= easee.ModeAwaitingStart {
c.sessionEnergy = 0
c.currentSessionID = 0
c.obsTime[easee.SESSION_ENERGY] = time.Now()
}
c.opMode = opMode
// startup completed
c.startDone()
case easee.REASON_FOR_NO_CURRENT:
c.reasonForNoCurrent = value.(int)
case easee.PILOT_MODE:
c.pilotMode = value.(string)
}
select {
case c.obsC <- res:
default:
}
}
// ChargerUpdate implements the signalr receiver
func (c *Easee) ChargerUpdate(i json.RawMessage) {
c.log.TRACE.Printf("ChargerUpdate: %s", i)
}
// SubscribeToMyProduct implements the signalr receiver
func (c *Easee) SubscribeToMyProduct(i json.RawMessage) {
c.log.TRACE.Printf("SubscribeToMyProduct: %s", i)
}
// CommandResponse implements the signalr receiver
func (c *Easee) CommandResponse(i json.RawMessage) {
var res easee.SignalRCommandResponse
if err := json.Unmarshal(i, &res); err != nil {
c.log.ERROR.Printf("invalid message: %s %v", i, err)
return
}
c.log.TRACE.Printf("CommandResponse %s: %+v", res.SerialNumber, res)
c.dispatcher.Dispatch(res)
}
func (c *Easee) chargers() ([]easee.Charger, error) {
var res []easee.Charger
uri := fmt.Sprintf("%s/chargers", easee.API)
err := c.GetJSON(uri, &res)
return res, err
}
// Status implements the api.Charger interface
func (c *Easee) Status() (api.ChargeStatus, error) {
c.updateSmartCharging()
c.mux.RLock()
defer c.mux.RUnlock()
res := api.StatusNone
switch c.opMode {
case easee.ModeDisconnected:
res = api.StatusA
case easee.ModeAwaitingStart, easee.ModeCompleted, easee.ModeReadyToCharge,
easee.ModeAwaitingAuthentication, easee.ModeDeauthenticating:
res = api.StatusB
case easee.ModeCharging:
res = api.StatusC
default:
return res, fmt.Errorf("invalid opmode: %d", c.opMode)
}
return res, nil
}
// Enabled implements the api.Charger interface
func (c *Easee) Enabled() (bool, error) {
c.mux.RLock()
defer c.mux.RUnlock()
return c.enabled, nil
}
// Enable implements the api.Charger interface
func (c *Easee) Enable(enable bool) (err error) {
c.mux.Lock()
enablingRequired := enable && !c.chargerEnabled
opMode := c.opMode
c.mux.Unlock()
defer func() {
if err == nil {
c.enabled = enable
}
}()
// enable charger once if it's switched off
if enablingRequired {
data := easee.ChargerSettings{
Enabled: &enable,
}
uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger)
if _, err := c.dispatcher.Send(uri, data); err != nil {
return err
}
}
// do not send pause/resume if disconnected or unauthenticated without automatic authorization
if opMode == easee.ModeDisconnected || (opMode == easee.ModeAwaitingAuthentication && !(enable && c.authorize)) {
return nil
}
// resume/stop charger
action := easee.ChargePause
var targetCurrent float64
if enable {
action = easee.ChargeResume
if opMode == easee.ModeAwaitingAuthentication && c.authorize {
action = easee.ChargeStart
}
targetCurrent = 32
}
uri := fmt.Sprintf("%s/chargers/%s/commands/%s", easee.API, c.charger, action)
if _, err := c.dispatcher.Send(uri, nil); err != nil {
return err
}
if err := c.waitForChargerEnabledState(enable); err != nil {
return err
}
if action == easee.ChargeStart { // ChargeStart does not mingle with DCC, no need for below operations
return nil
}
if err := c.waitForDynamicChargerCurrent(targetCurrent); err != nil {
return err
}
if enable {
// reset currents after enable, as easee automatically resets to maxA
return c.MaxCurrent(int64(c.current))
}
return nil
}
func (c *Easee) inExpectedOpMode(enable bool) bool {
c.mux.RLock()
defer c.mux.RUnlock()
// start/resume
if enable {
return c.opMode == easee.ModeCharging ||
c.opMode == easee.ModeCompleted ||
c.opMode == easee.ModeAwaitingStart ||
c.opMode == easee.ModeReadyToCharge
}
// paused/stopped
return c.opMode == easee.ModeAwaitingStart || c.opMode == easee.ModeAwaitingAuthentication
}
// wait for opMode become expected op mode
func (c *Easee) waitForChargerEnabledState(expEnabled bool) error {
// check any updates received meanwhile
if c.inExpectedOpMode(expEnabled) {
return nil
}
timer := time.NewTimer(c.Client.Timeout)
for {
select {
case obs := <-c.obsC:
if obs.ID != easee.CHARGER_OP_MODE {
continue
}
if c.inExpectedOpMode(expEnabled) {
return nil
}
case <-timer.C: // time is up, bail after one final check
if c.inExpectedOpMode(expEnabled) {
return nil
}
return api.ErrTimeout
}
}
}
// wait for current become targetCurrent
func (c *Easee) waitForDynamicChargerCurrent(targetCurrent float64) error {
// check any updates received meanwhile
c.mux.RLock()
if c.dynamicChargerCurrent == targetCurrent {
c.mux.RUnlock()
return nil
}
c.mux.RUnlock()
timer := time.NewTimer(c.Client.Timeout)
for {
select {
case obs := <-c.obsC:
if obs.ID != easee.DYNAMIC_CHARGER_CURRENT {
continue
}
value, err := obs.TypedValue()
if err != nil {
continue
}
if value.(float64) == targetCurrent {
return nil
}
case <-timer.C: // time is up, bail after one final check
c.mux.RLock()
defer c.mux.RUnlock()
if c.dynamicChargerCurrent == targetCurrent {
return nil
}
return api.ErrTimeout
}
}
}
// MaxCurrent implements the api.Charger interface
func (c *Easee) MaxCurrent(current int64) error {
cur := float64(current)
if c.maxChargerCurrent != 0 {
cur = min(cur, c.maxChargerCurrent)
}
data := easee.ChargerSettings{
DynamicChargerCurrent: &cur,
}
uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger)
noop, err := c.dispatcher.Send(uri, data)
if err != nil {
return err
}
if !noop {
if err := c.waitForDynamicChargerCurrent(cur); err != nil {
return err
}
}
c.mux.Lock()
defer c.mux.Unlock()
c.current = cur
return nil
}
var _ api.CurrentGetter = (*Easee)(nil)
// GetMaxCurrent implements the api.CurrentGetter interface
func (c *Easee) GetMaxCurrent() (float64, error) {
c.mux.RLock()
defer c.mux.RUnlock()
return c.dynamicChargerCurrent, nil
}
var _ api.Meter = (*Easee)(nil)
// CurrentPower implements the api.Meter interface
func (c *Easee) CurrentPower() (float64, error) {
if status, err := c.Status(); err != nil || status == api.StatusA {
return 0, err
}
c.mux.RLock()
defer c.mux.RUnlock()
if time.Since(c.lastObsReceived) > observationTimeout {
return 0, nil
}
return c.currentPower, nil
}
var _ api.ChargeRater = (*Easee)(nil)
// ChargedEnergy implements the api.ChargeRater interface
func (c *Easee) ChargedEnergy() (float64, error) {
c.mux.RLock()
defer c.mux.RUnlock()
return c.sessionEnergy, nil
}
var _ api.PhaseCurrents = (*Easee)(nil)
// Currents implements the api.PhaseCurrents interface
func (c *Easee) Currents() (float64, float64, float64, error) {
c.mux.RLock()
defer c.mux.RUnlock()
if time.Since(c.lastObsReceived) > observationTimeout {
return 0, 0, 0, nil
}
return c.currentL1, c.currentL2, c.currentL3, nil
}
var _ api.MeterEnergy = (*Easee)(nil)
// TotalEnergy implements the api.MeterEnergy interface
func (c *Easee) TotalEnergy() (float64, error) {
c.mux.RLock()
defer c.mux.RUnlock()
// updates for this are only sent once an hour, so inaccurate by design
// see also https://github.com/evcc-io/evcc/issues/20594
return c.totalEnergy, nil
}
var _ api.PhaseSwitcher = (*Easee)(nil)
// Phases1p3p implements the api.PhaseSwitcher interface
func (c *Easee) Phases1p3p(phases int) error {
var err error
if c.circuit != 0 {
// circuit level
uri := fmt.Sprintf("%s/sites/%d/circuits/%d/settings", easee.API, c.site, c.circuit)
var res easee.CircuitSettings
if err := c.GetJSON(uri, &res); err != nil {
return err
}
if res.MaxCircuitCurrentP1 == nil || res.MaxCircuitCurrentP2 == nil || res.MaxCircuitCurrentP3 == nil {
return errors.New("MaxCircuitCurrent must not be nil")
}
var zero float64
max1 := *res.MaxCircuitCurrentP1
max2 := *res.MaxCircuitCurrentP2
max3 := *res.MaxCircuitCurrentP3
data := easee.CircuitSettings{
DynamicCircuitCurrentP1: &max1,
DynamicCircuitCurrentP2: &zero,
DynamicCircuitCurrentP3: &zero,
}
if phases > 1 {
data.DynamicCircuitCurrentP2 = &max2
data.DynamicCircuitCurrentP3 = &max3
}
// Register before POST so the SignalR CommandResponse that the Easee
// cloud sends on HTTP 200 (sync) responses is silently consumed rather
// than logged as rogue. On error we undo the registration.
c.dispatcher.ExpectOrphan(easee.CIRCUIT_MAX_CURRENT_P1)
_, err = c.dispatcher.Send(uri, data)
if err != nil {
c.dispatcher.CancelOrphan(easee.CIRCUIT_MAX_CURRENT_P1)
}
// Sending DCC:7 to skip charge pause after scaling down to 1p.
// The loadpoint's next control interval will send the real target current.
if err == nil && phases == 1 {
override := 7.0
chargerData := easee.ChargerSettings{
DynamicChargerCurrent: &override,
}
chargerURI := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger)
noop, sendErr := c.dispatcher.Send(chargerURI, chargerData)
if sendErr != nil {
c.log.WARN.Printf("phase switch: failed to set charger current override: %v", sendErr)
} else if !noop {
if waitErr := c.waitForDynamicChargerCurrent(override); waitErr != nil {
c.log.WARN.Printf("phase switch: charger current override confirmation timeout: %v", waitErr)
}
}
c.mux.Lock()
c.current = override
c.mux.Unlock()
}
} else {
// charger level
if phases == 3 {
phases = 2 // mode 2 means auto
}
// change phaseMode only if necessary
if phases != c.phaseMode {
data := easee.ChargerSettings{
PhaseMode: &phases,
}
uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger)
if _, err = c.dispatcher.Send(uri, data); err != nil {
return err
}
}
// disable charger to activate changed settings (loadpoint will reenable it)
err = c.Enable(false)
}
return err
}
var _ api.PhaseGetter = (*Easee)(nil)
// GetPhases implements the api.PhaseGetter interface
func (c *Easee) GetPhases() (int, error) {
c.mux.RLock()
defer c.mux.RUnlock()
var phases int
if c.circuit != 0 {
// circuit level controlled charger
for _, dcc := range c.dynamicCircuitCurrent {
if dcc > 0 {
phases++
}
}
} else {
// charger level
phases = c.phaseMode
if phases == 2 { // map automatic to 3p
phases = 3
}
}
return phases, nil
}
var _ api.StatusReasoner = (*Easee)(nil)
// StatusReason implements the api.StatusReasoner interface
func (c *Easee) StatusReason() (api.Reason, error) {
c.mux.RLock()
defer c.mux.RUnlock()
switch c.opMode {
case easee.ModeAwaitingAuthentication:
return api.ReasonWaitingForAuthorization, nil
}
return api.ReasonUnknown, nil
}
var _ api.Identifier = (*Easee)(nil)
// Currents implements the api.PhaseCurrents interface
func (c *Easee) Identify() (string, error) {
c.mux.Lock()
defer c.mux.Unlock()
return c.rfid, nil
}
// Set smart charging status to update the chargers led (smart=blue, fast=white)
func (c *Easee) updateSmartCharging() {
if c.lp == nil {
return
}
mode := c.lp.GetMode()
isSmartCharging := mode == api.ModePV || mode == api.ModeMinPV
c.mux.Lock()
updateNeeded := c.opMode != easee.ModeDisconnected && isSmartCharging != c.smartCharging
c.mux.Unlock()
if updateNeeded {
data := easee.ChargerSettings{
SmartCharging: &isSmartCharging,
}
uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger)
if _, err := c.dispatcher.Send(uri, data); err != nil {
c.log.WARN.Printf("smart charging: %v", err)
return
}
c.mux.Lock()
c.smartCharging = isSmartCharging
c.mux.Unlock()
}
}
var _ loadpoint.Controller = (*Easee)(nil)
// LoadpointControl implements loadpoint.Controller
func (c *Easee) LoadpointControl(lp loadpoint.API) {
c.lp = lp
}