-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a97fefc
Showing
30 changed files
with
5,147 additions
and
0 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 @@ | ||
*.secret |
Large diffs are not rendered by default.
Oops, something went wrong.
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,91 @@ | ||
# sense | ||
|
||
This package is an incomplete implementation of an entirely UNOFFICIAL | ||
and UNSUPPORTED API to access data for a [Sense](https://sense.com/) | ||
Energy Monitor account. This repository has no affiliation with Sense. | ||
|
||
Because this is unsupported, this package may stop working at any time. | ||
|
||
The API in this package is not stable and I may change it at any time. | ||
|
||
## Usage | ||
|
||
```go | ||
import ( | ||
"github.com/dnesting/sense" | ||
"github.com/dnesting/sense/realtime" | ||
) | ||
|
||
func main() { | ||
ctx := context.Background() | ||
client, err := sense.Connect(ctx, sense.PasswordCredentials{ | ||
Email: "you@example.com", | ||
Password: "secret", | ||
}) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
fmt.Println("Monitors configured under account", client.AccountID) | ||
for _, m := range client.Monitors { | ||
fmt.Println("-", m.ID) | ||
|
||
// Use the realtime Stream API to grab one data point. | ||
fn := func(_ context.Context, msg realtime.Message) error { | ||
if rt, ok := msg.(*realtime.RealtimeUpdate); ok { | ||
fmt.Println(" current power consumption", rt.W, "W") | ||
return realtime.Stop | ||
} | ||
return nil | ||
} | ||
if err := client.Stream(ctx, m.ID, fn); err != nil { | ||
log.Fatal(err) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
### MFA | ||
|
||
If your account requires multi-factor authentication, you can accommodate that like: | ||
|
||
```go | ||
mfaFunc := func() (string, error) { | ||
// obtain your MFA code somehow, we'll just use a fixed value to demonstrate | ||
return "12345", nil | ||
} | ||
client, err := sense.Connect(ctx, sense.PasswordCredentials{ | ||
Email: "you@example.com", | ||
Password: "secret", | ||
MfaFn: mfaFunc, | ||
}) | ||
``` | ||
|
||
Your `mfaFunc` will be called when needed. | ||
|
||
## Notes | ||
|
||
This implementation is incomplete, and what's there is incompletely tested. | ||
If you wish to contribute, here's how the project is laid out: | ||
|
||
``` | ||
|-- internal | ||
| |-- client contains an (incomplete) OpenAPI spec and | ||
| | auto-generated code that does the heavy lifting | ||
| |-- ratelimited implements some HTTP rate limiting | ||
| `-- senseutil helper functions, mocks for testing, etc. | ||
|-- realtime contains a complete-ish AsyncAPI spec but | ||
| hand-generated code implementing the real-time | ||
| WebSockets API | ||
|-- senseauth implements the Sense artisinal OAuth | ||
`-- sensecli helpers that CLI tools might find useful | ||
``` | ||
|
||
### Debugging | ||
|
||
If you need the gory internals to figure something out: | ||
|
||
```go | ||
httpClient := sense.SetDebug(log.Default(), nil) | ||
client, err := sense.Connect(ctx, credentials, sense.WithHTTPClient(httpClient)) | ||
``` |
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,61 @@ | ||
// This is a dummy command used to do a basic test that the package is working correctly. | ||
package main | ||
|
||
import ( | ||
"context" | ||
"flag" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"os" | ||
|
||
"github.com/dnesting/sense" | ||
"github.com/dnesting/sense/realtime" | ||
"github.com/dnesting/sense/sensecli" | ||
) | ||
|
||
var ( | ||
flagDebug = flag.Bool("debug", false, "enable debugging") | ||
// note: other flags set by sensecli.SetupStandardFlags() | ||
) | ||
|
||
func main() { | ||
configFile, flagCreds := sensecli.SetupStandardFlags() | ||
flag.Parse() | ||
|
||
ctx := context.Background() | ||
httpClient := http.DefaultClient | ||
if *flagDebug { | ||
// enable HTTP client logging | ||
httpClient = sense.SetDebug(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), httpClient) | ||
} | ||
|
||
clients, err := sensecli.CreateClients(ctx, | ||
configFile, flagCreds, | ||
sense.WithHttpClient(httpClient)) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
for _, client := range clients { | ||
fmt.Println("account:", client.AccountID) | ||
fmt.Println("monitors:") | ||
for _, monitor := range client.Monitors { | ||
fmt.Println("- monitor: ", monitor.ID, monitor.SerialNumber) | ||
var count = 3 | ||
fn := func(_ context.Context, msg realtime.Message) error { | ||
if rt, ok := msg.(*realtime.RealtimeUpdate); ok { | ||
fmt.Printf(" W: %.1f\n", rt.W) | ||
count-- | ||
if count == 0 { | ||
return realtime.Stop | ||
} | ||
} | ||
return nil | ||
} | ||
if err := client.Stream(ctx, monitor.ID, fn); err != nil { | ||
log.Println(err) | ||
} | ||
} | ||
} | ||
} |
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,55 @@ | ||
package sense | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/dnesting/sense/internal/senseutil" | ||
"github.com/dnesting/sense/realtime" | ||
"github.com/dnesting/sense/senseauth" | ||
) | ||
|
||
var debugLogger *log.Logger | ||
|
||
type loggingTransport struct { | ||
transport http.RoundTripper | ||
} | ||
|
||
func debug(args ...interface{}) { | ||
if debugLogger != nil { | ||
debugLogger.Output(2, strings.TrimRight(fmt.Sprint(args...), "\n")) | ||
} | ||
} | ||
|
||
func (s *loggingTransport) RoundTrip(r *http.Request) (*http.Response, error) { | ||
senseutil.DumpRequest(debugLogger, r) | ||
|
||
tr := s.transport | ||
if tr == nil { | ||
tr = http.DefaultTransport | ||
} | ||
resp, err := tr.RoundTrip(r) | ||
if err != nil { | ||
debug("error:", err) | ||
return nil, err | ||
} | ||
senseutil.DumpResponse(debugLogger, resp) | ||
return resp, err | ||
} | ||
|
||
// SetDebug enables debug logging using the given logger and returns an | ||
// [*http.Client] that wraps baseClient, logging requests and responses | ||
// to the same logger. Passing nil will disable debug logging. | ||
func SetDebug(l *log.Logger, baseClient *http.Client) *http.Client { | ||
if baseClient == nil { | ||
baseClient = http.DefaultClient | ||
} | ||
debugLogger = l | ||
senseauth.SetDebug(l) | ||
realtime.SetDebug(l) | ||
return &http.Client{ | ||
Transport: &loggingTransport{baseClient.Transport}, | ||
} | ||
} |
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,60 @@ | ||
package sense | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/dnesting/sense/internal/client" | ||
) | ||
|
||
type Device struct { | ||
ID string | ||
Name string | ||
Type string | ||
Make string | ||
} | ||
|
||
// I'm not entirely sure what the relationship between these fields is, so | ||
// just pick one that seems reasonable. | ||
func getType(d client.Device) string { | ||
tags := deref(d.Tags) | ||
|
||
for _, tag := range []string{ | ||
"UserDeviceType", | ||
"Type", | ||
"DefaultUserDeviceType", | ||
} { | ||
if s := stringOrEmpty(tags[tag]); s != "" { | ||
return s | ||
} | ||
} | ||
return "" | ||
} | ||
|
||
func stringOrEmpty(v interface{}) string { | ||
if s, ok := v.(string); ok { | ||
return s | ||
} | ||
return "" | ||
} | ||
|
||
// GetDevices returns a list of devices known to the given monitor. | ||
func (s *Client) GetDevices(ctx context.Context, monitorID int, includeMerged bool) (devs []Device, err error) { | ||
res, err1 := s.client.GetDevicesWithResponse( | ||
ctx, | ||
monitorID, | ||
&client.GetDevicesParams{ | ||
IncludeMerged: &includeMerged, | ||
}) | ||
if err := client.Ensure(err1, "GetDevices", res, 200); err != nil { | ||
return nil, err | ||
} | ||
for _, d := range deref(res.JSON200.Devices) { | ||
devs = append(devs, Device{ | ||
ID: deref(d.Id), | ||
Name: deref(d.Name), | ||
Type: getType(d), | ||
Make: stringOrEmpty(deref(d.Tags)["Make"]), | ||
}) | ||
} | ||
return devs, nil | ||
} |
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,43 @@ | ||
package sense | ||
|
||
/* | ||
// This is constructed like any other exported API but for now we'll just make | ||
// it accessible through WithEnvironment() until there's a clear need for it. | ||
// Environment is a Sense environment. This type might be useful if you worked | ||
// for Sense and wanted to use another environment. | ||
type environment struct { | ||
ID string | ||
Name string | ||
ApiURL string | ||
} | ||
type environments []environment | ||
// Get returns the environment with the given ID, or nil if not found. | ||
func (e environments) Get(id string) *environment { | ||
// a map is overkill since there are few items and we will do this search rarely. | ||
for _, env := range e { | ||
if env.ID == id { | ||
return &env | ||
} | ||
} | ||
return nil | ||
} | ||
// getEnvironments returns a list of environments from the Sense API. | ||
func (s *Client) getEnvironments(ctx context.Context) (envs environments, err error) { | ||
res, err1 := s.client.GetEnvironmentsWithResponse(ctx) | ||
if err := client.Ensure(err1, "getEnvironments", res, 200); err != nil { | ||
return nil, err | ||
} | ||
for _, e := range *res.JSON200 { | ||
envs = append(envs, environment{ | ||
ID: deref(e.Environment), | ||
Name: deref(e.DisplayName), | ||
ApiURL: deref(e.ApiUrl), | ||
}) | ||
} | ||
return envs, nil | ||
} | ||
*/ |
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,26 @@ | ||
module github.com/dnesting/sense | ||
|
||
go 1.20 | ||
|
||
require ( | ||
github.com/deepmap/oapi-codegen v1.12.4 | ||
golang.org/x/oauth2 v0.8.0 | ||
gopkg.in/yaml.v3 v3.0.1 | ||
) | ||
|
||
require ( | ||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect | ||
github.com/klauspost/compress v1.10.3 // indirect | ||
) | ||
|
||
require ( | ||
github.com/golang/protobuf v1.5.2 // indirect | ||
github.com/google/uuid v1.3.0 | ||
github.com/gorilla/websocket v1.5.0 // indirect | ||
github.com/ugorji/go/codec v1.2.11 // indirect | ||
golang.org/x/net v0.10.0 // indirect | ||
golang.org/x/time v0.3.0 | ||
google.golang.org/appengine v1.6.7 // indirect | ||
google.golang.org/protobuf v1.28.1 // indirect | ||
nhooyr.io/websocket v1.8.7 | ||
) |
Oops, something went wrong.