Add Zero Motorcycles (#14823)

This commit is contained in:
kscholty 2024-08-03 17:00:53 +02:00 • committed by GitHub
parent b0ea0582e2
commit dc9f02b676
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 288 additions and 0 deletions

View file

@ -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" . }}

71
vehicle/zero/api.go Normal file
View file

@ -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/<vin>/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
}

85
vehicle/zero/provider.go Normal file
View file

@ -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
}

59
vehicle/zero/types.go Normal file
View file

@ -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
}

View file

@ -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
}