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:
Sam Lewis
2022-10-11 01:22:23 -04:00
parent 689a6ce4d3
commit 7bcca889f9
13 changed files with 296 additions and 93 deletions

59
app.go
View File

@@ -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
} }

View File

@@ -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")
} }

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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
// }

View 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}
}

View 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

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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())
}

View File

@@ -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
View 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
}