diff --git a/charger/nrgble/nrg_linux.go b/charger/nrg/ble/nrg_linux.go similarity index 99% rename from charger/nrgble/nrg_linux.go rename to charger/nrg/ble/nrg_linux.go index 680a52d2f..f6388458f 100644 --- a/charger/nrgble/nrg_linux.go +++ b/charger/nrg/ble/nrg_linux.go @@ -1,4 +1,4 @@ -package nrgble +package ble import ( "errors" diff --git a/charger/nrgble/service.go b/charger/nrg/ble/types.go similarity index 99% rename from charger/nrgble/service.go rename to charger/nrg/ble/types.go index c076c1483..815061726 100644 --- a/charger/nrgble/service.go +++ b/charger/nrg/ble/types.go @@ -1,4 +1,4 @@ -package nrgble +package ble const ( InfoService = "8f75bba0-c903-11e4-9fe8-0002a5d6b15d" diff --git a/charger/nrg/connect/types.go b/charger/nrg/connect/types.go new file mode 100644 index 000000000..e30975b3d --- /dev/null +++ b/charger/nrg/connect/types.go @@ -0,0 +1,51 @@ +package connect + +const ( + SettingsPath = "settings" + MeasurementsPath = "measurements" +) + +// Measurements is the /api/measurements response +type Measurements struct { + Message string `json:"omitempty"` // api message if not ok + ChargingEnergy float64 + ChargingEnergyOverAll float64 + ChargingPower float64 + ChargingPowerPhase [3]float64 + ChargingCurrentPhase [3]float64 + Frequency float64 +} + +// Settings is the /api/settings request/response +type Settings struct { + Message string `json:",omitempty"` // api message if not ok + Info Info `json:",omitempty"` + Values Values +} + +// Info is Settings.Info +type Info struct { + Connected bool `json:",omitempty"` +} + +// Values is Settings.Values +type Values struct { + ChargingStatus *ChargingStatus `json:",omitempty"` + ChargingCurrent *ChargingCurrent `json:",omitempty"` + DeviceMetadata DeviceMetadata +} + +// ChargingStatus is Settings.Values.ChargingStatus +type ChargingStatus struct { + Charging bool +} + +// ChargingCurrent is Settings.Values.ChargingCurrent +type ChargingCurrent struct { + Value float64 +} + +// DeviceMetadata is Settings.Values.DeviceMetadata +type DeviceMetadata struct { + Password string +} diff --git a/charger/nrgble_linux.go b/charger/nrgble_linux.go index 5371a0981..2c11ed556 100644 --- a/charger/nrgble_linux.go +++ b/charger/nrgble_linux.go @@ -8,7 +8,7 @@ import ( "time" "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/charger/nrgble" + "github.com/evcc-io/evcc/charger/nrg/ble" "github.com/evcc-io/evcc/util" "github.com/godbus/dbus/v5" "github.com/lunixbochs/struc" @@ -121,12 +121,12 @@ func NewNRGKickBLE(device, mac string, pin int) (*NRGKickBLE, error) { } func (nrg *NRGKickBLE) connect() (*device.Device1, error) { - dev, err := nrgble.FindDevice(nrg.adapter, nrg.mac, nrgTimeout) + dev, err := ble.FindDevice(nrg.adapter, nrg.mac, nrgTimeout) if err != nil { return nil, fmt.Errorf("find device: %s", err) } - err = nrgble.Connect(dev, nrg.agent, nrg.device) + err = ble.Connect(dev, nrg.agent, nrg.device) if err != nil { return nil, err } @@ -199,8 +199,8 @@ func (nrg *NRGKickBLE) write(service string, val interface{}) error { return nil } -func (nrg *NRGKickBLE) mergeSettings(info nrgble.Info) nrgble.Settings { - return nrgble.Settings{ +func (nrg *NRGKickBLE) mergeSettings(info ble.Info) ble.Settings { + return ble.Settings{ PIN: nrg.pin, ChargingEnergyLimit: 19997, // magic const for "disable" KWhPer100: info.KWhPer100, @@ -214,8 +214,8 @@ func (nrg *NRGKickBLE) mergeSettings(info nrgble.Info) nrgble.Settings { // Status implements the api.Charger interface func (nrg *NRGKickBLE) Status() (api.ChargeStatus, error) { - res := nrgble.Power{} - if err := nrg.read(nrgble.PowerService, &res); err != nil { + res := ble.Power{} + if err := nrg.read(ble.PowerService, &res); err != nil { return api.StatusF, err } @@ -235,8 +235,8 @@ func (nrg *NRGKickBLE) Status() (api.ChargeStatus, error) { // Enabled implements the api.Charger interface func (nrg *NRGKickBLE) Enabled() (bool, error) { - res := nrgble.Info{} - if err := nrg.read(nrgble.InfoService, &res); err != nil { + res := ble.Info{} + if err := nrg.read(ble.InfoService, &res); err != nil { return false, err } @@ -249,8 +249,8 @@ func (nrg *NRGKickBLE) Enabled() (bool, error) { // Enable implements the api.Charger interface func (nrg *NRGKickBLE) Enable(enable bool) error { - res := nrgble.Info{} - if err := nrg.read(nrgble.InfoService, &res); err != nil { + res := ble.Info{} + if err := nrg.read(ble.InfoService, &res); err != nil { return err } @@ -261,7 +261,7 @@ func (nrg *NRGKickBLE) Enable(enable bool) error { settings := nrg.mergeSettings(res) nrg.log.TRACE.Printf("write settings (workaround): %+v", settings) - if err := nrg.write(nrgble.SettingsService, &settings); err != nil { + if err := nrg.write(ble.SettingsService, &settings); err != nil { return err } } @@ -271,13 +271,13 @@ func (nrg *NRGKickBLE) Enable(enable bool) error { nrg.log.TRACE.Printf("write settings: %+v", settings) - return nrg.write(nrgble.SettingsService, &settings) + return nrg.write(ble.SettingsService, &settings) } // MaxCurrent implements the api.Charger interface func (nrg *NRGKickBLE) MaxCurrent(current int64) error { - res := nrgble.Info{} - if err := nrg.read(nrgble.InfoService, &res); err != nil { + res := ble.Info{} + if err := nrg.read(ble.InfoService, &res); err != nil { return err } @@ -286,15 +286,15 @@ func (nrg *NRGKickBLE) MaxCurrent(current int64) error { nrg.log.TRACE.Printf("write settings: %+v", settings) - return nrg.write(nrgble.SettingsService, &settings) + return nrg.write(ble.SettingsService, &settings) } var _ api.Meter = (*NRGKickBLE)(nil) // CurrentPower implements the api.Meter interface func (nrg *NRGKickBLE) CurrentPower() (float64, error) { - res := nrgble.Power{} - if err := nrg.read(nrgble.PowerService, &res); err != nil { + res := ble.Power{} + if err := nrg.read(ble.PowerService, &res); err != nil { return 0, err } @@ -307,8 +307,8 @@ var _ api.MeterEnergy = (*NRGKickBLE)(nil) // TotalEnergy implements the api.MeterEnergy interface func (nrg *NRGKickBLE) TotalEnergy() (float64, error) { - res := nrgble.Energy{} - if err := nrg.read(nrgble.EnergyService, &res); err != nil { + res := ble.Energy{} + if err := nrg.read(ble.EnergyService, &res); err != nil { return 0, err } @@ -321,8 +321,8 @@ var _ api.PhaseCurrents = (*NRGKickBLE)(nil) // Currents implements the api.PhaseCurrents interface func (nrg *NRGKickBLE) Currents() (float64, float64, float64, error) { - res := nrgble.VoltageCurrent{} - if err := nrg.read(nrgble.VoltageCurrentService, &res); err != nil { + res := ble.VoltageCurrent{} + if err := nrg.read(ble.VoltageCurrentService, &res); err != nil { return 0, 0, 0, err } @@ -337,8 +337,8 @@ func (nrg *NRGKickBLE) Currents() (float64, float64, float64, error) { // ChargedEnergy implements the ChargeRater interface // NOTE: apparently shows energy of a stopped charging session, hence substituted by TotalEnergy // func (nrg *NRGKickBLE) ChargedEnergy() (float64, error) { -// res := nrgble.Energy{} -// if err := nrg.read(nrgble.EnergyService, &res); err != nil { +// res := ble.Energy{} +// if err := nrg.read(ble.EnergyService, &res); err != nil { // return 0, err // } // nrg.log.TRACE.Printf("energy: %+v", res) diff --git a/charger/nrgconnect.go b/charger/nrgconnect.go index 6aa4f6624..a8ab6b28e 100644 --- a/charger/nrgconnect.go +++ b/charger/nrgconnect.go @@ -3,71 +3,28 @@ package charger import ( "errors" "fmt" + "io" "net/http" + "time" "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/charger/nrg/connect" + "github.com/evcc-io/evcc/provider" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" ) // https://www.nrgkick.com/wp-content/uploads/2019/08/20190814_API-Dokumentation_04.pdf -const ( - nrgSettings = "settings" - nrgMeasurements = "measurements" -) - -// NRGMeasurements is the /api/measurements response -type NRGMeasurements struct { - Message string `json:"omitempty"` // api message if not ok - ChargingEnergy float64 - ChargingEnergyOverAll float64 - ChargingPower float64 - ChargingPowerPhase [3]float64 - ChargingCurrentPhase [3]float64 - Frequency float64 -} - -// NRGSettings is the /api/settings request/response -type NRGSettings struct { - Message string `json:",omitempty"` // api message if not ok - Info NRGInfo `json:",omitempty"` - Values NRGValues -} - -// NRGInfo is NRGSettings.Info -type NRGInfo struct { - Connected bool `json:",omitempty"` -} - -// NRGValues is NRGSettings.Values -type NRGValues struct { - ChargingStatus NRGChargingStatus - ChargingCurrent NRGChargingCurrent - DeviceMetadata NRGDeviceMetadata -} - -// NRGChargingStatus is NRGSettings.Values.ChargingStatus -type NRGChargingStatus struct { - Charging *bool `json:",omitempty"` // use pointer to allow omitting false -} - -// NRGChargingCurrent is NRGSettings.Values.ChargingCurrent -type NRGChargingCurrent struct { - Value float64 `json:",omitempty"` -} - -// NRGDeviceMetadata is NRGSettings.Values.DeviceMetadata -type NRGDeviceMetadata struct { - Password string -} - // NRGKickConnect charger implementation type NRGKickConnect struct { *request.Helper - uri string - mac string - password string + uri string + mac string + password string + enabled bool + settingsG provider.Cacheable[connect.Settings] + measurementsG provider.Cacheable[connect.Measurements] } func init() { @@ -76,18 +33,22 @@ func init() { // NewNRGKickConnectFromConfig creates a NRGKickConnect charger from generic config func NewNRGKickConnectFromConfig(other map[string]interface{}) (api.Charger, error) { - var cc struct { + cc := struct { URI, Mac, Password string + Cache time.Duration + }{ + Cache: time.Second, } + if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - return NewNRGKickConnect(cc.URI, cc.Mac, cc.Password) + return NewNRGKickConnect(cc.URI, cc.Mac, cc.Password, cc.Cache) } // NewNRGKickConnect creates NRGKickConnect charger -func NewNRGKickConnect(uri, mac, password string) (*NRGKickConnect, error) { +func NewNRGKickConnect(uri, mac, password string, cache time.Duration) (*NRGKickConnect, error) { nrg := &NRGKickConnect{ Helper: request.NewHelper(util.NewLogger("nrgconn")), uri: util.DefaultScheme(uri, "http"), @@ -95,6 +56,28 @@ func NewNRGKickConnect(uri, mac, password string) (*NRGKickConnect, error) { password: password, } + nrg.settingsG = provider.ResettableCached(func() (connect.Settings, error) { + var res connect.Settings + + err := nrg.GetJSON(nrg.apiURL(connect.SettingsPath), &res) + if err != nil && res.Message != "" { + err = errors.New(res.Message) + } + + return res, err + }, cache) + + nrg.measurementsG = provider.ResettableCached(func() (connect.Measurements, error) { + var res connect.Measurements + + err := nrg.GetJSON(nrg.apiURL(connect.MeasurementsPath), &res) + if err != nil && res.Message != "" { + err = errors.New(res.Message) + } + + return res, err + }, cache) + return nrg, nil } @@ -106,15 +89,22 @@ func (nrg *NRGKickConnect) putJSON(url string, data interface{}) error { req, err := request.New(http.MethodPut, url, request.MarshalJSON(data), request.JSONEncoding) if err == nil { - var resp struct { + var res struct { Message string } - if err = nrg.DoJSON(req, &resp); err != nil { - if resp.Message != "" { - return fmt.Errorf("response: %s", resp.Message) + if err = nrg.DoJSON(req, &res); err != nil { + if err == io.EOF { + err = nil + } else if res.Message != "" { + return errors.New(res.Message) } } + + if err == nil { + nrg.settingsG.Reset() + nrg.measurementsG.Reset() + } } return err @@ -122,63 +112,83 @@ func (nrg *NRGKickConnect) putJSON(url string, data interface{}) error { // Status implements the api.Charger interface func (nrg *NRGKickConnect) Status() (api.ChargeStatus, error) { - return api.StatusC, nil + res, err := nrg.settingsG.Get() + if err != nil { + return api.StatusNone, err + } + + if res.Values.ChargingStatus == nil { + return api.StatusNone, errors.New("unknown status") + } + + if res.Values.ChargingStatus.Charging { + return api.StatusC, nil + } + + return api.StatusB, nil } // Enabled implements the api.Charger interface func (nrg *NRGKickConnect) Enabled() (bool, error) { - var res NRGSettings - err := nrg.GetJSON(nrg.apiURL(nrgSettings), &res) - if err != nil { - if res.Message != "" { - err = errors.New(res.Message) - } - - return false, err - } - - return *res.Values.ChargingStatus.Charging, nil + return nrg.enabled, nil } // Enable implements the api.Charger interface func (nrg *NRGKickConnect) Enable(enable bool) error { - settings := NRGSettings{} - settings.Values.DeviceMetadata.Password = nrg.password - settings.Values.ChargingStatus.Charging = &enable + data := connect.Settings{ + Values: connect.Values{ + ChargingStatus: &connect.ChargingStatus{ + Charging: enable, + }, + DeviceMetadata: connect.DeviceMetadata{ + Password: nrg.password, + }, + }, + } - return nrg.putJSON(nrg.apiURL(nrgSettings), settings) + err := nrg.putJSON(nrg.apiURL(connect.SettingsPath), data) + if err == nil { + nrg.enabled = enable + } + + return err } // MaxCurrent implements the api.Charger interface func (nrg *NRGKickConnect) MaxCurrent(current int64) error { - settings := NRGSettings{} - settings.Values.DeviceMetadata.Password = nrg.password - settings.Values.ChargingCurrent.Value = float64(current) + data := connect.Settings{ + Values: connect.Values{ + ChargingCurrent: &connect.ChargingCurrent{ + Value: float64(current), + }, + DeviceMetadata: connect.DeviceMetadata{ + Password: nrg.password, + }, + }, + } - return nrg.putJSON(nrg.apiURL(nrgSettings), settings) + return nrg.putJSON(nrg.apiURL(connect.SettingsPath), data) } var _ api.Meter = (*NRGKickConnect)(nil) // CurrentPower implements the api.Meter interface func (nrg *NRGKickConnect) CurrentPower() (float64, error) { - var res NRGMeasurements - err := nrg.GetJSON(nrg.apiURL(nrgMeasurements), &res) - if err != nil && res.Message != "" { - err = errors.New(res.Message) + res, err := nrg.measurementsG.Get() + if err != nil { + return 0, err } - return 1000 * res.ChargingPower, err + return res.ChargingPower * 1e3, err } var _ api.MeterEnergy = (*NRGKickConnect)(nil) // TotalEnergy implements the api.MeterEnergy interface func (nrg *NRGKickConnect) TotalEnergy() (float64, error) { - var res NRGMeasurements - err := nrg.GetJSON(nrg.apiURL(nrgMeasurements), &res) - if err != nil && res.Message != "" { - err = errors.New(res.Message) + res, err := nrg.measurementsG.Get() + if err != nil { + return 0, err } return res.ChargingEnergyOverAll, err @@ -188,10 +198,9 @@ var _ api.PhaseCurrents = (*NRGKickConnect)(nil) // Currents implements the api.PhaseCurrents interface func (nrg *NRGKickConnect) Currents() (float64, float64, float64, error) { - var res NRGMeasurements - err := nrg.GetJSON(nrg.apiURL(nrgMeasurements), &res) - if err != nil && res.Message != "" { - err = errors.New(res.Message) + res, err := nrg.measurementsG.Get() + if err != nil { + return 0, 0, 0, err } if len(res.ChargingCurrentPhase) != 3 { @@ -207,7 +216,7 @@ func (nrg *NRGKickConnect) Currents() (float64, float64, float64, error) { // ChargedEnergy implements the ChargeRater interface // NOTE: apparently shows energy of a stopped charging session, hence substituted by TotalEnergy // func (nrg *NRGKickConnect) ChargedEnergy() (float64, error) { -// var res NRGMeasurements -// err := nrg.GetJSON(nrg.apiURL(nrgMeasurements), &res) +// var res connect.Measurements +// err := nrg.GetJSON(nrg.apiURL(connect.MeasurementsPath), &res) // return res.ChargingEnergy, err // }