Skip to content

Commit

Permalink
VW: add odometer (#1468)
Browse files Browse the repository at this point in the history
Decode rolesrights and vehicle status
  • Loading branch information
andig authored Nov 26, 2021
1 parent 92b40dc commit 51d412c
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 120 deletions.
69 changes: 0 additions & 69 deletions vehicle/audi/api.go

This file was deleted.

39 changes: 0 additions & 39 deletions vehicle/audi/provider.go

This file was deleted.

67 changes: 62 additions & 5 deletions vehicle/vw/api.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package vw

import (
"encoding/json"
"fmt"
"net/http"
"strings"
Expand All @@ -22,6 +21,7 @@ type API struct {
*request.Helper
brand, country string
baseURI string
statusURI string
}

// NewAPI creates a new api client
Expand Down Expand Up @@ -71,12 +71,69 @@ func (v *API) HomeRegion(vin string) error {
return err
}

// RolesRights updates the home region for the given vehicle
func (v *API) RolesRights(vin string) (string, error) {
var res json.RawMessage
// RolesRights implements the /rolesrights/operationlist response
func (v *API) RolesRights(vin string) (RolesRights, error) {
var res RolesRights
uri := fmt.Sprintf("%s/rolesrights/operationlist/v3/vehicles/%s", RegionAPI, vin)
err := v.GetJSON(uri, &res)
return string(res), err
return res, err
}

// ServiceURI renders the service URI for the given vin and service
func (v *API) ServiceURI(vin, service string, rr RolesRights) (uri string) {
if si := rr.ServiceByID(service); si != nil {
uri = si.InvocationUrl.Content
uri = strings.ReplaceAll(uri, "{vin}", vin)
uri = strings.ReplaceAll(uri, "{brand}", v.brand)
uri = strings.ReplaceAll(uri, "{country}", v.country)
}

return uri
}

// Status implements the /status response
func (v *API) Status(vin string) (StatusResponse, error) {
var res StatusResponse
uri := fmt.Sprintf("%s/bs/vsr/v1/vehicles/%s/status", RegionAPI, vin)
if v.statusURI != "" {
uri = v.statusURI
}

headers := map[string]string{
"Accept": request.JSONContent,
"X-App-Name": "foo", // required
"X-App-Version": "foo", // required
}

req, err := request.New(http.MethodGet, uri, nil, headers)
if err == nil {
err = v.DoJSON(req, &res)
}

if _, ok := err.(request.StatusError); ok {
var rr RolesRights
rr, err = v.RolesRights(vin)

if err == nil {
if uri = v.ServiceURI(vin, StatusService, rr); uri == "" {
err = fmt.Errorf("%s not found", StatusService)
}
}

if err == nil {
if strings.HasSuffix(uri, fmt.Sprintf("%s/", vin)) {
uri += "status"
}

if req, err = request.New(http.MethodGet, uri, nil, headers); err == nil {
if err = v.DoJSON(req, &res); err == nil {
v.statusURI = uri
}
}
}
}

return res, err
}

// Charger implements the /charger response
Expand Down
72 changes: 72 additions & 0 deletions vehicle/vw/provider.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
package vw

import (
"fmt"
"math"
"os"
"sort"
"strconv"
"strings"
"text/tabwriter"
"time"

"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/provider"
"github.com/thoas/go-funk"
)

// Provider implements the evcc vehicle api
type Provider struct {
chargerG func() (interface{}, error)
statusG func() (interface{}, error)
climateG func() (interface{}, error)
action func(action, value string) error
rr func() (RolesRights, error)
}

// NewProvider provides the evcc vehicle api provider
Expand All @@ -22,12 +30,18 @@ func NewProvider(api *API, vin string, cache time.Duration) *Provider {
chargerG: provider.NewCached(func() (interface{}, error) {
return api.Charger(vin)
}, cache).InterfaceGetter(),
statusG: provider.NewCached(func() (interface{}, error) {
return api.Status(vin)
}, cache).InterfaceGetter(),
climateG: provider.NewCached(func() (interface{}, error) {
return api.Climater(vin)
}, cache).InterfaceGetter(),
action: func(action, value string) error {
return api.Action(vin, action, value)
},
rr: func() (RolesRights, error) {
return api.RolesRights(vin)
},
}
return impl
}
Expand Down Expand Up @@ -99,6 +113,26 @@ func (v *Provider) Range() (rng int64, err error) {
return rng, err
}

var _ api.VehicleOdometer = (*Provider)(nil)

// Odometer implements the api.VehicleOdometer interface
func (v *Provider) Odometer() (float64, error) {
res, err := v.statusG()
if res, ok := res.(StatusResponse); err == nil && ok {
err = api.ErrNotAvailable

if sd := res.ServiceByID(ServiceOdometer); sd != nil {
if fd := sd.FieldByID(ServiceOdometer); fd != nil {
if val, err := strconv.ParseFloat(fd.Value, 64); err == nil {
return val, nil
}
}
}
}

return 0, err
}

var _ api.VehicleClimater = (*Provider)(nil)

// Climater implements the api.VehicleClimater interface
Expand Down Expand Up @@ -133,3 +167,41 @@ var _ api.VehicleStopCharge = (*Provider)(nil)
func (v *Provider) StopCharge() error {
return v.action(ActionCharge, ActionChargeStop)
}

// var _ api.Diagnosis = (*Provider)(nil)

// Diagnose implements the api.Diagnosis interface
func (v *Provider) Diagnose2() {
rr, err := v.rr()
if err != nil {
return
}

tw := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)

sort.Slice(rr.OperationList.ServiceInfo, func(i, j int) bool {
return rr.OperationList.ServiceInfo[i].ServiceId < rr.OperationList.ServiceInfo[j].ServiceId
})

for _, si := range rr.OperationList.ServiceInfo {
if si.InvocationUrl.Content != "" {
fmt.Fprintf(tw, "%s:\t%s\n", si.ServiceId, si.InvocationUrl.Content)
}
}

// list remaining service
services := funk.Map(rr.OperationList.ServiceInfo, func(si ServiceInfo) string {
if si.InvocationUrl.Content == "" {
return si.ServiceId
}
return ""
}).([]string)

services = funk.FilterString(services, func(s string) bool {
return s != ""
})

fmt.Fprintf(tw, "without uri:\t%s\n", strings.Join(services, ","))

tw.Flush()
}
39 changes: 39 additions & 0 deletions vehicle/vw/types_rolesrights.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package vw

const StatusService = "statusreport_v1"

// RolesRights is the /rolesrights/operationlist response
type RolesRights struct {
OperationList struct {
VIN, UserId, Role, Status string
ServiceInfo []ServiceInfo
}
}

func (rr RolesRights) ServiceByID(id string) *ServiceInfo {
for _, s := range rr.OperationList.ServiceInfo {
if s.ServiceId == id {
return &s
}
}
return nil
}

// ServiceInfo is the rolesrights service information
type ServiceInfo struct {
ServiceId string
ServiceType string
ServiceStatus struct {
Status string
}
LicenseRequired bool
CumulatedLicense map[string]interface{}
PrimaryUserRequired bool
TermsAndConditionsRequired bool
ServiceEol string
RolesAndRightsRequired bool
InvocationUrl struct {
Content string
}
Operation []map[string]interface{}
}
11 changes: 4 additions & 7 deletions vehicle/audi/types.go → vehicle/vw/types_status.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package audi
package vw

const ServiceOdometer = "0x0101010002"

type StatusResponse struct {
StoredVehicleDataResponse struct {
Expand All @@ -7,12 +9,7 @@ type StatusResponse struct {
Data []ServiceDefinition
}
}
Error Error
}

type Error struct {
ErrorCode string
Description string
Error *Error
}

func (s *StatusResponse) ServiceByID(id string) *ServiceDefinition {
Expand Down

0 comments on commit 51d412c

Please sign in to comment.