From 136d7636f2e4dd4d3f3c93096658d24b78582b65 Mon Sep 17 00:00:00 2001 From: Daniel Simmons Date: Sun, 29 Dec 2024 20:37:52 +0100 Subject: [PATCH] Add human override option --- automations.go | 10 ++-- automations/print_debug.go | 2 +- automations/sensor_lights.go | 92 ++++++++++++++++++++++++++++++++---- automations/timer.go | 3 +- connection.go | 2 +- timerutil/timer.go | 44 +++++++++++++++++ 6 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 timerutil/timer.go diff --git a/automations.go b/automations.go index 9c21d46..ace4765 100644 --- a/automations.go +++ b/automations.go @@ -10,11 +10,11 @@ type Automation interface { Entities() Entities // Action is called when the automation is triggered. - Action() + Action(trigger EntityInterface) } type AutomationConfig struct { - action func() + action func(trigger EntityInterface) entities Entities name string } @@ -27,15 +27,15 @@ func (c *AutomationConfig) Entities() Entities { return c.entities } -func (c *AutomationConfig) Action() { - c.action() +func (c *AutomationConfig) Action(trigger EntityInterface) { + c.action(trigger) } func (c *AutomationConfig) Name() string { return c.name } -func (c *AutomationConfig) WithAction(action func()) *AutomationConfig { +func (c *AutomationConfig) WithAction(action func(trigger EntityInterface)) *AutomationConfig { c.action = action if c.name == "" { diff --git a/automations/print_debug.go b/automations/print_debug.go index 897b8de..fd45719 100644 --- a/automations/print_debug.go +++ b/automations/print_debug.go @@ -24,7 +24,7 @@ func (p *PrintDebug) Entities() hal.Entities { return p.entities } -func (p *PrintDebug) Action() { +func (p *PrintDebug) Action(_ hal.EntityInterface) { for _, entity := range p.entities { log.Printf("[%s] Entity %s state: %+v", p.name, entity.GetID(), entity.GetState()) } diff --git a/automations/sensor_lights.go b/automations/sensor_lights.go index 9eaa631..f556d06 100644 --- a/automations/sensor_lights.go +++ b/automations/sensor_lights.go @@ -5,6 +5,7 @@ import ( "time" "github.com/dansimau/hal" + "github.com/dansimau/hal/timerutil" ) type ConditionScene struct { @@ -28,15 +29,17 @@ type SensorsTriggerLights struct { brightness float64 scene map[string]any - condition func() bool // optional: func that must return true for the automation to run - conditionScene []ConditionScene - sensors []hal.EntityInterface - turnsOnLights []hal.LightInterface - turnsOffLights []hal.LightInterface - turnsOffAfter *time.Duration // optional: duration after which lights will turn off after being turned on - - dimLightsTimer *time.Timer - turnOffTimer *time.Timer + condition func() bool // optional: func that must return true for the automation to run + conditionScene []ConditionScene + humanOverrideFor *time.Duration // optional: duration after which lights will turn off after being turned on from outside this system + sensors []hal.EntityInterface + turnsOnLights []hal.LightInterface + turnsOffLights []hal.LightInterface + turnsOffAfter *time.Duration // optional: duration after which lights will turn off after being turned on + + dimLightsTimer *time.Timer + humanOverrideTimer *timerutil.Timer + turnOffTimer *time.Timer } func NewSensorsTriggerLights() *SensorsTriggerLights { @@ -70,6 +73,14 @@ func (a *SensorsTriggerLights) WithConditionScene(condition func() bool, scene m return a } +// WithHumanOverrideFor sets a secondary timer that will kick in if the light +// was turned on from outside this system. +func (a *SensorsTriggerLights) WithHumanOverrideFor(duration time.Duration) *SensorsTriggerLights { + a.humanOverrideFor = &duration + + return a +} + // WithLights sets the lights that will be turned on and off. Overrides // TurnsOnLights and TurnsOffLights. func (a *SensorsTriggerLights) WithLights(lights ...hal.LightInterface) *SensorsTriggerLights { @@ -137,6 +148,16 @@ func (a *SensorsTriggerLights) triggered() bool { return false } +func (a *SensorsTriggerLights) lightsOn() bool { + for _, light := range a.turnsOnLights { + if light.GetState().State == "on" { + return true + } + } + + return false +} + func (a *SensorsTriggerLights) startDimLightsTimer() { if a.turnsOffAfter == nil { return @@ -178,6 +199,8 @@ func (a *SensorsTriggerLights) stopTurnOffTimer() { func (a *SensorsTriggerLights) stopDimLightsTimer() { if a.dimLightsTimer != nil { a.dimLightsTimer.Stop() + // TODO: Detect if the timer was actually running and if so print a log + // message saying we stopped the timer. } } @@ -215,6 +238,7 @@ func (a *SensorsTriggerLights) dimLights() { brightness := light.GetBrightness() if brightness < 2 { a.log.Info("Light is already at minimum brightness, skipping dimming", "light", light.GetID()) + continue } @@ -236,7 +260,33 @@ func (a *SensorsTriggerLights) turnOffLights() { } } -func (a *SensorsTriggerLights) Action() { +func (a *SensorsTriggerLights) lightTriggered(trigger hal.EntityInterface) bool { + for _, light := range a.turnsOnLights { + if light.GetID() == trigger.GetID() { + return true + } + } + + return false +} + +func (a *SensorsTriggerLights) sensorTriggered(trigger hal.EntityInterface) bool { + for _, sensor := range a.sensors { + if sensor.GetID() == trigger.GetID() { + return true + } + } + + return false +} + +func (a *SensorsTriggerLights) handleSensorTriggered() { + if a.humanOverrideTimer.IsRunning() { + a.log.Info("Light overridden by human, skipping") + + return + } + if a.condition != nil && !a.condition() { a.log.Info("Condition not met, skipping") @@ -254,6 +304,28 @@ func (a *SensorsTriggerLights) Action() { } } +func (a *SensorsTriggerLights) handleLightTriggered() { + a.stopDimLightsTimer() + + if a.humanOverrideFor != nil { + a.stopTurnOffTimer() + + if a.lightsOn() { + a.humanOverrideTimer.Reset(*a.humanOverrideFor) + } else { + a.humanOverrideTimer.Stop() + } + } +} + +func (a *SensorsTriggerLights) Action(trigger hal.EntityInterface) { + if a.sensorTriggered(trigger) { + a.handleSensorTriggered() + } else if a.lightTriggered(trigger) { + a.handleLightTriggered() + } +} + func (a *SensorsTriggerLights) Entities() hal.Entities { return hal.Entities(a.sensors) } diff --git a/automations/timer.go b/automations/timer.go index dcea808..d3ee9bf 100644 --- a/automations/timer.go +++ b/automations/timer.go @@ -75,9 +75,10 @@ func (a *Timer) Entities() hal.Entities { return a.entities } -func (a *Timer) Action() { +func (a *Timer) Action(_ hal.EntityInterface) { if a.condition != nil && !a.condition() { slog.Info("Condition not met, not starting timer", "automation", a.name) + return } diff --git a/connection.go b/connection.go index c12b2c3..cfde310 100644 --- a/connection.go +++ b/connection.go @@ -137,6 +137,6 @@ func (h *Connection) StateChangeEvent(event hassws.EventMessage) { // Dispatch automations for _, automation := range h.automations[event.Event.EventData.EntityID] { slog.Info("Running automation", "name", automation.Name()) - automation.Action() + automation.Action(entity) } } diff --git a/timerutil/timer.go b/timerutil/timer.go new file mode 100644 index 0000000..23fa911 --- /dev/null +++ b/timerutil/timer.go @@ -0,0 +1,44 @@ +package timerutil + +import "time" + +// Timer wraps time.Timer to add functionality for checking if the timer is running. +type Timer struct { + timer *time.Timer + running bool +} + +// NewTimer creates a new Timer that will execute fn after the specified delay. +func NewTimer(d time.Duration, fn func()) *Timer { + t := &Timer{} + t.timer = time.AfterFunc(d, func() { + t.running = false + + fn() + }) + + t.running = true + + return t +} + +// Stop stops the timer and returns whether it was running. +func (t *Timer) Stop() bool { + wasRunning := t.timer.Stop() + t.running = false + + return wasRunning +} + +// Reset stops the timer and resets it to a new duration. +func (t *Timer) Reset(d time.Duration) bool { + wasRunning := t.timer.Reset(d) + t.running = true + + return wasRunning +} + +// IsRunning returns whether the timer is currently running. +func (t *Timer) IsRunning() bool { + return t.running +}