diff --git a/templates/definition/vehicle/zero.yaml b/templates/definition/vehicle/zero.yaml new file mode 100644 index 000000000..e60838664 --- /dev/null +++ b/templates/definition/vehicle/zero.yaml @@ -0,0 +1,10 @@ +template: zero +products: + - brand: Zero Motorcycles +params: + - preset: vehicle-base + - preset: vehicle-identify +render: | + type: zero + {{ include "vehicle-base" . }} + {{ include "vehicle-identify" . }} diff --git a/vehicle/zero/api.go b/vehicle/zero/api.go new file mode 100644 index 000000000..69712f7ab --- /dev/null +++ b/vehicle/zero/api.go @@ -0,0 +1,71 @@ +package zero + +import ( + "fmt" + "net/url" + + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" +) + +const BaseUrl = "https://mongol.brono.com/mongol/api.php" + +// See https://www.electricmotorcycleforum.com/boards/index.php?topic=9520.0 +// API is quite simple +// 1 Acquire unit id(s) by calling +// https://mongol.brono.com/mongol/api.php?commandname=get_units&format=json&user=yourusername&pass=yourpass +// 2 Query last dataset +// https://mongol.brono.com/mongol/api.php?commandname=get_last_transmit&format=json&user=yourusername&pass=yourpass&unitnumber=0000000 + +// API is an api.Vehicle implementation for Zero Motorcycles +type API struct { + *request.Helper + user string + password string +} + +// NewAPI creates a new vehicle +func NewAPI(log *util.Logger, user, password string) (*API, error) { + var err error + v := &API{ + Helper: request.NewHelper(log), + user: user, + password: password, + } + return v, err +} + +func (v *API) Vehicles() ([]Unit, error) { + var res []Unit + + params := url.Values{ + "user": {v.user}, + "pass": {v.password}, + "commandname": {"get_units"}, + "format": {"json"}, + } + + uri := fmt.Sprintf("%s?%s", BaseUrl, params.Encode()) + err := v.GetJSON(uri, &res) + return res, err +} + +// Status implements the /user/vehicles//status api +func (v *API) Status(unitId string) (State, error) { + var res []State + + params := url.Values{ + "user": {v.user}, + "pass": {v.password}, + "unitnumber": {unitId}, + "commandname": {"get_last_transmit"}, + "format": {"json"}, + } + + uri := fmt.Sprintf("%s?%s", BaseUrl, params.Encode()) + if err := v.GetJSON(uri, &res); err != nil { + return State{}, err + } + + return res[0], nil +} diff --git a/vehicle/zero/provider.go b/vehicle/zero/provider.go new file mode 100644 index 000000000..05fba1e2b --- /dev/null +++ b/vehicle/zero/provider.go @@ -0,0 +1,85 @@ +package zero + +import ( + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/provider" +) + +// Provider implements the vehicle api +type Provider struct { + status provider.Cacheable[State] +} + +// NewProvider creates a vehicle api provider +func NewProvider(api *API, unitId string, cache time.Duration) *Provider { + impl := &Provider{ + status: provider.ResettableCached(func() (State, error) { + return api.Status(unitId) + }, cache), + } + + return impl +} + +var _ api.Battery = (*Provider)(nil) + +// Soc implements the api.Vehicle interface +func (v *Provider) Soc() (float64, error) { + res, err := v.status.Get() + return float64(res.Soc), err +} + +var _ api.ChargeState = (*Provider)(nil) + +// Status implements the api.ChargeState interface +func (v *Provider) Status() (api.ChargeStatus, error) { + res, err := v.status.Get() + if err != nil { + return api.StatusNone, err + } + + status := api.StatusA + if res.Pluggedin != 0 { + status = api.StatusB + } + if res.Charging != 0 { + status = api.StatusC + } + + return status, nil +} + +var _ api.VehicleFinishTimer = (*Provider)(nil) + +// FinishTime implements the api.VehicleFinishTimer interface +func (v *Provider) FinishTime() (time.Time, error) { + res, err := v.status.Get() + if err != nil { + return time.Time{}, err + } + + t, err := time.Parse("20060102150405", res.DatetimeActual) + if err != nil { + t = time.Now() + } + + return t.Add(time.Duration(res.Chargingtimeleft) * time.Minute), nil +} + +var _ api.VehicleOdometer = (*Provider)(nil) + +// Odometer implements the api.VehicleOdometer interface +func (v *Provider) Odometer() (float64, error) { + res, err := v.status.Get() + return res.Mileage, err +} + +var _ api.VehiclePosition = (*Provider)(nil) + +// Position implements the api.VehiclePosition interface +func (v *Provider) Position() (float64, float64, error) { + res, err := v.status.Get() + return res.Latitude, res.Longitude, err +} diff --git a/vehicle/zero/types.go b/vehicle/zero/types.go new file mode 100644 index 000000000..9dca1103d --- /dev/null +++ b/vehicle/zero/types.go @@ -0,0 +1,59 @@ +package zero + +type Unit struct { + UnitNumber string //"123456", + Name string +} + +type ErrorAnswer struct { + Error string +} + +type State struct { + Unitnumber string //"123456", + Name string //"538ZFAZ76LCK00000", + Unittype string //"5", + Unitmodel string //"6", + Mileage float64 `json:",string"` //"4382.46", + Software_version string //"190430", + Logic_state string //"2" + Reason string //"2", + Response string //"0", + Driver string //"0", + Latitude float64 // 51.5000, + Longitude float64 // 4.5000, + Altitude string //:"0", + Gps_valid string //:"0", + Gps_connected string //:"1", + Satellites string //"0", + Velocity string //"1", + Heading string //"344", + Emergency string //:"0", + Shock string //:"", + Ignition string //:"0", + Door string //:"0", + Hood string //:"0", + Volume string //:"0", + Water_temp string //:"", + Oil_pressure string //:"0", + Main_voltage float64 //:13.08, + Analog1 float64 //":"0.09", + Analog2 float64 //":"0.09", + Analog3 float64 //":"0.09", + Siren string //:"0", + Lock string //:"0", + Int_lights string //:"0", + DatetimeUtc string `json:"datetime_utc"` //:"20191030162309", + DatetimeActual string `json:"datetime_actual"` //:"20191102113548" + Address string //:"YourCity, YourStreet", + Perimeter string //:"", + Color int //:2, + Soc int //:91, + Tipover int //:0, + Charging int //:1, + Chargecomplete int // 0, + Pluggedin int //:1, + Chargingtimeleft int //:0 + Storage int + Battery int +} diff --git a/vehicle/zeromotorcycles.go b/vehicle/zeromotorcycles.go new file mode 100644 index 000000000..afd7f7d6e --- /dev/null +++ b/vehicle/zeromotorcycles.go @@ -0,0 +1,63 @@ +package vehicle + +import ( + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/vehicle/zero" +) + +// Zero is an api.Vehicle implementation for probably all ZERO Motorcycles +type ZeroMotorcycle struct { + *embed + *zero.Provider // provides the api implementations +} + +func init() { + registry.Add("zero", NewZeroFromConfig) +} + +// NewZeroFromConfig creates a new vehicle +func NewZeroFromConfig(other map[string]interface{}) (api.Vehicle, error) { + var res *zero.API + var err error + + cc := struct { + embed `mapstructure:",squash"` + User, Password, VIN string + Cache time.Duration + }{ + Cache: interval, + } + + if err = util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + if cc.User == "" || cc.Password == "" { + return nil, api.ErrMissingCredentials + } + + log := util.NewLogger("Zero").Redact(cc.User, cc.Password) + + if res, err = zero.NewAPI(log, cc.User, cc.Password); err != nil { + return nil, err + } + + vehicle, err := ensureVehicleEx(cc.VIN, res.Vehicles, + func(v zero.Unit) (string, error) { + return v.Name, nil + }, + ) + if err != nil { + return nil, err + } + + v := &ZeroMotorcycle{ + embed: &cc.embed, + Provider: zero.NewProvider(res, vehicle.UnitNumber, cc.Cache), + } + + return v, nil +}