sense

package module
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 4, 2023 License: AGPL-3.0 Imports: 16 Imported by: 2

README

sense

This package is an incomplete implementation of an entirely UNOFFICIAL and UNSUPPORTED API to access data for a Sense 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

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:

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:

httpClient := sense.SetDebug(log.Default(), nil)
client, err := sense.Connect(ctx, credentials, sense.WithHTTPClient(httpClient))

Documentation

Overview

Package sense implements a high-level client for the UNSUPPORTED Sense Energy API.

WARNING: Sense does not provide a supported API. This package may stop working without notice.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"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)
		}
	}
}
Output:

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrAuthenticationNeeded = client.ErrAuthenticationNeeded

ErrAuthenticationNeeded is wrapped by errors returned from many functions in this package whenever authentication is needed and the client is unauthenticated or its credentials are no longer valid.

Test for this using errors.Is(err, sense.ErrAuthenticationNeeded).

Functions

func SetDebug

func SetDebug(l *log.Logger, baseClient *http.Client) *http.Client

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.

Types

type Client

type Client struct {
	// Account fields are set after successful authentication.
	UserID    int
	AccountID int
	Monitors  []Monitor
	// contains filtered or unexported fields
}

Client is the primary high-level object used to interact with the Sense API. It represents an "account", which can have some number of Monitors. Instantiate a Client using New or Connect.

func Connect

func Connect(ctx context.Context, creds Credentials, opts ...Option) (*Client, error)

Connect instantiates a new Sense Client, configured with the provided options.

If credentials are provided, the client will be authenticated using those credentials. Otherwise it will be unauthenticated and will have limited abilities. This function is equivalent to calling New (opts...), possibly followed by Client.Authenticate (ctx).

Example
package main

import (
	"context"
	"log"

	"github.com/dnesting/sense"
)

func doSomethingWith(c *sense.Client) {}

func main() {
	client, err := sense.Connect(
		context.Background(),
		sense.PasswordCredentials{
			Email:    "you@example.com",
			Password: "secret",
		})
	if err != nil {
		log.Fatal(err)
	}
	doSomethingWith(client)
}
Output:

Example (WithMFA)
package main

import (
	"context"
	"log"

	"github.com/dnesting/sense"
)

func doSomethingWith(c *sense.Client) {}

func main() {
	mfaFunc := func(_ context.Context) (string, error) {
		// obtain your MFA code somehow, we'll just use a fixed value to demonstrate
		return "12345", nil
	}
	client, err := sense.Connect(
		context.Background(),
		sense.PasswordCredentials{
			Email:    "you@example.com",
			Password: "secret",
			MfaFn:    mfaFunc, // <--
		})
	if err != nil {
		log.Fatal(err)
	}
	doSomethingWith(client)
}
Output:

func New

func New(opts ...Option) *Client

New creates a new unauthenticated Sense client, configured according to the provided options.

Most callers will prefer to use Connect instead.

func (*Client) Authenticate

func (s *Client) Authenticate(ctx context.Context, creds Credentials) error

Authenticate authenticates the client using the provided credentials. If the client was previously authenticated (including with Connect), those credentials will be replaced. If creds is nil, the client will be unauthenticated.

See the senseauth package if you need more direct control over how the user is authenticated. This package can generate an HTTP client that you can use here with WithHttpClient.

func (*Client) GetDevices

func (s *Client) GetDevices(ctx context.Context, monitorID int, includeMerged bool) (devs []Device, err error)

GetDevices returns a list of devices known to the given monitor.

func (*Client) Stream

func (s *Client) Stream(ctx context.Context, monitor int, callback realtime.Callback) error

Stream begins streaming real-time data via callback. If the callback returns realtime.Stop, the stream will be closed and this function will return without error. Otherwise, if any other error occurs, it will be returned.

Example
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	"github.com/dnesting/sense"
	"github.com/dnesting/sense/internal/senseutil"
	"github.com/dnesting/sense/realtime"
)

func mockForExample() []sense.Option {
	type mockMsg = senseutil.RTMsg

	ch := make(chan mockMsg)
	go func() {
		ch <- mockMsg{M: &realtime.Hello{}, E: nil}
		ch <- mockMsg{M: &realtime.RealtimeUpdate{W: 590.4}, E: nil}
		ch <- mockMsg{M: &realtime.RealtimeUpdate{W: 591.4}, E: nil}
		ch <- mockMsg{M: &realtime.RealtimeUpdate{W: 592.4}, E: nil}
		ch <- mockMsg{M: &realtime.RealtimeUpdate{W: 593.4}, E: nil}
		close(ch)
	}()
	return []sense.Option{
		sense.WithInternalClient(nil, &senseutil.MockRTClient{Ch: ch}),
		sense.WithHttpClient(&http.Client{
			Transport: &senseutil.MockTransport{
				RT: func(req *http.Request) (*http.Response, error) {
					fmt.Fprintln(os.Stderr, req.Method, req.URL)
					return &http.Response{
						StatusCode: http.StatusOK,
						Header:     http.Header{"Content-Type": []string{"application/json"}},
						Body:       io.NopCloser(strings.NewReader(`{"access_token":"fake-token"}`)),
					}, nil
				},
			},
		})}
}

func main() {
	// instantiate a Sense client (error checking omitted)
	client, _ := sense.Connect(
		context.Background(),
		sense.PasswordCredentials{
			Email:    "test@example.com",
			Password: "pass",
		},
		mockForExample()...)

	// start a stream and collect 3 data points
	stopAfter := 3
	client.Stream(
		context.Background(),
		123, // monitor ID to stream events from

		func(_ context.Context, msg realtime.Message) error {
			switch msg := msg.(type) {

			case *realtime.Hello:
				fmt.Println("We're online!")

			case *realtime.RealtimeUpdate:
				fmt.Printf("Power consumption is now: %.1f W\n", msg.W)
				stopAfter--
				if stopAfter == 0 {
					return realtime.Stop
				}

			}
			return nil
		})

}
Output:

We're online!
Power consumption is now: 590.4 W
Power consumption is now: 591.4 W
Power consumption is now: 592.4 W

type Credentials

type Credentials interface {
	// contains filtered or unexported methods
}

Credentials holds the credentials used to authenticate to the Sense API. The only implementation of this is PasswordCredentials.

type Device

type Device struct {
	ID       string
	Name     string
	Type     string
	Make     string
	Model    string
	Location string
}

type Monitor

type Monitor struct {
	ID           int
	SerialNumber string
}

Monitor is a Sense monitor, which is a physical device that measures power usage. One account can have multiple Monitors.

type Option

type Option func(*newOptions)

Option is a function that can be passed to New or Connect to configure the resulting Client.

func WithApiUrl

func WithApiUrl(apiUrl, realtimeApiUrl string) Option

WithApiUrl sets the base URLs for the Sense API. If this option is not provided, the standard production API URLs will be used (https://api.sense.com/apiservice/api/v1/).

func WithDeviceID

func WithDeviceID(id string) Option

WithDeviceID sets the X-Sense-Device-Id header on requests to the Sense API. This appears to be intended to be a unique identifier for the client installation. If this option is not provided, a random value will be generated and used for all clients for the life of this process. Set this value to "" explicitly to disable this header.

func WithHttpClient

func WithHttpClient(httpClient *http.Client) Option

WithHttpClient sets the HTTP client used to make requests to the Sense API. If this option is not provided, http.DefaultClient will be used.

This option can be useful if you need special handling for proxies or TLS certificates, or if you have your own approach to authenticating with Sense.

func WithInternalClient deprecated

func WithInternalClient(cl internalClient, acl internalRealtimeClient) Option

WithInternalClient is for internal use. All other options will be ignored.

Deprecated: For internal use.

func WithRateLimit

func WithRateLimit(limit rate.Limit) Option

WithRateLimit applies a rate limit to requests made to the Sense API. Without this option, a default rate limit of 10 requests/second will be applied. You can disable rate limiting by providing a limit of 0.

type PasswordCredentials

type PasswordCredentials struct {
	Email    string
	Password string
	MfaFn    func(ctx context.Context) (string, error)
}

PasswordCredentials holds the credentials used to authenticate to the Sense API.

MfaFn is an optional function that will be called (if it is provided) if the Sense API requests an MFA code for the account. It should return the MFA code. If it returns an error, authentication will fail with that error.

Directories

Path Synopsis
cmd
dummy
This is a dummy command used to do a basic test that the package is working correctly.
This is a dummy command used to do a basic test that the package is working correctly.
internal
client
Package client provides primitives to interact with the openapi HTTP API.
Package client provides primitives to interact with the openapi HTTP API.
ratelimited
Package ratelimited provides a rate-limited HTTP client.
Package ratelimited provides a rate-limited HTTP client.
Package realtime implements the unofficial and unsupported Sense real-time API.
Package realtime implements the unofficial and unsupported Sense real-time API.
Package senseauth implements the api.sense.com OAuth flow.
Package senseauth implements the api.sense.com OAuth flow.
Package sensecli just contains some helpers used to set up binaries that need Sense credentials.
Package sensecli just contains some helpers used to set up binaries that need Sense credentials.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL