event listeners working

This commit is contained in:
Sam Lewis
2022-10-19 00:25:15 -04:00
parent ccd6805d40
commit 83789184f0
8 changed files with 278 additions and 60 deletions

36
app.go
View File

@@ -27,6 +27,7 @@ type app struct {
schedules pq.PriorityQueue
entityListeners map[string][]*entityListener
entityListenersId int64
eventListeners map[string][]*eventListener
}
type TimeString string
@@ -53,6 +54,7 @@ func NewApp(connString string) app {
state: state,
schedules: pq.New(),
entityListeners: map[string][]*entityListener{},
eventListeners: map[string][]*eventListener{},
}
}
@@ -83,12 +85,23 @@ func (a *app) RegisterSchedule(s schedule) {
a.schedules.Insert(s, float64(startTime.Unix()))
}
func (a *app) RegisterEntityListener(el entityListener) {
for _, entity := range el.entityIds {
func (a *app) RegisterEntityListener(etl entityListener) {
for _, entity := range etl.entityIds {
if elList, ok := a.entityListeners[entity]; ok {
a.entityListeners[entity] = append(elList, &el)
a.entityListeners[entity] = append(elList, &etl)
} else {
a.entityListeners[entity] = []*entityListener{&el}
a.entityListeners[entity] = []*entityListener{&etl}
}
}
}
func (a *app) RegisterEventListener(evl eventListener) {
for _, eventType := range evl.eventTypes {
if elList, ok := a.eventListeners[eventType]; ok {
a.eventListeners[eventType] = append(elList, &evl)
} else {
ws.SubscribeToEventType(eventType, a.conn, a.ctx)
a.eventListeners[eventType] = []*eventListener{&evl}
}
}
}
@@ -144,24 +157,13 @@ func carbon2TimeString(c carbon.Carbon) string {
return 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)
ws.SubscribeToStateChangedEvents(id, a.conn, a.ctx)
a.entityListenersId = id
// entity listeners
@@ -173,6 +175,8 @@ func (a *app) Start() {
msg = <-elChan
if a.entityListenersId == msg.Id {
go callEntityListeners(a, msg.Raw)
} else {
go callEventListeners(a, msg)
}
}
}

View File

@@ -2,12 +2,10 @@ package gomeassistant
import (
"encoding/json"
"errors"
"log"
"time"
"github.com/golang-module/carbon"
i "github.com/saml-dev/gome-assistant/internal"
)
type entityListener struct {
@@ -19,7 +17,6 @@ type entityListener struct {
betweenEnd string
throttle time.Duration
lastRan carbon.Carbon
err error
}
type entityListenerCallback func(*Service, EntityData)
@@ -68,7 +65,7 @@ type elBuilder1 struct {
func (b elBuilder1) EntityIds(entityIds ...string) elBuilder2 {
if len(entityIds) == 0 {
b.err = errors.New("must pass at least one entityId to EntityIds()")
log.Fatalln("must pass at least one entityId to EntityIds()")
} else {
b.entityListener.entityIds = entityIds
}
@@ -80,9 +77,7 @@ type elBuilder2 struct {
}
func (b elBuilder2) Call(callback entityListenerCallback) elBuilder3 {
if b.err == nil {
b.entityListener.callback = callback
}
return elBuilder3(b)
}
@@ -142,44 +137,17 @@ func callEntityListeners(app *app, msgBytes []byte) {
}
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) {
// Check conditions
if c := CheckWithinTimeRange(l.betweenStart, l.betweenEnd); c.fail {
return
}
// otherwise just check individual before/after
} else if l.betweenStart != "" && i.ParseTime(l.betweenStart).IsFuture() {
return
} else if l.betweenEnd != "" && i.ParseTime(l.betweenEnd).IsPast() {
if c := CheckStatesMatch(l.fromState, data.OldState.State); c.fail {
return
}
// don't run callback if fromState or toState are set and don't match
if l.fromState != "" && l.fromState != data.OldState.State {
if c := CheckStatesMatch(l.toState, data.NewState.State); c.fail {
return
}
if l.toState != "" && l.toState != data.NewState.State {
return
}
// don't run callback if Throttle is set and that duration hasn't passed since lastRan
if l.throttle.Seconds() > 0 &&
l.lastRan.DiffAbsInSeconds(carbon.Now()) < int64(l.throttle.Seconds()) {
if c := CheckThrottle(l.throttle, l.lastRan); c.fail {
return
}

View File

@@ -1,4 +1,122 @@
package gomeassistant
// TODO: impl eventListener. could probably create generic listener struct for
// code reuse between eventListener and entityListener
// code reuse between eventListener and eventListener
import (
"encoding/json"
"log"
"time"
"github.com/golang-module/carbon"
ws "github.com/saml-dev/gome-assistant/internal/websocket"
)
type eventListener struct {
eventTypes []string
callback eventListenerCallback
betweenStart string
betweenEnd string
throttle time.Duration
lastRan carbon.Carbon
}
type eventListenerCallback func(*Service, EventData)
type EventData struct {
Type string
RawEventJSON []byte
}
/* Methods */
func EventListenerBuilder() eventListenerBuilder1 {
return eventListenerBuilder1{eventListener{
lastRan: carbon.Now().StartOfCentury(),
}}
}
type eventListenerBuilder1 struct {
eventListener
}
func (b eventListenerBuilder1) EventType(ets ...string) eventListenerBuilder2 {
b.eventTypes = ets
return eventListenerBuilder2(b)
}
type eventListenerBuilder2 struct {
eventListener
}
func (b eventListenerBuilder2) Call(callback eventListenerCallback) eventListenerBuilder3 {
b.eventListener.callback = callback
return eventListenerBuilder3(b)
}
type eventListenerBuilder3 struct {
eventListener
}
func (b eventListenerBuilder3) OnlyBetween(start string, end string) eventListenerBuilder3 {
b.eventListener.betweenStart = start
b.eventListener.betweenEnd = end
return b
}
func (b eventListenerBuilder3) OnlyAfter(start string) eventListenerBuilder3 {
b.eventListener.betweenStart = start
return b
}
func (b eventListenerBuilder3) OnlyBefore(end string) eventListenerBuilder3 {
b.eventListener.betweenEnd = end
return b
}
func (b eventListenerBuilder3) Throttle(s TimeString) eventListenerBuilder3 {
d, err := time.ParseDuration(string(s))
if err != nil {
log.Fatalf("Couldn't parse string duration passed to Throttle(): \"%s\" see https://pkg.go.dev/time#ParseDuration for valid time units", s)
}
b.eventListener.throttle = d
return b
}
func (b eventListenerBuilder3) Build() eventListener {
return b.eventListener
}
type BaseEventMsg struct {
Event struct {
EventType string `json:"event_type"`
} `json:"event"`
}
/* Functions */
func callEventListeners(app *app, msg ws.ChanMsg) {
baseEventMsg := BaseEventMsg{}
json.Unmarshal(msg.Raw, &baseEventMsg)
listeners, ok := app.eventListeners[baseEventMsg.Event.EventType]
if !ok {
// no listeners registered for this event type
return
}
for _, l := range listeners {
// Check conditions
if c := CheckWithinTimeRange(l.betweenStart, l.betweenEnd); c.fail {
return
}
if c := CheckThrottle(l.throttle, l.lastRan); c.fail {
return
}
eventData := EventData{
Type: baseEventMsg.Event.EventType,
RawEventJSON: msg.Raw,
}
go l.callback(app.service, eventData)
l.lastRan = carbon.Now()
}
}

View File

@@ -14,23 +14,34 @@ func main() {
EntityIds("binary_sensor.pantry_door").
Call(pantryLights).
Build()
zwaveEventListener := ga.
EventListenerBuilder().
EventType("zwave_js_value_notification").
Call(onEvent).
Build()
app.RegisterEntityListener(pantryDoor)
app.RegisterSchedule(ga.ScheduleBuilder().Call(cool).Every("5s").Build())
app.RegisterEventListener(zwaveEventListener)
app.Start()
}
func pantryLights(service *ga.Service, data ga.EntityData) {
l := "group.kitchen_ceiling_lights"
// 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})
if data.ToState == "on" {
service.HomeAssistant.TurnOn("switch.pantry_light_2")
service.HomeAssistant.TurnOn(l)
} else {
service.HomeAssistant.TurnOff("switch.pantry_light_2")
service.HomeAssistant.TurnOff(l)
}
}
func onEvent(service *ga.Service, data ga.EventData) {
service.HomeAssistant.Toggle("light.el_gato_key_lights")
}
func cool(service *ga.Service, state *ga.State) {
// service.InputDatetime.Set("input_datetime.garage_last_triggered_ts", time.Now())
// service.Light.TurnOn("light.entryway_lamp")
@@ -42,5 +53,5 @@ func c(service *ga.Service, state *ga.State) {
}
func listenerCB(service *ga.Service, data ga.EntityData) {
log.Default().Println("hi katie")
log.Default().Println("hi")
}

View File

@@ -23,7 +23,6 @@ func ListenWebsocket(conn *websocket.Conn, ctx context.Context, c chan ChanMsg)
bytes, _ := ReadMessage(conn, ctx)
base := BaseMessage{}
json.Unmarshal(bytes, &base)
chanMsg := ChanMsg{
Type: base.Type,
Id: base.Id,

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/gorilla/websocket"
i "github.com/saml-dev/gome-assistant/internal"
)
type AuthMessage struct {
@@ -106,3 +107,33 @@ func VerifyAuthResponse(conn *websocket.Conn, ctx context.Context) error {
return nil
}
type SubEvent struct {
Id int64 `json:"id"`
Type string `json:"type"`
EventType string `json:"event_type"`
}
func SubscribeToStateChangedEvents(id int64, conn *websocket.Conn, ctx context.Context) {
SubscribeToEventType("state_changed", conn, ctx, id)
}
func SubscribeToEventType(eventType string, conn *websocket.Conn, ctx context.Context, id ...int64) {
var finalId int64
if len(id) == 0 {
finalId = i.GetId()
} else {
finalId = id[0]
}
e := SubEvent{
Id: finalId,
Type: "subscribe_events",
EventType: eventType,
}
err := WriteMessage(e, conn, ctx)
if err != nil {
log.Fatalln("Error writing to websocket: ", err)
}
// m, _ := ReadMessage(conn, ctx)
// log.Default().Println(string(m))
}

62
listeners.go Normal file
View File

@@ -0,0 +1,62 @@
package gomeassistant
import (
"time"
"github.com/golang-module/carbon"
i "github.com/saml-dev/gome-assistant/internal"
)
type conditionCheck struct {
fail bool
}
func CheckWithinTimeRange(startTime, endTime string) conditionCheck {
cc := conditionCheck{fail: false}
// if betweenStart and betweenEnd both set, first account for midnight
// overlap, then check if between those times.
if startTime != "" && endTime != "" {
parsedStart := i.ParseTime(startTime)
parsedEnd := i.ParseTime(endTime)
// check for midnight overlap
if parsedEnd.Lt(parsedStart) { // example turn on night lights when motion from 23:00 to 07:00
if parsedEnd.IsPast() { // such as at 15:00, 22:00
parsedEnd = parsedEnd.AddDay()
} else {
parsedStart = parsedStart.SubDay() // such as at 03:00, 05:00
}
}
// skip callback if not inside the range
if !carbon.Now().BetweenIncludedStart(parsedStart, parsedEnd) {
cc.fail = true
}
// otherwise just check individual before/after
} else if startTime != "" && i.ParseTime(startTime).IsFuture() {
cc.fail = true
} else if endTime != "" && i.ParseTime(endTime).IsPast() {
cc.fail = true
}
return cc
}
func CheckStatesMatch(listenerState, s string) conditionCheck {
cc := conditionCheck{fail: false}
// check if fromState or toState are set and don't match
if listenerState != "" && listenerState != s {
cc.fail = true
}
return cc
}
func CheckThrottle(throttle time.Duration, lastRan carbon.Carbon) conditionCheck {
cc := conditionCheck{fail: false}
// check if Throttle is set and that duration hasn't passed since lastRan
if throttle.Seconds() > 0 &&
lastRan.DiffAbsInSeconds(carbon.Now()) < int64(throttle.Seconds()) {
cc.fail = true
}
return cc
}

25
types.go Normal file
View File

@@ -0,0 +1,25 @@
package gomeassistant
import "time"
type EventZWaveJSValueNotification struct {
EventType string `yaml:"event_type"`
Data struct {
Domain string `yaml:"domain"`
NodeID int `yaml:"node_id"`
HomeID int64 `yaml:"home_id"`
Endpoint int `yaml:"endpoint"`
DeviceID string `yaml:"device_id"`
CommandClass int `yaml:"command_class"`
CommandClassName string `yaml:"command_class_name"`
Label string `yaml:"label"`
Property string `yaml:"property"`
PropertyName string `yaml:"property_name"`
PropertyKey string `yaml:"property_key"`
PropertyKeyName string `yaml:"property_key_name"`
Value string `yaml:"value"`
ValueRaw int `yaml:"value_raw"`
} `yaml:"data"`
Origin string `yaml:"origin"`
TimeFired time.Time `yaml:"time_fired"`
}