mirror of
https://github.com/Xevion/go-ha.git
synced 2025-12-05 21:15:06 -06:00
docs: improve schedule.go
This commit is contained in:
104
schedule.go
104
schedule.go
@@ -1,3 +1,6 @@
|
||||
// Package gomeassistant provides a Go library for creating Home Assistant automations
|
||||
// and schedules. This file contains the scheduling system that allows you to create
|
||||
// daily schedules with various conditions and callbacks.
|
||||
package gomeassistant
|
||||
|
||||
import (
|
||||
@@ -10,44 +13,67 @@ import (
|
||||
"github.com/dromara/carbon/v2"
|
||||
)
|
||||
|
||||
// ScheduleCallback is a function type that gets called when a schedule triggers.
|
||||
// It receives the service instance and current state as parameters.
|
||||
type ScheduleCallback func(*Service, State)
|
||||
|
||||
// DailySchedule represents a recurring daily schedule with various conditions.
|
||||
// It can be configured to run at specific times, sunrise/sunset, or based on
|
||||
// entity states and date restrictions.
|
||||
type DailySchedule struct {
|
||||
// 0-23
|
||||
// Hour of the day (0-23) when the schedule should run
|
||||
hour int
|
||||
// 0-59
|
||||
// Minute of the hour (0-59) when the schedule should run
|
||||
minute int
|
||||
|
||||
callback ScheduleCallback
|
||||
// Function to call when the schedule triggers
|
||||
callback ScheduleCallback
|
||||
// Next time this schedule should run
|
||||
nextRunTime time.Time
|
||||
|
||||
// If true, schedule runs at sunrise instead of fixed time
|
||||
isSunrise bool
|
||||
isSunset bool
|
||||
// If true, schedule runs at sunset instead of fixed time
|
||||
isSunset bool
|
||||
// Offset from sunrise/sunset (e.g., "-30m", "+1h")
|
||||
sunOffset types.DurationString
|
||||
|
||||
// Dates when this schedule should NOT run
|
||||
exceptionDates []time.Time
|
||||
// Dates when this schedule is ONLY allowed to run (if empty, runs on all dates)
|
||||
allowlistDates []time.Time
|
||||
|
||||
enabledEntities []internal.EnabledDisabledInfo
|
||||
// Entities that must be in specific states for this schedule to run
|
||||
enabledEntities []internal.EnabledDisabledInfo
|
||||
// Entities that must NOT be in specific states for this schedule to run
|
||||
disabledEntities []internal.EnabledDisabledInfo
|
||||
}
|
||||
|
||||
// Hash returns a unique string identifier for this schedule based on its
|
||||
// time and callback function.
|
||||
func (s DailySchedule) Hash() string {
|
||||
return fmt.Sprint(s.hour, s.minute, s.callback)
|
||||
}
|
||||
|
||||
// scheduleBuilder is used in the fluent API to build schedules step by step.
|
||||
type scheduleBuilder struct {
|
||||
schedule DailySchedule
|
||||
}
|
||||
|
||||
// scheduleBuilderCall represents the state after setting the callback function.
|
||||
type scheduleBuilderCall struct {
|
||||
schedule DailySchedule
|
||||
}
|
||||
|
||||
// scheduleBuilderEnd represents the final state where time and conditions are set.
|
||||
type scheduleBuilderEnd struct {
|
||||
schedule DailySchedule
|
||||
}
|
||||
|
||||
// NewDailySchedule creates a new schedule builder with default values.
|
||||
// Use the fluent API to configure the schedule:
|
||||
//
|
||||
// NewDailySchedule().Call(myFunction).At("15:30").Build()
|
||||
func NewDailySchedule() scheduleBuilder {
|
||||
return scheduleBuilder{
|
||||
DailySchedule{
|
||||
@@ -58,6 +84,7 @@ func NewDailySchedule() scheduleBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a human-readable representation of the schedule.
|
||||
func (s DailySchedule) String() string {
|
||||
return fmt.Sprintf("Schedule{ call %q daily at %s }",
|
||||
internal.GetFunctionName(s.callback),
|
||||
@@ -65,16 +92,20 @@ func (s DailySchedule) String() string {
|
||||
)
|
||||
}
|
||||
|
||||
// stringHourMinute formats hour and minute as "HH:MM".
|
||||
func stringHourMinute(hour, minute int) string {
|
||||
return fmt.Sprintf("%02d:%02d", hour, minute)
|
||||
}
|
||||
|
||||
// Call sets the callback function that will be executed when the schedule triggers.
|
||||
// This is the first step in the fluent API chain.
|
||||
func (sb scheduleBuilder) Call(callback ScheduleCallback) scheduleBuilderCall {
|
||||
sb.schedule.callback = callback
|
||||
return scheduleBuilderCall(sb)
|
||||
}
|
||||
|
||||
// At takes a string in 24hr format time like "15:30".
|
||||
// At sets the schedule to run at a specific time in 24-hour format.
|
||||
// Examples: "15:30", "09:00", "23:45"
|
||||
func (sb scheduleBuilderCall) At(s string) scheduleBuilderEnd {
|
||||
t := internal.ParseTime(s)
|
||||
sb.schedule.hour = t.Hour()
|
||||
@@ -82,8 +113,13 @@ func (sb scheduleBuilderCall) At(s string) scheduleBuilderEnd {
|
||||
return scheduleBuilderEnd(sb)
|
||||
}
|
||||
|
||||
// Sunrise takes an optional duration string that is passed to time.ParseDuration.
|
||||
// Examples include "-1.5h", "30m", etc. See https://pkg.go.dev/time#ParseDuration for the full list.
|
||||
// Sunrise configures the schedule to run at sunrise with an optional offset.
|
||||
// The offset parameter is a duration string (e.g., "-30m", "+1h", "-1.5h").
|
||||
// Only the first offset, if provided, is considered.
|
||||
// Examples:
|
||||
// - Sunrise() - runs at sunrise
|
||||
// - Sunrise("-30m") - runs 30 minutes before sunrise
|
||||
// - Sunrise("+1h") - runs 1 hour after sunrise
|
||||
func (sb scheduleBuilderCall) Sunrise(offset ...types.DurationString) scheduleBuilderEnd {
|
||||
sb.schedule.isSunrise = true
|
||||
if len(offset) > 0 {
|
||||
@@ -92,8 +128,13 @@ func (sb scheduleBuilderCall) Sunrise(offset ...types.DurationString) scheduleBu
|
||||
return scheduleBuilderEnd(sb)
|
||||
}
|
||||
|
||||
// Sunset takes an optional duration string that is passed to time.ParseDuration.
|
||||
// Examples include "-1.5h", "30m", etc. See https://pkg.go.dev/time#ParseDuration for the full list.
|
||||
// Sunset configures the schedule to run at sunset with an optional offset.
|
||||
// The offset parameter is a duration string (e.g., "-30m", "+1h", "-1.5h").
|
||||
// Only the first offset, if provided, is considered.
|
||||
// Examples:
|
||||
// - Sunset() - runs at sunset
|
||||
// - Sunset("-30m") - runs 30 minutes before sunset
|
||||
// - Sunset("+1h") - runs 1 hour after sunset
|
||||
func (sb scheduleBuilderCall) Sunset(offset ...types.DurationString) scheduleBuilderEnd {
|
||||
sb.schedule.isSunset = true
|
||||
if len(offset) > 0 {
|
||||
@@ -102,18 +143,27 @@ func (sb scheduleBuilderCall) Sunset(offset ...types.DurationString) scheduleBui
|
||||
return scheduleBuilderEnd(sb)
|
||||
}
|
||||
|
||||
// ExceptionDates adds dates when this schedule should NOT run.
|
||||
// You can pass multiple dates: ExceptionDates(date1, date2, date3)
|
||||
func (sb scheduleBuilderEnd) ExceptionDates(t time.Time, tl ...time.Time) scheduleBuilderEnd {
|
||||
sb.schedule.exceptionDates = append(tl, t)
|
||||
return sb
|
||||
}
|
||||
|
||||
// OnlyOnDates restricts the schedule to run ONLY on the specified dates.
|
||||
// If no dates are specified, the schedule runs on all dates.
|
||||
// You can pass multiple dates: OnlyOnDates(date1, date2, date3)
|
||||
func (sb scheduleBuilderEnd) OnlyOnDates(t time.Time, tl ...time.Time) scheduleBuilderEnd {
|
||||
sb.schedule.allowlistDates = append(tl, t)
|
||||
return sb
|
||||
}
|
||||
|
||||
// EnabledWhen enables 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.
|
||||
// EnabledWhen makes this schedule only run when the specified entity is in the given state.
|
||||
// If there's a network error while checking the entity state, the schedule runs
|
||||
// only if runOnNetworkError is true.
|
||||
// Examples:
|
||||
// - EnabledWhen("light.living_room", "on", true) - only run when light is on
|
||||
// - EnabledWhen("sensor.motion", "detected", false) - only run when motion detected, fail on network error
|
||||
func (sb scheduleBuilderEnd) EnabledWhen(entityId, state string, runOnNetworkError bool) scheduleBuilderEnd {
|
||||
if entityId == "" {
|
||||
panic(fmt.Sprintf("entityId is empty in EnabledWhen entityId='%s' state='%s'", entityId, state))
|
||||
@@ -127,8 +177,11 @@ func (sb scheduleBuilderEnd) EnabledWhen(entityId, state string, runOnNetworkErr
|
||||
return sb
|
||||
}
|
||||
|
||||
// DisabledWhen disables 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.
|
||||
// DisabledWhen prevents this schedule from running when the specified entity is in the given state.
|
||||
// If there's a network error while checking the entity state, the schedule runs only if runOnNetworkError is true.
|
||||
// Examples:
|
||||
// - DisabledWhen("light.living_room", "off", true) - don't run when light is off
|
||||
// - DisabledWhen("sensor.motion", "detected", false) - don't run when motion detected, fail on network error
|
||||
func (sb scheduleBuilderEnd) DisabledWhen(entityId, state string, runOnNetworkError bool) scheduleBuilderEnd {
|
||||
if entityId == "" {
|
||||
panic(fmt.Sprintf("entityId is empty in EnabledWhen entityId='%s' state='%s'", entityId, state))
|
||||
@@ -142,11 +195,15 @@ func (sb scheduleBuilderEnd) DisabledWhen(entityId, state string, runOnNetworkEr
|
||||
return sb
|
||||
}
|
||||
|
||||
// Build finalizes the schedule configuration and returns the DailySchedule.
|
||||
// This is the final step in the fluent API chain.
|
||||
func (sb scheduleBuilderEnd) Build() DailySchedule {
|
||||
return sb.schedule
|
||||
}
|
||||
|
||||
// app.Start() functions
|
||||
// runSchedules is the main goroutine that manages all schedules.
|
||||
// It continuously processes schedules, running them when their time comes
|
||||
// and requeuing them for the next day.
|
||||
func runSchedules(a *App) {
|
||||
if a.schedules.Len() == 0 {
|
||||
return
|
||||
@@ -162,7 +219,7 @@ func runSchedules(a *App) {
|
||||
|
||||
sched := popSchedule(a)
|
||||
|
||||
// run callback for all schedules before now in case they overlap
|
||||
// Run callback for all schedules that are overdue in case they overlap
|
||||
for sched.nextRunTime.Before(time.Now()) {
|
||||
sched.maybeRunCallback(a)
|
||||
requeueSchedule(a, sched)
|
||||
@@ -172,7 +229,7 @@ func runSchedules(a *App) {
|
||||
|
||||
slog.Info("Next schedule", "start_time", sched.nextRunTime)
|
||||
|
||||
// Use context-aware sleep
|
||||
// Wait until the next schedule time or context cancellation
|
||||
select {
|
||||
case <-time.After(time.Until(sched.nextRunTime)):
|
||||
// Time elapsed, continue
|
||||
@@ -186,6 +243,13 @@ func runSchedules(a *App) {
|
||||
}
|
||||
}
|
||||
|
||||
// maybeRunCallback checks all conditions and runs the callback if they're all met.
|
||||
// Conditions checked:
|
||||
// 1. Exception dates (schedule should not run on these dates)
|
||||
// 2. Allowlist dates (schedule should only run on these dates)
|
||||
// 3. Enabled entities (required entity states)
|
||||
// 4. Disabled entities (forbidden entity states)
|
||||
// The callback runs in a goroutine to avoid blocking the scheduler.
|
||||
func (s DailySchedule) maybeRunCallback(a *App) {
|
||||
if c := CheckExceptionDates(s.exceptionDates); c.fail {
|
||||
return
|
||||
@@ -202,15 +266,19 @@ func (s DailySchedule) maybeRunCallback(a *App) {
|
||||
go s.callback(a.service, a.state)
|
||||
}
|
||||
|
||||
// popSchedule removes and returns the next schedule from the priority queue.
|
||||
func popSchedule(a *App) DailySchedule {
|
||||
_sched, _ := a.schedules.Get(1)
|
||||
return _sched[0].(Item).Value.(DailySchedule)
|
||||
}
|
||||
|
||||
// requeueSchedule calculates the next run time for a schedule and adds it back to the queue.
|
||||
// For sunrise/sunset schedules, it calculates the next sunrise/sunset time.
|
||||
// For fixed-time schedules, it adds one day to the current run time.
|
||||
func requeueSchedule(a *App, s DailySchedule) {
|
||||
if s.isSunrise || s.isSunset {
|
||||
var nextSunTime *carbon.Carbon
|
||||
// "0s" is default value
|
||||
// "0s" is the default value for no offset
|
||||
if s.sunOffset != "0s" {
|
||||
nextSunTime = getNextSunRiseOrSet(a, s.isSunrise, s.sunOffset)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user