refactor & ddisable alarms feature

This commit is contained in:
Niklas Kapelle 2023-10-02 19:54:00 +02:00
parent 7cb4f6c104
commit cb6f86207a
Signed by: niklas
GPG Key ID: 4EB651B36D841D16
4 changed files with 197 additions and 102 deletions

View File

@ -1,14 +1,12 @@
package morningalarm package morningalarm
import ( import (
"encoding/json"
"errors"
"os"
"time" "time"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
) )
// nextAlarm returns the time of the next alarm to fire from the perspective of cron
func (ma *MorningAlarm) nextAlarm() *time.Time { func (ma *MorningAlarm) nextAlarm() *time.Time {
if len(ma.cr.Entries()) == 0 { if len(ma.cr.Entries()) == 0 {
return nil return nil
@ -25,102 +23,17 @@ func (ma *MorningAlarm) nextAlarm() *time.Time {
return &min return &min
} }
func (ma *MorningAlarm) addAlarm(spec string, name string) (cron.EntryID, error) { // armAlarm arms an alarm to fire at the specified time
// Check if alarm already exists func (ma *MorningAlarm) armAlarm(alarm *alarm) (cron.EntryID, error) {
if ma.getAlarm(name) != nil { return ma.cr.AddFunc(alarm.Time, ma.fireAlarm)
return 0, errors.New("Alarm already exists")
} }
id, err := ma.cr.AddFunc(spec, ma.fireAlarm) // disarmAlarm disarms an alarm and prevents it from firing at the specified time
func (ma *MorningAlarm) disarmAlarm(alarm *alarm) {
if err != nil { ma.cr.Remove(alarm.cronID)
return 0, err
}
ma.alarms = append(ma.alarms, alarm{
id: id,
Time: spec,
Name: name,
})
return id, nil
}
func (ma *MorningAlarm) saveAlarms() error {
content, err := json.Marshal(ma.alarms)
if err != nil {
return err
}
err = os.WriteFile("alarms.json", content, 0644)
if err != nil {
return err
}
return nil
}
func (ma *MorningAlarm) loadAlarms() error {
if _, err := os.Stat("alarms.json"); os.IsNotExist(err) {
ma.alarms = []alarm{}
return nil
}
content, err := os.ReadFile("alarms.json")
if err != nil {
return err
}
var ent []alarm
err = json.Unmarshal(content, &ent)
if err != nil {
return err
}
for _, entry := range ent {
_, err := ma.addAlarm(entry.Time, entry.Name)
if err != nil {
return err
}
}
return nil
} }
// fireAlarm function that is called when an alarm is fired
func (ma *MorningAlarm) fireAlarm() { func (ma *MorningAlarm) fireAlarm() {
ma.playWakeUpMusic() ma.playWakeUpMusic()
} }
func (ma *MorningAlarm) getAlarm(name string) *alarm {
for _, alarm := range ma.alarms {
if alarm.Name == name {
return &alarm
}
}
return nil
}
func (ma *MorningAlarm) deleteAlarm(name string) bool {
for _, alarm := range ma.alarms {
if alarm.Name == name {
ma.cr.Remove(alarm.id)
for i, a := range ma.alarms {
if a.Name == name {
ma.alarms = append(ma.alarms[:i], ma.alarms[i+1:]...)
break
}
}
return true
}
}
return false
}

View File

@ -9,7 +9,8 @@ import (
type alarm struct { type alarm struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
Time string `json:"time" binding:"required"` Time string `json:"time" binding:"required"`
id cron.EntryID Disabled bool `json:"disabled"`
cronID cron.EntryID
} }
type MorningAlarmConfig struct { type MorningAlarmConfig struct {
@ -36,7 +37,10 @@ func New(config MorningAlarmConfig) *MorningAlarm {
func (ma *MorningAlarm) Start() { func (ma *MorningAlarm) Start() {
ma.cr = cron.New() ma.cr = cron.New()
ma.loadAlarms()
ma.loadAlarmsFromFile()
ma.armAllAlarms()
ma.cr.Start() ma.cr.Start()
ma.ro = gin.Default() ma.ro = gin.Default()

138
internal/store.go Normal file
View File

@ -0,0 +1,138 @@
package morningalarm
import (
"encoding/json"
"errors"
"os"
)
// addAlarm adds an alarm to the list of alarms and starts a cron job to fire it at the specified time
func (ma *MorningAlarm) addAlarm(time, name string) (*alarm, error) {
// Check if alarm already exists
if ma.getAlarm(name) != nil {
return nil, errors.New("Alarm with name " + name + " already exists")
}
alarm := alarm{
Name: name,
Time: time,
Disabled: false,
}
alarm.cronID, _ = ma.armAlarm(&alarm)
ma.alarms = append(ma.alarms, alarm)
return &alarm, nil
}
func (ma *MorningAlarm) getAlarm(name string) *alarm {
for _, alarm := range ma.alarms {
if alarm.Name == name {
return &alarm
}
}
return nil
}
// loadAlarmsFromFile loads the list of alarms from alarms.json into memory
func (ma *MorningAlarm) loadAlarmsFromFile() error {
if _, err := os.Stat("alarms.json"); os.IsNotExist(err) {
ma.alarms = []alarm{}
return nil
}
content, err := os.ReadFile("alarms.json")
if err != nil {
return err
}
err = json.Unmarshal(content, &ma.alarms)
if err != nil {
return err
}
return nil
}
// saveAlarmsToFile saves the list of alarms to alarms.json
func (ma *MorningAlarm) saveAlarmsToFile() error {
content, err := json.Marshal(ma.alarms)
if err != nil {
return err
}
err = os.WriteFile("alarms.json", content, 0644)
if err != nil {
return err
}
return nil
}
// removeAlarm removes an alarm from the list of alarms and stops cron from firing it. Returns true if the alarm was removed, false if it was not found
func (ma *MorningAlarm) removeAlarm(name string) bool {
for _, alarm := range ma.alarms {
if alarm.Name == name {
// Stop cron from firing the alarm
ma.disarmAlarm(&alarm)
// Remove alarm from list of alarms
for i, a := range ma.alarms {
if a.Name == name {
ma.alarms = append(ma.alarms[:i], ma.alarms[i+1:]...)
break
}
}
return true
}
}
return false
}
// armAllAlarms arms all alarms in the list of alarms that are not disabled and returns an error if any of them fail to arm
func (ma *MorningAlarm) armAllAlarms() error {
for _, alarm := range ma.alarms {
if !alarm.Disabled {
id, err := ma.armAlarm(&alarm)
if err != nil {
return err
}
alarm.cronID = id
}
}
return nil
}
// setDisabled sets the disabled status of an alarm
func (ma *MorningAlarm) setDisabled(name string, disabled bool) {
for i, alarm := range ma.alarms {
if alarm.Name == name {
if alarm.Disabled == disabled {
return
}
ma.alarms[i].Disabled = disabled
if disabled {
ma.disarmAlarm(&alarm)
} else {
id, _ := ma.armAlarm(&alarm)
ma.alarms[i].cronID = id
}
return
}
}
}

View File

@ -12,7 +12,12 @@ type Device struct {
ID string `json:"id"` ID string `json:"id"`
} }
type AlarmPatch struct {
Disabled bool `json:"disabled"`
}
func (ma *MorningAlarm) setupWebserver() { func (ma *MorningAlarm) setupWebserver() {
// Create a new alarm
ma.ro.POST("/api/alarm", func(c *gin.Context) { ma.ro.POST("/api/alarm", func(c *gin.Context) {
var body alarm var body alarm
@ -40,7 +45,7 @@ func (ma *MorningAlarm) setupWebserver() {
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
} else { } else {
err = ma.saveAlarms() err = ma.saveAlarmsToFile()
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
@ -52,6 +57,7 @@ func (ma *MorningAlarm) setupWebserver() {
} }
}) })
// Get an alarm
ma.ro.GET("/api/alarm/:id", func(c *gin.Context) { ma.ro.GET("/api/alarm/:id", func(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
@ -65,15 +71,17 @@ func (ma *MorningAlarm) setupWebserver() {
c.JSON(http.StatusOK, alarm) c.JSON(http.StatusOK, alarm)
}) })
// Get all alarms
ma.ro.GET("/api/alarm", func(c *gin.Context) { ma.ro.GET("/api/alarm", func(c *gin.Context) {
c.JSON(http.StatusOK, ma.alarms) c.JSON(http.StatusOK, ma.alarms)
}) })
// Delete an alarm
ma.ro.DELETE("/api/alarm/:id", func(c *gin.Context) { ma.ro.DELETE("/api/alarm/:id", func(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
if ma.deleteAlarm(id) { if ma.removeAlarm(id) {
err := ma.saveAlarms() err := ma.saveAlarmsToFile()
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
@ -86,6 +94,7 @@ func (ma *MorningAlarm) setupWebserver() {
} }
}) })
// Get info about the server and the alarms
ma.ro.GET("/api/info", func(c *gin.Context) { ma.ro.GET("/api/info", func(c *gin.Context) {
spotifyUser, err := ma.sp.CurrentUser(c) spotifyUser, err := ma.sp.CurrentUser(c)
@ -117,6 +126,7 @@ func (ma *MorningAlarm) setupWebserver() {
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
}) })
// Get available spotify devices
ma.ro.GET("/api/device", func(c *gin.Context) { ma.ro.GET("/api/device", func(c *gin.Context) {
devices, err := ma.getAvailableDevices() devices, err := ma.getAvailableDevices()
@ -128,6 +138,7 @@ func (ma *MorningAlarm) setupWebserver() {
c.JSON(http.StatusOK, devices) c.JSON(http.StatusOK, devices)
}) })
// Trigger an alarm immediately
ma.ro.POST("/api/trigger", func(c *gin.Context) { ma.ro.POST("/api/trigger", func(c *gin.Context) {
if _, err := ma.playWakeUpMusic(); err != nil { if _, err := ma.playWakeUpMusic(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
@ -136,4 +147,33 @@ func (ma *MorningAlarm) setupWebserver() {
c.JSON(http.StatusOK, gin.H{}) c.JSON(http.StatusOK, gin.H{})
}) })
ma.ro.PATCH("/api/alarm/:id", func(c *gin.Context) {
id := c.Param("id")
alarm := ma.getAlarm(id)
if alarm == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Alarm not found"})
return
}
var body AlarmPatch
if err := c.BindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ma.setDisabled(id, body.Disabled)
err := ma.saveAlarmsToFile()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{})
})
} }