Skip to content

Commit

Permalink
Add human override option
Browse files Browse the repository at this point in the history
  • Loading branch information
dansimau committed Dec 29, 2024
1 parent 5a8462a commit 136d763
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 18 deletions.
10 changes: 5 additions & 5 deletions automations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 == "" {
Expand Down
2 changes: 1 addition & 1 deletion automations/print_debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
92 changes: 82 additions & 10 deletions automations/sensor_lights.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"github.com/dansimau/hal"
"github.com/dansimau/hal/timerutil"
)

type ConditionScene struct {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
}
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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")

Expand All @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion automations/timer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
44 changes: 44 additions & 0 deletions timerutil/timer.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 136d763

Please sign in to comment.