Skip to content

Commit 95b1f41

Browse files
committed
Added home assistant
1 parent b457ccb commit 95b1f41

File tree

2 files changed

+188
-4
lines changed

2 files changed

+188
-4
lines changed

cmd/agent/main.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
kong "github.com/alecthomas/kong"
1212
client "github.com/mutablelogic/go-client"
1313
agent "github.com/mutablelogic/go-client/pkg/agent"
14+
"github.com/mutablelogic/go-client/pkg/homeassistant"
1415
"github.com/mutablelogic/go-client/pkg/ipify"
1516
"github.com/mutablelogic/go-client/pkg/newsapi"
1617
ollama "github.com/mutablelogic/go-client/pkg/ollama"
@@ -22,10 +23,12 @@ import (
2223
// TYPES
2324

2425
type Globals struct {
25-
OllamaUrl string `name:"ollama-url" help:"URL of Ollama service (can be set from OLLAMA_URL env)" default:"${OLLAMA_URL}"`
26-
OpenAIKey string `name:"openai-key" help:"API key for OpenAI service (can be set from OPENAI_API_KEY env)" default:"${OPENAI_API_KEY}"`
27-
WeatherKey string `name:"weather-key" help:"API key for WeatherAPI service (can be set from WEATHERAPI_KEY env)" default:"${WEATHERAPI_KEY}"`
28-
NewsKey string `name:"news-key" help:"API key for NewsAPI service (can be set from NEWSAPI_KEY env)" default:"${NEWSAPI_KEY}"`
26+
OllamaUrl string `name:"ollama-url" help:"URL of Ollama service (can be set from OLLAMA_URL env)" default:"${OLLAMA_URL}"`
27+
OpenAIKey string `name:"openai-key" help:"API key for OpenAI service (can be set from OPENAI_API_KEY env)" default:"${OPENAI_API_KEY}"`
28+
WeatherKey string `name:"weather-key" help:"API key for WeatherAPI service (can be set from WEATHERAPI_KEY env)" default:"${WEATHERAPI_KEY}"`
29+
NewsKey string `name:"news-key" help:"API key for NewsAPI service (can be set from NEWSAPI_KEY env)" default:"${NEWSAPI_KEY}"`
30+
HomeAssistantUrl string `name:"homeassistant-url" help:"URL of HomeAssistant service (can be set from HA_ENDPOINT env)" default:"${HA_ENDPOINT}"`
31+
HomeAssistantKey string `name:"homeassistant-key" help:"API key for HomeAssistant service (can be set from HA_TOKEN env)" default:"${HA_TOKEN}"`
2932

3033
// Debugging
3134
Debug bool `name:"debug" help:"Enable debug output"`
@@ -73,6 +76,8 @@ func main() {
7376
"OPENAI_API_KEY": envOrDefault("OPENAI_API_KEY", ""),
7477
"WEATHERAPI_KEY": envOrDefault("WEATHERAPI_KEY", ""),
7578
"NEWSAPI_KEY": envOrDefault("NEWSAPI_KEY", ""),
79+
"HA_TOKEN": envOrDefault("HA_TOKEN", ""),
80+
"HA_ENDPOINT": envOrDefault("HA_ENDPOINT", ""),
7681
},
7782
)
7883

@@ -96,6 +101,12 @@ func main() {
96101
cmd.FatalIfErrorf(err)
97102
cli.Globals.tools = append(cli.Globals.tools, news.Tools()...)
98103
}
104+
if cli.HomeAssistantKey != "" && cli.HomeAssistantUrl != "" {
105+
ha, err := homeassistant.New(cli.HomeAssistantUrl, cli.HomeAssistantKey, clientOpts(&cli)...)
106+
cmd.FatalIfErrorf(err)
107+
cli.Globals.tools = append(cli.Globals.tools, ha.Tools()...)
108+
}
109+
99110
// Add ipify
100111
ipify, err := ipify.New(clientOpts(&cli)...)
101112
cmd.FatalIfErrorf(err)

pkg/homeassistant/agent.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package homeassistant
2+
3+
import (
4+
"context"
5+
"slices"
6+
"strings"
7+
8+
// Packages
9+
agent "github.com/mutablelogic/go-client/pkg/agent"
10+
)
11+
12+
///////////////////////////////////////////////////////////////////////////////
13+
// TYPES
14+
15+
type tool struct {
16+
name string
17+
description string
18+
params []agent.ToolParameter
19+
run func(context.Context, *agent.ToolCall) (*agent.ToolResult, error)
20+
}
21+
22+
// Ensure tool satisfies the agent.Tool interface
23+
var _ agent.Tool = (*tool)(nil)
24+
25+
///////////////////////////////////////////////////////////////////////////////
26+
// PUBLIC METHODS
27+
28+
// Return all the agent tools for the weatherapi
29+
func (c *Client) Tools() []agent.Tool {
30+
return []agent.Tool{
31+
&tool{
32+
name: "devices",
33+
description: "Lookup all device id's in the home, or search for a device ny name",
34+
run: c.agentGetDeviceIds,
35+
params: []agent.ToolParameter{
36+
{
37+
Name: "name",
38+
Description: "Name to filter devices",
39+
},
40+
},
41+
}, &tool{
42+
name: "get_device_state",
43+
description: "Return the current state of a device, given the device id",
44+
run: c.agentGetDeviceState,
45+
params: []agent.ToolParameter{
46+
{
47+
Name: "device",
48+
Description: "The device id",
49+
Required: true,
50+
},
51+
},
52+
},
53+
}
54+
}
55+
56+
///////////////////////////////////////////////////////////////////////////////
57+
// PRIVATE METHODS - TOOL
58+
59+
func (*tool) Provider() string {
60+
return "homeassistant"
61+
}
62+
63+
func (t *tool) Name() string {
64+
return t.name
65+
}
66+
67+
func (t *tool) Description() string {
68+
return t.description
69+
}
70+
71+
func (t *tool) Params() []agent.ToolParameter {
72+
return t.params
73+
}
74+
75+
func (t *tool) Run(ctx context.Context, call *agent.ToolCall) (*agent.ToolResult, error) {
76+
return t.run(ctx, call)
77+
}
78+
79+
///////////////////////////////////////////////////////////////////////////////
80+
// PRIVATE METHODS - TOOL
81+
82+
var (
83+
allowedClasses = []string{
84+
"temperature",
85+
"humidity",
86+
"battery",
87+
"select",
88+
"number",
89+
"switch",
90+
"enum",
91+
"light",
92+
"sensor",
93+
"binary_sensor",
94+
"remote",
95+
"climate",
96+
"occupancy",
97+
"motion",
98+
"button",
99+
"door",
100+
"lock",
101+
"tv",
102+
"vacuum",
103+
}
104+
)
105+
106+
// Return the current devices and their id's
107+
func (c *Client) agentGetDeviceIds(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) {
108+
name, err := call.String("name")
109+
if err != nil {
110+
return nil, err
111+
}
112+
113+
// Query all devices
114+
devices, err := c.States()
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
// Make the device id's
120+
type DeviceId struct {
121+
Id string `json:"id"`
122+
Name string `json:"name"`
123+
}
124+
var result []DeviceId
125+
for _, device := range devices {
126+
if !slices.Contains(allowedClasses, device.Class()) {
127+
continue
128+
}
129+
var found bool
130+
if name != "" {
131+
if strings.Contains(strings.ToLower(device.Name()), strings.ToLower(name)) {
132+
found = true
133+
} else if strings.Contains(strings.ToLower(device.Class()), strings.ToLower(name)) {
134+
found = true
135+
}
136+
if !found {
137+
continue
138+
}
139+
}
140+
result = append(result, DeviceId{
141+
Id: device.Entity,
142+
Name: device.Name(),
143+
})
144+
}
145+
return &agent.ToolResult{
146+
Id: call.Id,
147+
Result: map[string]any{
148+
"type": "text",
149+
"devices": result,
150+
},
151+
}, nil
152+
}
153+
154+
// Return a device state
155+
func (c *Client) agentGetDeviceState(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) {
156+
device, err := call.String("device")
157+
if err != nil {
158+
return nil, err
159+
}
160+
161+
state, err := c.State(device)
162+
if err != nil {
163+
return nil, err
164+
}
165+
166+
return &agent.ToolResult{
167+
Id: call.Id,
168+
Result: map[string]any{
169+
"type": "text",
170+
"device": state,
171+
},
172+
}, nil
173+
}

0 commit comments

Comments
 (0)