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

107
app.go
View File

@@ -2,10 +2,12 @@ package gomeassistant
import ( import (
"context" "context"
"fmt"
"log" "log"
"os" "os"
"time" "time"
"github.com/golang-module/carbon"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/saml-dev/gome-assistant/internal" "github.com/saml-dev/gome-assistant/internal"
"github.com/saml-dev/gome-assistant/internal/http" "github.com/saml-dev/gome-assistant/internal/http"
@@ -24,9 +26,15 @@ type app struct {
schedules pq.PriorityQueue schedules pq.PriorityQueue
entityListeners map[string][]entityListener 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 NewApp establishes the websocket connection and returns an object
you can use to register schedules and listeners. you can use to register schedules and listeners.
@@ -49,7 +57,6 @@ func NewApp(connString string) app {
state: state, state: state,
schedules: pq.New(), schedules: pq.New(),
entityListeners: map[string][]entityListener{}, entityListeners: map[string][]entityListener{},
entityListenerIds: map[int64]entityListenerCallback{},
} }
} }
@@ -71,10 +78,9 @@ func (a *app) RegisterSchedule(s schedule) {
// TODO: consider moving all time stuff to carbon? // TODO: consider moving all time stuff to carbon?
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.Minutes() > 0 { 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 // 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) { func (a *app) RegisterEntityListener(el entityListener) {
for _, entity := range el.entityIds { for _, entity := range el.entityIds {
id := internal.GetId() if elList, ok := a.entityListeners[entity]; ok {
subscribeTriggerMsg := subscribeMsg{ a.entityListeners[entity] = append(elList, el)
Id: id, } else {
Type: "subscribe_trigger", a.entityListeners[entity] = []entityListener{el}
Trigger: subscribeMsgTrigger{
Platform: "state",
EntityId: entity,
},
} }
if el.fromState != "" {
subscribeTriggerMsg.Trigger.From = el.fromState
} }
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
} }
// 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])
}
}
// 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() { func (a *app) Start() {
// schedules // schedules
go RunSchedules(a) 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 // entity listeners
elChan := make(chan ws.ChanMsg) elChan := make(chan ws.ChanMsg)
go ws.ListenWebsocket(a.conn, a.ctx, elChan) go ws.ListenWebsocket(a.conn, a.ctx, elChan)
log.Default().Println(a.entityListenerIds)
var msg ws.ChanMsg var msg ws.ChanMsg
for { for {
msg = <-elChan msg = <-elChan
log.Default().Println(string(msg.Raw)) if a.entityListenersId == msg.Id {
if callback, ok := a.entityListenerIds[msg.Id]; ok { go callEntityListeners(a, msg.Raw)
log.Default().Println(msg, callback)
} }
} }

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"log" "log"
"time"
ga "github.com/saml-dev/gome-assistant" ga "github.com/saml-dev/gome-assistant"
) )
@@ -10,31 +9,25 @@ import (
func main() { func main() {
app := ga.NewApp("192.168.86.67:8123") app := ga.NewApp("192.168.86.67:8123")
defer app.Cleanup() defer app.Cleanup()
s := ga.ScheduleBuilder().Call(lightsOut).Every(time.Second * 5).Build() s := ga.ScheduleBuilder().Call(lightsOut).Daily().At(app.Sunset("1h")).Build()
s2 := ga.ScheduleBuilder().Call(cool).Every(time.Millisecond * 500).Build()
s3 := ga.ScheduleBuilder().Call(c).Every(time.Minute * 1).Build()
app.RegisterSchedule(s) app.RegisterSchedule(s)
app.RegisterSchedule(s2)
app.RegisterSchedule(s3)
simpleListener := ga.EntityListenerBuilder(). simpleListener := ga.EntityListenerBuilder().
EntityIds("light.entryway_lamp"). EntityIds("group.office_ceiling_lights").
Call(listenerCB). Call(listenerCB).
// OnlyBetween(ga.TimeOfDay(22, 00), ga.TimeOfDay(07, 00)). // OnlyBetween("07:00", "14:00").
Build() Build()
app.RegisterEntityListener(simpleListener) app.RegisterEntityListener(simpleListener)
app.Start() app.Start()
log.Println(s)
log.Println(s2)
} }
func lightsOut(service *ga.Service, state *ga.State) { func lightsOut(service *ga.Service, state *ga.State) {
// service.InputDatetime.Set("input_datetime.garage_last_triggered_ts", time.Now()) // 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.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.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") // service.HomeAssistant.Toggle("light.entryway_lamp")
// log.Default().Println("A") // log.Default().Println("A")
} }
@@ -48,4 +41,8 @@ func c(service *ga.Service, state *ga.State) {
// log.Default().Println("C") // 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 package gomeassistant
import ( import (
"encoding/json"
"errors" "errors"
"time" "time"
"github.com/golang-module/carbon"
i "github.com/saml-dev/gome-assistant/internal"
) )
type entityListener struct { type entityListener struct {
@@ -10,12 +14,12 @@ type entityListener struct {
callback entityListenerCallback callback entityListenerCallback
fromState string fromState string
toState string toState string
betweenStart time.Duration betweenStart Time
betweenEnd time.Duration betweenEnd Time
err error err error
} }
type entityListenerCallback func(*Service, *EntityData) type entityListenerCallback func(*Service, EntityData)
// TODO: use this to flatten json sent from HA for trigger event // TODO: use this to flatten json sent from HA for trigger event
type EntityData struct { type EntityData struct {
@@ -27,6 +31,27 @@ type EntityData struct {
LastChanged time.Time 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 { type triggerMsg struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Type string `json:"type"` Type string `json:"type"`
@@ -60,7 +85,7 @@ type subscribeMsgTrigger struct {
To string `json:"to"` To string `json:"to"`
} }
/* Builders */ /* Methods */
func EntityListenerBuilder() elBuilder1 { func EntityListenerBuilder() elBuilder1 {
return elBuilder1{entityListener{}} return elBuilder1{entityListener{}}
@@ -94,7 +119,7 @@ type elBuilder3 struct {
entityListener 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.betweenStart = start
b.entityListener.betweenEnd = end b.entityListener.betweenEnd = end
return b return b
@@ -113,3 +138,64 @@ func (b elBuilder3) ToState(s string) elBuilder3 {
func (b elBuilder3) Build() entityListener { func (b elBuilder3) Build() entityListener {
return b.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 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 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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 package internal
import (
"log"
"time"
"github.com/golang-module/carbon"
)
var id int64 = 0 var id int64 = 0
func GetId() int64 { func GetId() int64 {
id += 1 id += 1
return id 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 ( import (
"context" "context"
"fmt" "fmt"
"log"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/saml-dev/gome-assistant/internal" "github.com/saml-dev/gome-assistant/internal"
@@ -36,7 +35,6 @@ type BaseServiceRequest struct {
func NewBaseServiceRequest(entityId string) BaseServiceRequest { func NewBaseServiceRequest(entityId string) BaseServiceRequest {
id := internal.GetId() id := internal.GetId()
log.Default().Println("service id", id)
bsr := BaseServiceRequest{ bsr := BaseServiceRequest{
Id: fmt.Sprint(id), Id: fmt.Sprint(id),
RequestType: "call_service", RequestType: "call_service",

View File

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

View File

@@ -2,66 +2,13 @@ package gomeassistant
import ( import (
"fmt" "fmt"
"log"
"reflect" "reflect"
"runtime" "runtime"
"time" "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 scheduleCallback func(*Service, *State)
type schedule struct { type schedule struct {
@@ -125,7 +72,7 @@ func ScheduleBuilder() scheduleBuilder {
return scheduleBuilder{ return scheduleBuilder{
schedule{ schedule{
frequency: 0, frequency: 0,
offset: TimeOfDay(0, 0), offset: 0,
}, },
} }
} }
@@ -162,8 +109,10 @@ func (sb scheduleBuilderCall) Daily() scheduleBuilderDaily {
return scheduleBuilderDaily(sb) return scheduleBuilderDaily(sb)
} }
func (sb scheduleBuilderDaily) At(t timeOfDay) scheduleBuilderEnd { // At takes a string 24hr format time like "15:30".
sb.schedule.offset = convertTimeOfDayToActualOffset(t) 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) return scheduleBuilderEnd(sb)
} }
@@ -189,28 +138,6 @@ func getFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() 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 // app.Start() functions
func RunSchedules(a *app) { func RunSchedules(a *app) {
if a.schedules.Len() == 0 { if a.schedules.Len() == 0 {

View File

@@ -1,20 +1,35 @@
package gomeassistant 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. // State is used to retrieve state from Home Assistant.
type State struct { type State struct {
httpClient *http.HttpClient 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 { func NewState(c *http.HttpClient) *State {
return &State{httpClient: c} 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) resp, err := s.httpClient.GetState(entityId)
if err != nil { if err != nil {
return "", err return EntityState{}, err
} }
return string(resp), nil es := EntityState{}
json.Unmarshal(resp, &es)
return es, nil
} }