From e086bfca874392742961ed1df18f2661faa61785 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Sat, 23 Nov 2024 23:52:06 +0100 Subject: [PATCH 1/2] add `context` param to all functions to handle deadlines/cancellation --- api/client.go | 41 ++++++++++++++++++++++------------------- cmd/fhome/main.go | 2 +- cmd/fhomed/daemon.go | 2 +- cmd/fhomed/main.go | 3 ++- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/api/client.go b/api/client.go index d8e79aa..dcbd8a6 100644 --- a/api/client.go +++ b/api/client.go @@ -3,6 +3,7 @@ package api import ( + "context" "crypto/sha1" "encoding/base64" "encoding/json" @@ -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 @@ -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) } @@ -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 + } } } } diff --git a/cmd/fhome/main.go b/cmd/fhome/main.go index 34a41e1..6442bc1 100644 --- a/cmd/fhome/main.go +++ b/cmd/fhome/main.go @@ -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"), } } diff --git a/cmd/fhomed/daemon.go b/cmd/fhomed/daemon.go index 54f56f4..d5594a8 100644 --- a/cmd/fhomed/daemon.go +++ b/cmd/fhomed/daemon.go @@ -12,7 +12,7 @@ import ( "github.com/bartekpacia/fhome/highlevel" ) -func daemon(name, pin string) error { +func daemon(ctx context.Context, name, pin string) error { client, err := highlevel.Connect(config, nil) if err != nil { return fmt.Errorf("failed to create api client: %v", err) diff --git a/cmd/fhomed/main.go b/cmd/fhomed/main.go index b29067c..1372bf4 100644 --- a/cmd/fhomed/main.go +++ b/cmd/fhomed/main.go @@ -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) @@ -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)) From 960766ba8f284a63af71409516ce4ae185f61e1a Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Sun, 24 Nov 2024 00:00:19 +0100 Subject: [PATCH 2/2] introduce `context` as first argument to all API methods --- api/client.go | 12 ++++++------ cmd/fhome/commands.go | 32 ++++++++++++++++---------------- cmd/fhomed/daemon.go | 20 ++++++++++---------- cmd/fhomed/hack.go | 7 ++++--- highlevel/connect.go | 5 +++-- 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/api/client.go b/api/client.go index dcbd8a6..4abceed 100644 --- a/api/client.go +++ b/api/client.go @@ -156,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 { @@ -180,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) } @@ -196,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() @@ -210,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) } @@ -314,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() @@ -332,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 } diff --git a/cmd/fhome/commands.go b/cmd/fhome/commands.go index 3d6c9da..b061245 100644 --- a/cmd/fhome/commands.go +++ b/cmd/fhome/commands.go @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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 { @@ -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) } diff --git a/cmd/fhomed/daemon.go b/cmd/fhomed/daemon.go index d5594a8..2b978bf 100644 --- a/cmd/fhomed/daemon.go +++ b/cmd/fhomed/daemon.go @@ -13,12 +13,12 @@ import ( ) func daemon(ctx context.Context, name, pin string) 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) } - 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 @@ -28,7 +28,7 @@ func daemon(ctx context.Context, 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 @@ -44,8 +44,8 @@ func daemon(ctx context.Context, 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. @@ -60,7 +60,7 @@ func daemon(ctx context.Context, 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...) @@ -77,7 +77,7 @@ func daemon(ctx context.Context, 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...) @@ -94,7 +94,7 @@ func daemon(ctx context.Context, 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...) @@ -111,7 +111,7 @@ func daemon(ctx context.Context, 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...) @@ -131,7 +131,7 @@ func daemon(ctx context.Context, 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 diff --git a/cmd/fhomed/hack.go b/cmd/fhomed/hack.go index 2f8e8ba..3666139 100644 --- a/cmd/fhomed/hack.go +++ b/cmd/fhomed/hack.go @@ -1,6 +1,7 @@ package main import ( + "context" "embed" "fmt" "html/template" @@ -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) @@ -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)) diff --git a/highlevel/connect.go b/highlevel/connect.go index 1f3d92c..d8f90b3 100644 --- a/highlevel/connect.go +++ b/highlevel/connect.go @@ -3,6 +3,7 @@ package highlevel import ( + "context" "fmt" "log/slog" @@ -17,7 +18,7 @@ type Config struct { } // Connect returns a client that is ready to use. -func Connect(config *Config, dialer *websocket.Dialer) (*api.Client, error) { +func Connect(ctx context.Context, config *Config, dialer *websocket.Dialer) (*api.Client, error) { client, err := api.NewClient(dialer) if err != nil { slog.Error("failed to create API client", slog.Any("error", err)) @@ -45,7 +46,7 @@ func Connect(config *Config, dialer *websocket.Dialer) (*api.Client, error) { slog.String("type", myResources.ResourceType0), ) - err = client.OpenResourceSession(config.ResourcePassword) + err = client.OpenResourceSession(ctx, config.ResourcePassword) if err != nil { slog.Error("failed to open client to resource session", slog.Any("error", err)) return nil, fmt.Errorf("open resource session: %w", err)