Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add context param to all functions to handle deadlines/cancellation #30

Merged
merged 2 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 28 additions & 25 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package api

import (
"context"
"crypto/sha1"
"encoding/base64"
"encoding/json"
Expand Down Expand Up @@ -155,7 +156,7 @@ func (c *Client) GetMyResources() (*GetMyResourcesResponse, error) {
// user has.
//
// Currently, it assumes that a user has only one resource.
func (c *Client) OpenResourceSession(resourcePassword string) error {
func (c *Client) OpenResourceSession(ctx context.Context, resourcePassword string) error {
// We can't use the connection that was used to connect to Cloud.
conn, err := connect(c.dialer)
if err != nil {
Expand All @@ -179,7 +180,7 @@ func (c *Client) OpenResourceSession(resourcePassword string) error {

go c.reader()

_, err = c.ReadMessage(actionName, token)
_, err = c.ReadMessage(ctx, actionName, token)
if err != nil {
return fmt.Errorf("failed to read %s: %v", actionName, err)
}
Expand All @@ -195,7 +196,7 @@ func (c *Client) OpenResourceSession(resourcePassword string) error {
// Configuration returned by this method is set in the desktop configurator app.
//
// This action is named "Touches" in F&Home's terminology.
func (c *Client) GetSystemConfig() (*TouchesResponse, error) {
func (c *Client) GetSystemConfig(ctx context.Context) (*TouchesResponse, error) {
actionName := ActionGetSystemConfig
token := generateRequestToken()

Expand All @@ -209,7 +210,7 @@ func (c *Client) GetSystemConfig() (*TouchesResponse, error) {
return nil, fmt.Errorf("failed to write %s: %v", actionName, err)
}

msg, err := c.ReadMessage(actionName, token)
msg, err := c.ReadMessage(ctx, actionName, token)
if err != nil {
return nil, fmt.Errorf("failed to read message: %v", err)
}
Expand All @@ -226,7 +227,7 @@ func (c *Client) GetSystemConfig() (*TouchesResponse, error) {
// GetUserConfig returns configuration of cells and panels.
//
// Configuration returned by this method is set in the web or mobile app.
func (c *Client) GetUserConfig() (*UserConfig, error) {
func (c *Client) GetUserConfig(ctx context.Context) (*UserConfig, error) {
token := generateRequestToken()

actionName := ActionGetUserConfig
Expand All @@ -240,7 +241,7 @@ func (c *Client) GetUserConfig() (*UserConfig, error) {
return nil, fmt.Errorf("failed to write %s to conn: %v", actionName, err)
}

msg, err := c.ReadMessage(actionName, token)
msg, err := c.ReadMessage(ctx, actionName, token)
if err != nil {
return nil, fmt.Errorf("failed to read messagee: %v", err)
}
Expand All @@ -267,28 +268,30 @@ func (c *Client) GetUserConfig() (*UserConfig, error) {
// with matching actionName is returned.
//
// If its status is not "ok", it returns an error.
func (c *Client) ReadMessage(actionName string, requestToken string) (*Message, error) {
func (c *Client) ReadMessage(ctx context.Context, actionName string, requestToken string) (*Message, error) {
for {
ch := c.read()
msg := <-ch

if msg.Status != nil {
if *msg.Status != "ok" {
return nil, fmt.Errorf("message status is %s", *msg.Status)
select {
case <-ctx.Done():
return nil, fmt.Errorf("context is done")
case msg := <-c.read():
if msg.Status != nil {
if *msg.Status != "ok" {
return nil, fmt.Errorf("message status is %s", *msg.Status)
}
}
}

tokenOk := true
if requestToken != "" {
if msg.RequestToken == nil {
tokenOk = false
} else if requestToken != *msg.RequestToken {
tokenOk = false
tokenOk := true
if requestToken != "" {
if msg.RequestToken == nil {
tokenOk = false
} else if requestToken != *msg.RequestToken {
tokenOk = false
}
}
}

if actionName == msg.ActionName && tokenOk {
return &msg, nil
if actionName == msg.ActionName && tokenOk {
return &msg, nil
}
}
}
}
Expand All @@ -311,7 +314,7 @@ func (c *Client) ReadAnyMessage() (*Message, error) {
// SendEvent sends an event containing value to the cell.
//
// Events are named "Xevents" in F&Home's terminology.
func (c *Client) SendEvent(cellID int, value string) error {
func (c *Client) SendEvent(ctx context.Context, cellID int, value string) error {
actionName := ActionEvent
token := generateRequestToken()

Expand All @@ -329,7 +332,7 @@ func (c *Client) SendEvent(cellID int, value string) error {
return fmt.Errorf("failed to write %s to conn: %v", actionName, err)
}

_, err = c.ReadMessage(actionName, token)
_, err = c.ReadMessage(ctx, actionName, token)
return err
}

Expand Down
32 changes: 16 additions & 16 deletions cmd/fhome/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,18 @@ var configCommand = cli.Command{
return fmt.Errorf("cannot use both --system and --user")
}

client, err := highlevel.Connect(config, nil)
client, err := highlevel.Connect(ctx, config, nil)
if err != nil {
return fmt.Errorf("failed to create api client: %v", err)
}

sysConfig, err := client.GetSystemConfig()
sysConfig, err := client.GetSystemConfig(ctx)
if err != nil {
return fmt.Errorf("failed to get sysConfig: %v", err)
}
log.Println("got system config")

userConfig, err := client.GetUserConfig()
userConfig, err := client.GetUserConfig(ctx)
if err != nil {
return fmt.Errorf("failed to get user config: %v", err)
}
Expand Down Expand Up @@ -138,7 +138,7 @@ var eventCommand = cli.Command{
Name: "watch",
Usage: "Print all incoming messages",
Action: func(ctx context.Context, c *cli.Command) error {
client, err := highlevel.Connect(config, nil)
client, err := highlevel.Connect(ctx, config, nil)
if err != nil {
return fmt.Errorf("failed to create api client: %v", err)
}
Expand Down Expand Up @@ -180,7 +180,7 @@ var objectCommand = cli.Command{
return fmt.Errorf("object not specified")
}

client, err := highlevel.Connect(config, nil)
client, err := highlevel.Connect(ctx, config, nil)
if err != nil {
return fmt.Errorf("failed to create api client: %v", err)
}
Expand All @@ -190,12 +190,12 @@ var objectCommand = cli.Command{
// string
log.Printf("looking for object with name %q", object)

userConfig, err := client.GetUserConfig()
userConfig, err := client.GetUserConfig(ctx)
if err != nil {
return fmt.Errorf("failed to get user config: %v", err)
}

sysConfig, err := client.GetSystemConfig()
sysConfig, err := client.GetSystemConfig(ctx)
if err != nil {
return fmt.Errorf("failed to get system config: %v", err)
}
Expand All @@ -212,7 +212,7 @@ var objectCommand = cli.Command{

log.Printf("selected object %q with id %d with %d%% confidence\n", bestObject.Name, bestObject.ID, int(bestScore*100))

err = client.SendEvent(bestObject.ID, api.ValueToggle)
err = client.SendEvent(ctx, bestObject.ID, api.ValueToggle)
if err != nil {
return fmt.Errorf("failed to send event to object %q with id %d", bestObject.Name, bestObject.ID)
}
Expand All @@ -222,7 +222,7 @@ var objectCommand = cli.Command{
} else {
// int

err = client.SendEvent(objectID, api.ValueToggle)
err = client.SendEvent(ctx, objectID, api.ValueToggle)
if err != nil {
return fmt.Errorf("failed to send event to object with id %d: %v", objectID, err)
}
Expand All @@ -232,13 +232,13 @@ var objectCommand = cli.Command{
}
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
client, err := highlevel.Connect(config, nil)
client, err := highlevel.Connect(ctx, config, nil)
if err != nil {
panic(err)
}

// TODO: Save to cache because it's slow
userConfig, err := client.GetUserConfig()
userConfig, err := client.GetUserConfig(ctx)
if err != nil {
panic(err)
}
Expand All @@ -264,7 +264,7 @@ var objectCommand = cli.Command{
return fmt.Errorf("invalid value: %v", err)
}

client, err := highlevel.Connect(config, nil)
client, err := highlevel.Connect(ctx, config, nil)
if err != nil {
return fmt.Errorf("failed to create api client: %v", err)
}
Expand All @@ -274,12 +274,12 @@ var objectCommand = cli.Command{
// string
slog.Info("looking for object", slog.String("name", object))

userConfig, err := client.GetUserConfig()
userConfig, err := client.GetUserConfig(ctx)
if err != nil {
return fmt.Errorf("failed to get user config: %v", err)
}

sysConfig, err := client.GetSystemConfig()
sysConfig, err := client.GetSystemConfig(ctx)
if err != nil {
return fmt.Errorf("failed to get system config: %v", err)
}
Expand All @@ -301,7 +301,7 @@ var objectCommand = cli.Command{
)

value := api.MapLighting(value)
err = client.SendEvent(bestObject.ID, value)
err = client.SendEvent(ctx, bestObject.ID, value)
if err != nil {
return fmt.Errorf("failed to send event to object %q with id %d", bestObject.Name, bestObject.ID)
} else {
Expand All @@ -313,7 +313,7 @@ var objectCommand = cli.Command{
return nil
}
} else {
err = client.SendEvent(objectID, api.MapLighting(value))
err = client.SendEvent(ctx, objectID, api.MapLighting(value))
if err != nil {
return fmt.Errorf("sent event to object with id %d: %v", objectID, err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/fhome/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,6 @@ func loadConfig() {
config = &highlevel.Config{
Email: k.MustString("FHOME_EMAIL"),
Password: k.MustString("FHOME_CLOUD_PASSWORD"),
ResourcePassword: k.String("FHOME_RESOURCE_PASSWORD"),
ResourcePassword: k.MustString("FHOME_RESOURCE_PASSWORD"),
}
}
22 changes: 11 additions & 11 deletions cmd/fhomed/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import (
"github.com/bartekpacia/fhome/highlevel"
)

func daemon(name, pin string) error {
client, err := highlevel.Connect(config, nil)
func daemon(ctx context.Context, name, pin string) error {
client, err := highlevel.Connect(ctx, config, nil)
if err != nil {
return fmt.Errorf("failed to create api client: %v", err)
}

userConfig, err := client.GetUserConfig()
userConfig, err := client.GetUserConfig(ctx)
if err != nil {
slog.Error("failed to get user config", slog.Any("error", err))
return err
Expand All @@ -28,7 +28,7 @@ func daemon(name, pin string) error {
slog.Int("cells", len(userConfig.Cells)),
)

systemConfig, err := client.GetSystemConfig()
systemConfig, err := client.GetSystemConfig(ctx)
if err != nil {
slog.Error("failed to get system config", slog.Any("error", err))
return err
Expand All @@ -44,8 +44,8 @@ func daemon(name, pin string) error {
return err
}

go serviceListener(client)
go websiteListener(config)
go serviceListener(ctx, client)
go websiteListener(ctx, config)

// Here we listen to HomeKit events and convert them to API calls to F&Home
// to keep the state in sync.
Expand All @@ -60,7 +60,7 @@ func daemon(name, pin string) error {
slog.String("callback", "OnLightbulbUpdate"),
}

err := client.SendEvent(ID, value)
err := client.SendEvent(ctx, ID, value)
if err != nil {
attrs = append(attrs, slog.Any("error", err))
slog.LogAttrs(context.TODO(), slog.LevelError, "failed to send event", attrs...)
Expand All @@ -77,7 +77,7 @@ func daemon(name, pin string) error {
slog.String("callback", "OnLEDUpdate"),
}

err := client.SendEvent(ID, value)
err := client.SendEvent(ctx, ID, value)
if err != nil {
attrs = append(attrs, slog.Any("error", err))
slog.LogAttrs(context.TODO(), slog.LevelError, "failed to send event", attrs...)
Expand All @@ -94,7 +94,7 @@ func daemon(name, pin string) error {
slog.String("callback", "OnGarageDoorUpdate"),
}

err := client.SendEvent(ID, value)
err := client.SendEvent(ctx, ID, value)
if err != nil {
attrs = append(attrs, slog.Any("error", err))
slog.LogAttrs(context.TODO(), slog.LevelError, "failed to send event", attrs...)
Expand All @@ -111,7 +111,7 @@ func daemon(name, pin string) error {
slog.String("callback", "OnGarageDoorUpdate"),
}

err = client.SendEvent(ID, value)
err = client.SendEvent(ctx, ID, value)
if err != nil {
attrs = append(attrs, slog.Any("error", err))
slog.LogAttrs(context.TODO(), slog.LevelError, "failed to send event", attrs...)
Expand All @@ -131,7 +131,7 @@ func daemon(name, pin string) error {
// In this loop, we listen to events from F&Home and send updates to HomeKit
// to keep the state in sync.
for {
msg, err := client.ReadMessage(api.ActionStatusTouchesChanged, "")
msg, err := client.ReadMessage(ctx, api.ActionStatusTouchesChanged, "")
if err != nil {
slog.Error("failed to read message", slog.Any("error", err))
return err
Expand Down
7 changes: 4 additions & 3 deletions cmd/fhomed/hack.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"embed"
"fmt"
"html/template"
Expand All @@ -22,10 +23,10 @@ var tmpl = template.Must(template.ParseFS(templates, "templates/*"))
const port = 9001

// Hacky workaround for myself to open my gate from my phone.
func serviceListener(client *api.Client) {
func serviceListener(ctx context.Context, client *api.Client) {
http.HandleFunc("GET /gate", func(w http.ResponseWriter, r *http.Request) {
var result string
err := client.SendEvent(260, api.ValueToggle)
err := client.SendEvent(ctx, 260, api.ValueToggle)
if err != nil {
result = fmt.Sprintf("Failed to send event: %v", err)
w.WriteHeader(http.StatusInternalServerError)
Expand All @@ -44,7 +45,7 @@ func serviceListener(client *api.Client) {
}

// Stupid webserver to display some state about my smart devices.
func websiteListener(homeConfig *api.Config) {
func websiteListener(ctx context.Context, homeConfig *api.Config) {
http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
slog.Info("got request", slog.String("method", r.Method), slog.String("path", r.URL.Path))

Expand Down
3 changes: 2 additions & 1 deletion cmd/fhomed/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func main() {
name := cmd.String("name")
pin := cmd.String("pin")

return daemon(name, pin)
return daemon(ctx, name, pin)
},
CommandNotFound: func(ctx context.Context, cmd *cli.Command, command string) {
log.Printf("invalid command '%s'. See 'fhomed --help'\n", command)
Expand All @@ -137,6 +137,7 @@ func main() {

func loadConfig() {
k := koanf.New(".")

p := "/etc/fhomed/config.toml"
if err := k.Load(file.Provider(p), toml.Parser()); err != nil {
slog.Debug("failed to load config file", slog.Any("error", err))
Expand Down
Loading