696 lines
23 KiB
Go
696 lines
23 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/teslamotors/vehicle-command/pkg/account"
|
|
"github.com/teslamotors/vehicle-command/pkg/cli"
|
|
"github.com/teslamotors/vehicle-command/pkg/protocol"
|
|
"github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/vcsec"
|
|
"github.com/teslamotors/vehicle-command/pkg/vehicle"
|
|
)
|
|
|
|
var ErrCommandLineArgs = errors.New("invalid command line arguments")
|
|
|
|
type Argument struct {
|
|
name string
|
|
help string
|
|
}
|
|
|
|
type Handler func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error
|
|
|
|
type Command struct {
|
|
help string
|
|
requiresAuth bool // True if command requires client-to-vehicle authentication (private key)
|
|
requiresFleetAPI bool // True if command requires client-to-server authentication (OAuth token)
|
|
args []Argument
|
|
optional []Argument
|
|
handler Handler
|
|
}
|
|
|
|
// configureAndVerifyFlags verifies that c contains all the information required to execute a command.
|
|
func configureFlags(c *cli.Config, commandName string, forceBLE bool) error {
|
|
info, ok := commands[commandName]
|
|
if !ok {
|
|
return ErrUnknownCommand
|
|
}
|
|
c.Flags = cli.FlagBLE
|
|
if info.requiresAuth {
|
|
c.Flags |= cli.FlagPrivateKey | cli.FlagVIN
|
|
}
|
|
if !info.requiresFleetAPI {
|
|
c.Flags |= cli.FlagVIN
|
|
}
|
|
if forceBLE {
|
|
if info.requiresFleetAPI {
|
|
return ErrRequiresOAuth
|
|
}
|
|
} else {
|
|
c.Flags |= cli.FlagOAuth
|
|
}
|
|
|
|
// Verify all required parameters are present.
|
|
havePrivateKey := !(c.KeyringKeyName == "" && c.KeyFilename == "")
|
|
haveOAuth := !(c.KeyringTokenName == "" && c.TokenFilename == "")
|
|
haveVIN := c.VIN != ""
|
|
_, err := checkReadiness(commandName, havePrivateKey, haveOAuth, haveVIN)
|
|
return err
|
|
}
|
|
|
|
var (
|
|
ErrRequiresOAuth = errors.New("command requires a FleetAPI OAuth token")
|
|
ErrRequiresVIN = errors.New("command requires a VIN")
|
|
ErrRequiresPrivateKey = errors.New("command requires a private key")
|
|
ErrUnknownCommand = errors.New("unrecognized command")
|
|
)
|
|
|
|
func checkReadiness(commandName string, havePrivateKey, haveOAuth, haveVIN bool) (*Command, error) {
|
|
info, ok := commands[commandName]
|
|
if !ok {
|
|
return nil, ErrUnknownCommand
|
|
}
|
|
if info.requiresFleetAPI {
|
|
if !haveOAuth {
|
|
return nil, ErrRequiresOAuth
|
|
}
|
|
} else {
|
|
// Currently, commands supported by this application either target the account (and
|
|
// therefore require FleetAPI credentials but not a VIN) or target a vehicle (and therefore
|
|
// require a VIN but not FleetAPI credentials).
|
|
if !haveVIN {
|
|
return nil, ErrRequiresVIN
|
|
}
|
|
}
|
|
if info.requiresAuth && !havePrivateKey {
|
|
return nil, ErrRequiresPrivateKey
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
func execute(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New("missing COMMAND")
|
|
}
|
|
|
|
info, err := checkReadiness(args[0], car != nil && car.PrivateKeyAvailable(), acct != nil, car != nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(args)-1 < len(info.args) || len(args)-1 > len(info.args)+len(info.optional) {
|
|
writeErr("Invalid number of command line arguments: %d (%d required, %d optional).", len(args), len(info.args), len(info.optional))
|
|
err = ErrCommandLineArgs
|
|
} else {
|
|
keywords := make(map[string]string)
|
|
for i, argInfo := range info.args {
|
|
keywords[argInfo.name] = args[i+1]
|
|
}
|
|
index := len(info.args) + 1
|
|
for _, argInfo := range info.optional {
|
|
if index >= len(args) {
|
|
break
|
|
}
|
|
keywords[argInfo.name] = args[index]
|
|
index++
|
|
}
|
|
err = info.handler(ctx, acct, car, keywords)
|
|
}
|
|
|
|
// Print command-specific help
|
|
if errors.Is(err, ErrCommandLineArgs) {
|
|
info.Usage(args[0])
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (c *Command) Usage(name string) {
|
|
fmt.Printf("Usage: %s", name)
|
|
maxLength := 0
|
|
for _, arg := range c.args {
|
|
fmt.Printf(" %s", arg.name)
|
|
if len(arg.name) > maxLength {
|
|
maxLength = len(arg.name)
|
|
}
|
|
}
|
|
if len(c.optional) > 0 {
|
|
fmt.Printf(" [")
|
|
}
|
|
for _, arg := range c.optional {
|
|
fmt.Printf(" %s", arg.name)
|
|
if len(arg.name) > maxLength {
|
|
maxLength = len(arg.name)
|
|
}
|
|
}
|
|
if len(c.optional) > 0 {
|
|
fmt.Printf(" ]")
|
|
}
|
|
fmt.Printf("\n%s\n", c.help)
|
|
maxLength++
|
|
for _, arg := range c.args {
|
|
fmt.Printf(" %s:%s%s\n", arg.name, strings.Repeat(" ", maxLength-len(arg.name)), arg.help)
|
|
}
|
|
for _, arg := range c.optional {
|
|
fmt.Printf(" %s:%s%s\n", arg.name, strings.Repeat(" ", maxLength-len(arg.name)), arg.help)
|
|
}
|
|
}
|
|
|
|
var commands = map[string]*Command{
|
|
"unlock": &Command{
|
|
help: "Unlock vehicle",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.Unlock(ctx)
|
|
},
|
|
},
|
|
"lock": &Command{
|
|
help: "Lock vehicle",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.Lock(ctx)
|
|
},
|
|
},
|
|
"drive": &Command{
|
|
help: "Remote start vehicle",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.RemoteDrive(ctx)
|
|
},
|
|
},
|
|
"climate-on": &Command{
|
|
help: "Turn on climate control",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.ClimateOn(ctx)
|
|
},
|
|
},
|
|
"climate-off": &Command{
|
|
help: "Turn off climate control",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.ClimateOff(ctx)
|
|
},
|
|
},
|
|
"climate-set-temp": &Command{
|
|
help: "Set temperature (Celsius)",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
args: []Argument{
|
|
Argument{name: "TEMP", help: "Desired temperature (e.g., 70f or 21c; defaults to Celsius)"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
var degrees float32
|
|
var unit string
|
|
if _, err := fmt.Sscanf(args["TEMP"], "%f%s", °rees, &unit); err != nil {
|
|
return fmt.Errorf("failed to parse temperature: format as 22C or 72F")
|
|
}
|
|
if unit == "F" || unit == "f" {
|
|
degrees = (5.0 * degrees / 9.0) + 32.0
|
|
} else if unit != "C" && unit != "c" {
|
|
return fmt.Errorf("temperature units must be C or F")
|
|
}
|
|
return car.ChangeClimateTemp(ctx, degrees, degrees)
|
|
},
|
|
},
|
|
"add-key": &Command{
|
|
help: "Add PUBLIC_KEY to vehicle whitelist with ROLE and FORM_FACTOR",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
args: []Argument{
|
|
Argument{name: "PUBLIC_KEY", help: "file containing public key (or corresponding private key)"},
|
|
Argument{name: "ROLE", help: "One of: owner, driver"},
|
|
Argument{name: "FORM_FACTOR", help: "One of: nfc_card, ios_device, android_device, cloud_key"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
role := strings.ToUpper(args["ROLE"])
|
|
if role != "OWNER" && role != "DRIVER" {
|
|
return fmt.Errorf("%w: invalid ROLE", ErrCommandLineArgs)
|
|
}
|
|
formFactor, ok := vcsec.KeyFormFactor_value["KEY_FORM_FACTOR_"+strings.ToUpper(args["FORM_FACTOR"])]
|
|
if !ok {
|
|
return fmt.Errorf("%w: unrecognized FORM_FACTOR", ErrCommandLineArgs)
|
|
}
|
|
publicKey, err := protocol.LoadPublicKey(args["PUBLIC_KEY"])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid public key: %s", err)
|
|
}
|
|
return car.AddKey(ctx, publicKey, role == "OWNER", vcsec.KeyFormFactor(formFactor))
|
|
},
|
|
},
|
|
"add-key-request": &Command{
|
|
help: "Requset NFC-card approval for a enrolling PUBLIC_KEY with ROLE and FORM_FACTOR",
|
|
requiresAuth: false,
|
|
requiresFleetAPI: false,
|
|
args: []Argument{
|
|
Argument{name: "PUBLIC_KEY", help: "file containing public key (or corresponding private key)"},
|
|
Argument{name: "ROLE", help: "One of: owner, driver"},
|
|
Argument{name: "FORM_FACTOR", help: "One of: nfc_card, ios_device, android_device, cloud_key"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
role := strings.ToUpper(args["ROLE"])
|
|
if role != "OWNER" && role != "DRIVER" {
|
|
return fmt.Errorf("%w: invalid ROLE", ErrCommandLineArgs)
|
|
}
|
|
formFactor, ok := vcsec.KeyFormFactor_value["KEY_FORM_FACTOR_"+strings.ToUpper(args["FORM_FACTOR"])]
|
|
if !ok {
|
|
return fmt.Errorf("%w: unrecognized FORM_FACTOR", ErrCommandLineArgs)
|
|
}
|
|
publicKey, err := protocol.LoadPublicKey(args["PUBLIC_KEY"])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid public key: %s", err)
|
|
}
|
|
if err := car.SendAddKeyRequest(ctx, publicKey, role == "OWNER", vcsec.KeyFormFactor(formFactor)); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Sent add-key request to %s. Confirm by tapping NFC card on center console.\n", car.VIN())
|
|
return nil
|
|
},
|
|
},
|
|
"remove-key": &Command{
|
|
help: "Remove PUBLIC_KEY from vehicle whitelist",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
args: []Argument{
|
|
Argument{name: "PUBLIC_KEY", help: "file containing public key (or corresponding private key)"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
publicKey, err := protocol.LoadPublicKey(args["PUBLIC_KEY"])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid public key: %s", err)
|
|
}
|
|
return car.RemoveKey(ctx, publicKey)
|
|
},
|
|
},
|
|
"rename-key": &Command{
|
|
help: "Change the human-readable metadata of PUBLIC_KEY to NAME, MODEL, KIND",
|
|
requiresAuth: false,
|
|
requiresFleetAPI: true,
|
|
args: []Argument{
|
|
Argument{name: "PUBLIC_KEY", help: "file containing public key (or corresponding private key)"},
|
|
Argument{name: "NAME", help: "New human-readable name for the public key (e.g., Dave's Phone)"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
publicKey, err := protocol.LoadPublicKey(args["PUBLIC_KEY"])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid public key: %s", err)
|
|
}
|
|
return acct.UpdateKey(ctx, publicKey, args["NAME"])
|
|
},
|
|
},
|
|
"get": &Command{
|
|
help: "GET an owner API http ENDPOINT. Hostname will be taken from -config.",
|
|
requiresAuth: false,
|
|
requiresFleetAPI: true,
|
|
args: []Argument{
|
|
Argument{name: "ENDPOINT", help: "Fleet API endpoint"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
reply, err := acct.Get(ctx, args["ENDPOINT"])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(string(reply))
|
|
return nil
|
|
},
|
|
},
|
|
"post": &Command{
|
|
help: "POST to ENDPOINT the contents of FILE. Hostname will be taken from -config.",
|
|
requiresAuth: false,
|
|
requiresFleetAPI: true,
|
|
args: []Argument{
|
|
Argument{name: "ENDPOINT", help: "Fleet API endpoint"},
|
|
},
|
|
optional: []Argument{
|
|
Argument{name: "FILE", help: "JSON file to POST"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
var jsonBytes []byte
|
|
var err error
|
|
if filename, ok := args["FILE"]; ok {
|
|
jsonBytes, err = os.ReadFile(filename)
|
|
} else {
|
|
jsonBytes, err = io.ReadAll(os.Stdin)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply, err := acct.Post(ctx, args["ENDPOINT"], jsonBytes)
|
|
// reply can be set where there's an error; typically a JSON blob providing details
|
|
if reply != nil {
|
|
fmt.Println(string(reply))
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
"list-keys": &Command{
|
|
help: "List public keys enrolled on vehicle",
|
|
requiresAuth: false,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
summary, err := car.KeySummary(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
slot := uint32(0)
|
|
var details *vcsec.WhitelistEntryInfo
|
|
for mask := summary.GetSlotMask(); mask > 0; mask >>= 1 {
|
|
if mask&1 == 1 {
|
|
details, err = car.KeyInfoBySlot(ctx, slot)
|
|
if err != nil {
|
|
writeErr("Error fetching slot %d: %s", slot, err)
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
return err
|
|
}
|
|
}
|
|
if details != nil {
|
|
fmt.Printf("%02x\t%s\t%s\n", details.GetPublicKey().GetPublicKeyRaw(), details.GetKeyRole(), details.GetMetadataForKey().GetKeyFormFactor())
|
|
}
|
|
}
|
|
slot++
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
"honk": &Command{
|
|
help: "Honk horn",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.HonkHorn(ctx)
|
|
},
|
|
},
|
|
"ping": &Command{
|
|
help: "Ping vehicle",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.Ping(ctx)
|
|
},
|
|
},
|
|
"flash-lights": &Command{
|
|
help: "Flash lights",
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.FlashLights(ctx)
|
|
},
|
|
},
|
|
"charging-set-limit": &Command{
|
|
help: "Set charge limit to PERCENT",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
args: []Argument{
|
|
Argument{name: "PERCENT", help: "Charging limit"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
limit, err := strconv.Atoi(args["PERCENT"])
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing PERCENT")
|
|
}
|
|
return car.ChangeChargeLimit(ctx, int32(limit))
|
|
},
|
|
},
|
|
"charging-start": &Command{
|
|
help: "Start charging",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.ChargeStart(ctx)
|
|
},
|
|
},
|
|
"charging-stop": &Command{
|
|
help: "Stop charging",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.ChargeStop(ctx)
|
|
},
|
|
},
|
|
"media-set-volume": &Command{
|
|
help: "Set volume",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
args: []Argument{
|
|
Argument{name: "VOLUME", help: "Set volume (0.0-10.0"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
volume, err := strconv.ParseFloat(args["VOLUME"], 32)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse volume")
|
|
}
|
|
return car.SetVolume(ctx, float32(volume))
|
|
},
|
|
},
|
|
"software-update-start": &Command{
|
|
help: "Start software update after DELAY",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
args: []Argument{
|
|
Argument{
|
|
name: "DELAY",
|
|
help: "Time to wait before starting update. Examples: 2h, 10m.",
|
|
},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
delay, err := time.ParseDuration(args["DELAY"])
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing DELAY. Valid times are <n><unit>, where <n> is a number (decimals are allowed) and <unit> is 's, 'm', or 'h'")
|
|
// ...or 'ns'/'µs' if that's your cup of tea.
|
|
}
|
|
return car.ScheduleSoftwareUpdate(ctx, delay)
|
|
},
|
|
},
|
|
"software-update-cancel": &Command{
|
|
help: "Cancel a pending software update",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.CancelSoftwareUpdate(ctx)
|
|
},
|
|
},
|
|
"sentry-mode": &Command{
|
|
help: "Set sentry mode to STATE ('on' or 'off')",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
args: []Argument{
|
|
Argument{name: "STATE", help: "'on' or 'off'"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
var state bool
|
|
switch args["STATE"] {
|
|
case "on":
|
|
state = true
|
|
case "off":
|
|
state = false
|
|
default:
|
|
return fmt.Errorf("sentry mode state must be 'on' or 'off'")
|
|
}
|
|
return car.SetSentryMode(ctx, state)
|
|
},
|
|
},
|
|
"wake": &Command{
|
|
help: "Wake up vehicle",
|
|
requiresAuth: false,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.Wakeup(ctx)
|
|
},
|
|
},
|
|
"trunk-open": &Command{
|
|
help: "Open vehicle trunk. Note that trunk-close only works on certain vehicle types.",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.OpenTrunk(ctx)
|
|
},
|
|
},
|
|
"trunk-move": &Command{
|
|
help: "Toggle trunk open/closed. Closing is only available on certain vehicle types.",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.ActuateTrunk(ctx)
|
|
},
|
|
},
|
|
"trunk-close": &Command{
|
|
help: "Closes vehicle trunk. Only available on certain vehicle types.",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.CloseTrunk(ctx)
|
|
},
|
|
},
|
|
"frunk-open": &Command{
|
|
help: "Open vehicle frunk. Note that there's no frunk-close command!",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.OpenFrunk(ctx)
|
|
},
|
|
},
|
|
"charge-port-open": &Command{
|
|
help: "Open charge port",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.OpenChargePort(ctx)
|
|
},
|
|
},
|
|
"charge-port-close": &Command{
|
|
help: "Close charge port",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.CloseChargePort(ctx)
|
|
},
|
|
},
|
|
"autosecure-modelx": &Command{
|
|
help: "Close falcon-wing doors and lock vehicle. Model X only.",
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
return car.AutoSecureVehicle(ctx)
|
|
},
|
|
},
|
|
"session-info": &Command{
|
|
help: "Retrieve session info for PUBLIC_KEY from DOMAIN",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
args: []Argument{
|
|
Argument{name: "PUBLIC_KEY", help: "file containing public key (or corresponding private key)"},
|
|
Argument{name: "DOMAIN", help: "'vcsec' or 'infotainment'"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
// See SeatPosition definition for controlling backrest heaters (limited models).
|
|
domains := map[string]protocol.Domain{
|
|
"vcsec": protocol.DomainVCSEC,
|
|
"infotainment": protocol.DomainInfotainment,
|
|
}
|
|
domain, ok := domains[args["DOMAIN"]]
|
|
if !ok {
|
|
return fmt.Errorf("invalid domain %s", args["DOMAIN"])
|
|
}
|
|
publicKey, err := protocol.LoadPublicKey(args["PUBLIC_KEY"])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid public key: %s", err)
|
|
}
|
|
info, err := car.SessionInfo(ctx, publicKey, domain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s\n", info)
|
|
return nil
|
|
},
|
|
},
|
|
"seat-heater": &Command{
|
|
help: "Set seat heater at POSITION to LEVEL",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
args: []Argument{
|
|
Argument{name: "SEAT", help: "<front|2nd-row|3rd-row>-<left|center|right> (e.g., 2nd-row-left)"},
|
|
Argument{name: "LEVEL", help: "off, low, medium, or high"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
// See SeatPosition definition for controlling backrest heaters (limited models).
|
|
seats := map[string]vehicle.SeatPosition{
|
|
"front-left": vehicle.SeatFrontLeft,
|
|
"front-right": vehicle.SeatFrontRight,
|
|
"2nd-row-left": vehicle.SeatSecondRowLeft,
|
|
"2nd-row-center": vehicle.SeatSecondRowCenter,
|
|
"2nd-row-right": vehicle.SeatSecondRowRight,
|
|
"3rd-row-left": vehicle.SeatThirdRowLeft,
|
|
"3rd-row-right": vehicle.SeatThirdRowRight,
|
|
}
|
|
position, ok := seats[args["SEAT"]]
|
|
if !ok {
|
|
return fmt.Errorf("invalid seat position")
|
|
}
|
|
levels := map[string]vehicle.Level{
|
|
"off": vehicle.LevelOff,
|
|
"low": vehicle.LevelLow,
|
|
"medium": vehicle.LevelMed,
|
|
"high": vehicle.LevelHigh,
|
|
}
|
|
level, ok := levels[args["LEVEL"]]
|
|
if !ok {
|
|
return fmt.Errorf("invalid seat heater level")
|
|
}
|
|
spec := map[vehicle.SeatPosition]vehicle.Level{
|
|
position: level,
|
|
}
|
|
return car.SetSeatHeater(ctx, spec)
|
|
},
|
|
},
|
|
"steering-wheel-heater": &Command{
|
|
help: "Set steering wheel mode to STATE ('on' or 'off')",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
args: []Argument{
|
|
Argument{name: "STATE", help: "'on' or 'off'"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
var state bool
|
|
switch args["STATE"] {
|
|
case "on":
|
|
state = true
|
|
case "off":
|
|
state = false
|
|
default:
|
|
return fmt.Errorf("steering wheel state must be 'on' or 'off'")
|
|
}
|
|
return car.SetSteeringWheelHeater(ctx, state)
|
|
},
|
|
},
|
|
"product-info": &Command{
|
|
help: "Print JSON product info",
|
|
requiresAuth: false,
|
|
requiresFleetAPI: true,
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
productsJSON, err := acct.Get(ctx, "api/1/products")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(string(productsJSON))
|
|
return nil
|
|
},
|
|
},
|
|
"auto-seat-and-climate": &Command{
|
|
help: "Turn on automatic seat heating and HVAC",
|
|
requiresAuth: true,
|
|
requiresFleetAPI: false,
|
|
args: []Argument{
|
|
Argument{name: "POSITIONS", help: "'L' (left), 'R' (right), or 'LR'"},
|
|
},
|
|
optional: []Argument{
|
|
Argument{name: "STATE", help: "'on' (default) or 'off'"},
|
|
},
|
|
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
|
var positions []vehicle.SeatPosition
|
|
if strings.Contains(args["POSITIONS"], "L") {
|
|
positions = append(positions, vehicle.SeatFrontLeft)
|
|
}
|
|
if strings.Contains(args["POSITIONS"], "R") {
|
|
positions = append(positions, vehicle.SeatFrontRight)
|
|
}
|
|
if len(positions) != len(args["POSITIONS"]) {
|
|
return fmt.Errorf("invalid seat position")
|
|
}
|
|
enabled := true
|
|
if state, ok := args["STATE"]; ok && strings.ToUpper(state) == "OFF" {
|
|
enabled = false
|
|
}
|
|
return car.AutoSeatAndClimate(ctx, positions, enabled)
|
|
},
|
|
},
|
|
}
|