feat: new schedule triggering, new builder pattern in internal

This commit is contained in:
2025-08-02 00:45:31 -05:00
parent 79f810b1f8
commit bbba55574f
5 changed files with 881 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
package scheduling
import (
"fmt"
"time"
"github.com/Xevion/go-ha/types"
)
type DailyScheduleBuilder struct {
errors []error
hashes map[uint64]bool
triggers []Trigger
}
func NewSchedule() *DailyScheduleBuilder {
return &DailyScheduleBuilder{
hashes: make(map[uint64]bool),
}
}
// tryAddTrigger adds a trigger to the builder if it is not already present.
// If the trigger is already present, an error will be added to the builder's errors.
// It will return the builder for chaining.
func (b *DailyScheduleBuilder) tryAddTrigger(trigger Trigger) *DailyScheduleBuilder {
hash := trigger.Hash()
if _, ok := b.hashes[hash]; ok {
b.errors = append(b.errors, fmt.Errorf("duplicate trigger: %v", trigger))
return b
}
b.triggers = append(b.triggers, trigger)
b.hashes[hash] = true
return b
}
func (b *DailyScheduleBuilder) onSun(sunset bool, offset ...types.DurationString) *DailyScheduleBuilder {
if len(offset) == 0 {
b.errors = append(b.errors, fmt.Errorf("no offset provided for sun"))
return b
}
offsetDuration, err := time.ParseDuration(string(offset[0]))
if err != nil {
b.errors = append(b.errors, err)
return b
}
return b.tryAddTrigger(&SunTrigger{
sunset: sunset,
offset: &offsetDuration,
})
}
// OnSunrise adds a trigger for sunrise with an optional offset.
// Only the first offset is considered.
// You can call this multiple times to add multiple triggers for sunrise with different offsets.
func (b *DailyScheduleBuilder) OnSunrise(offset ...types.DurationString) *DailyScheduleBuilder {
return b.onSun(false, offset...)
}
// OnSunset adds a trigger for sunset with an optional offset.
// Only the first offset is considered.
func (b *DailyScheduleBuilder) OnSunset(offset ...types.DurationString) *DailyScheduleBuilder {
return b.onSun(true, offset...)
}
// OnFixedTime adds a trigger for a fixed time each day.
// The time is in the local timezone.
// This will error if the integer values are not in the range 0-23 for the hour and 0-59 for the minute.
func (b *DailyScheduleBuilder) OnFixedTime(hour, minute int) *DailyScheduleBuilder {
errored := false
if hour < 0 || hour > 23 {
b.errors = append(b.errors, fmt.Errorf("hour must be between 0 and 23"))
errored = true
}
if minute < 0 || minute > 59 {
b.errors = append(b.errors, fmt.Errorf("minute must be between 0 and 59"))
errored = true
}
if errored {
return b
}
return b.tryAddTrigger(&FixedTimeTrigger{
Hour: hour,
Minute: minute,
})
}
// Build returns a Trigger that will trigger at the configured times.
// It will return an error if any errors occurred during configuration.
func (b *DailyScheduleBuilder) Build() (Trigger, error) {
// If there are no triggers, add an error.
if len(b.triggers) == 0 {
b.errors = append(b.errors, fmt.Errorf("no triggers provided"))
}
// If there are errors, return an error.
if len(b.errors) > 0 {
return nil, fmt.Errorf("errors occurred: %v", b.errors)
}
// If there is only one trigger, return it.
if len(b.triggers) == 1 {
return b.triggers[0], nil
}
// Otherwise, return a composite schedule that combines all the triggers.
return &CompositeDailySchedule{triggers: b.triggers}, nil
}