Compare commits

...

3 Commits

Author SHA1 Message Date
9fa5a378e1 Do federation wakeup/stop
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-07-25 19:38:02 +02:00
435d1d8f98 ui: Can enable federation on alarms 2024-07-25 19:03:21 +02:00
3a6187d791 Add federation settings 2024-07-25 18:53:00 +02:00
17 changed files with 250 additions and 21 deletions

View File

@ -1,11 +1,14 @@
package api package api
import ( import (
"fmt"
"log"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.nemunai.re/nemunaire/reveil/config" "git.nemunai.re/nemunaire/reveil/config"
"git.nemunai.re/nemunaire/reveil/model"
"git.nemunai.re/nemunaire/reveil/player" "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) { router.POST("/alarm/run", func(c *gin.Context) {
if player.CommonPlayer == nil { if player.CommonPlayer == nil {
err := player.WakeUp(cfg, nil) err := player.WakeUp(cfg, nil, true)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return return
@ -51,6 +54,21 @@ func declareAlarmRoutes(cfg *config.Config, router *gin.RouterGroup) {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return 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) c.JSON(http.StatusOK, true)

View File

@ -13,7 +13,7 @@ import (
func declareAlarmsRoutes(cfg *config.Config, db *reveil.LevelDBStorage, resetTimer func(), router *gin.RouterGroup) { func declareAlarmsRoutes(cfg *config.Config, db *reveil.LevelDBStorage, resetTimer func(), router *gin.RouterGroup) {
router.GET("/alarms/next", func(c *gin.Context) { router.GET("/alarms/next", func(c *gin.Context) {
alarm, _, err := reveil.GetNextAlarm(cfg, db) alarm, _, _, err := reveil.GetNextAlarm(cfg, db)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return return

50
api/federation.go Normal file
View File

@ -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)
})
}

View File

@ -13,6 +13,7 @@ func DeclareRoutes(router *gin.Engine, cfg *config.Config, db *reveil.LevelDBSto
declareActionsRoutes(cfg, apiRoutes) declareActionsRoutes(cfg, apiRoutes)
declareAlarmRoutes(cfg, apiRoutes) declareAlarmRoutes(cfg, apiRoutes)
declareAlarmsRoutes(cfg, db, resetTimer, apiRoutes) declareAlarmsRoutes(cfg, db, resetTimer, apiRoutes)
declareFederationRoutes(cfg, apiRoutes)
declareGongsRoutes(cfg, apiRoutes) declareGongsRoutes(cfg, apiRoutes)
declareHistoryRoutes(cfg, apiRoutes) declareHistoryRoutes(cfg, apiRoutes)
declareQuotesRoutes(cfg, apiRoutes) declareQuotesRoutes(cfg, apiRoutes)

4
app.go
View File

@ -79,7 +79,7 @@ func (app *App) ResetTimer() {
app.nextAlarm = nil 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 = time.AfterFunc(time.Until(*na), func() {
app.nextAlarm = nil app.nextAlarm = nil
reveil.RemoveOldAlarmsSingle(app.db) reveil.RemoveOldAlarmsSingle(app.db)
@ -87,7 +87,7 @@ func (app *App) ResetTimer() {
// Rearm timer for the next time // Rearm timer for the next time
app.ResetTimer() app.ResetTimer()
err := player.WakeUp(app.cfg, routines) err := player.WakeUp(app.cfg, routines, federated)
if err != nil { if err != nil {
log.Println(err.Error()) log.Println(err.Error())
return return

View File

@ -40,25 +40,27 @@ func (h *Hour) UnmarshalJSON(src []byte) error {
return nil 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) alarmsRepeated, err := GetAlarmsRepeated(db)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, false, err
} }
var closestAlarm *time.Time var closestAlarm *time.Time
var closestAlarmRoutines []Identifier var closestAlarmRoutines []Identifier
var closestAlarmFederated bool
for _, alarm := range alarmsRepeated { for _, alarm := range alarmsRepeated {
next := alarm.GetNextOccurence(cfg, db) next := alarm.GetNextOccurence(cfg, db)
if next != nil && (closestAlarm == nil || closestAlarm.After(*next)) { if next != nil && (closestAlarm == nil || closestAlarm.After(*next)) {
closestAlarm = next closestAlarm = next
closestAlarmRoutines = alarm.FollowingRoutines closestAlarmRoutines = alarm.FollowingRoutines
closestAlarmFederated = alarm.EnableFederation
} }
} }
alarmsSingle, err := GetAlarmsSingle(db) alarmsSingle, err := GetAlarmsSingle(db)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, false, err
} }
now := time.Now() 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)) { if closestAlarm == nil || (closestAlarm.After(alarm.Time) && alarm.Time.After(now)) {
closestAlarm = &alarm.Time closestAlarm = &alarm.Time
closestAlarmRoutines = alarm.FollowingRoutines 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) { 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 { func DropNextAlarm(cfg *config.Config, db *LevelDBStorage) error {
timenext, _, err := GetNextAlarm(cfg, db) timenext, _, _, err := GetNextAlarm(cfg, db)
if err != nil { if err != nil {
return err return err
} }
@ -152,6 +155,7 @@ type AlarmRepeated struct {
Disabled bool `json:"disabled,omitempty"` Disabled bool `json:"disabled,omitempty"`
Excepts Exceptions `json:"excepts,omitempty"` Excepts Exceptions `json:"excepts,omitempty"`
NextTime *time.Time `json:"next_time,omitempty"` NextTime *time.Time `json:"next_time,omitempty"`
EnableFederation bool `json:"enable_federation,omitempty"`
} }
func (a *AlarmRepeated) FillExcepts(cfg *config.Config, db *LevelDBStorage) error { func (a *AlarmRepeated) FillExcepts(cfg *config.Config, db *LevelDBStorage) error {
@ -273,6 +277,7 @@ type AlarmSingle struct {
Time time.Time `json:"time"` Time time.Time `json:"time"`
FollowingRoutines []Identifier `json:"routines"` FollowingRoutines []Identifier `json:"routines"`
Comment string `json:"comment,omitempty"` Comment string `json:"comment,omitempty"`
EnableFederation bool `json:"enable_federation,omitempty"`
} }
func GetAlarmSingle(db *LevelDBStorage, id Identifier) (alarm *AlarmSingle, err error) { func GetAlarmSingle(db *LevelDBStorage, id Identifier) (alarm *AlarmSingle, err error) {

View File

@ -6,14 +6,19 @@ import (
"time" "time"
) )
type FederationSettings struct {
URL string `json:"url"`
}
// Settings represents the settings panel. // Settings represents the settings panel.
type Settings struct { type Settings struct {
Language string `json:"language"` Language string `json:"language"`
GongInterval time.Duration `json:"gong_interval"` GongInterval time.Duration `json:"gong_interval"`
WeatherDelay time.Duration `json:"weather_delay"` WeatherDelay time.Duration `json:"weather_delay"`
WeatherAction string `json:"weather_action"` WeatherAction string `json:"weather_action"`
MaxRunTime time.Duration `json:"max_run_time"` MaxRunTime time.Duration `json:"max_run_time"`
MaxVolume uint16 `json:"max_volume"` MaxVolume uint16 `json:"max_volume"`
Federation map[string]FederationSettings `json:"federation"`
} }
// ExistsSettings checks if the settings file can by found at the given path. // ExistsSettings checks if the settings file can by found at the given path.

35
player/federation.go Normal file
View File

@ -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
}

View File

@ -43,13 +43,34 @@ type Player struct {
playedItem int 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 { if CommonPlayer != nil {
return fmt.Errorf("Unable to start the player: a player is already running") return fmt.Errorf("Unable to start the player: a player is already running")
} }
seed := time.Now().Unix() seed := time.Now().Unix()
seed -= seed % 172800 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) rand.Seed(seed)
CommonPlayer, err = NewPlayer(cfg, routine) CommonPlayer, err = NewPlayer(cfg, routine)

View File

@ -24,7 +24,7 @@ func DeclareNoJSRoutes(router *gin.Engine, cfg *config.Config, db *reveil.LevelD
router.SetHTMLTemplate(templ) router.SetHTMLTemplate(templ)
router.GET("/nojs.html", func(c *gin.Context) { router.GET("/nojs.html", func(c *gin.Context) {
alarm, _, err := reveil.GetNextAlarm(cfg, db) alarm, _, _, err := reveil.GetNextAlarm(cfg, db)
if err != nil { if err != nil {
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"errmsg": err.Error()}) c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"errmsg": err.Error()})
return return
@ -122,7 +122,7 @@ func DeclareNoJSRoutes(router *gin.Engine, cfg *config.Config, db *reveil.LevelD
case "start": case "start":
if player.CommonPlayer == nil { if player.CommonPlayer == nil {
err := player.WakeUp(cfg, nil) err := player.WakeUp(cfg, nil, true)
if err != nil { if err != nil {
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"errmsg": err.Error()}) c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"errmsg": err.Error()})
return return

View File

@ -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.id = id;
this.weekday = weekday; this.weekday = weekday;
this.time = time; this.time = time;
@ -13,6 +13,7 @@ export class AlarmRepeated {
this.ignore_exceptions = ignore_exceptions; this.ignore_exceptions = ignore_exceptions;
this.comment = comment; this.comment = comment;
this.disabled = disabled == true; this.disabled = disabled == true;
this.enable_federation = enable_federation == true;
if (excepts !== undefined) if (excepts !== undefined)
this.excepts = excepts; this.excepts = excepts;
if (next_time !== undefined) if (next_time !== undefined)

View File

@ -5,11 +5,12 @@ export class AlarmSingle {
} }
} }
update({ id, time, routines, comment }) { update({ id, time, routines, comment, enable_federation }) {
this.id = id; this.id = id;
this.time = new Date(time); this.time = new Date(time);
this.routines = routines == null ? [] : routines; this.routines = routines == null ? [] : routines;
this.comment = comment; this.comment = comment;
this.enable_federation = enable_federation == true;
if (this.routines.length < 1) { if (this.routines.length < 1) {
this.routines.push(""); this.routines.push("");

View File

@ -0,0 +1,68 @@
<script>
import { createEventDispatcher } from 'svelte';
import {
Col,
Input,
Row,
} from '@sveltestrap/sveltestrap';
const dispatch = createEventDispatcher();
export let id = "";
export let value = { };
function changeKey(bak, to) {
if (bak === null && to.target.value) {
if (!value) value = { };
value[to.target.value] = { url:"" };
to.target.value = "";
value = value;
} else {
value[to.target.value] = value[bak];
delete value[bak];
}
}
</script>
{#if value}
{#each Object.keys(value) as key}
<Row>
<Col>
<Input
type="string"
value={key}
on:change={() => dispatch("input")}
on:input={(e) => changeKey(key, e)}
/>
</Col>
<Col>
<Input
type="string"
placeholder="https://reveil.fr/"
bind:value={value[key].url}
on:change={() => dispatch("input")}
/>
</Col>
</Row>
{/each}
{/if}
<Row>
<Col>
<Input
type="string"
{id}
placeholder="name"
value=""
on:input={(e) => changeKey(null, e)}
/>
</Col>
<Col>
<Input
type="string"
placeholder="https://reveil.fr/"
disabled
value=""
/>
</Col>
</Row>

View File

@ -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.language = language;
this.gong_interval = gong_interval; this.gong_interval = gong_interval;
this.weather_delay = weather_delay; this.weather_delay = weather_delay;
this.weather_action = weather_action; this.weather_action = weather_action;
this.max_run_time = max_run_time; this.max_run_time = max_run_time;
this.max_volume = max_volume; this.max_volume = max_volume;
this.federation = federation;
} }
async save() { async save() {

View File

@ -95,6 +95,10 @@
<ListGroupItem> <ListGroupItem>
<strong>Heure du réveil</strong> <DateFormat date={alarm.time} timeStyle="long" /> <strong>Heure du réveil</strong> <DateFormat date={alarm.time} timeStyle="long" />
</ListGroupItem> </ListGroupItem>
<ListGroupItem class="d-flex">
<strong>Fédération activée&nbsp;?</strong>
<Input type="switch" class="ms-2" on:change={() => {obj.enable_federation = !obj.enable_federation; obj.save();}} checked={obj.enable_federation} /> {obj.enable_federation?"oui":"non"}
</ListGroupItem>
</ListGroup> </ListGroup>
{/await} {/await}
{:else if $page.params["kind"] == "repeated"} {:else if $page.params["kind"] == "repeated"}
@ -126,6 +130,10 @@
<strong>Ignorer les exceptions&nbsp;?</strong> <strong>Ignorer les exceptions&nbsp;?</strong>
<Input type="switch" class="ms-2" on:change={() => {obj.ignore_exceptions = !obj.ignore_exceptions; obj.save();}} checked={obj.ignore_exceptions} /> {obj.ignore_exceptions?"oui":"non"} <Input type="switch" class="ms-2" on:change={() => {obj.ignore_exceptions = !obj.ignore_exceptions; obj.save();}} checked={obj.ignore_exceptions} /> {obj.ignore_exceptions?"oui":"non"}
</ListGroupItem> </ListGroupItem>
<ListGroupItem class="d-flex">
<strong>Fédération activée&nbsp;?</strong>
<Input type="switch" class="ms-2" on:change={() => {obj.enable_federation = !obj.enable_federation; obj.save();}} checked={obj.enable_federation} /> {obj.enable_federation?"oui":"non"}
</ListGroupItem>
{#if alarm.next_time} {#if alarm.next_time}
<ListGroupItem> <ListGroupItem>
<strong>Prochaine occurrence</strong> <DateFormat date={new Date(obj.next_time)} dateStyle="long" /> <strong>Prochaine occurrence</strong> <DateFormat date={new Date(obj.next_time)} dateStyle="long" />

View File

@ -154,6 +154,11 @@
<Input id="exceptionEnd" type="date" required bind:value={obj.end} /> <Input id="exceptionEnd" type="date" required bind:value={obj.end} />
</FormGroup> </FormGroup>
{/if} {/if}
{#if $page.params["kind"] != "exceptions"}
<FormGroup>
<Input id="enable_federation" type="checkbox" label="Activer la fédération" bind:checked={obj.enable_federation} />
</FormGroup>
{/if}
<FormGroup> <FormGroup>
<Label for="comment">Commentaire</Label> <Label for="comment">Commentaire</Label>

View File

@ -15,6 +15,7 @@
import { actions } from '$lib/stores/actions'; import { actions } from '$lib/stores/actions';
import { getSettings } from '$lib/settings'; import { getSettings } from '$lib/settings';
import FederationSettings from '$lib/components/FederationSettings.svelte';
let settingsP = getSettings(); let settingsP = getSettings();
@ -130,6 +131,15 @@
/> />
</InputGroup> </InputGroup>
</FormGroup> </FormGroup>
<FormGroup>
<Label for="federation">Federation</Label>
<FederationSettings
id="federation"
bind:value={settings.federation}
on:input={submitSettings}
/>
</FormGroup>
{/await} {/await}
</Form> </Form>
</Container> </Container>