mirror of
https://github.com/Xevion/go-ha.git
synced 2025-12-06 01:15:10 -06:00
docs: improve schedule.go
This commit is contained in:
98
schedule.go
98
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
|
package gomeassistant
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -10,44 +13,67 @@ import (
|
|||||||
"github.com/dromara/carbon/v2"
|
"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)
|
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 {
|
type DailySchedule struct {
|
||||||
// 0-23
|
// Hour of the day (0-23) when the schedule should run
|
||||||
hour int
|
hour int
|
||||||
// 0-59
|
// Minute of the hour (0-59) when the schedule should run
|
||||||
minute int
|
minute int
|
||||||
|
|
||||||
|
// Function to call when the schedule triggers
|
||||||
callback ScheduleCallback
|
callback ScheduleCallback
|
||||||
|
// Next time this schedule should run
|
||||||
nextRunTime time.Time
|
nextRunTime time.Time
|
||||||
|
|
||||||
|
// If true, schedule runs at sunrise instead of fixed time
|
||||||
isSunrise bool
|
isSunrise bool
|
||||||
|
// If true, schedule runs at sunset instead of fixed time
|
||||||
isSunset bool
|
isSunset bool
|
||||||
|
// Offset from sunrise/sunset (e.g., "-30m", "+1h")
|
||||||
sunOffset types.DurationString
|
sunOffset types.DurationString
|
||||||
|
|
||||||
|
// Dates when this schedule should NOT run
|
||||||
exceptionDates []time.Time
|
exceptionDates []time.Time
|
||||||
|
// Dates when this schedule is ONLY allowed to run (if empty, runs on all dates)
|
||||||
allowlistDates []time.Time
|
allowlistDates []time.Time
|
||||||
|
|
||||||
|
// Entities that must be in specific states for this schedule to run
|
||||||
enabledEntities []internal.EnabledDisabledInfo
|
enabledEntities []internal.EnabledDisabledInfo
|
||||||
|
// Entities that must NOT be in specific states for this schedule to run
|
||||||
disabledEntities []internal.EnabledDisabledInfo
|
disabledEntities []internal.EnabledDisabledInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hash returns a unique string identifier for this schedule based on its
|
||||||
|
// time and callback function.
|
||||||
func (s DailySchedule) Hash() string {
|
func (s DailySchedule) Hash() string {
|
||||||
return fmt.Sprint(s.hour, s.minute, s.callback)
|
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 {
|
type scheduleBuilder struct {
|
||||||
schedule DailySchedule
|
schedule DailySchedule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scheduleBuilderCall represents the state after setting the callback function.
|
||||||
type scheduleBuilderCall struct {
|
type scheduleBuilderCall struct {
|
||||||
schedule DailySchedule
|
schedule DailySchedule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scheduleBuilderEnd represents the final state where time and conditions are set.
|
||||||
type scheduleBuilderEnd struct {
|
type scheduleBuilderEnd struct {
|
||||||
schedule DailySchedule
|
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 {
|
func NewDailySchedule() scheduleBuilder {
|
||||||
return scheduleBuilder{
|
return scheduleBuilder{
|
||||||
DailySchedule{
|
DailySchedule{
|
||||||
@@ -58,6 +84,7 @@ func NewDailySchedule() scheduleBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns a human-readable representation of the schedule.
|
||||||
func (s DailySchedule) String() string {
|
func (s DailySchedule) String() string {
|
||||||
return fmt.Sprintf("Schedule{ call %q daily at %s }",
|
return fmt.Sprintf("Schedule{ call %q daily at %s }",
|
||||||
internal.GetFunctionName(s.callback),
|
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 {
|
func stringHourMinute(hour, minute int) string {
|
||||||
return fmt.Sprintf("%02d:%02d", hour, minute)
|
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 {
|
func (sb scheduleBuilder) Call(callback ScheduleCallback) scheduleBuilderCall {
|
||||||
sb.schedule.callback = callback
|
sb.schedule.callback = callback
|
||||||
return scheduleBuilderCall(sb)
|
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 {
|
func (sb scheduleBuilderCall) At(s string) scheduleBuilderEnd {
|
||||||
t := internal.ParseTime(s)
|
t := internal.ParseTime(s)
|
||||||
sb.schedule.hour = t.Hour()
|
sb.schedule.hour = t.Hour()
|
||||||
@@ -82,8 +113,13 @@ func (sb scheduleBuilderCall) At(s string) scheduleBuilderEnd {
|
|||||||
return scheduleBuilderEnd(sb)
|
return scheduleBuilderEnd(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sunrise takes an optional duration string that is passed to time.ParseDuration.
|
// Sunrise configures the schedule to run at sunrise with an optional offset.
|
||||||
// Examples include "-1.5h", "30m", etc. See https://pkg.go.dev/time#ParseDuration for the full list.
|
// 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 {
|
func (sb scheduleBuilderCall) Sunrise(offset ...types.DurationString) scheduleBuilderEnd {
|
||||||
sb.schedule.isSunrise = true
|
sb.schedule.isSunrise = true
|
||||||
if len(offset) > 0 {
|
if len(offset) > 0 {
|
||||||
@@ -92,8 +128,13 @@ func (sb scheduleBuilderCall) Sunrise(offset ...types.DurationString) scheduleBu
|
|||||||
return scheduleBuilderEnd(sb)
|
return scheduleBuilderEnd(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sunset takes an optional duration string that is passed to time.ParseDuration.
|
// Sunset configures the schedule to run at sunset with an optional offset.
|
||||||
// Examples include "-1.5h", "30m", etc. See https://pkg.go.dev/time#ParseDuration for the full list.
|
// 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 {
|
func (sb scheduleBuilderCall) Sunset(offset ...types.DurationString) scheduleBuilderEnd {
|
||||||
sb.schedule.isSunset = true
|
sb.schedule.isSunset = true
|
||||||
if len(offset) > 0 {
|
if len(offset) > 0 {
|
||||||
@@ -102,18 +143,27 @@ func (sb scheduleBuilderCall) Sunset(offset ...types.DurationString) scheduleBui
|
|||||||
return scheduleBuilderEnd(sb)
|
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 {
|
func (sb scheduleBuilderEnd) ExceptionDates(t time.Time, tl ...time.Time) scheduleBuilderEnd {
|
||||||
sb.schedule.exceptionDates = append(tl, t)
|
sb.schedule.exceptionDates = append(tl, t)
|
||||||
return sb
|
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 {
|
func (sb scheduleBuilderEnd) OnlyOnDates(t time.Time, tl ...time.Time) scheduleBuilderEnd {
|
||||||
sb.schedule.allowlistDates = append(tl, t)
|
sb.schedule.allowlistDates = append(tl, t)
|
||||||
return sb
|
return sb
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnabledWhen enables this schedule only when the current state of {entityId} matches {state}.
|
// EnabledWhen makes this schedule only run when the specified entity is in the given state.
|
||||||
// If there is a network error while retrieving state, the schedule runs if {runOnNetworkError} is true.
|
// 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 {
|
func (sb scheduleBuilderEnd) EnabledWhen(entityId, state string, runOnNetworkError bool) scheduleBuilderEnd {
|
||||||
if entityId == "" {
|
if entityId == "" {
|
||||||
panic(fmt.Sprintf("entityId is empty in EnabledWhen entityId='%s' state='%s'", entityId, state))
|
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
|
return sb
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisabledWhen disables this schedule when the current state of {entityId} matches {state}.
|
// DisabledWhen prevents this schedule from running when the specified entity is in the given state.
|
||||||
// If there is a network error while retrieving state, the schedule runs if {runOnNetworkError} is true.
|
// 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 {
|
func (sb scheduleBuilderEnd) DisabledWhen(entityId, state string, runOnNetworkError bool) scheduleBuilderEnd {
|
||||||
if entityId == "" {
|
if entityId == "" {
|
||||||
panic(fmt.Sprintf("entityId is empty in EnabledWhen entityId='%s' state='%s'", entityId, state))
|
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
|
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 {
|
func (sb scheduleBuilderEnd) Build() DailySchedule {
|
||||||
return sb.schedule
|
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) {
|
func runSchedules(a *App) {
|
||||||
if a.schedules.Len() == 0 {
|
if a.schedules.Len() == 0 {
|
||||||
return
|
return
|
||||||
@@ -162,7 +219,7 @@ func runSchedules(a *App) {
|
|||||||
|
|
||||||
sched := popSchedule(a)
|
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()) {
|
for sched.nextRunTime.Before(time.Now()) {
|
||||||
sched.maybeRunCallback(a)
|
sched.maybeRunCallback(a)
|
||||||
requeueSchedule(a, sched)
|
requeueSchedule(a, sched)
|
||||||
@@ -172,7 +229,7 @@ func runSchedules(a *App) {
|
|||||||
|
|
||||||
slog.Info("Next schedule", "start_time", sched.nextRunTime)
|
slog.Info("Next schedule", "start_time", sched.nextRunTime)
|
||||||
|
|
||||||
// Use context-aware sleep
|
// Wait until the next schedule time or context cancellation
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Until(sched.nextRunTime)):
|
case <-time.After(time.Until(sched.nextRunTime)):
|
||||||
// Time elapsed, continue
|
// 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) {
|
func (s DailySchedule) maybeRunCallback(a *App) {
|
||||||
if c := CheckExceptionDates(s.exceptionDates); c.fail {
|
if c := CheckExceptionDates(s.exceptionDates); c.fail {
|
||||||
return
|
return
|
||||||
@@ -202,15 +266,19 @@ func (s DailySchedule) maybeRunCallback(a *App) {
|
|||||||
go s.callback(a.service, a.state)
|
go s.callback(a.service, a.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// popSchedule removes and returns the next schedule from the priority queue.
|
||||||
func popSchedule(a *App) DailySchedule {
|
func popSchedule(a *App) DailySchedule {
|
||||||
_sched, _ := a.schedules.Get(1)
|
_sched, _ := a.schedules.Get(1)
|
||||||
return _sched[0].(Item).Value.(DailySchedule)
|
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) {
|
func requeueSchedule(a *App, s DailySchedule) {
|
||||||
if s.isSunrise || s.isSunset {
|
if s.isSunrise || s.isSunset {
|
||||||
var nextSunTime *carbon.Carbon
|
var nextSunTime *carbon.Carbon
|
||||||
// "0s" is default value
|
// "0s" is the default value for no offset
|
||||||
if s.sunOffset != "0s" {
|
if s.sunOffset != "0s" {
|
||||||
nextSunTime = getNextSunRiseOrSet(a, s.isSunrise, s.sunOffset)
|
nextSunTime = getNextSunRiseOrSet(a, s.isSunrise, s.sunOffset)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user