diff --git a/templates/definition/vehicle/aiways.yaml b/templates/definition/vehicle/aiways.yaml new file mode 100644 index 000000000..311a21e95 --- /dev/null +++ b/templates/definition/vehicle/aiways.yaml @@ -0,0 +1,12 @@ +template: aiways +products: + - brand: Aiways +params: + - preset: vehiclebase + - preset: vehicleidentify + - name: vin + required: true +render: | + type: aiways + {{ include "vehicle-base" . }} + {{ include "vehicle-identify" . }} diff --git a/templates/docs/vehicle/aiways_0.yaml b/templates/docs/vehicle/aiways_0.yaml new file mode 100644 index 000000000..c668b3749 --- /dev/null +++ b/templates/docs/vehicle/aiways_0.yaml @@ -0,0 +1,28 @@ +product: + brand: Aiways +render: + - default: | + type: template + template: aiways + title: # Wird in der Benutzeroberfläche angezeigt # Optional + user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) + password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) + vin: W... # Erforderlich, wenn mehrere Fahrzeuge des Herstellers vorhanden sind + capacity: 50 # Akkukapazität in kWh # Optional + advanced: | + type: template + template: aiways + title: # Wird in der Benutzeroberfläche angezeigt # Optional + user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) + password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) + vin: W... # Erforderlich, wenn mehrere Fahrzeuge des Herstellers vorhanden sind + capacity: 50 # Akkukapazität in kWh # Optional + phases: 3 # Die maximale Anzahl der Phasen welche genutzt werden können # Optional + icon: car # Darstellung in der UI. Mögliche Werte: bike, bus, car, heater, heatercooler, heatpump, moped, motorcycle, plug, scooter, tractor, van, waterheater # Optional + cache: 15m # Zeitintervall nach dem Daten erneut vom Fahrzeug abgefragt werden # Optional + mode: # Möglich sind Off, Now, MinPV und PV, oder leer wenn keiner definiert werden soll # Optional + minSoc: 25 # Lade sofort mit maximaler Geschwindigkeit bis zu dem angegeben Ladestand, wenn der Lademodus nicht auf 'Aus' steht # Optional + targetSoc: 80 # Bis zu welchem Ladestand (Soc) soll das Fahrzeug geladen werden # Optional + minCurrent: 6 # Definiert die minimale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll # Optional + maxCurrent: 16 # Definiert die maximale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll # Optional + identifiers: # Kann meist erst später eingetragen werden, siehe: https://docs.evcc.io/docs/guides/vehicles/#erkennung-des-fahrzeugs-an-der-wallbox # Optional diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json index 68f230d59..40ff7ec49 100644 --- a/templates/evcc.io/brands.json +++ b/templates/evcc.io/brands.json @@ -131,6 +131,7 @@ "Victron" ], "Vehicles": [ + "Aiways", "Audi", "BMW", "Citroën", diff --git a/vehicle/aiways.go b/vehicle/aiways.go new file mode 100644 index 000000000..bbb9ebd1b --- /dev/null +++ b/vehicle/aiways.go @@ -0,0 +1,67 @@ +package vehicle + +import ( + "strconv" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/vehicle/aiways" +) + +// https://github.com/davidgiga1993/AiwaysAPI +// https://github.com/TA2k/ioBroker.vw-connect + +// Aiways is an api.Vehicle implementation for Aiways cars +type Aiways struct { + *embed + *aiways.Provider // provides the api implementations +} + +func init() { + registry.Add("aiways", NewAiwaysFromConfig) +} + +// NewAiwaysFromConfig creates a new vehicle +func NewAiwaysFromConfig(other map[string]interface{}) (api.Vehicle, error) { + cc := struct { + embed `mapstructure:",squash"` + User, Password, VIN string + Cache time.Duration + Timeout time.Duration + }{ + Cache: interval, + Timeout: request.Timeout, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + v := &Aiways{ + embed: &cc.embed, + } + + log := util.NewLogger("aiways").Redact(cc.User, cc.Password, cc.VIN) + + identity := aiways.NewIdentity(log) + user, err := identity.Login(cc.User, cc.Password) + if err != nil { + return nil, err + } + + log.Redact(strconv.FormatInt(user, 10)) + + api := aiways.NewAPI(log, identity) + api.Client.Timeout = cc.Timeout + + // _, err := api.Vehicles() + // cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles) + + if err == nil { + v.Provider = aiways.NewProvider(api, user, cc.VIN, cc.Cache) + } + + return v, err +} diff --git a/vehicle/aiways/api.go b/vehicle/aiways/api.go new file mode 100644 index 000000000..694607185 --- /dev/null +++ b/vehicle/aiways/api.go @@ -0,0 +1,60 @@ +package aiways + +import ( + "errors" + "fmt" + "net/http" + + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" +) + +// https://github.com/snaptec/openWB/blob/master/modules/soc_aiways/aiways_get_soc.py + +const URI = "https://coiapp-api-eu.ai-ways.com:10443" + +// API implements the Aiways api +type API struct { + *request.Helper + identity TokenProvider +} + +// New creates a new Aiways API +func NewAPI(log *util.Logger, identity TokenProvider) *API { + v := &API{ + Helper: request.NewHelper(log), + identity: identity, + } + + return v +} + +// func (v *API) Vehicles() ([]Vehicle, error) { +// } + +func (v *API) Status(user int64, vin string) (StatusResponse, error) { + var res StatusResponse + + data2 := struct { + UserId int64 `json:"userId"` + VIN string `json:"vin"` + }{ + UserId: user, + VIN: vin, + } + + uri := fmt.Sprintf("%s/app/vc/getCondition", URI) + req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data2), map[string]string{ + "Content-Type": request.JSONContent, + "Accept": request.JSONContent, + "Token": v.identity.Token(), + }) + + if err == nil { + if err = v.DoJSON(req, &res); err == nil && res.Data == nil { + err = errors.New(res.Message) + } + } + + return res, err +} diff --git a/vehicle/aiways/identity.go b/vehicle/aiways/identity.go new file mode 100644 index 000000000..d09c900af --- /dev/null +++ b/vehicle/aiways/identity.go @@ -0,0 +1,68 @@ +package aiways + +import ( + "crypto/md5" + "encoding/hex" + "errors" + "fmt" + "net/http" + + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" +) + +type Identity struct { + *request.Helper + user, hash, token string +} + +type TokenProvider interface { + Token() string +} + +// NewIdentity creates BMW identity +func NewIdentity(log *util.Logger) *Identity { + v := &Identity{ + Helper: request.NewHelper(log), + } + + return v +} + +func (v *Identity) Login(user, password string) (int64, error) { + hash := md5.New() + hash.Write([]byte(password)) + + v.user = user + v.hash = hex.EncodeToString(hash.Sum(nil)) + + var res User + + data := struct { + Account string `json:"account"` + Password string `json:"password"` + }{ + Account: v.user, + Password: v.hash, + } + + uri := fmt.Sprintf("%s/aiways-passport-service/passport/login/password", URI) + req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + + if err == nil { + if err = v.DoJSON(req, &res); err == nil && res.Data == nil { + err = errors.New(res.Message) + } + + if err == nil { + v.token = res.Data.Token + return res.Data.UserID, nil + } + } + + return 0, err +} + +func (v *Identity) Token() string { + return v.token +} diff --git a/vehicle/aiways/provider.go b/vehicle/aiways/provider.go new file mode 100644 index 000000000..53ea4a9ba --- /dev/null +++ b/vehicle/aiways/provider.go @@ -0,0 +1,89 @@ +package aiways + +import ( + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/provider" +) + +// Provider implements the vehicle api +type Provider struct { + statusG func() (StatusResponse, error) +} + +// NewProvider creates a vehicle api provider +func NewProvider(api *API, user int64, vin string, cache time.Duration) *Provider { + impl := &Provider{ + statusG: provider.Cached(func() (StatusResponse, error) { + return api.Status(user, vin) + }, cache), + } + return impl +} + +var _ api.Battery = (*Provider)(nil) + +// Soc implements the api.Vehicle interface +func (v *Provider) Soc() (float64, error) { + res, err := v.statusG() + if err == nil { + return float64(res.Data.Vc.Soc), nil + } + return 0, err +} + +// var _ api.ChargeState = (*Provider)(nil) + +// // Status implements the api.ChargeState interface +// func (v *Provider) Status() (api.ChargeStatus, error) { +// status := api.StatusA // disconnected + +// res, err := v.statusG() +// if err == nil { +// if res.Charger.Status.PlugStatusData.PlugState.Content == "connected" { +// status = api.StatusB +// } +// if res.Charger.Status.ChargingStatusData.ChargingState.Content == "charging" { +// status = api.StatusC +// } +// } + +// return status, err +// } + +var _ api.VehicleRange = (*Provider)(nil) + +// Range implements the api.VehicleRange interface +func (v *Provider) Range() (int64, error) { + res, err := v.statusG() + if err == nil { + return int64(res.Data.Vc.DrivingRange), nil + } + + return 0, err +} + +var _ api.VehicleOdometer = (*Provider)(nil) + +// Odometer implements the api.VehicleOdometer interface +func (v *Provider) Odometer() (float64, error) { + res, err := v.statusG() + if err == nil { + return res.Data.Vc.VehicleMileage, nil + } + + return 0, err +} + +// var _ api.VehiclePosition = (*Provider)(nil) + +// // Position implements the api.VehiclePosition interface +// func (v *Provider) Position() (float64, float64, error) { +// res, err := v.statusG() +// if err == nil { +// return res.Data.Vc.Lat, res.Data.Vc.Lon, nil +// } + +// return 0, 0, err +// } diff --git a/vehicle/aiways/types.go b/vehicle/aiways/types.go new file mode 100644 index 000000000..b65e7626d --- /dev/null +++ b/vehicle/aiways/types.go @@ -0,0 +1,129 @@ +package aiways + +type User struct { + Code int64 `json:"code"` + Message string `json:"message"` + Data *struct { + Email string `json:"email"` + HeadURL string `json:"headUrl"` + IsFirstLogin int64 `json:"isFirstLogin"` + Mobile interface{} `json:"mobile"` + Nickname string `json:"nickname"` + Sex interface{} `json:"sex"` + Token string `json:"token"` + UserID int64 `json:"userId"` + UserType int64 `json:"userType"` + Username string `json:"username"` + } `json:"data"` + Timestamp int64 `json:"timestamp"` +} + +type StatusResponse struct { + Code string `json:"code"` + Message string `json:"message"` + Data *struct { + Vc struct { + DataTime string `json:"dataTime"` + DataTimeTS int64 `json:"dataTimeTS"` + // NorthLat string `json:"northLat"` + // EastLon string `json:"eastLon"` + Lat float64 `json:"lat,string"` + Lon float64 `json:"lon,string"` + Address string `json:"address"` + ChargeSts int `json:"chargeSts,string"` + // SocLow string `json:"socLow"` + // LastFlameoutTime string `json:"lastFlameoutTime"` + // VehicleSts string `json:"vehicleSts"` + // LowSpdMuteSts string `json:"lowSpdMuteSts"` + // DoorLockSts string `json:"doorLockSts"` + // TrunkLockSts string `json:"trunkLockSts"` + // DoorStsReserved string `json:"doorStsReserved"` + // DoorOpenStsLf string `json:"doorOpenStsLf"` + // DoorOpenStsRf string `json:"doorOpenStsRf"` + // DoorOpenStsLb string `json:"doorOpenStsLb"` + // DoorOpenStsRb string `json:"doorOpenStsRb"` + // FrontHatchCoverOpenSts string `json:"frontHatchCoverOpenSts"` + // TrunkDoorOpenSts string `json:"trunkDoorOpenSts"` + // ChargingCoverOpenSts string `json:"chargingCoverOpenSts"` + // WindowOpenStsLf string `json:"windowOpenStsLf"` + // WindowOpenStsRf string `json:"windowOpenStsRf"` + // WindowOpenStsLb string `json:"windowOpenStsLb"` + // WindowOpenStsRb string `json:"windowOpenStsRb"` + // LeftTurnLightOpenSts string `json:"leftTurnLightOpenSts"` + // RightTurnLightOpenSts string `json:"rightTurnLightOpenSts"` + // BackFrogLightOpenSts string `json:"backFrogLightOpenSts"` + // HighLightOpenSts string `json:"highLightOpenSts"` + // LowLightOpenSts string `json:"lowLightOpenSts"` + // WidthLightOpenSts string `json:"widthLightOpenSts"` + // BrakeLightOpenSts string `json:"brakeLightOpenSts"` + // ReversingLightOpenSts string `json:"reversingLightOpenSts"` + // DayRunningLightOpenSts string `json:"dayRunningLightOpenSts"` + // AutoLightOpenSts string `json:"autoLightOpenSts"` + // SunroofStsSts string `json:"sunroofStsSts"` + // SunroofHorizontalOpen string `json:"sunroofHorizontalOpen"` + // SunroofSunshadeOpen string `json:"sunroofSunshadeOpen"` + // TirePressureStsLf string `json:"tirePressureStsLf"` + // TirePressureStsRf string `json:"tirePressureStsRf"` + // TirePressureStsLb string `json:"tirePressureStsLb"` + // TirePressureStsRb string `json:"tirePressureStsRb"` + // TirePressureLf string `json:"tirePressureLf"` + // TirePressureRf string `json:"tirePressureRf"` + // TirePressureLb string `json:"tirePressureLb"` + // TirePressureRb string `json:"tirePressureRb"` + // TireTempHighStsLf string `json:"tireTempHighStsLf"` + // TireTempHighStsRf string `json:"tireTempHighStsRf"` + // TireTempHighStsLb string `json:"tireTempHighStsLb"` + // TireTempHighStsRb string `json:"tireTempHighStsRb"` + // TireTempLf string `json:"tireTempLf"` + // TireTempRf string `json:"tireTempRf"` + // TireTempLb string `json:"tireTempLb"` + // TireTempRb string `json:"tireTempRb"` + // AirconAcSts string `json:"airconAcSts"` + // AirconAutoSts string `json:"airconAutoSts"` + // AirconWindMode string `json:"airconWindMode"` + // AirconWindVolume string `json:"airconWindVolume"` + // AirconCycleSts string `json:"airconCycleSts"` + // AirconTempDisLf string `json:"airconTempDisLf"` + // AirconTempDisRf string `json:"airconTempDisRf"` + // AirconSyncSts string `json:"airconSyncSts"` + // AirconRunSts string `json:"airconRunSts"` + // AirconOutsideTemp string `json:"airconOutsideTemp"` + // AirconInsideTemp string `json:"airconInsideTemp"` + // SafetyBeltStsDrv string `json:"safetyBeltStsDrv"` + // SafetyBeltStsPass string `json:"safetyBeltStsPass"` + // SeatBeltStsBl string `json:"seatBeltStsBl"` + // SeatBeltStsBm string `json:"seatBeltStsBm"` + // SeatBeltStsBr string `json:"seatBeltStsBr"` + // SeatHeatStsDrv string `json:"seatHeatStsDrv"` + // SeatHeatStsPass string `json:"seatHeatStsPass"` + // SeatHeatStsSys string `json:"seatHeatStsSys"` + // ChgConnStsDc string `json:"chgConnStsDc"` + // ChgConnStsAc string `json:"chgConnStsAc"` + // KeyLowPowerWarn string `json:"keyLowPowerWarn"` + DrivingRange float64 `json:"drivingRange,string"` + VehicleMileage float64 `json:"vehicleMileage,string"` + // Speed string `json:"speed"` + // CrashSts string `json:"crashSts"` + // ElecModeFlg string `json:"elecModeFlg"` + // PowerMode string `json:"powerMode"` + // SteeringAngle string `json:"steeringAngle"` + // EpbSts string `json:"epbSts"` + Soc int `json:"soc,string"` + // AgpsSts string `json:"agpsSts"` + // BtConnMac string `json:"btConnMac"` + // BtConnUserID string `json:"btConnUserId"` + // WifiConnCount string `json:"wifiConnCount"` + // HasSkylight string `json:"hasSkylight"` + // Iccid string `json:"iccid"` + // DataFlow string `json:"dataFlow"` + // AuthStatus string `json:"authStatus"` + // SenceSeason int `json:"senceSeason"` + // SenceSeasonText string `json:"senceSeasonText"` + // CarSenceSeasonText string `json:"carSenceSeasonText"` + // BmsChgRemTime string `json:"bmsChgRemTime"` + // BmsPreThemalMode string `json:"bmsPreThemalMode"` + // BcmRemoteControlSt string `json:"bcmRemoteControlSt"` + Active bool `json:"active"` + } `json:"vc"` + } `json:"data"` +}