diff --git a/app.go b/app.go index 464be37..3858719 100644 --- a/app.go +++ b/app.go @@ -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) } } diff --git a/cmd/main/testing.go b/cmd/main/testing.go index 8486e4d..b07d93b 100644 --- a/cmd/main/testing.go +++ b/cmd/main/testing.go @@ -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 diff --git a/entitylistener.go b/entitylistener.go index 2428461..f284c05 100644 --- a/entitylistener.go +++ b/entitylistener.go @@ -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) + } +} diff --git a/go.mod b/go.mod index a20c756..e510c14 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum index e5a03d4..f85c10e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/internal.go b/internal/internal.go index 32e5ad2..2f1f9a2 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -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()) +} diff --git a/internal/services/services.go b/internal/services/services.go index d5da2fc..256a0ae 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -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", diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index 3d87604..cc16d92 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -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) diff --git a/schedule.go b/schedule.go index c11f139..85baf18 100644 --- a/schedule.go +++ b/schedule.go @@ -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 { diff --git a/state.go b/state.go index f901be6..ba153d1 100644 --- a/state.go +++ b/state.go @@ -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 }