evcc-io/charger/smart-evse.go

422 lines
11 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 (
"fmt"
"net/http"
"strings"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/api/implement"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
)
// SmartEVSE-3.5 REST API charger implementation
// https://github.com/dingo35/SmartEVSE-3.5/blob/master/docs/REST_API.md
// SmartEVSE3 charger implementation
type SmartEVSE3 struct {
*request.Helper
implement.Caps
uri string
curr int64
mode int
apiG util.Cacheable[smartEvseRestSettings]
}
// smartEvseRestSettings represents the JSON returned by GET /settings
type smartEvseRestSettings struct {
Mode string `json:"mode"`
ModeID int `json:"mode_id"`
CarConnected bool `json:"car_connected"`
Evse struct {
Connected bool `json:"connected"`
Access int `json:"access"`
Mode int `json:"mode"`
ChargeTimer int `json:"charge_timer"`
State string `json:"state"`
StateID int `json:"state_id"`
Error string `json:"error"`
ErrorID int `json:"error_id"`
RFID string `json:"rfid"`
RFIDReader string `json:"rfidreader"`
RFIDLastRead string `json:"rfid_lastread"`
NrOfPhases int `json:"nrofphases"`
} `json:"evse"`
Settings struct {
ChargeCurrent int `json:"charge_current"`
OverrideCurrent int `json:"override_current"`
CurrentMin int `json:"current_min"`
CurrentMax int `json:"current_max"`
CurrentMain int `json:"current_main"`
EnableC2 string `json:"enable_C2"`
} `json:"settings"`
EvMeter struct {
Description string `json:"description"`
Address int `json:"address"`
ImportActivePower float64 `json:"import_active_power"`
TotalWh float64 `json:"total_wh"`
ChargedWh float64 `json:"charged_wh"`
Currents struct {
Total float64 `json:"TOTAL"`
L1 float64 `json:"L1"`
L2 float64 `json:"L2"`
L3 float64 `json:"L3"`
} `json:"currents"`
ImportActiveEnergy float64 `json:"import_active_energy"`
ExportActiveEnergy float64 `json:"export_active_energy"`
} `json:"ev_meter"`
}
// SmartEVSE-3.5 operating mode IDs
const (
smartEvse3ModeOff = 0
smartEvse3ModeNormal = 1
smartEvse3ModeSolar = 2
smartEvse3ModeSmart = 3
smartEvse3ModePause = 4
)
// SmartEVSE-3.5 C2 contactor IDs
const (
smartEvse3C2NotPresent = 0
smartEvse3C2AlwaysOff = 1 // single-phase
smartEvse3C2SolarOff = 2
smartEvse3C2AlwaysOn = 3 // three-phase
smartEvse3C2Auto = 4
)
func init() {
registry.Add("smart-evse", NewSmartEVSE3FromConfig)
}
// NewSmartEVSE3FromConfig creates a SmartEVSE-3.5 REST charger from generic config
func NewSmartEVSE3FromConfig(other map[string]any) (api.Charger, error) {
cc := struct {
URI string
Cache time.Duration
ChargeMode string
}{
Cache: time.Second,
ChargeMode: "normal",
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if cc.URI == "" {
return nil, fmt.Errorf("missing uri")
}
mode := smartEvse3ModeNormal
if cc.ChargeMode == "smart" {
mode = smartEvse3ModeSmart
}
return NewSmartEVSE3(cc.URI, cc.Cache, mode)
}
// NewSmartEVSE3 creates a new SmartEVSE-3.5 REST charger
func NewSmartEVSE3(uri string, cache time.Duration, mode int) (api.Charger, error) {
log := util.NewLogger("smart-evse")
wb := &SmartEVSE3{
Helper: request.NewHelper(log),
Caps: implement.New(),
uri: strings.TrimRight(util.DefaultScheme(uri, "http"), "/"),
curr: 60, // 6 A in 1/10 A
mode: mode,
}
wb.Helper.Client.Transport = &http.Transport{
DisableKeepAlives: true,
}
wb.apiG = util.ResettableCached(func() (smartEvseRestSettings, error) {
var res smartEvseRestSettings
err := wb.GetJSON(wb.uri+"/settings", &res)
return res, err
}, cache)
// verify connectivity
res, err := wb.apiG.Get()
if err != nil {
return nil, err
}
// decorate optional EV meter if configured in SmartEVSE
if res.EvMeter.Description != "" && res.EvMeter.Description != "Disabled" {
implement.Has(wb, implement.Meter(wb.currentPower))
implement.Has(wb, implement.MeterEnergy(wb.totalEnergy))
implement.Has(wb, implement.PhaseCurrents(wb.currents))
}
// decorate optional 1P/3P phase switching via C2 contactor
if res.Settings.EnableC2 != "" && res.Settings.EnableC2 != "Not present" {
implement.Has(wb, implement.PhaseSwitcher(wb.phases1p3p))
implement.Has(wb, implement.PhaseGetter(wb.getPhases))
}
// decorate optional RFID identification and status reason
if res.Evse.RFIDReader != "" && res.Evse.RFIDReader != "Disabled" {
implement.Has(wb, implement.Identifier(wb.identify))
implement.Has(wb, implement.StatusReasoner(wb.statusReason))
}
return wb, nil
}
// post issues a POST request to the SmartEVSE with given query parameters.
// Mongoose webserver requires an empty request body to avoid timeout.
func (wb *SmartEVSE3) post(path string, query string) error {
uri := fmt.Sprintf("%s/%s?%s", wb.uri, strings.TrimLeft(path, "/"), query)
req, err := request.New(http.MethodPost, uri, strings.NewReader(""), request.URLEncoding)
if err != nil {
return err
}
_, err = wb.DoBody(req)
wb.apiG.Reset()
return err
}
func (wb *SmartEVSE3) setMode(mode int) error {
return wb.post("settings", fmt.Sprintf("mode=%d", mode))
}
func (wb *SmartEVSE3) setOverrideCurrent(deciAmps int64) error {
return wb.post("settings", fmt.Sprintf("override_current=%d", deciAmps))
}
// Status implements the api.Charger interface
func (wb *SmartEVSE3) Status() (api.ChargeStatus, error) {
res, err := wb.apiG.Get()
if err != nil {
return api.StatusNone, err
}
if !res.CarConnected {
return api.StatusA, nil
}
// state IDs from SmartEVSE firmware (main_c.h)
switch res.Evse.StateID {
case 2, 3, 6, 7, 10:
// STATE_C, STATE_D, STATE_COMM_C, STATE_COMM_C_OK, STATE_C1
return api.StatusC, nil
default:
return api.StatusB, nil
}
}
// Enabled implements the api.Charger interface
func (wb *SmartEVSE3) Enabled() (bool, error) {
res, err := wb.apiG.Get()
if err != nil {
return false, err
}
// STATE_B1 / STATE_C1: EVSE not ready, no PWM signal
if res.Evse.StateID == 9 || res.Evse.StateID == 10 {
return false, nil
}
if res.ModeID == smartEvse3ModeOff || res.ModeID == smartEvse3ModePause {
return false, nil
}
return true, nil
}
// Enable implements the api.Charger interface
func (wb *SmartEVSE3) Enable(enable bool) error {
res, err := wb.apiG.Get()
if err != nil {
return err
}
if enable {
if res.ModeID != wb.mode {
return wb.setMode(wb.mode)
}
return nil
}
if res.ModeID != smartEvse3ModeOff && res.ModeID != smartEvse3ModePause {
return wb.setMode(smartEvse3ModePause)
}
return nil
}
// MaxCurrent implements the api.Charger interface
func (wb *SmartEVSE3) MaxCurrent(current int64) error {
if current < 6 {
return fmt.Errorf("invalid current %d", current)
}
deciAmps := current * 10
if err := wb.setOverrideCurrent(deciAmps); err != nil {
return err
}
wb.curr = deciAmps
return nil
}
var _ api.ChargerEx = (*SmartEVSE3)(nil)
// MaxCurrentMillis implements the api.ChargerEx interface
func (wb *SmartEVSE3) MaxCurrentMillis(current float64) error {
if current < 6 {
return fmt.Errorf("invalid current %.1f", current)
}
deciAmps := int64(current * 10)
if err := wb.setOverrideCurrent(deciAmps); err != nil {
return err
}
wb.curr = deciAmps
return nil
}
// currentPower implements the api.Meter interface
func (wb *SmartEVSE3) currentPower() (float64, error) {
res, err := wb.apiG.Get()
if err != nil {
return 0, err
}
// import_active_power is reported in W
return res.EvMeter.ImportActivePower, nil
}
// totalEnergy implements the api.MeterEnergy interface
func (wb *SmartEVSE3) totalEnergy() (float64, error) {
res, err := wb.apiG.Get()
if err != nil {
return 0, err
}
// import_active_energy is reported in Wh
return res.EvMeter.ImportActiveEnergy / 1e3, nil
}
// phases1p3p implements the api.PhaseSwitcher interface
func (wb *SmartEVSE3) phases1p3p(phases int) error {
var c2 int
switch phases {
case 1:
c2 = smartEvse3C2AlwaysOff
case 3:
c2 = smartEvse3C2AlwaysOn
default:
return fmt.Errorf("invalid phases: %d", phases)
}
// C2 switching takes effect on next state change; disable charger during switch
return whenDisabled(wb, func() error {
return wb.post("settings", fmt.Sprintf("enable_C2=%d", c2))
})
}
// getPhases implements the api.PhaseGetter interface
func (wb *SmartEVSE3) getPhases() (int, error) {
res, err := wb.apiG.Get()
if err != nil {
return 0, err
}
return res.Evse.NrOfPhases, nil
}
// currents implements the api.PhaseCurrents interface
func (wb *SmartEVSE3) currents() (float64, float64, float64, error) {
res, err := wb.apiG.Get()
if err != nil {
return 0, 0, 0, err
}
// phase currents are reported in 1/10 A
return res.EvMeter.Currents.L1 / 10, res.EvMeter.Currents.L2 / 10, res.EvMeter.Currents.L3 / 10, nil
}
// statusReason implements the api.StatusReasoner interface
func (wb *SmartEVSE3) statusReason() (api.Reason, error) {
res, err := wb.apiG.Get()
if err != nil {
return api.ReasonUnknown, err
}
// RFID reader enabled, car connected, but access not yet granted
if res.CarConnected && res.Evse.Access == 0 {
return api.ReasonWaitingForAuthorization, nil
}
return api.ReasonUnknown, nil
}
// identify implements the api.Identifier interface
func (wb *SmartEVSE3) identify() ([]string, error) {
res, err := wb.apiG.Get()
if err != nil {
return nil, err
}
return []string{res.Evse.RFIDLastRead}, nil
}
var _ api.Diagnosis = (*SmartEVSE3)(nil)
// Diagnose implements the api.Diagnosis interface
func (wb *SmartEVSE3) Diagnose() {
res, err := wb.apiG.Get()
if err != nil {
fmt.Printf("\tError: %v\n", err)
return
}
fmt.Printf("\tMode: %s (%d)\n", res.Mode, res.ModeID)
fmt.Printf("\tCar connected: %t\n", res.CarConnected)
fmt.Printf("\tEVSE connected: %t\n", res.Evse.Connected)
fmt.Printf("\tEVSE access: %d\n", res.Evse.Access)
fmt.Printf("\tEVSE mode: %d\n", res.Evse.Mode)
fmt.Printf("\tEVSE charge timer: %d\n", res.Evse.ChargeTimer)
fmt.Printf("\tEVSE state: %s (%d)\n", res.Evse.State, res.Evse.StateID)
fmt.Printf("\tEVSE error: %s (%d)\n", res.Evse.Error, res.Evse.ErrorID)
fmt.Printf("\tEVSE RFID: %s\n", res.Evse.RFID)
fmt.Printf("\tEVSE RFID reader: %s\n", res.Evse.RFIDReader)
fmt.Printf("\tEVSE RFID last read: %s\n", res.Evse.RFIDLastRead)
fmt.Printf("\tEVSE nr of phases: %d\n", res.Evse.NrOfPhases)
fmt.Printf("\tCharge current: %d A\n", res.Settings.ChargeCurrent)
fmt.Printf("\tOverride current: %.1f A\n", float64(res.Settings.OverrideCurrent)/10)
fmt.Printf("\tCurrent min/max: %d/%d A\n", res.Settings.CurrentMin, res.Settings.CurrentMax)
fmt.Printf("\tCurrent main: %d A\n", res.Settings.CurrentMain)
fmt.Printf("\tEnable C2: %s\n", res.Settings.EnableC2)
}