diff --git a/api/alarm.go b/api/alarm.go index 23179d4..9bc0dac 100644 --- a/api/alarm.go +++ b/api/alarm.go @@ -1,11 +1,14 @@ package api import ( + "fmt" + "log" "net/http" "github.com/gin-gonic/gin" "git.nemunai.re/nemunaire/reveil/config" + "git.nemunai.re/nemunaire/reveil/model" "git.nemunai.re/nemunaire/reveil/player" ) @@ -20,7 +23,7 @@ func declareAlarmRoutes(cfg *config.Config, router *gin.RouterGroup) { router.POST("/alarm/run", func(c *gin.Context) { if player.CommonPlayer == nil { - err := player.WakeUp(cfg, nil) + err := player.WakeUp(cfg, nil, true) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return @@ -51,6 +54,21 @@ func declareAlarmRoutes(cfg *config.Config, router *gin.RouterGroup) { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } + + settings, err := reveil.ReadSettings(cfg.SettingsFile) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("Unable to read settings: %s", err.Error())}) + return + } + + for k, srv := range settings.Federation { + err = player.FederatedStop(srv) + if err != nil { + log.Printf("Unable to do federated wakeup on %s: %s", k, err.Error()) + } else { + log.Printf("Federated wakeup on %s: launched!", k) + } + } } c.JSON(http.StatusOK, true) diff --git a/api/alarms.go b/api/alarms.go index b571514..b47d99f 100644 --- a/api/alarms.go +++ b/api/alarms.go @@ -13,7 +13,7 @@ import ( func declareAlarmsRoutes(cfg *config.Config, db *reveil.LevelDBStorage, resetTimer func(), router *gin.RouterGroup) { router.GET("/alarms/next", func(c *gin.Context) { - alarm, _, err := reveil.GetNextAlarm(cfg, db) + alarm, _, _, err := reveil.GetNextAlarm(cfg, db) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return diff --git a/api/federation.go b/api/federation.go new file mode 100644 index 0000000..fc80285 --- /dev/null +++ b/api/federation.go @@ -0,0 +1,50 @@ +package api + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + + "git.nemunai.re/nemunaire/reveil/config" + "git.nemunai.re/nemunaire/reveil/player" +) + +func declareFederationRoutes(cfg *config.Config, router *gin.RouterGroup) { + router.POST("/federation/wakeup", func(c *gin.Context) { + var s map[string]interface{} + c.ShouldBind(s) + + if player.CommonPlayer == nil { + var seed int64 + if tmp, ok := s["seed"].(int64); ok { + seed = tmp + } else { + seed := time.Now().Unix() + seed -= seed % 172800 + } + + err := player.WakeUpFromFederation(cfg, seed, nil) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + } else { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Player already running"}) + return + } + + c.JSON(http.StatusOK, true) + }) + router.POST("/federation/wakeok", func(c *gin.Context) { + if player.CommonPlayer != nil { + err := player.CommonPlayer.Stop() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + } + + c.JSON(http.StatusOK, true) + }) +} diff --git a/api/routes.go b/api/routes.go index 38b3f40..16c155b 100644 --- a/api/routes.go +++ b/api/routes.go @@ -13,6 +13,7 @@ func DeclareRoutes(router *gin.Engine, cfg *config.Config, db *reveil.LevelDBSto declareActionsRoutes(cfg, apiRoutes) declareAlarmRoutes(cfg, apiRoutes) declareAlarmsRoutes(cfg, db, resetTimer, apiRoutes) + declareFederationRoutes(cfg, apiRoutes) declareGongsRoutes(cfg, apiRoutes) declareHistoryRoutes(cfg, apiRoutes) declareQuotesRoutes(cfg, apiRoutes) diff --git a/app.go b/app.go index dbfb2a0..9d1f823 100644 --- a/app.go +++ b/app.go @@ -79,7 +79,7 @@ func (app *App) ResetTimer() { app.nextAlarm = nil } - if na, routines, err := reveil.GetNextAlarm(app.cfg, app.db); err == nil && na != nil { + if na, routines, federated, err := reveil.GetNextAlarm(app.cfg, app.db); err == nil && na != nil { app.nextAlarm = time.AfterFunc(time.Until(*na), func() { app.nextAlarm = nil reveil.RemoveOldAlarmsSingle(app.db) @@ -87,7 +87,7 @@ func (app *App) ResetTimer() { // Rearm timer for the next time app.ResetTimer() - err := player.WakeUp(app.cfg, routines) + err := player.WakeUp(app.cfg, routines, federated) if err != nil { log.Println(err.Error()) return diff --git a/model/alarm.go b/model/alarm.go index f9cc19b..7e8bca4 100644 --- a/model/alarm.go +++ b/model/alarm.go @@ -40,25 +40,27 @@ func (h *Hour) UnmarshalJSON(src []byte) error { return nil } -func GetNextAlarm(cfg *config.Config, db *LevelDBStorage) (*time.Time, []Identifier, error) { +func GetNextAlarm(cfg *config.Config, db *LevelDBStorage) (*time.Time, []Identifier, bool, error) { alarmsRepeated, err := GetAlarmsRepeated(db) if err != nil { - return nil, nil, err + return nil, nil, false, err } var closestAlarm *time.Time var closestAlarmRoutines []Identifier + var closestAlarmFederated bool for _, alarm := range alarmsRepeated { next := alarm.GetNextOccurence(cfg, db) if next != nil && (closestAlarm == nil || closestAlarm.After(*next)) { closestAlarm = next closestAlarmRoutines = alarm.FollowingRoutines + closestAlarmFederated = alarm.EnableFederation } } alarmsSingle, err := GetAlarmsSingle(db) if err != nil { - return nil, nil, err + return nil, nil, false, err } now := time.Now() @@ -66,10 +68,11 @@ func GetNextAlarm(cfg *config.Config, db *LevelDBStorage) (*time.Time, []Identif if closestAlarm == nil || (closestAlarm.After(alarm.Time) && alarm.Time.After(now)) { closestAlarm = &alarm.Time closestAlarmRoutines = alarm.FollowingRoutines + closestAlarmFederated = alarm.EnableFederation } } - return closestAlarm, closestAlarmRoutines, nil + return closestAlarm, closestAlarmRoutines, closestAlarmFederated, nil } func GetNextException(cfg *config.Config, db *LevelDBStorage) (*time.Time, error) { @@ -90,7 +93,7 @@ func GetNextException(cfg *config.Config, db *LevelDBStorage) (*time.Time, error } func DropNextAlarm(cfg *config.Config, db *LevelDBStorage) error { - timenext, _, err := GetNextAlarm(cfg, db) + timenext, _, _, err := GetNextAlarm(cfg, db) if err != nil { return err } @@ -152,6 +155,7 @@ type AlarmRepeated struct { Disabled bool `json:"disabled,omitempty"` Excepts Exceptions `json:"excepts,omitempty"` NextTime *time.Time `json:"next_time,omitempty"` + EnableFederation bool `json:"enable_federation,omitempty"` } func (a *AlarmRepeated) FillExcepts(cfg *config.Config, db *LevelDBStorage) error { @@ -273,6 +277,7 @@ type AlarmSingle struct { Time time.Time `json:"time"` FollowingRoutines []Identifier `json:"routines"` Comment string `json:"comment,omitempty"` + EnableFederation bool `json:"enable_federation,omitempty"` } func GetAlarmSingle(db *LevelDBStorage, id Identifier) (alarm *AlarmSingle, err error) { diff --git a/model/settings.go b/model/settings.go index 7d1471e..8638727 100644 --- a/model/settings.go +++ b/model/settings.go @@ -6,14 +6,19 @@ import ( "time" ) +type FederationSettings struct { + URL string `json:"url"` +} + // Settings represents the settings panel. type Settings struct { - Language string `json:"language"` - GongInterval time.Duration `json:"gong_interval"` - WeatherDelay time.Duration `json:"weather_delay"` - WeatherAction string `json:"weather_action"` - MaxRunTime time.Duration `json:"max_run_time"` - MaxVolume uint16 `json:"max_volume"` + Language string `json:"language"` + GongInterval time.Duration `json:"gong_interval"` + WeatherDelay time.Duration `json:"weather_delay"` + WeatherAction string `json:"weather_action"` + MaxRunTime time.Duration `json:"max_run_time"` + MaxVolume uint16 `json:"max_volume"` + Federation map[string]FederationSettings `json:"federation"` } // ExistsSettings checks if the settings file can by found at the given path. diff --git a/player/federation.go b/player/federation.go new file mode 100644 index 0000000..b62fc58 --- /dev/null +++ b/player/federation.go @@ -0,0 +1,35 @@ +package player + +import ( + "bytes" + "encoding/json" + "net/http" + + "git.nemunai.re/nemunaire/reveil/model" +) + +func FederatedWakeUp(srv reveil.FederationSettings, seed int64) error { + req := map[string]interface{}{"seed": seed} + req_enc, err := json.Marshal(req) + if err != nil { + return err + } + + res, err := http.Post(srv.URL+"/api/federation/wakeup", "application/json", bytes.NewBuffer(req_enc)) + if err != nil { + return err + } + res.Body.Close() + + return nil +} + +func FederatedStop(srv reveil.FederationSettings) error { + res, err := http.Post(srv.URL+"/api/federation/wakeok", "application/json", nil) + if err != nil { + return err + } + res.Body.Close() + + return nil +} diff --git a/player/player.go b/player/player.go index dcee619..3d46e8e 100644 --- a/player/player.go +++ b/player/player.go @@ -43,13 +43,34 @@ type Player struct { playedItem int } -func WakeUp(cfg *config.Config, routine []reveil.Identifier) (err error) { +func WakeUp(cfg *config.Config, routine []reveil.Identifier, federated bool) (err error) { if CommonPlayer != nil { return fmt.Errorf("Unable to start the player: a player is already running") } seed := time.Now().Unix() seed -= seed % 172800 + + if federated { + settings, err := reveil.ReadSettings(cfg.SettingsFile) + if err != nil { + return fmt.Errorf("Unable to read settings: %w", err) + } + + for k, srv := range settings.Federation { + err = FederatedWakeUp(srv, seed) + if err != nil { + log.Printf("Unable to do federated wakeup on %s: %s", k, err.Error()) + } else { + log.Printf("Federated wakeup on %s: launched!", k) + } + } + } + + return WakeUpFromFederation(cfg, seed, routine) +} + +func WakeUpFromFederation(cfg *config.Config, seed int64, routine []reveil.Identifier) (err error) { rand.Seed(seed) CommonPlayer, err = NewPlayer(cfg, routine) diff --git a/ui/nojs.go b/ui/nojs.go index d942c27..438868d 100644 --- a/ui/nojs.go +++ b/ui/nojs.go @@ -24,7 +24,7 @@ func DeclareNoJSRoutes(router *gin.Engine, cfg *config.Config, db *reveil.LevelD router.SetHTMLTemplate(templ) router.GET("/nojs.html", func(c *gin.Context) { - alarm, _, err := reveil.GetNextAlarm(cfg, db) + alarm, _, _, err := reveil.GetNextAlarm(cfg, db) if err != nil { c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"errmsg": err.Error()}) return @@ -122,7 +122,7 @@ func DeclareNoJSRoutes(router *gin.Engine, cfg *config.Config, db *reveil.LevelD case "start": if player.CommonPlayer == nil { - err := player.WakeUp(cfg, nil) + err := player.WakeUp(cfg, nil, true) if err != nil { c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"errmsg": err.Error()}) return diff --git a/ui/package-lock.json b/ui/package-lock.json index 77a89e4..7792ba3 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -3616,9 +3616,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/ui/src/lib/alarmrepeated.js b/ui/src/lib/alarmrepeated.js index fa16855..53ff489 100644 --- a/ui/src/lib/alarmrepeated.js +++ b/ui/src/lib/alarmrepeated.js @@ -5,7 +5,7 @@ export class AlarmRepeated { } } - update({ id, weekday, time, routines, disabled, ignore_exceptions, comment, excepts, next_time }) { + update({ id, weekday, time, routines, disabled, ignore_exceptions, comment, excepts, next_time, enable_federation }) { this.id = id; this.weekday = weekday; this.time = time; @@ -13,6 +13,7 @@ export class AlarmRepeated { this.ignore_exceptions = ignore_exceptions; this.comment = comment; this.disabled = disabled == true; + this.enable_federation = enable_federation == true; if (excepts !== undefined) this.excepts = excepts; if (next_time !== undefined) diff --git a/ui/src/lib/alarmsingle.js b/ui/src/lib/alarmsingle.js index 8a171b3..35d156d 100644 --- a/ui/src/lib/alarmsingle.js +++ b/ui/src/lib/alarmsingle.js @@ -5,11 +5,12 @@ export class AlarmSingle { } } - update({ id, time, routines, comment }) { + update({ id, time, routines, comment, enable_federation }) { this.id = id; this.time = new Date(time); this.routines = routines == null ? [] : routines; this.comment = comment; + this.enable_federation = enable_federation == true; if (this.routines.length < 1) { this.routines.push(""); diff --git a/ui/src/lib/components/FederationSettings.svelte b/ui/src/lib/components/FederationSettings.svelte new file mode 100644 index 0000000..86e7e69 --- /dev/null +++ b/ui/src/lib/components/FederationSettings.svelte @@ -0,0 +1,68 @@ + + +{#if value} +{#each Object.keys(value) as key} + + + dispatch("input")} + on:input={(e) => changeKey(key, e)} + /> + + + dispatch("input")} + /> + + +{/each} +{/if} + + + changeKey(null, e)} + /> + + + + + diff --git a/ui/src/lib/settings.js b/ui/src/lib/settings.js index 29f11c6..e5f0d54 100644 --- a/ui/src/lib/settings.js +++ b/ui/src/lib/settings.js @@ -5,13 +5,14 @@ export class Settings { } } - update({ language, gong_interval, weather_delay, weather_action, max_run_time, max_volume }) { + update({ language, gong_interval, weather_delay, weather_action, max_run_time, max_volume, federation }) { this.language = language; this.gong_interval = gong_interval; this.weather_delay = weather_delay; this.weather_action = weather_action; this.max_run_time = max_run_time; this.max_volume = max_volume; + this.federation = federation; } async save() { diff --git a/ui/src/routes/alarms/[kind]/[aid]/+page.svelte b/ui/src/routes/alarms/[kind]/[aid]/+page.svelte index f76ae41..265e70f 100644 --- a/ui/src/routes/alarms/[kind]/[aid]/+page.svelte +++ b/ui/src/routes/alarms/[kind]/[aid]/+page.svelte @@ -95,6 +95,10 @@ Heure du réveil + + Fédération activée ? + {obj.enable_federation = !obj.enable_federation; obj.save();}} checked={obj.enable_federation} /> {obj.enable_federation?"oui":"non"} + {/await} {:else if $page.params["kind"] == "repeated"} @@ -126,6 +130,10 @@ Ignorer les exceptions ? {obj.ignore_exceptions = !obj.ignore_exceptions; obj.save();}} checked={obj.ignore_exceptions} /> {obj.ignore_exceptions?"oui":"non"} + + Fédération activée ? + {obj.enable_federation = !obj.enable_federation; obj.save();}} checked={obj.enable_federation} /> {obj.enable_federation?"oui":"non"} + {#if alarm.next_time} Prochaine occurrence diff --git a/ui/src/routes/alarms/[kind]/new/+page.svelte b/ui/src/routes/alarms/[kind]/new/+page.svelte index c48c453..3f4d70e 100644 --- a/ui/src/routes/alarms/[kind]/new/+page.svelte +++ b/ui/src/routes/alarms/[kind]/new/+page.svelte @@ -154,6 +154,11 @@ {/if} + {#if $page.params["kind"] != "exceptions"} + + + + {/if} diff --git a/ui/src/routes/settings/+page.svelte b/ui/src/routes/settings/+page.svelte index f43e57b..c17e402 100644 --- a/ui/src/routes/settings/+page.svelte +++ b/ui/src/routes/settings/+page.svelte @@ -15,6 +15,7 @@ import { actions } from '$lib/stores/actions'; import { getSettings } from '$lib/settings'; + import FederationSettings from '$lib/components/FederationSettings.svelte'; let settingsP = getSettings(); @@ -130,6 +131,15 @@ /> + + + + + {/await}