-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support multiple paths to a single Tesla Gateway
This adds support to have multiple IP/Ports to calling a single Tesla Gateway, such as if the Gateway is connected to your LAN while the machine running the proxy is also connected to its Wifi network. Will return the first successful response from the gateay, defaulting to the first error otherwise.
- Loading branch information
Showing
8 changed files
with
332 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
tesla-powerwall-proxy |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,3 +53,11 @@ RestartSec=5 | |
[Install] | ||
WantedBy=multi-user.target | ||
``` | ||
|
||
## Multiple Powerwall hostnames | ||
|
||
If your Powerwall is connected to your home network as well as your machine is connected to the Powerwall's wifi network, you can have the proxy attempt both (or more) hostnames to try and retrieve the data. This is useful in situations where the Powerwall may disconnect from your home network but its Wifi network is still working. To do this, add the IP address of the Powerwall as another `-h` in your configuration. | ||
|
||
``` | ||
ExecStart=/home/pi/go/bin/tesla-powerwall-proxy -h 192.168.91.1 -h 192.168.0.7 -u [email protected] -p PASSWORD -l=:8043 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,17 @@ | ||
module github.com/mgb/tesla-powerwall-local | ||
|
||
go 1.15 | ||
go 1.17 | ||
|
||
require ( | ||
github.com/avast/retry-go v3.0.0+incompatible | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/google/go-cmp v0.5.9 | ||
github.com/spf13/pflag v1.0.5 | ||
github.com/stretchr/testify v1.7.0 | ||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 | ||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect | ||
github.com/stretchr/testify v1.8.1 | ||
golang.org/x/net v0.5.0 | ||
) | ||
|
||
require ( | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/pmezard/go-difflib v1.0.0 // indirect | ||
gopkg.in/yaml.v3 v3.0.1 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package httpsproxy | ||
|
||
import ( | ||
"bytes" | ||
"net/http" | ||
"sync" | ||
) | ||
|
||
type Handlers []http.Handler | ||
|
||
func (h Handlers) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
var wg sync.WaitGroup | ||
ch := make(chan bufferedResponseWriter) | ||
for _, handler := range h { | ||
handler := handler | ||
wg.Add(1) | ||
go func() { | ||
defer wg.Done() | ||
|
||
var b bufferedResponseWriter | ||
handler.ServeHTTP(&b, r) | ||
ch <- b | ||
}() | ||
} | ||
go func() { | ||
wg.Wait() | ||
close(ch) | ||
}() | ||
|
||
var badResponse *bufferedResponseWriter | ||
for b := range ch { | ||
if b.statusCode != 0 && b.statusCode != http.StatusOK { | ||
if badResponse != nil { | ||
// We only care about the first real failure, ignore the rest | ||
continue | ||
} | ||
if b.statusCode == http.StatusRequestTimeout { | ||
continue | ||
} | ||
|
||
// shadow b so it doesn't get overwritten | ||
b := b | ||
badResponse = &b | ||
continue | ||
} | ||
|
||
// First successful call gets returned, other calls will now get canceled. | ||
w.Write(b.response.Bytes()) | ||
return | ||
} | ||
|
||
if badResponse == nil { | ||
w.WriteHeader(http.StatusExpectationFailed) | ||
return | ||
} | ||
|
||
w.WriteHeader(badResponse.statusCode) | ||
w.Write(badResponse.response.Bytes()) | ||
} | ||
|
||
// bufferedResponseWriter is a helper struct to buffer the response from the handler | ||
type bufferedResponseWriter struct { | ||
response bytes.Buffer | ||
statusCode int | ||
} | ||
|
||
func (b *bufferedResponseWriter) Header() http.Header { | ||
return make(http.Header) | ||
} | ||
|
||
func (b *bufferedResponseWriter) WriteHeader(statusCode int) { | ||
b.statusCode = statusCode | ||
} | ||
|
||
func (b *bufferedResponseWriter) Write(p []byte) (int, error) { | ||
return b.response.Write(p) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package httpsproxy | ||
|
||
import ( | ||
"net/http" | ||
"testing" | ||
"time" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
) | ||
|
||
func TestHandlers_ServeHTTP(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
h Handlers | ||
|
||
wantStatus int | ||
wantBody string | ||
}{ | ||
{ | ||
name: "empty", | ||
h: Handlers{}, | ||
|
||
wantStatus: http.StatusExpectationFailed, | ||
}, | ||
{ | ||
name: "one handler, success", | ||
h: Handlers{ | ||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.Write([]byte("hello")) | ||
}), | ||
}, | ||
|
||
wantBody: "hello", | ||
}, | ||
{ | ||
name: "one handler, failure", | ||
h: Handlers{ | ||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusTeapot) | ||
}), | ||
}, | ||
|
||
wantStatus: http.StatusTeapot, | ||
}, | ||
{ | ||
name: "two handlers, get fastest success", | ||
h: Handlers{ | ||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.Write([]byte("hello")) | ||
}), | ||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
time.Sleep(100 * time.Millisecond) | ||
w.Write([]byte("world")) | ||
}), | ||
}, | ||
|
||
wantBody: "hello", | ||
}, | ||
{ | ||
name: "two handlers, first failure", | ||
h: Handlers{ | ||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusTeapot) | ||
}), | ||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
time.Sleep(100 * time.Millisecond) | ||
w.Write([]byte("world")) | ||
}), | ||
}, | ||
|
||
wantBody: "world", | ||
}, | ||
{ | ||
name: "two handlers, both failure, get fastest failure", | ||
h: Handlers{ | ||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusTeapot) | ||
}), | ||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
time.Sleep(100 * time.Millisecond) | ||
w.WriteHeader(http.StatusBadRequest) | ||
}), | ||
}, | ||
|
||
wantStatus: http.StatusTeapot, | ||
}, | ||
{ | ||
name: "two handlers, ignore failed with timeout, return slow success", | ||
h: Handlers{ | ||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusRequestTimeout) | ||
}), | ||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
time.Sleep(100 * time.Millisecond) | ||
w.Write([]byte("world")) | ||
}), | ||
}, | ||
|
||
wantBody: "world", | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
var b bufferedResponseWriter | ||
var r http.Request | ||
|
||
tt.h.ServeHTTP(&b, &r) | ||
|
||
if b.statusCode != tt.wantStatus { | ||
t.Errorf("statusCode = %v, want %v", b.statusCode, tt.wantStatus) | ||
} | ||
if diff := cmp.Diff(b.response.String(), tt.wantBody); diff != "" { | ||
t.Errorf("response body mismatch (-want +got):\n%s", diff) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestBufferedResponseWriter(t *testing.T) { | ||
var b bufferedResponseWriter | ||
|
||
header := b.Header() | ||
if header == nil { | ||
t.Errorf("header is nil") | ||
} | ||
|
||
b.WriteHeader(http.StatusTeapot) | ||
if b.statusCode != http.StatusTeapot { | ||
t.Errorf("statusCode = %v, want %v", b.statusCode, http.StatusTeapot) | ||
} | ||
|
||
b.Write([]byte("hello")) | ||
if b.response.String() != "hello" { | ||
t.Errorf("response = %v, want %v", b.response.String(), "hello") | ||
} | ||
} |
Oops, something went wrong.