From a06f62d1badc56e36d8f41d530f05aa42f3ff89a Mon Sep 17 00:00:00 2001 From: Sam Lewis Date: Sat, 7 Oct 2023 14:02:24 -0400 Subject: [PATCH] add enable/disable function to every builder --- listeners.go => checkers.go | 32 ++++++++++++++ entitylistener.go | 48 +++++++++++++++++++++ eventListener.go | 48 +++++++++++++++++++++ interval.go | 83 +++++++++++++++++++++++++++++-------- schedule.go | 47 +++++++++++++++++++++ state.go | 2 +- 6 files changed, 241 insertions(+), 19 deletions(-) rename listeners.go => checkers.go (81%) diff --git a/listeners.go b/checkers.go similarity index 81% rename from listeners.go rename to checkers.go index 6464fbb..aa09c64 100644 --- a/listeners.go +++ b/checkers.go @@ -86,6 +86,38 @@ func checkExceptionRanges(eList []timeRange) conditionCheck { return cc } +func checkEnabledEntity(s *State, eid, expectedState string, runOnNetworkError bool) conditionCheck { + cc := conditionCheck{fail: false} + if eid == "" || expectedState == "" { + return cc + } + + matches, err := s.Equals(eid, expectedState) + if err != nil { + cc.fail = !runOnNetworkError + return cc + } + + cc.fail = !matches + return cc +} + +func checkDisabledEntity(s *State, eid, expectedState string, runOnNetworkError bool) conditionCheck { + cc := conditionCheck{fail: false} + if eid == "" || expectedState == "" { + return cc + } + + matches, err := s.Equals(eid, expectedState) + if err != nil { + cc.fail = !runOnNetworkError + return cc + } + + cc.fail = matches + return cc +} + func checkAllowlistDates(eList []time.Time) conditionCheck { if len(eList) == 0 { return conditionCheck{fail: false} diff --git a/entitylistener.go b/entitylistener.go index 272dbbf..67ab949 100644 --- a/entitylistener.go +++ b/entitylistener.go @@ -2,6 +2,7 @@ package gomeassistant import ( "encoding/json" + "fmt" "time" "github.com/golang-module/carbon" @@ -27,6 +28,13 @@ type EntityListener struct { runOnStartup bool runOnStartupCompleted bool + + enabledEntity string + enabledEntityState string + enabledEntityRunOnError bool + disabledEntity string + disabledEntityState string + disabledEntityRunOnError bool } type EntityListenerCallback func(*Service, *State, EntityData) @@ -148,6 +156,40 @@ func (b elBuilder3) RunOnStartup() elBuilder3 { return b } +/* +Enable this listener only when the current state of {entityId} matches {state}. +If there is a network error while retrieving state, the listener runs if {runOnNetworkError} is true. +*/ +func (b elBuilder3) EnabledEntity(entityId, state string, runOnNetworkError bool) elBuilder3 { + if entityId == "" || state == "" { + panic(fmt.Sprintf("Either entityId or state is empty in EnabledEntity entityId='%s' state='%s'", entityId, state)) + } + if b.entityListener.enabledEntity != "" { + panic(fmt.Sprintf("You can't use EnabledEntity and DisabledEntity together. Error occurred while setting EnabledEntity on an entity listener with params entityId=%s state=%s runOnNetworkError=%t", entityId, state, runOnNetworkError)) + } + b.entityListener.enabledEntity = entityId + b.entityListener.enabledEntityState = state + b.entityListener.enabledEntityRunOnError = runOnNetworkError + return b +} + +/* +Disable this listener when the current state of {entityId} matches {state}. +If there is a network error while retrieving state, the listener runs if {runOnNetworkError} is true. +*/ +func (b elBuilder3) DisabledEntity(entityId, state string, runOnNetworkError bool) elBuilder3 { + if entityId == "" || state == "" { + panic(fmt.Sprintf("Either entityId or state is empty in EnabledEntity entityId='%s' state='%s'", entityId, state)) + } + if b.entityListener.enabledEntity != "" { + panic(fmt.Sprintf("You can't use EnabledEntity and DisabledEntity together. Error occurred while setting DisabledEntity on an entity listener with params entityId=%s state=%s runOnNetworkError=%t", entityId, state, runOnNetworkError)) + } + b.entityListener.disabledEntity = entityId + b.entityListener.disabledEntityState = state + b.entityListener.disabledEntityRunOnError = runOnNetworkError + return b +} + func (b elBuilder3) Build() EntityListener { return b.entityListener } @@ -195,6 +237,12 @@ func callEntityListeners(app *App, msgBytes []byte) { if c := checkExceptionRanges(l.exceptionRanges); c.fail { continue } + if c := checkEnabledEntity(app.state, l.enabledEntity, l.enabledEntityState, l.enabledEntityRunOnError); c.fail { + continue + } + if c := checkDisabledEntity(app.state, l.disabledEntity, l.disabledEntityState, l.disabledEntityRunOnError); c.fail { + continue + } entityData := EntityData{ TriggerEntityId: eid, diff --git a/eventListener.go b/eventListener.go index cc4543e..7c096de 100644 --- a/eventListener.go +++ b/eventListener.go @@ -2,6 +2,7 @@ package gomeassistant import ( "encoding/json" + "fmt" "time" "github.com/golang-module/carbon" @@ -19,6 +20,13 @@ type EventListener struct { exceptionDates []time.Time exceptionRanges []timeRange + + enabledEntity string + enabledEntityState string + enabledEntityRunOnError bool + disabledEntity string + disabledEntityState string + disabledEntityRunOnError bool } type EventListenerCallback func(*Service, *State, EventData) @@ -90,6 +98,40 @@ func (b eventListenerBuilder3) ExceptionRange(start, end time.Time) eventListene return b } +/* +Enable this listener only when the current state of {entityId} matches {state}. +If there is a network error while retrieving state, the listener runs if {runOnNetworkError} is true. +*/ +func (b eventListenerBuilder3) EnabledEntity(entityId, state string, runOnNetworkError bool) eventListenerBuilder3 { + if entityId == "" || state == "" { + panic(fmt.Sprintf("Either entityId or state is empty in eventListener.EnabledEntity entityId='%s' state='%s' runOnNetworkError='%t'", entityId, state, runOnNetworkError)) + } + if b.eventListener.enabledEntity != "" { + panic(fmt.Sprintf("You can't use EnabledEntity and DisabledEntity together. Error occurred while setting EnabledEntity entityId=%s state=%s runOnNetworkError=%t", entityId, state, runOnNetworkError)) + } + b.eventListener.enabledEntity = entityId + b.eventListener.enabledEntityState = state + b.eventListener.enabledEntityRunOnError = runOnNetworkError + return b +} + +/* +Disable this listener when the current state of {entityId} matches {state}. +If there is a network error while retrieving state, the listener runs if {runOnNetworkError} is true. +*/ +func (b eventListenerBuilder3) DisabledEntity(entityId, state string, runOnNetworkError bool) eventListenerBuilder3 { + if entityId == "" || state == "" { + panic(fmt.Sprintf("Either entityId or state is empty in eventListener.EnabledEntity entityId='%s' state='%s' runOnNetworkError='%t'", entityId, state, runOnNetworkError)) + } + if b.eventListener.enabledEntity != "" { + panic(fmt.Sprintf("You can't use EnabledEntity and DisabledEntity together. Error occurred while setting DisabledEntity entityId=%s state=%s runOnNetworkError=%t", entityId, state, runOnNetworkError)) + } + b.eventListener.disabledEntity = entityId + b.eventListener.disabledEntityState = state + b.eventListener.disabledEntityRunOnError = runOnNetworkError + return b +} + func (b eventListenerBuilder3) Build() EventListener { return b.eventListener } @@ -124,6 +166,12 @@ func callEventListeners(app *App, msg ws.ChanMsg) { if c := checkExceptionRanges(l.exceptionRanges); c.fail { continue } + if c := checkEnabledEntity(app.state, l.enabledEntity, l.enabledEntityState, l.enabledEntityRunOnError); c.fail { + continue + } + if c := checkDisabledEntity(app.state, l.disabledEntity, l.disabledEntityState, l.disabledEntityRunOnError); c.fail { + continue + } eventData := EventData{ Type: baseEventMsg.Event.EventType, diff --git a/interval.go b/interval.go index 64c27c7..300f7ef 100644 --- a/interval.go +++ b/interval.go @@ -18,6 +18,13 @@ type Interval struct { exceptionDates []time.Time exceptionRanges []timeRange + + enabledEntity string + enabledEntityState string + enabledEntityRunOnError bool + disabledEntity string + disabledEntityState string + disabledEntityRunOnError bool } func (i Interval) Hash() string { @@ -69,38 +76,72 @@ func formatStartOrEndString(s TimeString, isStart bool) string { } } -func (sb intervalBuilder) Call(callback IntervalCallback) intervalBuilderCall { - sb.interval.callback = callback - return intervalBuilderCall(sb) +func (ib intervalBuilder) Call(callback IntervalCallback) intervalBuilderCall { + ib.interval.callback = callback + return intervalBuilderCall(ib) } // Takes a DurationString ("2h", "5m", etc) to set the frequency of the interval. -func (sb intervalBuilderCall) Every(s DurationString) intervalBuilderEnd { +func (ib intervalBuilderCall) Every(s DurationString) intervalBuilderEnd { d := internal.ParseDuration(string(s)) - sb.interval.frequency = d - return intervalBuilderEnd(sb) + ib.interval.frequency = d + return intervalBuilderEnd(ib) } // Takes a TimeString ("HH:MM") when this interval will start running for the day. -func (sb intervalBuilderEnd) StartingAt(s TimeString) intervalBuilderEnd { - sb.interval.startTime = s - return sb +func (ib intervalBuilderEnd) StartingAt(s TimeString) intervalBuilderEnd { + ib.interval.startTime = s + return ib } // Takes a TimeString ("HH:MM") when this interval will stop running for the day. -func (sb intervalBuilderEnd) EndingAt(s TimeString) intervalBuilderEnd { - sb.interval.endTime = s - return sb +func (ib intervalBuilderEnd) EndingAt(s TimeString) intervalBuilderEnd { + ib.interval.endTime = s + return ib } -func (sb intervalBuilderEnd) ExceptionDates(t time.Time, tl ...time.Time) intervalBuilderEnd { - sb.interval.exceptionDates = append(tl, t) - return sb +func (ib intervalBuilderEnd) ExceptionDates(t time.Time, tl ...time.Time) intervalBuilderEnd { + ib.interval.exceptionDates = append(tl, t) + return ib } -func (sb intervalBuilderEnd) ExceptionRange(start, end time.Time) intervalBuilderEnd { - sb.interval.exceptionRanges = append(sb.interval.exceptionRanges, timeRange{start, end}) - return sb +func (ib intervalBuilderEnd) ExceptionRange(start, end time.Time) intervalBuilderEnd { + ib.interval.exceptionRanges = append(ib.interval.exceptionRanges, timeRange{start, end}) + return ib +} + +/* +Enable this interval only when the current state of {entityId} matches {state}. +If there is a network error while retrieving state, the interval runs if {runOnNetworkError} is true. +*/ +func (ib intervalBuilderEnd) EnabledEntity(entityId, state string, runOnNetworkError bool) intervalBuilderEnd { + if entityId == "" || state == "" { + panic(fmt.Sprintf("Either entityId or state is empty in EnabledEntity entityId='%s' state='%s'", entityId, state)) + } + if ib.interval.enabledEntity != "" { + panic(fmt.Sprintf("You can't use EnabledEntity and DisabledEntity together. Error occurred while setting EnabledEntity on an entity listener with params entityId=%s state=%s runOnNetworkError=%t", entityId, state, runOnNetworkError)) + } + ib.interval.enabledEntity = entityId + ib.interval.enabledEntityState = state + ib.interval.enabledEntityRunOnError = runOnNetworkError + return ib +} + +/* +Disable this interval when the current state of {entityId} matches {state}. +If there is a network error while retrieving state, the interval runs if {runOnNetworkError} is true. +*/ +func (ib intervalBuilderEnd) DisabledEntity(entityId, state string, runOnNetworkError bool) intervalBuilderEnd { + if entityId == "" || state == "" { + panic(fmt.Sprintf("Either entityId or state is empty in EnabledEntity entityId='%s' state='%s'", entityId, state)) + } + if ib.interval.enabledEntity != "" { + panic(fmt.Sprintf("You can't use EnabledEntity and DisabledEntity together. Error occurred while setting DisabledEntity on an entity listener with params entityId=%s state=%s runOnNetworkError=%t", entityId, state, runOnNetworkError)) + } + ib.interval.disabledEntity = entityId + ib.interval.disabledEntityState = state + ib.interval.disabledEntityRunOnError = runOnNetworkError + return ib } func (sb intervalBuilderEnd) Build() Interval { @@ -143,6 +184,12 @@ func (i Interval) maybeRunCallback(a *App) { if c := checkExceptionRanges(i.exceptionRanges); c.fail { return } + if c := checkEnabledEntity(a.state, i.enabledEntity, i.enabledEntityState, i.enabledEntityRunOnError); c.fail { + return + } + if c := checkDisabledEntity(a.state, i.disabledEntity, i.disabledEntityState, i.disabledEntityRunOnError); c.fail { + return + } go i.callback(a.service, a.state) } diff --git a/schedule.go b/schedule.go index 3716511..7c9a631 100644 --- a/schedule.go +++ b/schedule.go @@ -26,6 +26,13 @@ type DailySchedule struct { exceptionDates []time.Time allowlistDates []time.Time + + enabledEntity string + enabledEntityState string + enabledEntityRunOnError bool + disabledEntity string + disabledEntityState string + disabledEntityRunOnError bool } func (s DailySchedule) Hash() string { @@ -110,6 +117,40 @@ func (sb scheduleBuilderEnd) OnlyOnDates(t time.Time, tl ...time.Time) scheduleB return sb } +/* +Enable this schedule only when the current state of {entityId} matches {state}. +If there is a network error while retrieving state, the schedule runs if {runOnNetworkError} is true. +*/ +func (sb scheduleBuilderEnd) EnabledEntity(entityId, state string, runOnNetworkError bool) scheduleBuilderEnd { + if entityId == "" || state == "" { + panic(fmt.Sprintf("Either entityId or state is empty in EnabledEntity entityId='%s' state='%s'", entityId, state)) + } + if sb.schedule.enabledEntity != "" { + panic(fmt.Sprintf("You can't use EnabledEntity and DisabledEntity together. Error occurred while setting EnabledEntity on a schedule with params entityId=%s state=%s runOnNetworkError=%t", entityId, state, runOnNetworkError)) + } + sb.schedule.enabledEntity = entityId + sb.schedule.enabledEntityState = state + sb.schedule.enabledEntityRunOnError = runOnNetworkError + return sb +} + +/* +Disable this schedule when the current state of {entityId} matches {state}. +If there is a network error while retrieving state, the schedule runs if {runOnNetworkError} is true. +*/ +func (sb scheduleBuilderEnd) DisabledEntity(entityId, state string, runOnNetworkError bool) scheduleBuilderEnd { + if entityId == "" || state == "" { + panic(fmt.Sprintf("Either entityId or state is empty in EnabledEntity entityId='%s' state='%s'", entityId, state)) + } + if sb.schedule.enabledEntity != "" { + panic(fmt.Sprintf("You can't use EnabledEntity and DisabledEntity together. Error occurred while setting DisabledEntity on a schedule with params entityId=%s state=%s runOnNetworkError=%t", entityId, state, runOnNetworkError)) + } + sb.schedule.disabledEntity = entityId + sb.schedule.disabledEntityState = state + sb.schedule.disabledEntityRunOnError = runOnNetworkError + return sb +} + func (sb scheduleBuilderEnd) Build() DailySchedule { return sb.schedule } @@ -145,6 +186,12 @@ func (s DailySchedule) maybeRunCallback(a *App) { if c := checkAllowlistDates(s.allowlistDates); c.fail { return } + if c := checkEnabledEntity(a.state, s.enabledEntity, s.enabledEntityState, s.enabledEntityRunOnError); c.fail { + return + } + if c := checkDisabledEntity(a.state, s.disabledEntity, s.disabledEntityState, s.disabledEntityRunOnError); c.fail { + return + } go s.callback(a.service, a.state) } diff --git a/state.go b/state.go index b37705c..84a7d02 100644 --- a/state.go +++ b/state.go @@ -36,7 +36,7 @@ func newState(c *http.HttpClient, homeZoneEntityId string) (*State, error) { func (s *State) getLatLong(c *http.HttpClient, homeZoneEntityId string) error { resp, err := s.Get(homeZoneEntityId) if err != nil { - return errors.New(fmt.Sprintf("couldn't get latitude/longitude from home assistant entity '%s'. Did you type it correctly? It should be a zone like 'zone.home'.\n", homeZoneEntityId)) + return fmt.Errorf("couldn't get latitude/longitude from home assistant entity '%s'. Did you type it correctly? It should be a zone like 'zone.home'", homeZoneEntityId) } if resp.Attributes["latitude"] != nil {