package reveil import ( "fmt" "sort" "time" "git.nemunai.re/nemunaire/reveil/config" ) type Date time.Time func (d *Date) MarshalJSON() (dst []byte, err error) { return []byte(fmt.Sprintf("\"%04d-%02d-%02d\"", time.Time(*d).Year(), time.Time(*d).Month(), time.Time(*d).Day())), nil } func (d *Date) UnmarshalJSON(src []byte) error { tmp, err := time.Parse("\"2006-01-02\"", string(src)) if err != nil { return err } *d = Date(tmp) return nil } type Hour time.Time func (h *Hour) MarshalJSON() (dst []byte, err error) { return []byte(fmt.Sprintf("\"%02d:%02d\"", time.Time(*h).Hour(), time.Time(*h).Minute())), nil } func (h *Hour) UnmarshalJSON(src []byte) error { tmp, err := time.Parse("\"15:04\"", string(src)) if err != nil { return err } *h = Hour(tmp) return nil } func GetNextAlarm(cfg *config.Config, db *LevelDBStorage) (*time.Time, []Identifier, error) { alarmsRepeated, err := GetAlarmsRepeated(db) if err != nil { return nil, nil, err } var closestAlarm *time.Time var closestAlarmRoutines []Identifier for _, alarm := range alarmsRepeated { next := alarm.GetNextOccurence(cfg, db) if next != nil && (closestAlarm == nil || closestAlarm.After(*next)) { closestAlarm = next closestAlarmRoutines = alarm.FollowingRoutines } } alarmsSingle, err := GetAlarmsSingle(db) if err != nil { return nil, nil, err } now := time.Now() for _, alarm := range alarmsSingle { if closestAlarm == nil || (closestAlarm.After(alarm.Time) && alarm.Time.After(now)) { closestAlarm = &alarm.Time closestAlarmRoutines = alarm.FollowingRoutines } } return closestAlarm, closestAlarmRoutines, nil } func GetNextException(cfg *config.Config, db *LevelDBStorage) (*time.Time, error) { alarmsExceptions, err := GetAlarmExceptions(db) if err != nil { return nil, err } var closestException *time.Time for _, except := range alarmsExceptions { if except != nil && time.Time(*except.End).After(time.Now()) && (closestException == nil || closestException.After(time.Time(*except.Start))) { tmp := time.Time(*except.Start) closestException = &tmp } } return closestException, nil } func DropNextAlarm(cfg *config.Config, db *LevelDBStorage) error { timenext, _, err := GetNextAlarm(cfg, db) if err != nil { return err } alarmsRepeated, err := GetAlarmsRepeated(db) if err != nil { return err } for _, alarm := range alarmsRepeated { next := alarm.GetNextOccurence(cfg, db) if next != nil && *next == *timenext { start := Date(*next) stop := Date((*next).Add(time.Second)) return PutAlarmException(db, &AlarmException{ Start: &start, End: &stop, Comment: fmt.Sprintf("Automatic exception to cancel recurrent alarm %s", next.Format("Mon at 15:04")), }) } } alarmsSingle, err := GetAlarmsSingle(db) if err != nil { return err } for _, alarm := range alarmsSingle { if alarm.Time == *timenext { return DeleteAlarmSingle(db, alarm) } } return fmt.Errorf("Unable to find the next alarm") } type Exceptions []time.Time func (e Exceptions) Len() int { return len(e) } func (e Exceptions) Less(i, j int) bool { return e[i].Before(e[j]) } func (e Exceptions) Swap(i, j int) { e[i], e[j] = e[j], e[i] } type AlarmRepeated struct { Id Identifier `json:"id"` Weekday time.Weekday `json:"weekday"` StartTime *Hour `json:"time"` FollowingRoutines []Identifier `json:"routines"` IgnoreExceptions bool `json:"ignore_exceptions"` Comment string `json:"comment,omitempty"` Disabled bool `json:"disabled,omitempty"` Excepts Exceptions `json:"excepts,omitempty"` NextTime *time.Time `json:"next_time,omitempty"` } func (a *AlarmRepeated) FillExcepts(cfg *config.Config, db *LevelDBStorage) error { if a.IgnoreExceptions { return nil } exceptions, err := GetAlarmExceptions(db) if err != nil { return err } now := time.Now() for _, exception := range exceptions { end := time.Time(*exception.End).AddDate(0, 0, 1) if now.After(end) { continue } for t := time.Time(*exception.Start); end.After(t); t = t.AddDate(0, 0, 1) { if t.Weekday() == a.Weekday { a.Excepts = append(a.Excepts, time.Date(t.Year(), t.Month(), t.Day(), time.Time(*a.StartTime).Hour(), time.Time(*a.StartTime).Minute(), time.Time(*a.StartTime).Second(), 0, time.Local)) t = t.AddDate(0, 0, 6) } } } sort.Sort(a.Excepts) return nil } func (a *AlarmRepeated) GetNextOccurence(cfg *config.Config, db *LevelDBStorage) *time.Time { if a.Disabled { return nil } if len(a.Excepts) == 0 { a.FillExcepts(cfg, db) } now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), time.Time(*a.StartTime).Hour(), time.Time(*a.StartTime).Minute(), time.Time(*a.StartTime).Second(), 0, time.Local) if now.After(today) { today = today.AddDate(0, 0, 1) } end := today.AddDate(0, 0, 7) var nextOccurence time.Time for nextOccurence = today; end.After(nextOccurence); nextOccurence = nextOccurence.AddDate(0, 0, 1) { if nextOccurence.Weekday() == a.Weekday { break } } for _, except := range a.Excepts { if except.Equal(nextOccurence) { nextOccurence = nextOccurence.AddDate(0, 0, 7) } } return &nextOccurence } func GetAlarmRepeated(db *LevelDBStorage, id Identifier) (alarm *AlarmRepeated, err error) { alarm = &AlarmRepeated{} err = db.get(fmt.Sprintf("alarm-repeated-%s", id.ToString()), alarm) return } func GetAlarmsRepeated(db *LevelDBStorage) (alarms []*AlarmRepeated, err error) { iter := db.search("alarm-repeated-") defer iter.Release() for iter.Next() { var a AlarmRepeated err = decodeData(iter.Value(), &a) if err != nil { return } alarms = append(alarms, &a) } return } func PutAlarmRepeated(db *LevelDBStorage, alarm *AlarmRepeated) (err error) { var key string if alarm.Id.IsEmpty() { var id Identifier key, id, err = db.findBytesKey("alarm-repeated-", IDENTIFIER_LEN) if err != nil { return err } alarm.Id = id } else { key = fmt.Sprintf("alarm-repeated-%s", alarm.Id.ToString()) } // Don't store this, this is autocalculated alarm.Excepts = nil alarm.NextTime = nil return db.put(key, alarm) } func DeleteAlarmRepeated(db *LevelDBStorage, alarm *AlarmRepeated) (err error) { return db.delete(fmt.Sprintf("alarm-repeated-%s", alarm.Id.ToString())) } type AlarmSingle struct { Id Identifier `json:"id"` Time time.Time `json:"time"` FollowingRoutines []Identifier `json:"routines"` Comment string `json:"comment,omitempty"` } func GetAlarmSingle(db *LevelDBStorage, id Identifier) (alarm *AlarmSingle, err error) { alarm = &AlarmSingle{} err = db.get(fmt.Sprintf("alarm-single-%s", id.ToString()), alarm) return } func GetAlarmsSingle(db *LevelDBStorage) (alarms []*AlarmSingle, err error) { iter := db.search("alarm-single-") defer iter.Release() for iter.Next() { var a AlarmSingle err = decodeData(iter.Value(), &a) if err != nil { return } alarms = append(alarms, &a) } return } func PutAlarmSingle(db *LevelDBStorage, alarm *AlarmSingle) (err error) { var key string if alarm.Id.IsEmpty() { var id Identifier key, id, err = db.findBytesKey("alarm-single-", IDENTIFIER_LEN) if err != nil { return err } alarm.Id = id } else { key = fmt.Sprintf("alarm-single-%s", alarm.Id.ToString()) } return db.put(key, alarm) } func DeleteAlarmSingle(db *LevelDBStorage, alarm *AlarmSingle) (err error) { return db.delete(fmt.Sprintf("alarm-single-%s", alarm.Id.ToString())) } func RemoveOldAlarmsSingle(db *LevelDBStorage) error { alarms, err := GetAlarmsSingle(db) if err != nil { return err } now := time.Now() for _, alarm := range alarms { if now.After(time.Time(alarm.Time)) { err = DeleteAlarmSingle(db, alarm) if err != nil { return err } } } return nil } type AlarmException struct { Id Identifier `json:"id"` Start *Date `json:"start"` End *Date `json:"end"` Comment string `json:"comment,omitempty"` } func GetAlarmException(db *LevelDBStorage, id Identifier) (alarm *AlarmException, err error) { alarm = &AlarmException{} err = db.get(fmt.Sprintf("alarm-exception-%s", id.ToString()), alarm) return } func GetAlarmExceptions(db *LevelDBStorage) (alarms []*AlarmException, err error) { iter := db.search("alarm-exception-") defer iter.Release() for iter.Next() { var a AlarmException err = decodeData(iter.Value(), &a) if err != nil { return } alarms = append(alarms, &a) } return } func PutAlarmException(db *LevelDBStorage, alarm *AlarmException) (err error) { var key string if alarm.Id.IsEmpty() { var id Identifier key, id, err = db.findBytesKey("alarm-exception-", IDENTIFIER_LEN) if err != nil { return err } alarm.Id = id } else { key = fmt.Sprintf("alarm-exception-%s", alarm.Id.ToString()) } return db.put(key, alarm) } func DeleteAlarmException(db *LevelDBStorage, alarm *AlarmException) (err error) { return db.delete(fmt.Sprintf("alarm-exception-%s", alarm.Id.ToString())) }