Skip to content

Commit 3c52b9b

Browse files
authored
Merge pull request #7 from mutablelogic/v1
Added home assistant client
2 parents b9740e8 + 4e523b5 commit 3c52b9b

File tree

8 files changed

+417
-0
lines changed

8 files changed

+417
-0
lines changed

pkg/homeassistant/client.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
homeassistant implements an API client for Home Assistant API
3+
https://developers.home-assistant.io/docs/api/rest/
4+
*/
5+
package homeassistant
6+
7+
import (
8+
// Packages
9+
"github.com/mutablelogic/go-client/pkg/client"
10+
)
11+
12+
///////////////////////////////////////////////////////////////////////////////
13+
// TYPES
14+
15+
type Client struct {
16+
*client.Client
17+
}
18+
19+
///////////////////////////////////////////////////////////////////////////////
20+
// LIFECYCLE
21+
22+
func New(endPoint, apiKey string, opts ...client.ClientOpt) (*Client, error) {
23+
// Add a final slash to the endpoint
24+
if endPoint[len(endPoint)-1] != '/' {
25+
endPoint += "/"
26+
}
27+
28+
// Create client
29+
client, err := client.New(append(opts, client.OptEndpoint(endPoint), client.OptReqToken(client.Token{
30+
Scheme: client.Bearer,
31+
Value: apiKey,
32+
}))...)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
// Return the client
38+
return &Client{client}, nil
39+
}

pkg/homeassistant/client_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package homeassistant_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
// Packages
8+
opts "github.com/mutablelogic/go-client/pkg/client"
9+
homeassistant "github.com/mutablelogic/go-client/pkg/homeassistant"
10+
assert "github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_client_001(t *testing.T) {
14+
assert := assert.New(t)
15+
client, err := homeassistant.New(GetEndPoint(t), GetApiKey(t), opts.OptTrace(os.Stderr, true))
16+
assert.NoError(err)
17+
assert.NotNil(client)
18+
t.Log(client)
19+
}
20+
21+
///////////////////////////////////////////////////////////////////////////////
22+
// ENVIRONMENT
23+
24+
func GetApiKey(t *testing.T) string {
25+
key := os.Getenv("HA_API_KEY")
26+
if key == "" {
27+
t.Skip("HA_API_KEY not set")
28+
t.SkipNow()
29+
}
30+
return key
31+
}
32+
33+
func GetEndPoint(t *testing.T) string {
34+
key := os.Getenv("HA_API_URL")
35+
if key == "" {
36+
t.Skip("HA_API_URL not set")
37+
t.SkipNow()
38+
}
39+
return key
40+
}

pkg/homeassistant/events.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package homeassistant
2+
3+
import "github.com/mutablelogic/go-client/pkg/client"
4+
5+
///////////////////////////////////////////////////////////////////////////////
6+
// TYPES
7+
8+
type Event struct {
9+
Event string `json:"event"`
10+
Listeners uint `json:"listener_count"`
11+
}
12+
13+
///////////////////////////////////////////////////////////////////////////////
14+
// API CALLS
15+
16+
// Events returns all the events and number of listeners
17+
func (c *Client) Events() ([]Event, error) {
18+
var response []Event
19+
payload := client.NewRequest(client.ContentTypeJson)
20+
if err := c.Do(payload, &response, client.OptPath("events")); err != nil {
21+
return nil, err
22+
}
23+
24+
// Return success
25+
return response, nil
26+
}

pkg/homeassistant/events_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package homeassistant_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
// Packages
8+
opts "github.com/mutablelogic/go-client/pkg/client"
9+
homeassistant "github.com/mutablelogic/go-client/pkg/homeassistant"
10+
assert "github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_events_001(t *testing.T) {
14+
assert := assert.New(t)
15+
client, err := homeassistant.New(GetEndPoint(t), GetApiKey(t), opts.OptTrace(os.Stderr, true))
16+
assert.NoError(err)
17+
assert.NotNil(client)
18+
19+
events, err := client.Events()
20+
assert.NoError(err)
21+
assert.NotNil(events)
22+
23+
t.Log(events)
24+
}

pkg/homeassistant/health.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package homeassistant
2+
3+
import "github.com/mutablelogic/go-client/pkg/client"
4+
5+
///////////////////////////////////////////////////////////////////////////////
6+
// API CALLS
7+
8+
// ListModels returns all the models
9+
func (c *Client) Health() (string, error) {
10+
// Response schema
11+
type responseHealth struct {
12+
Message string `json:"message"`
13+
}
14+
15+
// Return the response
16+
var response responseHealth
17+
payload := client.NewRequest(client.ContentTypeJson)
18+
if err := c.Do(payload, &response); err != nil {
19+
return "", err
20+
}
21+
22+
// Return success
23+
return response.Message, nil
24+
}

pkg/homeassistant/health_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package homeassistant_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
// Packages
8+
opts "github.com/mutablelogic/go-client/pkg/client"
9+
homeassistant "github.com/mutablelogic/go-client/pkg/homeassistant"
10+
assert "github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_health_001(t *testing.T) {
14+
assert := assert.New(t)
15+
client, err := homeassistant.New(GetEndPoint(t), GetApiKey(t), opts.OptTrace(os.Stderr, true))
16+
assert.NoError(err)
17+
assert.NotNil(client)
18+
19+
message, err := client.Health()
20+
assert.NoError(err)
21+
assert.NotEmpty(message)
22+
23+
t.Log(message)
24+
}

pkg/homeassistant/states.go

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package homeassistant
2+
3+
import (
4+
"encoding/json"
5+
"strings"
6+
"time"
7+
8+
"github.com/mutablelogic/go-client/pkg/client"
9+
)
10+
11+
///////////////////////////////////////////////////////////////////////////////
12+
// TYPES
13+
14+
type State struct {
15+
Entity string `json:"entity_id"`
16+
LastChanged time.Time `json:"last_changed"`
17+
State string `json:"state"`
18+
Attributes map[string]any `json:"attributes"`
19+
}
20+
21+
type Sensor struct {
22+
Type string `json:"type"`
23+
Entity string `json:"entity_id"`
24+
Name string `json:"friendly_name"`
25+
Value string `json:"state,omitempty"`
26+
Unit string `json:"unit_of_measurement,omitempty"`
27+
Class string `json:"device_class,omitempty"`
28+
}
29+
30+
///////////////////////////////////////////////////////////////////////////////
31+
// API CALLS
32+
33+
// States returns all the entities and their state
34+
func (c *Client) States() ([]State, error) {
35+
// Return the response
36+
var response []State
37+
payload := client.NewRequest(client.ContentTypeJson)
38+
if err := c.Do(payload, &response, client.OptPath("states")); err != nil {
39+
return nil, err
40+
}
41+
42+
// Return success
43+
return response, nil
44+
}
45+
46+
// Sensors returns all sensor entities and their state
47+
func (c *Client) Sensors() ([]Sensor, error) {
48+
// Return the response
49+
var response []State
50+
payload := client.NewRequest(client.ContentTypeJson)
51+
if err := c.Do(payload, &response, client.OptPath("states")); err != nil {
52+
return nil, err
53+
}
54+
55+
// Filter out sensors
56+
var sensors []Sensor
57+
for _, state := range response {
58+
if !strings.HasPrefix(state.Entity, "sensor.") && !strings.HasPrefix(state.Entity, "binary_sensor.") {
59+
continue
60+
}
61+
sensors = append(sensors, Sensor{
62+
Type: "sensor",
63+
Entity: state.Entity,
64+
Name: state.Name(),
65+
Value: state.State,
66+
Unit: state.UnitOfMeasurement(),
67+
Class: state.DeviceClass(),
68+
})
69+
}
70+
71+
// Return success
72+
return sensors, nil
73+
}
74+
75+
// Actuators returns all button, switch and lock entities and their state
76+
func (c *Client) Actuators() ([]Sensor, error) {
77+
// Return the response
78+
var response []State
79+
payload := client.NewRequest(client.ContentTypeJson)
80+
if err := c.Do(payload, &response, client.OptPath("states")); err != nil {
81+
return nil, err
82+
}
83+
84+
// Filter out buttons, locks, and switches
85+
var sensors []Sensor
86+
for _, state := range response {
87+
if !strings.HasPrefix(state.Entity, "button.") && !strings.HasPrefix(state.Entity, "lock.") && !strings.HasPrefix(state.Entity, "switch.") {
88+
continue
89+
}
90+
sensors = append(sensors, Sensor{
91+
Type: "actuator",
92+
Entity: state.Entity,
93+
Name: state.Name(),
94+
Value: state.State,
95+
Class: state.DeviceClass(),
96+
})
97+
}
98+
99+
// Return success
100+
return sensors, nil
101+
}
102+
103+
// Lights returns all light entities and their state
104+
func (c *Client) Lights() ([]Sensor, error) {
105+
// Return the response
106+
var response []State
107+
payload := client.NewRequest(client.ContentTypeJson)
108+
if err := c.Do(payload, &response, client.OptPath("states")); err != nil {
109+
return nil, err
110+
}
111+
112+
// Filter out sensors
113+
var lights []Sensor
114+
for _, state := range response {
115+
if !strings.HasPrefix(state.Entity, "light.") {
116+
continue
117+
}
118+
lights = append(lights, Sensor{
119+
Type: "light",
120+
Entity: state.Entity,
121+
Name: state.Name(),
122+
Value: state.State,
123+
})
124+
}
125+
126+
// Return success
127+
return lights, nil
128+
}
129+
130+
///////////////////////////////////////////////////////////////////////////////
131+
// STRINGIFY
132+
133+
func (s State) String() string {
134+
data, _ := json.MarshalIndent(s, "", " ")
135+
return string(data)
136+
}
137+
138+
func (s Sensor) String() string {
139+
data, _ := json.MarshalIndent(s, "", " ")
140+
return string(data)
141+
}
142+
143+
///////////////////////////////////////////////////////////////////////////////
144+
// METHODS
145+
146+
func (s State) Name() string {
147+
name, ok := s.Attributes["friendly_name"]
148+
if !ok {
149+
return s.Entity
150+
} else if name_, ok := name.(string); !ok {
151+
return s.Entity
152+
} else {
153+
return name_
154+
}
155+
}
156+
157+
func (s State) DeviceClass() string {
158+
class, ok := s.Attributes["device_class"]
159+
if !ok {
160+
return ""
161+
} else if class_, ok := class.(string); !ok {
162+
return ""
163+
} else {
164+
return class_
165+
}
166+
}
167+
168+
func (s State) UnitOfMeasurement() string {
169+
unit, ok := s.Attributes["unit_of_measurement"]
170+
if !ok {
171+
return ""
172+
} else if unit_, ok := unit.(string); !ok {
173+
return ""
174+
} else {
175+
return unit_
176+
}
177+
}

0 commit comments

Comments
 (0)