listeners working, sunrise/sunset working with string offset

This commit is contained in:
Sam Lewis
2022-10-16 22:23:46 -04:00
parent 8b888a2d89
commit 6ca78e88dd
10 changed files with 317 additions and 140 deletions

123
app.go
View File

@@ -2,10 +2,12 @@ package gomeassistant
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/golang-module/carbon"
"github.com/gorilla/websocket"
"github.com/saml-dev/gome-assistant/internal"
"github.com/saml-dev/gome-assistant/internal/http"
@@ -24,9 +26,15 @@ type app struct {
schedules pq.PriorityQueue
entityListeners map[string][]entityListener
entityListenerIds map[int64]entityListenerCallback
entityListenersId int64
}
/*
Time is a 24-hr format string with hour and minute,
e.g. "07:00" for 7AM or "23:00" for 11PM.
*/
type Time string
/*
NewApp establishes the websocket connection and returns an object
you can use to register schedules and listeners.
@@ -41,15 +49,14 @@ func NewApp(connString string) app {
state := NewState(httpClient)
return app{
conn: conn,
ctx: ctx,
ctxCancel: ctxCancel,
httpClient: httpClient,
service: service,
state: state,
schedules: pq.New(),
entityListeners: map[string][]entityListener{},
entityListenerIds: map[int64]entityListenerCallback{},
conn: conn,
ctx: ctx,
ctxCancel: ctxCancel,
httpClient: httpClient,
service: service,
state: state,
schedules: pq.New(),
entityListeners: map[string][]entityListener{},
}
}
@@ -71,10 +78,9 @@ func (a *app) RegisterSchedule(s schedule) {
// TODO: consider moving all time stuff to carbon?
now := time.Now()
startTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) // start at midnight today
// apply offset if set
if s.offset.Minutes() > 0 {
startTime.Add(s.offset)
startTime = startTime.Add(s.offset)
}
// advance first scheduled time by frequency until it is in the future
@@ -88,45 +94,94 @@ func (a *app) RegisterSchedule(s schedule) {
func (a *app) RegisterEntityListener(el entityListener) {
for _, entity := range el.entityIds {
id := internal.GetId()
subscribeTriggerMsg := subscribeMsg{
Id: id,
Type: "subscribe_trigger",
Trigger: subscribeMsgTrigger{
Platform: "state",
EntityId: entity,
},
if elList, ok := a.entityListeners[entity]; ok {
a.entityListeners[entity] = append(elList, el)
} else {
a.entityListeners[entity] = []entityListener{el}
}
if el.fromState != "" {
subscribeTriggerMsg.Trigger.From = el.fromState
}
}
// Sunrise take an optional string that is passed to time.ParseDuration.
// Examples include "-1.5h", "30m", etc. See https://pkg.go.dev/time#ParseDuration
// for full list.
func (a *app) Sunrise(offset ...string) Time {
return getSunriseSunset(a, true, offset)
}
// Sunset take an optional string that is passed to time.ParseDuration.
// Examples include "-1.5h", "30m", etc. See https://pkg.go.dev/time#ParseDuration
// for full list.
func (a *app) Sunset(offset ...string) Time {
return getSunriseSunset(a, false, offset)
}
func getSunriseSunset(a *app, sunrise bool, offset []string) Time {
printString := "Sunset"
attrKey := "next_setting"
if sunrise {
printString = "Sunrise"
attrKey = "next_rising"
}
var t time.Duration
var err error
if len(offset) == 1 {
t, err = time.ParseDuration(offset[0])
if err != nil {
log.Fatalf("Could not parse offset passed to %s: \"%s\"", printString, offset[0])
}
if el.toState != "" {
subscribeTriggerMsg.Trigger.To = el.toState
}
log.Default().Println(subscribeTriggerMsg)
ws.WriteMessage(subscribeTriggerMsg, a.conn, a.ctx)
msg, _ := ws.ReadMessage(a.conn, a.ctx)
log.Default().Println(string(msg))
a.entityListenerIds[id] = el.callback
}
// get next sunrise/sunset time from HA
state, err := a.state.Get("sun.sun")
if err != nil {
log.Fatalln("Couldn't get sun.sun state from HA to calculate", printString)
}
nextSetOrRise := carbon.Parse(state.Attributes[attrKey].(string))
log.Default().Println(nextSetOrRise)
// add offset if set, this code works for negative values too
if t.Microseconds() != 0 {
nextSetOrRise = nextSetOrRise.AddMinutes(int(t.Minutes()))
log.Default().Println(nextSetOrRise)
}
return carbon2TimeString(nextSetOrRise)
}
func carbon2TimeString(c carbon.Carbon) Time {
return Time(fmt.Sprintf("%02d:%02d", c.Hour(), c.Minute()))
}
type subEvent struct {
Id int64 `json:"id"`
Type string `json:"type"`
EventType string `json:"event_type"`
}
func (a *app) Start() {
// schedules
go RunSchedules(a)
// subscribe to state_changed events
id := internal.GetId()
e := subEvent{
Id: id,
Type: "subscribe_events",
EventType: "state_changed",
}
ws.WriteMessage(e, a.conn, a.ctx)
a.entityListenersId = id
// entity listeners
elChan := make(chan ws.ChanMsg)
go ws.ListenWebsocket(a.conn, a.ctx, elChan)
log.Default().Println(a.entityListenerIds)
var msg ws.ChanMsg
for {
msg = <-elChan
log.Default().Println(string(msg.Raw))
if callback, ok := a.entityListenerIds[msg.Id]; ok {
log.Default().Println(msg, callback)
if a.entityListenersId == msg.Id {
go callEntityListeners(a, msg.Raw)
}
}

View File

@@ -2,7 +2,6 @@ package main
import (
"log"
"time"
ga "github.com/saml-dev/gome-assistant"
)
@@ -10,31 +9,25 @@ import (
func main() {
app := ga.NewApp("192.168.86.67:8123")
defer app.Cleanup()
s := ga.ScheduleBuilder().Call(lightsOut).Every(time.Second * 5).Build()
s2 := ga.ScheduleBuilder().Call(cool).Every(time.Millisecond * 500).Build()
s3 := ga.ScheduleBuilder().Call(c).Every(time.Minute * 1).Build()
s := ga.ScheduleBuilder().Call(lightsOut).Daily().At(app.Sunset("1h")).Build()
app.RegisterSchedule(s)
app.RegisterSchedule(s2)
app.RegisterSchedule(s3)
simpleListener := ga.EntityListenerBuilder().
EntityIds("light.entryway_lamp").
EntityIds("group.office_ceiling_lights").
Call(listenerCB).
// OnlyBetween(ga.TimeOfDay(22, 00), ga.TimeOfDay(07, 00)).
// OnlyBetween("07:00", "14:00").
Build()
app.RegisterEntityListener(simpleListener)
app.Start()
log.Println(s)
log.Println(s2)
}
func lightsOut(service *ga.Service, state *ga.State) {
// service.InputDatetime.Set("input_datetime.garage_last_triggered_ts", time.Now())
// service.HomeAssistant.Toggle("group.living_room_lamps", map[string]any{"brightness_pct": 100})
// service.Light.Toggle("light.entryway_lamp", map[string]any{"brightness_pct": 100})
service.HomeAssistant.Toggle("light.el_gato_key_lights")
service.HomeAssistant.Toggle("light.entryway_lamp")
log.Default().Println("running lightsOut")
// service.HomeAssistant.Toggle("light.entryway_lamp")
// log.Default().Println("A")
}
@@ -48,4 +41,8 @@ func c(service *ga.Service, state *ga.State) {
// log.Default().Println("C")
}
func listenerCB(service *ga.Service, data *ga.EntityData) {}
func listenerCB(service *ga.Service, data ga.EntityData) {
log.Default().Println("hi katie")
}
// TODO: randomly placed, add .Throttle to Listener

View File

@@ -1,8 +1,12 @@
package gomeassistant
import (
"encoding/json"
"errors"
"time"
"github.com/golang-module/carbon"
i "github.com/saml-dev/gome-assistant/internal"
)
type entityListener struct {
@@ -10,12 +14,12 @@ type entityListener struct {
callback entityListenerCallback
fromState string
toState string
betweenStart time.Duration
betweenEnd time.Duration
betweenStart Time
betweenEnd Time
err error
}
type entityListenerCallback func(*Service, *EntityData)
type entityListenerCallback func(*Service, EntityData)
// TODO: use this to flatten json sent from HA for trigger event
type EntityData struct {
@@ -27,6 +31,27 @@ type EntityData struct {
LastChanged time.Time
}
type stateChangedMsg struct {
ID int `json:"id"`
Type string `json:"type"`
Event struct {
Data struct {
EntityID string `json:"entity_id"`
NewState msgState `json:"new_state"`
OldState msgState `json:"old_state"`
} `json:"data"`
EventType string `json:"event_type"`
Origin string `json:"origin"`
} `json:"event"`
}
type msgState struct {
EntityID string `json:"entity_id"`
LastChanged time.Time `json:"last_changed"`
State string `json:"state"`
Attributes map[string]any `json:"attributes"`
}
type triggerMsg struct {
Id int64 `json:"id"`
Type string `json:"type"`
@@ -60,7 +85,7 @@ type subscribeMsgTrigger struct {
To string `json:"to"`
}
/* Builders */
/* Methods */
func EntityListenerBuilder() elBuilder1 {
return elBuilder1{entityListener{}}
@@ -94,7 +119,7 @@ type elBuilder3 struct {
entityListener
}
func (b elBuilder3) OnlyBetween(start time.Duration, end time.Duration) elBuilder3 {
func (b elBuilder3) OnlyBetween(start Time, end Time) elBuilder3 {
b.entityListener.betweenStart = start
b.entityListener.betweenEnd = end
return b
@@ -113,3 +138,64 @@ func (b elBuilder3) ToState(s string) elBuilder3 {
func (b elBuilder3) Build() entityListener {
return b.entityListener
}
/* Functions */
func callEntityListeners(app *app, msgBytes []byte) {
msg := stateChangedMsg{}
json.Unmarshal(msgBytes, &msg)
data := msg.Event.Data
eid := data.EntityID
listeners, ok := app.entityListeners[eid]
if !ok {
// no listeners registered for this id
return
}
for _, l := range listeners {
// if betweenStart and betweenEnd both set, first account for midnight
// overlap, then only run if between those times.
if l.betweenStart != "" && l.betweenEnd != "" {
start := i.ParseTime(l.betweenStart)
end := i.ParseTime(l.betweenEnd)
// check for midnight overlap
if end.Lt(start) { // example turn on night lights when motion from 23:00 to 07:00
if end.IsPast() { // such as at 15:00, 22:00
end = end.AddDay()
} else {
start = start.SubDay() // such as at 03:00, 05:00
}
}
// skip callback if not inside the range
if !carbon.Now().BetweenIncludedStart(start, end) {
return
}
}
// otherwise, just check if before/after the individual times
if l.betweenStart != "" && i.ParseTime(l.betweenStart).IsFuture() {
return
}
if l.betweenEnd != "" && i.ParseTime(l.betweenEnd).IsPast() {
return
}
// don't run callback if fromState or toState are set and don't match
if l.fromState != "" && l.fromState != data.OldState.State {
return
}
if l.toState != "" && l.toState != data.NewState.State {
return
}
entityData := EntityData{
TriggerEntityId: eid,
FromState: data.OldState.State,
FromAttributes: data.OldState.Attributes,
ToState: data.NewState.State,
ToAttributes: data.NewState.Attributes,
LastChanged: data.OldState.LastChanged,
}
l.callback(app.service, entityData)
}
}

13
go.mod
View File

@@ -2,4 +2,15 @@ module github.com/saml-dev/gome-assistant
go 1.19
require github.com/gorilla/websocket v1.5.0
require (
github.com/golang-module/carbon v1.6.9
github.com/gorilla/websocket v1.5.0
)
require (
github.com/gobuffalo/envy v1.7.0 // indirect
github.com/gobuffalo/packd v0.3.0 // indirect
github.com/gobuffalo/packr v1.30.1 // indirect
github.com/joho/godotenv v1.3.0 // indirect
github.com/rogpeppe/go-internal v1.3.0 // indirect
)

74
go.sum
View File

@@ -1,2 +1,76 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
github.com/golang-module/carbon v1.6.9 h1:fobotpw4zUvU1ZPXLOe6qn5l5zSbiKeJNJSIBeUHgJo=
github.com/golang-module/carbon v1.6.9/go.mod h1:M/TDTYPp3qWtW68u49dLDJOyGmls6L6BXdo/pyvkMaU=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,8 +1,23 @@
package internal
import (
"log"
"time"
"github.com/golang-module/carbon"
)
var id int64 = 0
func GetId() int64 {
id += 1
return id
}
func ParseTime[T ~string](s T) carbon.Carbon {
t, err := time.Parse("15:04", string(s))
if err != nil {
log.Fatalf("Failed to parse time string \"%s\"; format must be HH:MM.", s)
}
return carbon.Now().StartOfDay().SetHour(t.Hour()).SetMinute(t.Minute())
}

View File

@@ -3,7 +3,6 @@ package services
import (
"context"
"fmt"
"log"
"github.com/gorilla/websocket"
"github.com/saml-dev/gome-assistant/internal"
@@ -36,7 +35,6 @@ type BaseServiceRequest struct {
func NewBaseServiceRequest(entityId string) BaseServiceRequest {
id := internal.GetId()
log.Default().Println("service id", id)
bsr := BaseServiceRequest{
Id: fmt.Sprint(id),
RequestType: "call_service",

View File

@@ -20,7 +20,6 @@ type ChanMsg struct {
func ListenWebsocket(conn *websocket.Conn, ctx context.Context, c chan ChanMsg) {
for {
// log.Default().Println("reading message")
bytes, _ := ReadMessage(conn, ctx)
base := BaseMessage{}
json.Unmarshal(bytes, &base)

View File

@@ -2,66 +2,13 @@ package gomeassistant
import (
"fmt"
"log"
"reflect"
"runtime"
"time"
"github.com/saml-dev/gome-assistant/internal"
)
type sunriseSunset struct {
base time.Duration
addition time.Duration
subtraction time.Duration
}
func Sunrise() *sunriseSunset {
return &sunriseSunset{
base: TimeOfDay(0, 10000),
addition: TimeOfDay(0, 0),
subtraction: TimeOfDay(0, 0),
}
}
func Sunset() *sunriseSunset {
return &sunriseSunset{
base: TimeOfDay(0, 20000),
addition: TimeOfDay(0, 0),
subtraction: TimeOfDay(0, 0),
}
}
func (ss *sunriseSunset) Add(hm time.Duration) *sunriseSunset {
ss.addition = hm
return ss
}
func (ss *sunriseSunset) Subtract(hm time.Duration) *sunriseSunset {
ss.subtraction = hm
return ss
}
func (ss *sunriseSunset) Minutes() float64 {
return ss.base.Minutes() + ss.addition.Minutes() - ss.subtraction.Minutes()
}
type timeOfDay interface {
// Time represented as number of Minutes
// after midnight. E.g. 02:00 would be 120.
Minutes() float64
}
// TimeOfDay is a helper function to easily represent
// a time of day as a time.Duration since midnight.
func TimeOfDay(hour, minute int) time.Duration {
return time.Hour*time.Duration(hour) + time.Minute*time.Duration(minute)
}
// Duration is a wrapper for TimeOfDay that makes
// semantic sense when used with Every()
func Duration(hour, minute int) time.Duration {
return TimeOfDay(hour, minute)
}
type scheduleCallback func(*Service, *State)
type schedule struct {
@@ -125,7 +72,7 @@ func ScheduleBuilder() scheduleBuilder {
return scheduleBuilder{
schedule{
frequency: 0,
offset: TimeOfDay(0, 0),
offset: 0,
},
}
}
@@ -162,8 +109,10 @@ func (sb scheduleBuilderCall) Daily() scheduleBuilderDaily {
return scheduleBuilderDaily(sb)
}
func (sb scheduleBuilderDaily) At(t timeOfDay) scheduleBuilderEnd {
sb.schedule.offset = convertTimeOfDayToActualOffset(t)
// At takes a string 24hr format time like "15:30".
func (sb scheduleBuilderDaily) At(s Time) scheduleBuilderEnd {
t := internal.ParseTime(s)
sb.schedule.offset = time.Duration(t.Hour())*time.Hour + time.Duration(t.Minute())*time.Minute
return scheduleBuilderEnd(sb)
}
@@ -189,28 +138,6 @@ func getFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
func convertTimeOfDayToActualOffset(t timeOfDay) time.Duration {
mins := t.Minutes()
if mins > 15000 {
// TODO: same as below but w/ sunset
// don't forget to subtract 20000 here
return TimeOfDay(0, 0)
} else if mins > 5000 {
// TODO: use httpClient to get state of sun.sun
// to get next sunrise time
// don't forget to subtract 10000 here to get +- from sunrise that user requested
// 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 mins >= 1440 {
log.Fatalln("Offset (set via At() or Offset()) cannot be more than 1 day (23h59m)")
}
return TimeOfDay(0, int(mins))
}
// app.Start() functions
func RunSchedules(a *app) {
if a.schedules.Len() == 0 {

View File

@@ -1,20 +1,35 @@
package gomeassistant
import "github.com/saml-dev/gome-assistant/internal/http"
import (
"encoding/json"
"time"
"github.com/saml-dev/gome-assistant/internal/http"
)
// State is used to retrieve state from Home Assistant.
type State struct {
httpClient *http.HttpClient
}
type EntityState struct {
EntityID string `json:"entity_id"`
State string `json:"state"`
Attributes map[string]any `json:"attributes"`
LastChanged time.Time `json:"last_changed"`
LastUpdated time.Time `json:"last_updated"`
}
func NewState(c *http.HttpClient) *State {
return &State{httpClient: c}
}
func (s *State) Get(entityId string) (string, error) {
func (s *State) Get(entityId string) (EntityState, error) {
resp, err := s.httpClient.GetState(entityId)
if err != nil {
return "", err
return EntityState{}, err
}
return string(resp), nil
es := EntityState{}
json.Unmarshal(resp, &es)
return es, nil
}