mirror of
https://github.com/Xevion/go-ha.git
synced 2025-12-10 14:07:26 -06:00
good progress yay:
- impl http client - create http client in App() - generic builder for Service.* - set Service on app to pass to callbacks later - impl State - set State on app to pass to callbacks later - change panic to log.Fatalln
This commit is contained in:
59
app.go
59
app.go
@@ -2,42 +2,51 @@ package gomeassistant
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/saml-dev/gome-assistant/internal/http"
|
||||||
"github.com/saml-dev/gome-assistant/internal/setup"
|
"github.com/saml-dev/gome-assistant/internal/setup"
|
||||||
"nhooyr.io/websocket"
|
"nhooyr.io/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type app struct {
|
type app struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
ctxCancel context.CancelFunc
|
ctxCancel context.CancelFunc
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
|
httpClient *http.HttpClient
|
||||||
|
|
||||||
|
service *Service
|
||||||
|
state *State
|
||||||
|
|
||||||
schedules []schedule
|
schedules []schedule
|
||||||
entityListeners []entityListener
|
entityListeners []entityListener
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
Sunrise hourMinute = hourMinute{1000, 0}
|
|
||||||
Sunset hourMinute = hourMinute{1001, 0}
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
App establishes the websocket connection and returns an object
|
App establishes the websocket connection and returns an object
|
||||||
you can use to register schedules and listeners.
|
you can use to register schedules and listeners.
|
||||||
*/
|
*/
|
||||||
func App(connString string) (app, error) {
|
func App(connString string) app {
|
||||||
conn, ctx, ctxCancel, err := setup.SetupConnection(connString)
|
token := os.Getenv("AUTH_TOKEN")
|
||||||
if err != nil {
|
conn, ctx, ctxCancel := setup.SetupConnection(connString)
|
||||||
return app{}, err
|
|
||||||
}
|
httpClient := http.NewHttpClient(connString, token)
|
||||||
|
|
||||||
|
service := NewService(conn, ctx, httpClient)
|
||||||
|
state := NewState(httpClient)
|
||||||
|
|
||||||
return app{
|
return app{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
ctxCancel: ctxCancel,
|
ctxCancel: ctxCancel,
|
||||||
|
httpClient: httpClient,
|
||||||
|
service: service,
|
||||||
|
state: state,
|
||||||
schedules: []schedule{},
|
schedules: []schedule{},
|
||||||
entityListeners: []entityListener{},
|
entityListeners: []entityListener{},
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *app) Cleanup() {
|
func (a *app) Cleanup() {
|
||||||
@@ -48,27 +57,20 @@ func (a *app) Cleanup() {
|
|||||||
|
|
||||||
func (a *app) RegisterSchedule(s schedule) {
|
func (a *app) RegisterSchedule(s schedule) {
|
||||||
if s.err != nil {
|
if s.err != nil {
|
||||||
panic(s.err) // something wasn't configured properly when the schedule was built
|
log.Fatalln(s.err) // something wasn't configured properly when the schedule was built
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.frequency == 0 {
|
if s.frequency == 0 {
|
||||||
panic("A schedule must call either Daily() or Every() when built.")
|
log.Fatalln("A schedule must call either Daily() or Every() when built.")
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
startTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) // start at midnight today
|
startTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) // start at midnight today
|
||||||
|
|
||||||
// apply offset if set
|
// apply offset if set
|
||||||
if s.offset.int() != 0 {
|
if s.offset.minutes() != 0 {
|
||||||
if s.offset.int() == Sunrise.int() {
|
startTime.Add(time.Hour * time.Duration(s.offset.hour))
|
||||||
// TODO: same as sunset w/ sunrise
|
startTime.Add(time.Minute * time.Duration(s.offset.minute))
|
||||||
} else if s.offset.int() == Sunset.int() {
|
|
||||||
// TODO: add an http client (w/ token) to *app, use it to get state of sun.sun
|
|
||||||
// to get next sunset time
|
|
||||||
} else {
|
|
||||||
startTime.Add(time.Hour * time.Duration(s.offset.Hour))
|
|
||||||
startTime.Add(time.Minute * time.Duration(s.offset.Minute))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// advance first scheduled time by frequency until it is in the future
|
// advance first scheduled time by frequency until it is in the future
|
||||||
@@ -81,6 +83,11 @@ func (a *app) RegisterSchedule(s schedule) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *app) Start() {
|
func (a *app) Start() {
|
||||||
|
// NOTE:should the prio queue and websocket listener both write to a channel or something?
|
||||||
|
// then select from that and spawn new goroutine to call callback?
|
||||||
|
|
||||||
|
// TODO: loop through schedules and create heap priority queue
|
||||||
|
|
||||||
// TODO: figure out looping listening to messages for
|
// TODO: figure out looping listening to messages for
|
||||||
// listeners
|
// listeners
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,28 +8,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app, err := ga.App("192.168.86.67:8123")
|
app := ga.App("192.168.86.67:8123")
|
||||||
defer app.Cleanup()
|
defer app.Cleanup()
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s := ga.ScheduleBuilder().Call(lightsOut).Daily().At(ga.HourMinute(22, 00)).Build()
|
s := ga.ScheduleBuilder().Call(lightsOut).Daily().At(ga.Sunset.Subtract(ga.TimeOfDay(0, 30))).Build()
|
||||||
s2 := ga.ScheduleBuilder().Call(lightsOut).Every(time.Hour*4 + time.Minute*30).Offset(ga.HourMinute(1, 0)).Build()
|
s2 := ga.ScheduleBuilder().Call(lightsOut).Every(time.Hour*4 + time.Minute*30).Offset(ga.TimeOfDay(1, 0)).Build()
|
||||||
app.RegisterSchedule(s2)
|
app.RegisterSchedule(s2)
|
||||||
// err = app.Start()
|
// err = app.Start()
|
||||||
|
|
||||||
simpleListener := ga.EntityListenerBuilder().
|
simpleListener := ga.EntityListenerBuilder().
|
||||||
EntityId("light.lights").
|
EntityId("light.lights").
|
||||||
Call(cool).
|
Call(cool).
|
||||||
OnlyBetween(ga.HourMinute(22, 00), ga.HourMinute(07, 00))
|
OnlyBetween(ga.TimeOfDay(22, 00), ga.TimeOfDay(07, 00))
|
||||||
fmt.Println(simpleListener)
|
fmt.Println(simpleListener)
|
||||||
|
|
||||||
fmt.Println(s, "\n", s2)
|
fmt.Println(s, "\n", s2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func lightsOut(service ga.Service) {
|
func lightsOut(service ga.Service, state ga.State) {
|
||||||
// ga.TurnOff("light.all_lights")
|
// ga.TurnOff("light.all_lights")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ type entityListener struct {
|
|||||||
callback entityListenerCallback
|
callback entityListenerCallback
|
||||||
fromState string
|
fromState string
|
||||||
toState string
|
toState string
|
||||||
betweenStart hourMinute
|
betweenStart timeOfDay
|
||||||
betweenEnd hourMinute
|
betweenEnd timeOfDay
|
||||||
}
|
}
|
||||||
|
|
||||||
type entityListenerCallback func(Service, Data)
|
type entityListenerCallback func(Service, Data)
|
||||||
|
|
||||||
type Data struct{}
|
type Data struct{}
|
||||||
|
|
||||||
func (b elBuilder3) OnlyBetween(start hourMinute, end hourMinute) elBuilder3 {
|
func (b elBuilder3) OnlyBetween(start timeOfDay, end timeOfDay) elBuilder3 {
|
||||||
b.entityListener.betweenStart = start
|
b.entityListener.betweenStart = start
|
||||||
b.entityListener.betweenEnd = end
|
b.entityListener.betweenEnd = end
|
||||||
return b
|
return b
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -5,5 +5,6 @@ go 1.19
|
|||||||
require nhooyr.io/websocket v1.8.7
|
require nhooyr.io/websocket v1.8.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/golang-module/carbon/v2 v2.1.9 // indirect
|
||||||
github.com/klauspost/compress v1.10.3 // indirect
|
github.com/klauspost/compress v1.10.3 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -17,6 +17,8 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
|||||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/golang-module/carbon/v2 v2.1.9 h1:OWkhYzTTPe+jPOUEL2JkvGwf6bKNQJoh4LVT1LUay80=
|
||||||
|
github.com/golang-module/carbon/v2 v2.1.9/go.mod h1:NF5unWf838+pyRY0o+qZdIwBMkFf7w0hmLIguLiEpzU=
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
@@ -47,6 +49,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
@@ -68,5 +71,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
|||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||||
|
|||||||
@@ -1,6 +1,83 @@
|
|||||||
// http is used to interact with the home assistant
|
// http is used to interact with the home assistant
|
||||||
// REST API, currently only for retrieving state for
|
// REST API. Currently only used to retrieve state for
|
||||||
// a single entity_id
|
// a single entity_id
|
||||||
package http
|
package http
|
||||||
|
|
||||||
// TODO: impl http struct, should be initialized as part of App()
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HttpClient struct {
|
||||||
|
url string
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpClient(url, token string) *HttpClient {
|
||||||
|
url = fmt.Sprintf("http://%s/api", url)
|
||||||
|
return &HttpClient{url, token}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HttpClient) GetState(entityId string) ([]byte, error) {
|
||||||
|
resp, err := get(c.url+"/states/"+entityId, c.token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func get(url, token string) ([]byte, error) {
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Error creating HTTP request: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Error on response.\n[ERROR] -" + err.Error())
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Error while reading the response bytes:" + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// func post(url string, token string, data any) ([]byte, error) {
|
||||||
|
// postBody, err := json.Marshal(data)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// req, err := http.NewRequest("GET", url, bytes.NewBuffer(postBody))
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, errors.New("Error building post request: " + err.Error())
|
||||||
|
// }
|
||||||
|
|
||||||
|
// req.Header.Add("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
|
// client := &http.Client{}
|
||||||
|
// resp, err := client.Do(req)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, errors.New("Error in post response: " + err.Error())
|
||||||
|
// }
|
||||||
|
// defer resp.Body.Close()
|
||||||
|
|
||||||
|
// if resp.StatusCode == 401 {
|
||||||
|
// log.Fatalln("ERROR: Auth token is invalid. Please double check it or create a new token in your Home Assistant profile")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// body, err := io.ReadAll(resp.Body)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatalln(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return body, nil
|
||||||
|
// }
|
||||||
|
|||||||
11
internal/services/builder.go
Normal file
11
internal/services/builder.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"nhooyr.io/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildService[T Light | HomeAssistant](conn *websocket.Conn, ctx context.Context) *T {
|
||||||
|
return &T{conn: conn, ctx: ctx}
|
||||||
|
}
|
||||||
18
internal/services/homeassistant.go
Normal file
18
internal/services/homeassistant.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/saml-dev/gome-assistant/internal/http"
|
||||||
|
"nhooyr.io/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HomeAssistant struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
ctx context.Context
|
||||||
|
httpClient *http.HttpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: design how much reuse I can get between request types. E.g.
|
||||||
|
// only difference between light.turnon and homeassistant.turnon is
|
||||||
|
// domain and extra data
|
||||||
@@ -3,13 +3,15 @@ package services
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/saml-dev/gome-assistant/internal/http"
|
||||||
"github.com/saml-dev/gome-assistant/internal/setup"
|
"github.com/saml-dev/gome-assistant/internal/setup"
|
||||||
"nhooyr.io/websocket"
|
"nhooyr.io/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Light struct {
|
type Light struct {
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
httpClient *http.HttpClient
|
||||||
}
|
}
|
||||||
|
|
||||||
type LightRequest struct {
|
type LightRequest struct {
|
||||||
@@ -22,7 +24,21 @@ type LightRequest struct {
|
|||||||
} `json:"target"`
|
} `json:"target"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LightOnRequest(entityId string) LightRequest {
|
/* Public API */
|
||||||
|
|
||||||
|
func (l Light) TurnOn(entityId string) {
|
||||||
|
req := newLightOnRequest(entityId)
|
||||||
|
setup.WriteMessage(req, l.conn, l.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Light) TurnOff(entityId string) {
|
||||||
|
req := newLightOffRequest(entityId)
|
||||||
|
setup.WriteMessage(req, l.conn, l.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Internal */
|
||||||
|
|
||||||
|
func newLightOnRequest(entityId string) LightRequest {
|
||||||
req := LightRequest{
|
req := LightRequest{
|
||||||
Id: 5,
|
Id: 5,
|
||||||
Type: "call_service",
|
Type: "call_service",
|
||||||
@@ -33,18 +49,8 @@ func LightOnRequest(entityId string) LightRequest {
|
|||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
func LightOffRequest(entityId string) LightRequest {
|
func newLightOffRequest(entityId string) LightRequest {
|
||||||
req := LightOnRequest(entityId)
|
req := newLightOnRequest(entityId)
|
||||||
req.Service = "turn_off"
|
req.Service = "turn_off"
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l Light) TurnOn(entityId string) {
|
|
||||||
req := LightOnRequest(entityId)
|
|
||||||
setup.WriteMessage(req, l.conn, l.ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Light) TurnOff(entityId string) {
|
|
||||||
req := LightOffRequest(entityId)
|
|
||||||
setup.WriteMessage(req, l.conn, l.ctx)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -39,40 +40,38 @@ func ReadMessage(conn *websocket.Conn, ctx context.Context) (string, error) {
|
|||||||
return string(msg), nil
|
return string(msg), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupConnection(connString string) (*websocket.Conn, context.Context, context.CancelFunc, error) {
|
func SetupConnection(connString string) (*websocket.Conn, context.Context, context.CancelFunc) {
|
||||||
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*5)
|
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||||
|
|
||||||
// Init websocket connection
|
// Init websocket connection
|
||||||
conn, _, err := websocket.Dial(ctx, fmt.Sprintf("ws://%s/api/websocket", connString), nil)
|
conn, _, err := websocket.Dial(ctx, fmt.Sprintf("ws://%s/api/websocket", connString), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("ERROR: Failed to connect to websocket at ws://%s/api/websocket. Check IP address and port\n", connString)
|
|
||||||
ctxCancel()
|
ctxCancel()
|
||||||
return nil, nil, nil, err
|
log.Fatalf("ERROR: Failed to connect to websocket at ws://%s/api/websocket. Check IP address and port\n", connString)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read auth_required message
|
// Read auth_required message
|
||||||
_, err = ReadMessage(conn, ctx)
|
_, err = ReadMessage(conn, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctxCancel()
|
ctxCancel()
|
||||||
return nil, nil, nil, err
|
log.Fatalln("Unknown error creating websocket client")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send auth message
|
// Send auth message
|
||||||
err = SendAuthMessage(conn, ctx)
|
err = SendAuthMessage(conn, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctxCancel()
|
ctxCancel()
|
||||||
return nil, nil, nil, err
|
log.Fatalln("Unknown error creating websocket client")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify auth message
|
// Verify auth message
|
||||||
err = VerifyAuthResponse(conn, ctx)
|
err = VerifyAuthResponse(conn, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("ERROR: Auth token is invalid. Please double check it or create a new token in your Home Assistant profile")
|
|
||||||
ctxCancel()
|
ctxCancel()
|
||||||
return nil, nil, nil, err
|
log.Fatalln("ERROR: Auth token is invalid. Please double check it or create a new token in your Home Assistant profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn, ctx, ctxCancel, err
|
return conn, ctx, ctxCancel
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendAuthMessage(conn *websocket.Conn, ctx context.Context) error {
|
func SendAuthMessage(conn *websocket.Conn, ctx context.Context) error {
|
||||||
|
|||||||
104
schedule.go
104
schedule.go
@@ -2,29 +2,76 @@ package gomeassistant
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type hourMinute struct {
|
type sunriseSunset struct {
|
||||||
Hour int
|
base timeOfDay
|
||||||
Minute int
|
addition timeOfDay
|
||||||
|
subtraction timeOfDay
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hm hourMinute) int() int {
|
var Sunrise *sunriseSunset = &sunriseSunset{
|
||||||
return hm.Hour + hm.Minute
|
base: TimeOfDay(0, 10000),
|
||||||
|
addition: TimeOfDay(0, 0),
|
||||||
|
subtraction: TimeOfDay(0, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
func HourMinute(Hour, Minute int) hourMinute {
|
var Sunset *sunriseSunset = &sunriseSunset{
|
||||||
return hourMinute{Hour, Minute}
|
base: TimeOfDay(0, 20000),
|
||||||
|
addition: TimeOfDay(0, 0),
|
||||||
|
subtraction: TimeOfDay(0, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hm hourMinute) String() string {
|
func (ss *sunriseSunset) Add(hm timeOfDay) *sunriseSunset {
|
||||||
return fmt.Sprintf("%02d:%02d", hm.Hour, hm.Minute)
|
ss.addition = hm
|
||||||
|
return ss
|
||||||
}
|
}
|
||||||
|
|
||||||
type scheduleCallback func(Service)
|
func (ss *sunriseSunset) Subtract(hm timeOfDay) *sunriseSunset {
|
||||||
|
ss.subtraction = hm
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *sunriseSunset) minutes() int {
|
||||||
|
return ss.base.minute +
|
||||||
|
(ss.addition.hour*60 + ss.addition.minute) -
|
||||||
|
(ss.subtraction.hour*60 + ss.subtraction.minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HourMinute is used to express a time of day
|
||||||
|
// but it shouldn't be used directly. Use
|
||||||
|
// HourMinute(), Sunset(), or Sunrise() to
|
||||||
|
// create one. Add() and Subtract() can be
|
||||||
|
// called on Sunset and Sunrise to offset
|
||||||
|
// from that time.
|
||||||
|
type timeOfDay struct {
|
||||||
|
hour int
|
||||||
|
minute int
|
||||||
|
}
|
||||||
|
|
||||||
|
type timeOfDayInterface interface {
|
||||||
|
// Time represented as number of minutes
|
||||||
|
// after midnight. E.g. 02:00 would be 120.
|
||||||
|
minutes() int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hm timeOfDay) minutes() int {
|
||||||
|
return hm.hour*60 + hm.minute
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeOfDay(Hour, Minute int) timeOfDay {
|
||||||
|
return timeOfDay{Hour, Minute}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hm timeOfDay) String() string {
|
||||||
|
return fmt.Sprintf("%02d:%02d", hm.hour, hm.minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
type scheduleCallback func(Service, State)
|
||||||
|
|
||||||
type schedule struct {
|
type schedule struct {
|
||||||
/*
|
/*
|
||||||
@@ -50,10 +97,10 @@ type schedule struct {
|
|||||||
offset: "0003"
|
offset: "0003"
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
offset hourMinute
|
offset timeOfDay
|
||||||
/*
|
/*
|
||||||
This will be set rather than returning an error to avoid checking err for nil on every schedule :)
|
This will be set rather than returning an error to avoid checking err for nil on every schedule :)
|
||||||
RegisterSchedule will panic if the error is set.
|
RegisterSchedule will exit if the error is set.
|
||||||
*/
|
*/
|
||||||
err error
|
err error
|
||||||
realStartTime time.Time
|
realStartTime time.Time
|
||||||
@@ -83,7 +130,7 @@ func ScheduleBuilder() scheduleBuilder {
|
|||||||
return scheduleBuilder{
|
return scheduleBuilder{
|
||||||
schedule{
|
schedule{
|
||||||
frequency: 0,
|
frequency: 0,
|
||||||
offset: hourMinute{0, 0},
|
offset: timeOfDay{0, 0},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,18 +160,18 @@ func (sb scheduleBuilderCall) Daily() scheduleBuilderDaily {
|
|||||||
return scheduleBuilderDaily(sb)
|
return scheduleBuilderDaily(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb scheduleBuilderDaily) At(t hourMinute) scheduleBuilderEnd {
|
func (sb scheduleBuilderDaily) At(t timeOfDayInterface) scheduleBuilderEnd {
|
||||||
sb.schedule.offset = t
|
sb.schedule.offset = convertTimeOfDayToActualOffset(t)
|
||||||
return scheduleBuilderEnd(sb)
|
return scheduleBuilderEnd(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb scheduleBuilderCall) Every(d time.Duration) scheduleBuilderCustom {
|
func (sb scheduleBuilderCall) Every(duration time.Duration) scheduleBuilderCustom {
|
||||||
sb.schedule.frequency = d
|
sb.schedule.frequency = duration
|
||||||
return scheduleBuilderCustom(sb)
|
return scheduleBuilderCustom(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb scheduleBuilderCustom) Offset(o hourMinute) scheduleBuilderEnd {
|
func (sb scheduleBuilderCustom) Offset(t timeOfDay) scheduleBuilderEnd {
|
||||||
sb.schedule.offset = o
|
sb.schedule.offset = t
|
||||||
return scheduleBuilderEnd(sb)
|
return scheduleBuilderEnd(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,3 +186,22 @@ func (sb scheduleBuilderEnd) Build() schedule {
|
|||||||
func getFunctionName(i interface{}) string {
|
func getFunctionName(i interface{}) string {
|
||||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertTimeOfDayToActualOffset(t timeOfDayInterface) timeOfDay {
|
||||||
|
if t.minutes() > 15000 {
|
||||||
|
// TODO: same as below but w/ sunset
|
||||||
|
return TimeOfDay(0, 0)
|
||||||
|
} else if t.minutes() > 5000 {
|
||||||
|
// TODO: use httpClient to get state of sun.sun
|
||||||
|
// to get next sunrise time
|
||||||
|
|
||||||
|
// retrieve next sunrise time
|
||||||
|
|
||||||
|
// use carbon.Parse() to create time.Time of that time
|
||||||
|
|
||||||
|
// return Time() of that many hours and minutes to set offset from midnight
|
||||||
|
} else if t.minutes() >= 1440 {
|
||||||
|
log.Fatalln("Offset (set via At() or Offset()) cannot be more than 1 day (23h59m)")
|
||||||
|
}
|
||||||
|
return TimeOfDay(0, t.minutes())
|
||||||
|
}
|
||||||
|
|||||||
18
service.go
18
service.go
@@ -3,21 +3,19 @@ package gomeassistant
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/saml-dev/gome-assistant/internal/http"
|
||||||
"github.com/saml-dev/gome-assistant/internal/services"
|
"github.com/saml-dev/gome-assistant/internal/services"
|
||||||
"nhooyr.io/websocket"
|
"nhooyr.io/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
HomeAssistant homeAssistant
|
HomeAssistant *services.HomeAssistant
|
||||||
Light services.Light
|
Light *services.Light
|
||||||
}
|
}
|
||||||
|
|
||||||
type homeAssistant struct {
|
func NewService(conn *websocket.Conn, ctx context.Context, httpClient *http.HttpClient) *Service {
|
||||||
conn websocket.Conn
|
return &Service{
|
||||||
ctx context.Context
|
Light: services.BuildService[services.Light](conn, ctx),
|
||||||
|
HomeAssistant: services.BuildService[services.HomeAssistant](conn, ctx),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// type light struct {
|
|
||||||
// conn websocket.Conn
|
|
||||||
// ctx context.Context
|
|
||||||
// }
|
|
||||||
|
|||||||
20
state.go
Normal file
20
state.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package gomeassistant
|
||||||
|
|
||||||
|
import "github.com/saml-dev/gome-assistant/internal/http"
|
||||||
|
|
||||||
|
// State is used to retrieve state from Home Assistant.
|
||||||
|
type State struct {
|
||||||
|
httpClient *http.HttpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewState(c *http.HttpClient) *State {
|
||||||
|
return &State{httpClient: c}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) Get(entityId string) (string, error) {
|
||||||
|
resp, err := s.httpClient.GetState(entityId)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(resp), nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user