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}