Skip to content

Commit

Permalink
Merge pull request #110 from GrimmiMeloni/energysite
Browse files Browse the repository at this point in the history
initial implementation for Powerwall control
  • Loading branch information
bogosj authored Nov 16, 2023
2 parents 94976be + 2df2cfa commit dd2e7a7
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 0 deletions.
116 changes: 116 additions & 0 deletions energysite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package tesla

import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
)

// this represents site_info endpoint
type EnergySite struct {
ID string `json:"id"`
SiteName string `json:"site_name"`
BackupReservePercent int64 `json:"backup_reserve_percent,omitempty"`
DefaultRealMode string `json:"default_real_mode,omitempty"`

productId int64
c *Client
}

type EnergySiteStatus struct {
ResourceType string `json:"resource_type"`
SiteName string `json:"site_name"`
GatewayId string `json:"gateway_id"`
EnergyLeft float64 `json:"energy_left"`
TotalPackEnergy uint64 `json:"total_pack_energy"`
PercentageCharged float64 `json:"percentage_charged"`
BatteryType string `json:"battery_type"`
BackupCapable bool `json:"backup_capable"`
BatteryPower int64 `json:"battery_power"`

c *Client
}

type SiteInfoResponse struct {
Response *EnergySite `json:"response"`
}

type SiteStatutsResponse struct {
Response *EnergySiteStatus `json:"response"`
}

// SiteCommandResponse is the response from the Tesla API after POSTing a command.
type SiteCommandResponse struct {
Response struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"response"`
}

// return fetches the energy site for the given product ID
func (c *Client) EnergySite(productID int64) (*EnergySite, error) {
siteInfoResponse := &SiteInfoResponse{}
if err := c.getJSON(c.baseURL+"/energy_sites/"+strconv.FormatInt(productID, 10)+"/site_info", siteInfoResponse); err != nil {
return nil, err
}
siteInfoResponse.Response.c = c
siteInfoResponse.Response.productId = productID
return siteInfoResponse.Response, nil
}

func (s *EnergySite) EnergySiteStatus() (*EnergySiteStatus, error) {
siteStatusResponse := &SiteStatutsResponse{}
if err := s.c.getJSON(s.statusPath(), siteStatusResponse); err != nil {
return nil, err
}
siteStatusResponse.Response.c = s.c
return siteStatusResponse.Response, nil
}

func (s *EnergySite) basePath() string {
return strings.Join([]string{s.c.baseURL, "energy_sites", strconv.FormatInt(s.productId, 10)}, "/")
}

func (s *EnergySite) statusPath() string {
return strings.Join([]string{s.basePath(), "site_status"}, "/")
}

func (s *EnergySite) SetBatteryReserve(percent uint64) error {
url := s.basePath() + "/backup"
payload := fmt.Sprintf(`{"backup_reserve_percent":%d}`, percent)
body, err := s.sendCommand(url, []byte(payload))
if err != nil {
return err
}

response := SiteCommandResponse{}
if err := json.Unmarshal(body, &response); err != nil {
return err
}

if response.Response.Code != 201 {
return fmt.Errorf("batteryReserve failed: %s", response.Response.Message)
}

return nil
}

// Sends a command to the vehicle
func (s *EnergySite) sendCommand(url string, reqBody []byte) ([]byte, error) {
body, err := s.c.post(url, reqBody)
if err != nil {
return nil, err
}
if len(body) > 0 {
response := &CommandResponse{}
if err := json.Unmarshal(body, response); err != nil {
return nil, err
}
if !response.Response.Result && response.Response.Reason != "" {
return nil, errors.New(response.Response.Reason)
}
}
return body, nil
}
73 changes: 73 additions & 0 deletions examples/show_energy_site_status/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package main

import (
"context"
"flag"
"fmt"
"os"

"github.com/bogosj/tesla"
)

var tokenPath = flag.String("token", "", "path to token file")
var reservePercentage = flag.Int64("backupPercentage", -1, "backup percentage to set")

// example that demos fetching of site information and optionally setting the battery reserve percentage for the site
func main() {
flag.Parse()

if *tokenPath == "" {
fmt.Println("--token must be specified")
os.Exit(1)
}

if err := run(context.Background(), *tokenPath, *reservePercentage); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

func run(ctx context.Context, tokenPath string, reservePercentage int64) error {
c, err := tesla.NewClient(ctx, tesla.WithTokenFile(tokenPath))
if err != nil {
return err
}

prods, err := c.Products()
if err != nil {
return err
}

for i, p := range prods {
if i > 0 {
fmt.Println("----")
}
fmt.Printf("ID: %s\n", p.ID)
fmt.Printf("ResourceType %s\n", p.ResourceType)
if p.EnergySiteId != 0 {
fmt.Printf("EnergySiteId: %d\n", p.EnergySiteId)

es, err := c.EnergySite(p.EnergySiteId)
if err != nil {
fmt.Printf("error fetching site info: %+v\n", err)
os.Exit(1)
}
fmt.Printf("EnergySite: %+v\n", *es)

esi, err := es.EnergySiteStatus()
if err != nil {
fmt.Printf("error fetching site status: %+v\n", err)
os.Exit(1)
}
fmt.Printf("EnergySiteInfo: %+v\n", *esi)

if reservePercentage != -1 {
if err := es.SetBatteryReserve(uint64(reservePercentage)); err != nil {
fmt.Printf("error setting battery reserve: %+v\n", err)
os.Exit(1)
}
}
}
}
return nil
}
36 changes: 36 additions & 0 deletions products.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package tesla

type Product struct {
EnergySiteId int64 `json:"energy_site_id,omitempty"`
ResourceType string `json:"resource_type"`
ID string `json:"id"`
AssetSiteId string `json:"asset_site_id,omitempty"`
GatewayId string `json:"gateway_id,omitempty"`
WarpSiteNumber string `json:"warp_site_number,omitempty"`
EnergyLeft float64 `json:"energy_left,omitempty"`
TotalPackEnergy uint64 `json:"total_pack_energy,omitempty"`
PercentageCharged float64 `json:"percentage_charged,omitempty"`
BatteryType string `json:"battery_type,omitempty"`
BackupCapable bool `json:"backup_capable,omitempty"`
BatteryPower int64 `json:"battery_power,omitempty"`

c *Client
}

// ProductResponse contains the product details from the Tesla API.
type ProductsResponse struct {
Response []*Product `json:"response"`
Count int `json:"count"`
}

// Products fetches the products associated to a Tesla account via the API.
func (c *Client) Products() ([]*Product, error) {
productsResponse := &ProductsResponse{}
if err := c.getJSON(c.baseURL+"/products", productsResponse); err != nil {
return nil, err
}
for _, v := range productsResponse.Response {
v.c = c
}
return productsResponse.Response, nil
}

0 comments on commit dd2e7a7

Please sign in to comment.