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 ( "errors" "fmt" "net/http" "strings" "time" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/charger/smaevcharger" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/sponsor" "github.com/hashicorp/go-version" "golang.org/x/oauth2" ) // smaevchager charger implementation type Smaevcharger struct { *request.Helper log *util.Logger uri string // 192.168.XXX.XXX cache time.Duration oldstate float64 measurementG util.Cacheable[[]smaevcharger.Measurements] parameterG util.Cacheable[[]smaevcharger.Parameters] isLegacyHw bool } func init() { // TODO remove deprecated registry.Add("smaevcharger", NewSmaevchargerFromConfig) } // NewSmaevchargerFromConfig creates a SMA EV Charger from generic config func NewSmaevchargerFromConfig(other map[string]any) (api.Charger, error) { cc := struct { Uri string User string Password string Cache time.Duration }{ Cache: 5 * time.Second, } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } if cc.Uri == "" { return nil, errors.New("missing uri") } if cc.User == "" || cc.Password == "" { return nil, api.ErrMissingCredentials } if cc.User == "admin" { return nil, errors.New(`user "admin" not allowed, create new user`) } return NewSmaevcharger(cc.Uri, cc.User, cc.Password, cc.Cache) } // NewSmaevcharger creates an SMA EV Charger func NewSmaevcharger(uri, user, password string, cache time.Duration) (api.Charger, error) { log := util.NewLogger("smaevcharger").Redact(user, password) wb := &Smaevcharger{ Helper: request.NewHelper(log), log: log, uri: util.DefaultScheme(strings.TrimRight(uri, "/"), "http") + "/api/v1", cache: cache, } if !sponsor.IsAuthorized() { return nil, api.ErrSponsorRequired } // setup cached values wb.measurementG = util.ResettableCached(wb._measurementData, wb.cache) wb.parameterG = util.ResettableCached(wb._parameterData, wb.cache) ts, err := smaevcharger.TokenSource(log, wb.uri, user, password) if err != nil { return nil, err } // replace client transport with authenticated transport wb.Client.Transport = &oauth2.Transport{ Source: ts, Base: wb.Client.Transport, } // get model model, err := wb.getParameter("Parameter.Nameplate.ModelStr") if err != nil { return nil, err } wb.isLegacyHw = !strings.HasSuffix(model, "-20") // get version pkgRev, err := wb.getParameter("Parameter.Nameplate.PkgRev") if err != nil { return nil, err } swVersion, err := version.NewVersion(strings.TrimSuffix(pkgRev, ".R")) if err != nil { return nil, err } refVersion, err := version.NewVersion(smaevcharger.MinAcceptedVersion) if err != nil { return nil, err } // check minimum version if swVersion.Compare(refVersion) < 0 { return nil, fmt.Errorf("charger firmware version not supported - please update >= %s", smaevcharger.MinAcceptedVersion) } // Prepare charger: disable App Lock functionality. // This option have been introduced with 1.2.23 and will lock the charger // until unlocked via SMA App. Unfortunately this Lock option will overwrite // the status of the charger and prevent ev detection err = wb.Send( value("Parameter.Chrg.ChrgLok", smaevcharger.ChargerAppLockDisabled), value("Parameter.Chrg.ChrgApv", smaevcharger.ChargerManualLockEnabled), ) if err != nil { return nil, err } return wb, nil } // Status implements the api.Charger interface func (wb *Smaevcharger) Status() (api.ChargeStatus, error) { state, err := wb.getMeasurement("Measurement.Operation.EVeh.ChaStt") if err != nil { return api.StatusNone, err } // Explicitly stop charging when the vehicle is connected to prevent unauthorised charging if wb.oldstate == smaevcharger.StatusA && state != smaevcharger.StatusA { if err := wb.Enable(false); err != nil { return api.StatusNone, err } } wb.oldstate = state switch state { case smaevcharger.StatusA: return api.StatusA, nil case smaevcharger.StatusB: return api.StatusB, nil case smaevcharger.StatusC: return api.StatusC, nil default: return api.StatusNone, fmt.Errorf("invalid state: %.0f", state) } } // Enabled implements the api.Charger interface func (wb *Smaevcharger) Enabled() (bool, error) { mode, err := wb.getParameter("Parameter.Chrg.ActChaMod") if err != nil { return false, err } switch mode { case smaevcharger.FastCharge, // Schnellladen - 4718 smaevcharger.OptiCharge, // Optimiertes Laden - 4719 smaevcharger.PlanCharge: // Laden mit Vorgabe - 4720 return true, nil case smaevcharger.StopCharge: // Ladestopp - 4721 return false, nil default: return false, fmt.Errorf("invalid charge mode: %s", mode) } } // Enable implements the api.Charger interface func (wb *Smaevcharger) Enable(enable bool) error { if enable { res, err := wb.getMeasurement("Measurement.Chrg.ModSw") if err != nil { return err } if wb.isLegacyHw && res == smaevcharger.SwitchOeko { // Hardware mode switch is in the solar charging position. // If the selector switch of the charger is in the wrong position (eco-charging and not fast charging), // the charging process is started with eco-charging when it is activated, // which may be desired when still integrated with SHM. // Since evcc does not have full control over the charging station in this mode, // a corresponding error is returned to indicate the incorrect switch position. // If the charger is installed without SHM, charging in eco mode is not possible. _ = wb.Send(value("Parameter.Chrg.ActChaMod", smaevcharger.OptiCharge)) return fmt.Errorf("charge mode switch not in fast-charging position - charger controlled by SHM") } // Switch in Fast charging position return wb.Send(value("Parameter.Chrg.ActChaMod", smaevcharger.FastCharge)) } // else return wb.Send(value("Parameter.Chrg.ActChaMod", smaevcharger.StopCharge)) } // MaxCurrent implements the api.Charger interface func (wb *Smaevcharger) MaxCurrent(current int64) error { return wb.MaxCurrentMillis(float64(current)) } var _ api.ChargerEx = (*Smaevcharger)(nil) // maxCurrentMillis implements the api.ChargerEx interface func (wb *Smaevcharger) MaxCurrentMillis(current float64) error { if current < 6 { return fmt.Errorf("invalid current %.5g", current) } return wb.Send(value("Parameter.Inverter.AcALim", fmt.Sprintf("%.2f", current))) } var _ api.MeterEnergy = (*Smaevcharger)(nil) // TotalEnergy implements the api.MeterEnergy interface func (wb *Smaevcharger) TotalEnergy() (float64, error) { res, err := wb.getMeasurement("Measurement.Metering.GridMs.TotWhIn") if _, ok := errors.AsType[*smaevcharger.ErrUnknownMeasurement](err); ok { res, err = wb.getMeasurement("Measurement.Metering.GridMs.TotWhIn.ChaSta") } return res / 1e3, err } var _ api.Meter = (*Smaevcharger)(nil) // CurrentPower implements the api.Meter interface func (wb *Smaevcharger) CurrentPower() (float64, error) { res, err := wb.getMeasurement("Measurement.Metering.GridMs.TotWIn") if _, ok := errors.AsType[*smaevcharger.ErrUnknownMeasurement](err); ok { res, err = wb.getMeasurement("Measurement.Metering.GridMs.TotWIn.ChaSta") } return res, err } var _ api.ChargeRater = (*Smaevcharger)(nil) // ChargedEnergy implements the api.ChargeRater interface func (wb *Smaevcharger) ChargedEnergy() (float64, error) { res, err := wb.getMeasurement("Measurement.ChaSess.WhIn") if _, ok := errors.AsType[*smaevcharger.ErrUnknownMeasurement](err); ok { res, err = wb.getMeasurement("Measurement.Metering.GridMs.TotWhIn.ChaSta") } return res / 1e3, err } var _ api.PhaseCurrents = (*Smaevcharger)(nil) // Currents implements the api.PhaseCurrents interface func (wb *Smaevcharger) Currents() (float64, float64, float64, error) { var res [3]float64 for i, phase := range []string{"A", "B", "C"} { val, err := wb.getMeasurement("Measurement.GridMs.A.phs" + phase) if err != nil { return 0, 0, 0, err } res[i] = -val } return res[0], res[1], res[2], nil } // reset cache func (wb *Smaevcharger) reset() { wb.measurementG.Reset() wb.parameterG.Reset() } func (wb *Smaevcharger) _measurementData() ([]smaevcharger.Measurements, error) { var res []smaevcharger.Measurements uri := fmt.Sprintf("%s/measurements/live", wb.uri) data := `[{"componentId": "IGULD:SELF"}]` req, err := request.New(http.MethodPost, uri, strings.NewReader(data), request.JSONEncoding) if err == nil { err = wb.DoJSON(req, &res) } return res, err } func (wb *Smaevcharger) _parameterData() ([]smaevcharger.Parameters, error) { var res []smaevcharger.Parameters uri := fmt.Sprintf("%s/parameters/search/", wb.uri) data := `{"queryItems":[{"componentId":"IGULD:SELF"}]}` req, err := request.New(http.MethodPost, uri, strings.NewReader(data), request.JSONEncoding) if err == nil { err = wb.DoJSON(req, &res) } return res, err } func (wb *Smaevcharger) getMeasurement(id string) (float64, error) { res, err := wb.measurementG.Get() if err != nil { return 0, err } for _, el := range res { if el.ChannelId == id { return el.Values[0].Value, nil } } return 0, &smaevcharger.ErrUnknownMeasurement{Measurement: id} } func (wb *Smaevcharger) getParameter(id string) (string, error) { res, err := wb.parameterG.Get() if err != nil { return "", err } for _, el := range res[0].Values { if el.ChannelId == id { return el.Value, nil } } return "", fmt.Errorf("unknown parameter: %s", id) } func (wb *Smaevcharger) Send(values ...smaevcharger.Value) error { uri := fmt.Sprintf("%s/parameters/IGULD:SELF/", wb.uri) data := smaevcharger.SendParameter{ Values: values, } req, err := request.New(http.MethodPut, uri, request.MarshalJSON(data), request.JSONEncoding) if err == nil { _, err = wb.DoBody(req) wb.reset() } return err } // value creates an smaevcharger.Value func value(id, value string) smaevcharger.Value { return smaevcharger.Value{ Timestamp: time.Now().UTC().Format(smaevcharger.TimestampFormat), ChannelId: id, Value: value, } }