From 5f551e31d1e73664282b5c13ec9cfa2621f1e175 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 15 May 2024 12:50:22 +0200 Subject: [PATCH 1/4] Updated home assistant --- cmd/api/cmd.go | 48 +--------- cmd/api/flags.go | 18 ++-- cmd/api/homeassistant.go | 106 ++++++++++++++-------- pkg/homeassistant/client_test.go | 8 +- pkg/homeassistant/services.go | 93 +++++++++++++++++++ pkg/homeassistant/services_test.go | 25 ++++++ pkg/homeassistant/states.go | 139 +++++++++-------------------- pkg/homeassistant/states_test.go | 50 +++-------- transport.go | 22 +++-- 9 files changed, 276 insertions(+), 233 deletions(-) create mode 100644 pkg/homeassistant/services.go create mode 100644 pkg/homeassistant/services_test.go diff --git a/cmd/api/cmd.go b/cmd/api/cmd.go index 0e82ba4..89db993 100644 --- a/cmd/api/cmd.go +++ b/cmd/api/cmd.go @@ -23,6 +23,7 @@ type Fn struct { Description string MinArgs uint MaxArgs uint + Syntax string Call func(context.Context, *tablewriter.Writer, []string) error } @@ -39,51 +40,8 @@ func (c *Cmd) Get(name string) *Fn { } func (fn *Fn) CheckArgs(args []string) error { - // Check number of arguments - if fn.MinArgs != 0 && uint(len(args)) < fn.MinArgs { - return fmt.Errorf("not enough arguments for %q (expected >= %d)", fn.Name, fn.MinArgs) - } - if fn.MaxArgs != 0 && uint(len(args)) > fn.MaxArgs { - return fmt.Errorf("too many arguments for %q (expected <= %d)", fn.Name, fn.MaxArgs) + if (fn.MinArgs != 0 && uint(len(args)) < fn.MinArgs) || (fn.MaxArgs != 0 && uint(len(args)) > fn.MaxArgs) { + return fmt.Errorf("syntax error: %s %s", fn.Name, fn.Syntax) } return nil } - -/* - if fn == nil { - return nil, fmt.Errorf("unknown command %q", name) - } - - return c.getFn(name), nil - // Get the command function - var fn *Fn - var nargs uint - var out []string - if len(args) == 0 { - fn = c.getFn("") - } else { - fn = c.getFn(args[0]) - nargs = uint(len(args) - 1) - out = args[1:] - } - if fn == nil { - // No arguments and no default command - return nil, nil, nil - } - - // Check number of arguments - name := fn.Name - if name == "" { - name = c.Name - } - if fn.MinArgs != 0 && nargs < fn.MinArgs { - return nil, nil, fmt.Errorf("not enough arguments for %q", name) - } - if fn.MaxArgs != 0 && nargs > fn.MaxArgs { - return nil, nil, fmt.Errorf("too many arguments for %q", name) - } - - // Return the command - return fn, out, nil -} -*/ diff --git a/cmd/api/flags.go b/cmd/api/flags.go index f9202c9..104d93e 100644 --- a/cmd/api/flags.go +++ b/cmd/api/flags.go @@ -79,7 +79,7 @@ func (flags *Flags) Parse(args []string) (*Fn, []string, error) { // Parse command line err := flags.FlagSet.Parse(args) - // If there is a version argument, print the version and exit + // Check for global commands if flags.NArg() == 1 { switch flags.Arg(0) { case "version": @@ -104,10 +104,16 @@ func (flags *Flags) Parse(args []string) (*Fn, []string, error) { flags.cmd = cmd flags.root = strings.Join([]string{flags.Name(), cmd.Name}, " ") flags.fn = flags.Arg(1) - flags.args = flags.Args()[1:] + if len(flags.Args()) > 1 { + flags.args = flags.Args()[2:] + } } } + if flags.GetBool("debug") { + fmt.Fprintf(os.Stderr, "Function: %q Args: %q\n", flags.fn, flags.args) + } + // Print usage if err != nil { if err != flag.ErrHelp { @@ -117,7 +123,7 @@ func (flags *Flags) Parse(args []string) (*Fn, []string, error) { } return nil, nil, err } else if flags.cmd == nil { - fmt.Fprintln(os.Stderr, "Unknown command, try -help") + fmt.Fprintf(os.Stderr, "Unknown command, try \"%s -help\"\n", flags.Name()) return nil, nil, ErrHelp } @@ -140,7 +146,7 @@ func (flags *Flags) Parse(args []string) (*Fn, []string, error) { // Set the function to call fn := flags.cmd.Get(flags.fn) if fn == nil { - fmt.Fprintf(os.Stderr, "Unknown command %q, try -help\n", flags.fn) + fmt.Fprintf(os.Stderr, "Unknown command, try \"%s -help\"\n", flags.Name()) return nil, nil, ErrHelp } @@ -151,7 +157,7 @@ func (flags *Flags) Parse(args []string) (*Fn, []string, error) { } // Return success - return fn, args, nil + return fn, flags.args, nil } // Get returns the value of a flag, and returns true if the flag exists @@ -252,7 +258,7 @@ func (flags *Flags) PrintCommandUsage(cmd *Cmd) { // Help for command sets fmt.Fprintln(w, "Commands:") for _, fn := range cmd.Fn { - fmt.Fprintln(w, " ", flags.root, fn.Name) + fmt.Fprintln(w, " ", flags.root, fn.Name, fn.Syntax) fmt.Fprintln(w, " ", fn.Description) fmt.Fprintln(w, "") } diff --git a/cmd/api/homeassistant.go b/cmd/api/homeassistant.go index 6759cc7..06db27f 100644 --- a/cmd/api/homeassistant.go +++ b/cmd/api/homeassistant.go @@ -2,8 +2,6 @@ package main import ( "context" - "fmt" - "slices" "strings" "time" @@ -16,16 +14,19 @@ import ( // TYPES type haEntity struct { - Id string `json:"entity_id"` + Id string `json:"entity_id,width:40"` Name string `json:"name,omitempty"` Class string `json:"class,omitempty"` + Domain string `json:"domain,omitempty"` State string `json:"state,omitempty"` Attributes map[string]interface{} `json:"attributes,omitempty,wrap"` - UpdatedAt time.Time `json:"last_updated,omitempty"` + UpdatedAt time.Time `json:"last_updated,omitempty,width:34"` + ChangedAt time.Time `json:"last_changed,omitempty,width:34"` } -type haClass struct { - Class string `json:"class,omitempty"` +type haDomain struct { + Name string `json:"domain"` + Services string `json:"services,omitempty"` } /////////////////////////////////////////////////////////////////////////////// @@ -49,8 +50,9 @@ func haRegister(flags *Flags) { Description: "Information from home assistant", Parse: haParse, Fn: []Fn{ - {Name: "classes", Call: haClasses, Description: "Return entity classes"}, - {Name: "states", Call: haStates, Description: "Return entity states"}, + {Name: "domains", Call: haDomains, Description: "Enumerate entity domains"}, + {Name: "states", Call: haStates, Description: "Show current entity states", MaxArgs: 1, Syntax: "()"}, + {Name: "services", Call: haServices, Description: "Show services for an entity", MinArgs: 1, MaxArgs: 1, Syntax: ""}, }, }) } @@ -71,14 +73,25 @@ func haParse(flags *Flags, opts ...client.ClientOpt) error { // METHODS func haStates(_ context.Context, w *tablewriter.Writer, args []string) error { - if states, err := haGetStates(args); err != nil { + var result []haEntity + states, err := haGetStates(nil) + if err != nil { return err - } else { - return w.Write(states) } + + for _, state := range states { + if len(args) == 1 { + if !haMatchString(args[0], state.Name, state.Id) { + continue + } + + } + result = append(result, state) + } + return w.Write(result) } -func haClasses(_ context.Context, w *tablewriter.Writer, args []string) error { +func haDomains(_ context.Context, w *tablewriter.Writer, args []string) error { states, err := haGetStates(nil) if err != nil { return err @@ -89,17 +102,42 @@ func haClasses(_ context.Context, w *tablewriter.Writer, args []string) error { classes[state.Class] = true } - result := []haClass{} + result := []haDomain{} for c := range classes { - result = append(result, haClass{Class: c}) + result = append(result, haDomain{ + Name: c, + }) } return w.Write(result) } +func haServices(_ context.Context, w *tablewriter.Writer, args []string) error { + service, err := haClient.State(args[0]) + if err != nil { + return err + } + services, err := haClient.Services(service.Domain()) + if err != nil { + return err + } + return w.Write(services) +} + /////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS -func haGetStates(classes []string) ([]haEntity, error) { +func haMatchString(q string, values ...string) bool { + q = strings.ToLower(q) + for _, v := range values { + v = strings.ToLower(v) + if strings.Contains(v, q) { + return true + } + } + return false +} + +func haGetStates(domains []string) ([]haEntity, error) { var result []haEntity // Get states from the remote service @@ -112,37 +150,29 @@ func haGetStates(classes []string) ([]haEntity, error) { for _, state := range states { entity := haEntity{ Id: state.Entity, - State: state.State, + Name: state.Name(), + Domain: state.Domain(), + Class: state.Class(), + State: state.Value(), Attributes: state.Attributes, - UpdatedAt: state.LastChanged, + UpdatedAt: state.LastUpdated, + ChangedAt: state.LastChanged, } - // Ignore entities without state - if entity.State == "" || entity.State == "unknown" || entity.State == "unavailable" { + // Ignore any fields where the state is empty + if entity.State == "" { continue } - // Set entity type and name from entity id - parts := strings.SplitN(entity.Id, ".", 2) - if len(parts) >= 2 { - entity.Class = strings.ToLower(parts[0]) - entity.Name = parts[1] - } - - // Set entity type from device class - if t, exists := state.Attributes["device_class"]; exists { - entity.Class = fmt.Sprint(t) + // Add unit of measurement + if unit := state.UnitOfMeasurement(); unit != "" { + entity.State += " " + unit } - // Filter classes - if len(classes) > 0 && !slices.Contains(classes, entity.Class) { - continue - } - - // Set entity name from attributes - if name, exists := state.Attributes["friendly_name"]; exists { - entity.Name = fmt.Sprint(name) - } + // Filter domains + //if len(domains) > 0 && !slices.Contains(domains, entity.Domain) { + // continue + //} // Append results result = append(result, entity) diff --git a/pkg/homeassistant/client_test.go b/pkg/homeassistant/client_test.go index 1f7cf0b..a288827 100644 --- a/pkg/homeassistant/client_test.go +++ b/pkg/homeassistant/client_test.go @@ -22,18 +22,18 @@ func Test_client_001(t *testing.T) { // ENVIRONMENT func GetApiKey(t *testing.T) string { - key := os.Getenv("HA_API_KEY") + key := os.Getenv("HA_TOKEN") if key == "" { - t.Skip("HA_API_KEY not set") + t.Skip("HA_TOKEN not set") t.SkipNow() } return key } func GetEndPoint(t *testing.T) string { - key := os.Getenv("HA_API_URL") + key := os.Getenv("HA_ENDPOINT") if key == "" { - t.Skip("HA_API_URL not set") + t.Skip("HA_ENDPOINT not set") t.SkipNow() } return key diff --git a/pkg/homeassistant/services.go b/pkg/homeassistant/services.go new file mode 100644 index 0000000..3c4fa91 --- /dev/null +++ b/pkg/homeassistant/services.go @@ -0,0 +1,93 @@ +package homeassistant + +import ( + "encoding/json" + + // Packages + "github.com/mutablelogic/go-client" + "golang.org/x/exp/maps" + + // Namespace imports + . "github.com/djthorpe/go-errors" +) + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Domain struct { + Domain string `json:"domain"` + Services map[string]Service `json:"services,omitempty"` +} + +type Service struct { + Name string `json:"name"` + Description string `json:"description,omitempty,wrap"` + Fields map[string]Field `json:"fields,omitempty,wrap"` +} + +type Field struct { + Required bool `json:"required,omitempty"` + Example any `json:"example,omitempty"` + Selector map[string]Selector `json:"selector,omitempty"` +} + +type Selector struct { + Text string `json:"text,omitempty"` + Mode string `json:"mode,omitempty"` + Min int `json:"min,omitempty"` + Max int `json:"max,omitempty"` + UnitOfMeasurement string `json:"unit_of_measurement,omitempty"` +} + +/////////////////////////////////////////////////////////////////////////////// +// API CALLS + +// Domains returns all domains and their associated service objects +func (c *Client) Domains() ([]Domain, error) { + var response []Domain + if err := c.Do(nil, &response, client.OptPath("services")); err != nil { + return nil, err + } + + // Return success + return response, nil +} + +// Return callable services for a domain +func (c *Client) Services(domain string) ([]Service, error) { + var response []Domain + if err := c.Do(nil, &response, client.OptPath("services")); err != nil { + return nil, err + } + for _, v := range response { + if v.Domain != domain { + continue + } + if len(v.Services) == 0 { + // No services found + return []Service{}, nil + } else { + return maps.Values(v.Services), nil + } + } + // Return not found + return nil, ErrNotFound.Withf("domain not found: %q", domain) +} + +/////////////////////////////////////////////////////////////////////////////// +// STRINGIFY + +func (v Domain) String() string { + data, _ := json.MarshalIndent(v, "", " ") + return string(data) +} + +func (v Service) String() string { + data, _ := json.MarshalIndent(v, "", " ") + return string(data) +} + +func (v Field) String() string { + data, _ := json.MarshalIndent(v, "", " ") + return string(data) +} diff --git a/pkg/homeassistant/services_test.go b/pkg/homeassistant/services_test.go new file mode 100644 index 0000000..5fafd6a --- /dev/null +++ b/pkg/homeassistant/services_test.go @@ -0,0 +1,25 @@ +package homeassistant_test + +import ( + "os" + "testing" + + // Packages + opts "github.com/mutablelogic/go-client" + homeassistant "github.com/mutablelogic/go-client/pkg/homeassistant" + assert "github.com/stretchr/testify/assert" +) + +func Test_services_001(t *testing.T) { + assert := assert.New(t) + client, err := homeassistant.New(GetEndPoint(t), GetApiKey(t), opts.OptTrace(os.Stderr, true)) + assert.NoError(err) + assert.NotNil(client) + + domains, err := client.Domains() + if !assert.NoError(err) { + t.FailNow() + } + assert.NotNil(domains) + t.Log(domains) +} diff --git a/pkg/homeassistant/states.go b/pkg/homeassistant/states.go index b5d6b32..843f90b 100644 --- a/pkg/homeassistant/states.go +++ b/pkg/homeassistant/states.go @@ -13,19 +13,17 @@ import ( // TYPES type State struct { - Entity string `json:"entity_id"` - LastChanged time.Time `json:"last_changed"` - State string `json:"state"` - Attributes map[string]any `json:"attributes"` -} - -type Sensor struct { - Type string `json:"type"` - Entity string `json:"entity_id"` - Name string `json:"friendly_name"` - Value string `json:"state,omitempty"` - Unit string `json:"unit_of_measurement,omitempty"` - Class string `json:"device_class,omitempty"` + Entity string `json:"entity_id"` + LastChanged time.Time `json:"last_changed,omitempty"` + LastReported time.Time `json:"last_reported,omitempty"` + LastUpdated time.Time `json:"last_updated,omitempty"` + State string `json:"state"` + Attributes map[string]any `json:"attributes"` + Context struct { + Id string `json:"id,omitempty"` + ParentId string `json:"parent_id,omitempty"` + UserId string `json:"user_id,omitempty"` + } `json:"context"` } /////////////////////////////////////////////////////////////////////////////// @@ -43,85 +41,16 @@ func (c *Client) States() ([]State, error) { return response, nil } -// Sensors returns all sensor entities and their state -func (c *Client) Sensors() ([]Sensor, error) { - // Return the response - var response []State - if err := c.Do(nil, &response, client.OptPath("states")); err != nil { - return nil, err - } - - // Filter out sensors - var sensors []Sensor - for _, state := range response { - if !strings.HasPrefix(state.Entity, "sensor.") && !strings.HasPrefix(state.Entity, "binary_sensor.") { - continue - } - sensors = append(sensors, Sensor{ - Type: "sensor", - Entity: state.Entity, - Name: state.Name(), - Value: state.State, - Unit: state.UnitOfMeasurement(), - Class: state.DeviceClass(), - }) - } - - // Return success - return sensors, nil -} - -// Actuators returns all button, switch and lock entities and their state -func (c *Client) Actuators() ([]Sensor, error) { - // Return the response - var response []State - if err := c.Do(nil, &response, client.OptPath("states")); err != nil { - return nil, err - } - - // Filter out buttons, locks, and switches - var sensors []Sensor - for _, state := range response { - if !strings.HasPrefix(state.Entity, "button.") && !strings.HasPrefix(state.Entity, "lock.") && !strings.HasPrefix(state.Entity, "switch.") { - continue - } - sensors = append(sensors, Sensor{ - Type: "actuator", - Entity: state.Entity, - Name: state.Name(), - Value: state.State, - Class: state.DeviceClass(), - }) - } - - // Return success - return sensors, nil -} - -// Lights returns all light entities and their state -func (c *Client) Lights() ([]Sensor, error) { +// State returns a state for a specific entity +func (c *Client) State(EntityId string) (State, error) { // Return the response - var response []State - if err := c.Do(nil, &response, client.OptPath("states")); err != nil { - return nil, err - } - - // Filter out sensors - var lights []Sensor - for _, state := range response { - if !strings.HasPrefix(state.Entity, "light.") { - continue - } - lights = append(lights, Sensor{ - Type: "light", - Entity: state.Entity, - Name: state.Name(), - Value: state.State, - }) + var response State + if err := c.Do(nil, &response, client.OptPath("states", EntityId)); err != nil { + return response, err } // Return success - return lights, nil + return response, nil } /////////////////////////////////////////////////////////////////////////////// @@ -132,14 +61,20 @@ func (s State) String() string { return string(data) } -func (s Sensor) String() string { - data, _ := json.MarshalIndent(s, "", " ") - return string(data) -} - /////////////////////////////////////////////////////////////////////////////// // METHODS +// Domain is used to determine the services which can be called on the entity +func (s State) Domain() string { + parts := strings.SplitN(s.Entity, ".", 2) + if len(parts) == 2 { + return parts[0] + } else { + return "" + } +} + +// Name is the friendly name of the entity func (s State) Name() string { name, ok := s.Attributes["friendly_name"] if !ok { @@ -151,10 +86,22 @@ func (s State) Name() string { } } -func (s State) DeviceClass() string { +// Value is the current state of the entity, or empty if the state is unavailable +func (s State) Value() string { + switch strings.ToLower(s.State) { + case "unavailable", "unknown", "--": + return "" + default: + return s.State + } +} + +// Class determines how the state should be interpreted, or will return "" if it's +// unknown +func (s State) Class() string { class, ok := s.Attributes["device_class"] if !ok { - return "" + return s.Domain() } else if class_, ok := class.(string); !ok { return "" } else { @@ -162,6 +109,8 @@ func (s State) DeviceClass() string { } } +// UnitOfMeasurement provides the unit of measurement for the state, or "" if there +// is no unit of measurement func (s State) UnitOfMeasurement() string { unit, ok := s.Attributes["unit_of_measurement"] if !ok { diff --git a/pkg/homeassistant/states_test.go b/pkg/homeassistant/states_test.go index b862834..d700102 100644 --- a/pkg/homeassistant/states_test.go +++ b/pkg/homeassistant/states_test.go @@ -20,44 +20,14 @@ func Test_states_001(t *testing.T) { assert.NoError(err) assert.NotNil(states) - t.Log(states) -} - -func Test_states_002(t *testing.T) { - assert := assert.New(t) - client, err := homeassistant.New(GetEndPoint(t), GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - - sensors, err := client.Sensors() - assert.NoError(err) - assert.NotNil(sensors) - - t.Log(sensors) -} - -func Test_states_003(t *testing.T) { - assert := assert.New(t) - client, err := homeassistant.New(GetEndPoint(t), GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - - lights, err := client.Lights() - assert.NoError(err) - assert.NotNil(lights) - - t.Log(lights) -} - -func Test_states_004(t *testing.T) { - assert := assert.New(t) - client, err := homeassistant.New(GetEndPoint(t), GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - - actuators, err := client.Actuators() - assert.NoError(err) - assert.NotNil(actuators) - - t.Log(actuators) + for _, state := range states { + t.Log("State:", state) + t.Logf(" Value: %q", state.Value()) + t.Log(" Name:", state.Name()) + t.Log(" Domain:", state.Domain()) + t.Log(" Class:", state.Class()) + if unit := state.UnitOfMeasurement(); unit != "" { + t.Logf(" UnitOfMeasurement: %q", unit) + } + } } diff --git a/transport.go b/transport.go index 596f057..69ede4d 100644 --- a/transport.go +++ b/transport.go @@ -93,14 +93,26 @@ func (transport *logtransport) RoundTrip(req *http.Request) (*http.Response, err // If verbose is switched on, read the body if transport.v && resp.Body != nil { + // Determine response content type contentType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) - if contentType == ContentTypeTextPlain || contentType == ContentTypeJson { - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err == nil { + + // Read in the body + body, _ := io.ReadAll(resp.Body) + resp.Body = io.NopCloser(bytes.NewReader(body)) + defer resp.Body.Close() + + switch contentType { + case ContentTypeJson: + dst := &bytes.Buffer{} + if err := json.Indent(dst, body, " ", " "); err != nil { fmt.Fprintf(transport.w, " <= %q\n", string(body)) + } else { + fmt.Fprintf(transport.w, " <= %v\n", dst.String()) } - resp.Body = io.NopCloser(bytes.NewReader(body)) + case ContentTypeTextPlain: + fmt.Fprintf(transport.w, " <= %q\n", string(body)) + default: + fmt.Fprintf(transport.w, " <= (not displaying response of type %q)\n", contentType) } } From 5ae1147e1d3288373e2b01379680961818aaa04e Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 15 May 2024 13:06:09 +0200 Subject: [PATCH 2/4] Added service call implementation --- cmd/api/homeassistant.go | 11 ++++++++++ pkg/homeassistant/services.go | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/cmd/api/homeassistant.go b/cmd/api/homeassistant.go index 06db27f..f0c9a08 100644 --- a/cmd/api/homeassistant.go +++ b/cmd/api/homeassistant.go @@ -53,6 +53,7 @@ func haRegister(flags *Flags) { {Name: "domains", Call: haDomains, Description: "Enumerate entity domains"}, {Name: "states", Call: haStates, Description: "Show current entity states", MaxArgs: 1, Syntax: "()"}, {Name: "services", Call: haServices, Description: "Show services for an entity", MinArgs: 1, MaxArgs: 1, Syntax: ""}, + {Name: "call", Call: haCall, Description: "Call a service for an entity", MinArgs: 2, MaxArgs: 2, Syntax: " "}, }, }) } @@ -123,6 +124,16 @@ func haServices(_ context.Context, w *tablewriter.Writer, args []string) error { return w.Write(services) } +func haCall(_ context.Context, w *tablewriter.Writer, args []string) error { + service := args[0] + entity := args[1] + states, err := haClient.Call(service, entity) + if err != nil { + return err + } + return w.Write(states) +} + /////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS diff --git a/pkg/homeassistant/services.go b/pkg/homeassistant/services.go index 3c4fa91..af6b709 100644 --- a/pkg/homeassistant/services.go +++ b/pkg/homeassistant/services.go @@ -2,6 +2,7 @@ package homeassistant import ( "encoding/json" + "strings" // Packages "github.com/mutablelogic/go-client" @@ -39,6 +40,10 @@ type Selector struct { UnitOfMeasurement string `json:"unit_of_measurement,omitempty"` } +type reqCall struct { + Entity string `json:"entity_id"` +} + /////////////////////////////////////////////////////////////////////////////// // API CALLS @@ -74,6 +79,30 @@ func (c *Client) Services(domain string) ([]Service, error) { return nil, ErrNotFound.Withf("domain not found: %q", domain) } +// Call a service for an entity. Returns a list of states that have +// changed while the service was being executed. +// TODO: This is a placeholder implementation, and requires fields to +// be passed in the request +func (c *Client) Call(service, entity string) ([]State, error) { + domain := domainForEntity(entity) + if domain == "" { + return nil, ErrBadParameter.Withf("Invalid entity: %q", entity) + } + + // Call the service + var response []State + if payload, err := client.NewJSONRequest(reqCall{ + Entity: entity, + }); err != nil { + return nil, err + } else if err := c.Do(payload, &response, client.OptPath("services", domain, service)); err != nil { + return nil, err + } + + // Return success + return response, nil +} + /////////////////////////////////////////////////////////////////////////////// // STRINGIFY @@ -91,3 +120,15 @@ func (v Field) String() string { data, _ := json.MarshalIndent(v, "", " ") return string(data) } + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func domainForEntity(entity string) string { + parts := strings.SplitN(entity, ".", 2) + if len(parts) == 2 { + return parts[0] + } else { + return "" + } +} From 93eb6942533930be0128a197a3db33b41fff5b19 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 15 May 2024 13:17:50 +0200 Subject: [PATCH 3/4] Added call to service --- cmd/api/flags.go | 8 ++++++-- cmd/api/homeassistant.go | 2 +- pkg/homeassistant/services.go | 19 +++++++++++-------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/cmd/api/flags.go b/cmd/api/flags.go index 104d93e..de1be31 100644 --- a/cmd/api/flags.go +++ b/cmd/api/flags.go @@ -97,8 +97,12 @@ func (flags *Flags) Parse(args []string) (*Fn, []string, error) { // If the name of the command is the same as the name of the application flags.cmd = cmd flags.root = cmd.Name - flags.fn = "" - flags.args = flags.Args() + if len(flags.Args()) > 0 { + flags.fn = flags.Arg(0) + if len(flags.Args()) > 1 { + flags.args = flags.Args()[1:] + } + } } else if flags.NArg() > 0 { if cmd := flags.getCommandSet(flags.Arg(0)); cmd != nil { flags.cmd = cmd diff --git a/cmd/api/homeassistant.go b/cmd/api/homeassistant.go index f0c9a08..ee0ac87 100644 --- a/cmd/api/homeassistant.go +++ b/cmd/api/homeassistant.go @@ -53,7 +53,7 @@ func haRegister(flags *Flags) { {Name: "domains", Call: haDomains, Description: "Enumerate entity domains"}, {Name: "states", Call: haStates, Description: "Show current entity states", MaxArgs: 1, Syntax: "()"}, {Name: "services", Call: haServices, Description: "Show services for an entity", MinArgs: 1, MaxArgs: 1, Syntax: ""}, - {Name: "call", Call: haCall, Description: "Call a service for an entity", MinArgs: 2, MaxArgs: 2, Syntax: " "}, + {Name: "call", Call: haCall, Description: "Call a service for an entity", MinArgs: 2, MaxArgs: 2, Syntax: " "}, }, }) } diff --git a/pkg/homeassistant/services.go b/pkg/homeassistant/services.go index af6b709..96b239e 100644 --- a/pkg/homeassistant/services.go +++ b/pkg/homeassistant/services.go @@ -16,12 +16,13 @@ import ( // TYPES type Domain struct { - Domain string `json:"domain"` - Services map[string]Service `json:"services,omitempty"` + Domain string `json:"domain"` + Services map[string]*Service `json:"services,omitempty"` } type Service struct { - Name string `json:"name"` + Call string `json:"call,omitempty"` + Name string `json:"name,omitempty"` Description string `json:"description,omitempty,wrap"` Fields map[string]Field `json:"fields,omitempty,wrap"` } @@ -59,7 +60,7 @@ func (c *Client) Domains() ([]Domain, error) { } // Return callable services for a domain -func (c *Client) Services(domain string) ([]Service, error) { +func (c *Client) Services(domain string) ([]*Service, error) { var response []Domain if err := c.Do(nil, &response, client.OptPath("services")); err != nil { return nil, err @@ -69,11 +70,13 @@ func (c *Client) Services(domain string) ([]Service, error) { continue } if len(v.Services) == 0 { - // No services found - return []Service{}, nil - } else { - return maps.Values(v.Services), nil + return nil, nil } + // Populate the Id field + for k, v := range v.Services { + v.Call = k + } + return maps.Values(v.Services), nil } // Return not found return nil, ErrNotFound.Withf("domain not found: %q", domain) From 316c55a1a05c41c62b12f98d5c63a64aa9d5bfc5 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Wed, 15 May 2024 13:22:02 +0200 Subject: [PATCH 4/4] Updated return types --- pkg/homeassistant/services.go | 8 ++++---- pkg/homeassistant/states.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/homeassistant/services.go b/pkg/homeassistant/services.go index 96b239e..0076ecd 100644 --- a/pkg/homeassistant/services.go +++ b/pkg/homeassistant/services.go @@ -49,8 +49,8 @@ type reqCall struct { // API CALLS // Domains returns all domains and their associated service objects -func (c *Client) Domains() ([]Domain, error) { - var response []Domain +func (c *Client) Domains() ([]*Domain, error) { + var response []*Domain if err := c.Do(nil, &response, client.OptPath("services")); err != nil { return nil, err } @@ -86,14 +86,14 @@ func (c *Client) Services(domain string) ([]*Service, error) { // changed while the service was being executed. // TODO: This is a placeholder implementation, and requires fields to // be passed in the request -func (c *Client) Call(service, entity string) ([]State, error) { +func (c *Client) Call(service, entity string) ([]*State, error) { domain := domainForEntity(entity) if domain == "" { return nil, ErrBadParameter.Withf("Invalid entity: %q", entity) } // Call the service - var response []State + var response []*State if payload, err := client.NewJSONRequest(reqCall{ Entity: entity, }); err != nil { diff --git a/pkg/homeassistant/states.go b/pkg/homeassistant/states.go index 843f90b..4572d6e 100644 --- a/pkg/homeassistant/states.go +++ b/pkg/homeassistant/states.go @@ -30,9 +30,9 @@ type State struct { // API CALLS // States returns all the entities and their state -func (c *Client) States() ([]State, error) { +func (c *Client) States() ([]*State, error) { // Return the response - var response []State + var response []*State if err := c.Do(nil, &response, client.OptPath("states")); err != nil { return nil, err } @@ -42,9 +42,9 @@ func (c *Client) States() ([]State, error) { } // State returns a state for a specific entity -func (c *Client) State(EntityId string) (State, error) { +func (c *Client) State(EntityId string) (*State, error) { // Return the response - var response State + var response *State if err := c.Do(nil, &response, client.OptPath("states", EntityId)); err != nil { return response, err }