evcc-io/charger/easee.go

948 lines
24 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"
"strings"
"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"
)
const obsTimeout = 5 * 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
rfid string
lp loadpoint.API
cmdMu sync.Mutex
pendingTicks map[int64]chan easee.SignalRCommandResponse
pendingByID map[easee.ObservationID]chan easee.SignalRCommandResponse
expectedOrphans map[easee.ObservationID]int
obsC chan easee.Observation
obsTime map[easee.ObservationID]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) }),
pendingTicks: make(map[int64]chan easee.SignalRCommandResponse),
pendingByID: make(map[easee.ObservationID]chan easee.SignalRCommandResponse),
expectedOrphans: make(map[easee.ObservationID]int),
obsC: make(chan easee.Observation),
obsTime: make(map[easee.ObservationID]time.Time),
}
c.Client.Timeout = 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
}
// find single charger per circuit
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
break
}
}
}
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")
}
func (c *Easee) registerPendingTick(tick int64, ch chan easee.SignalRCommandResponse) {
c.cmdMu.Lock()
c.pendingTicks[tick] = ch
c.cmdMu.Unlock()
}
func (c *Easee) unregisterPendingTick(tick int64) {
c.cmdMu.Lock()
delete(c.pendingTicks, tick)
c.cmdMu.Unlock()
}
func (c *Easee) registerPendingByID(id easee.ObservationID, ch chan easee.SignalRCommandResponse) {
c.cmdMu.Lock()
defer c.cmdMu.Unlock()
c.pendingByID[id] = ch
}
func (c *Easee) unregisterPendingByID(id easee.ObservationID) {
c.cmdMu.Lock()
defer c.cmdMu.Unlock()
delete(c.pendingByID, id)
}
func (c *Easee) registerExpectedOrphan(ids ...easee.ObservationID) {
c.cmdMu.Lock()
defer c.cmdMu.Unlock()
for _, id := range ids {
c.expectedOrphans[id]++
}
}
func (c *Easee) consumeExpectedOrphan(id easee.ObservationID) bool {
c.cmdMu.Lock()
defer c.cmdMu.Unlock()
if c.expectedOrphans[id] > 0 {
c.expectedOrphans[id]--
return true
}
return false
}
// 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
}
// 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
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:
c.totalEnergy = value.(float64)
case easee.IN_CURRENT_T3:
c.currentL1 = value.(float64)
case easee.IN_CURRENT_T4:
c.currentL2 = value.(float64)
case easee.IN_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.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.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)
obsID := easee.ObservationID(res.ID)
c.cmdMu.Lock()
chTick, tickOk := c.pendingTicks[res.Ticks]
chID, idOk := c.pendingByID[obsID]
c.cmdMu.Unlock()
if tickOk {
chTick <- res
return
}
if idOk {
chID <- res
return
}
if c.consumeExpectedOrphan(obsID) {
return
}
c.log.WARN.Printf("rogue CommandResponse: charger %s ObservationID=%s Ticks=%d "+
"(accepted=%v, resultCode=%d) which was not triggered by evcc — "+
"another system may be controlling this charger",
res.SerialNumber, obsID, res.Ticks, res.WasAccepted, res.ResultCode)
}
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.postJSONAndWait(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.postJSONAndWait(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
}
// posts JSON to the Easee API endpoint and waits for the async response
func (c *Easee) postJSONAndWait(uri string, data any) (bool, error) {
resp, err := c.Post(uri, request.JSONContent, request.MarshalJSON(data))
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 { // sync call
return false, nil
}
if resp.StatusCode == 202 { // async call, wait for response
var cmd easee.RestCommandResponse
if strings.Contains(uri, "/commands/") { // command endpoint
if err := json.NewDecoder(resp.Body).Decode(&cmd); err != nil {
return false, err
}
} else { // settings endpoint
var cmdArr []easee.RestCommandResponse
if err := json.NewDecoder(resp.Body).Decode(&cmdArr); err != nil {
return false, err
}
if len(cmdArr) != 0 {
cmd = cmdArr[0]
}
}
if cmd.Ticks == 0 { // api thinks this was a noop
return true, nil
}
ch := make(chan easee.SignalRCommandResponse, 1)
c.registerPendingTick(cmd.Ticks, ch)
defer c.unregisterPendingTick(cmd.Ticks)
obsID := easee.ObservationID(cmd.CommandId)
c.registerPendingByID(obsID, ch)
defer c.unregisterPendingByID(obsID)
return false, c.waitForTickResponse(ch)
}
// all other response codes lead to an error
return false, fmt.Errorf("invalid status: %d", resp.StatusCode)
}
func (c *Easee) waitForTickResponse(ch <-chan easee.SignalRCommandResponse) error {
select {
case cmdResp := <-ch:
if !cmdResp.WasAccepted {
return fmt.Errorf("command rejected: %d", cmdResp.Ticks)
}
return nil
case <-time.After(c.Client.Timeout):
return api.ErrTimeout
}
}
// 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.postJSONAndWait(uri, data)
if err != nil {
return err
}
if !noop {
if err := c.waitForDynamicChargerCurrent(float64(current)); 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 t, ok := c.obsTime[easee.TOTAL_POWER]; ok && time.Since(t) > obsTimeout {
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 t, ok := c.obsTime[easee.IN_CURRENT_T3]; ok && time.Since(t) > obsTimeout {
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; on 202/noop
// paths no extra CommandResponse is expected, so the counter is
// intentionally left to be consumed by the eventual SignalR message.
c.registerExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1)
if _, err = c.postJSONAndWait(uri, data); err != nil {
c.consumeExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1)
}
} 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.postJSONAndWait(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
case easee.ModeCompleted:
return api.ReasonDisconnectRequired, 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.postJSONAndWait(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
}