Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions backend/internal/app/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,16 @@ func (a *App) getStatus(c *gin.Context) {
type ToggleStatusRequest struct {
CardID string `json:"cardId"`
Hash string `json:"hash"`
// Reason marks a non-standard close. When set to "gelatino" the toggle
// becomes an idempotent close ("chiusa per gelatino") instead of flipping
// the previous state. Empty for a regular toggle.
Reason string `json:"reason,omitempty"`
}

// reasonGelatino is the canonical tag for the "chiusa per gelatino" closure,
// triggered by a fast double-click on the physical button.
const reasonGelatino = "gelatino"

func (a *App) toggleStatus(c *gin.Context) {
sp := spaceFrom(c)

Expand Down Expand Up @@ -117,9 +125,17 @@ func (a *App) toggleStatus(c *gin.Context) {
}
}

// "gelatino" is a forced close, not a flip: a double-click should always
// land in the closed state regardless of the previous one.
newIsOpen := !currentStatus.IsOpen
if req.Reason == reasonGelatino {
newIsOpen = false
}

newStatus := database.SedeStatus{
SpaceID: sp.ID,
IsOpen: !currentStatus.IsOpen,
IsOpen: newIsOpen,
Reason: req.Reason,
Timestamp: time.Now().UTC(),
}

Expand All @@ -136,6 +152,10 @@ func (a *App) toggleStatus(c *gin.Context) {
emoji = "🔴"
action = "chiusa"
}
if newStatus.Reason == reasonGelatino {
emoji = "🍦"
action = "chiusa per gelatino"
}

var msg string
if cardName != "" {
Expand Down Expand Up @@ -281,9 +301,19 @@ func (a *App) getSpaceAPI(c *gin.Context) {

var isOpen bool
var lastChange int64
var reason string
if !errors.Is(err, gorm.ErrRecordNotFound) {
isOpen = status.IsOpen
lastChange = status.Timestamp.Unix()
reason = status.Reason
}

// When the latest event is a gelatino closure, surface it in the SpaceAPI
// message field so any external consumer (websites, dashboards) sees the
// reason rather than just "closed".
message := sp.Message
if !isOpen && reason == reasonGelatino {
message = "Chiusa per gelatino 🍦"
}

var projects []string
Expand Down Expand Up @@ -313,7 +343,7 @@ func (a *App) getSpaceAPI(c *gin.Context) {
State: SpaceAPIState{
Open: isOpen,
LastChange: lastChange,
Message: sp.Message,
Message: message,
},
Contact: map[string]string{
"email": sp.ContactEmail,
Expand Down
6 changes: 6 additions & 0 deletions backend/internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,16 @@ type Space struct {
// SpaceID exists solely so that adding the column via SQLite ALTER TABLE on
// an existing single-space DB succeeds; new rows always set SpaceID
// explicitly, and boot-time backfill rewrites any legacy zeros.
//
// Reason is an optional tag explaining a non-standard closure (e.g. "gelatino"
// when the sede is closed because everyone went for ice cream). Empty on
// normal toggles. Stored as a plain string column to keep the schema simple
// and the ESP32 client free to send arbitrary reasons over the wire.
type SedeStatus struct {
ID uint `gorm:"primarykey"`
SpaceID uint `gorm:"not null;default:0;index:idx_space_timestamp,priority:1"`
IsOpen bool `gorm:"not null"`
Reason string `gorm:"default:''"`
Timestamp time.Time `gorm:"not null;index:idx_space_timestamp,priority:2"`
}

Expand Down
173 changes: 112 additions & 61 deletions hardware/config/pulsante-sede.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -170,67 +170,118 @@ binary_sensor:
mode: INPUT_PULLUP
inverted: true
name: "Pulsante Sede"
on_press:
then:
- light.turn_on:
id: rgb_light
effect: "Elaborazione"
red: 0%
green: 0%
blue: 100%
- http_request.post:
url: !secret sede_toggle_url
headers:
X-API-KEY: !secret sede_api_key
Content-Type: application/json
json:
cardId: !lambda 'return id(card_id);'
hash: !lambda 'return id(card_hash);'
capture_response: true
on_response:
then:
- if:
condition:
lambda: return response->status_code == 200;
then:
- if:
condition:
lambda: return body == "true";
then:
- light.turn_on:
id: rgb_light
red: 0%
green: 100%
blue: 0%
effect: none
- globals.set:
id: sede_state
value: 'true'
else:
- light.turn_on:
id: rgb_light
red: 100%
green: 0%
blue: 0%
effect: none
- globals.set:
id: sede_state
value: 'false'
else:
- light.turn_on:
id: rgb_light
effect: "Errore"
- globals.set:
id: sede_state
value: 'false'
on_error:
then:
- light.turn_on:
id: rgb_light
effect: "Errore"
- globals.set:
id: sede_state
value: 'false'
on_multi_click:
# Doppio click veloce -> sede chiusa per gelatino (LED azzurro)
- timing:
- ON for at most 0.5s
- OFF for at most 0.3s
- ON for at most 0.5s
- OFF for at least 0.2s
then:
- light.turn_on:
id: rgb_light
effect: "Elaborazione"
red: 0%
green: 50%
blue: 100%
- http_request.post:
url: !secret sede_toggle_url
headers:
X-API-KEY: !secret sede_api_key
Content-Type: application/json
json:
cardId: !lambda 'return id(card_id);'
hash: !lambda 'return id(card_hash);'
reason: "gelatino"
capture_response: true
on_response:
then:
- if:
condition:
lambda: return response->status_code == 200;
then:
- globals.set:
id: sede_state
value: 'false'
- light.turn_on:
id: rgb_light
red: 0%
green: 50%
blue: 100%
effect: none
else:
- light.turn_on:
id: rgb_light
effect: "Errore"
on_error:
then:
- light.turn_on:
id: rgb_light
effect: "Errore"
# Singolo click -> toggle standard
- timing:
- ON for at most 1s
- OFF for at least 0.5s
then:
- light.turn_on:
id: rgb_light
effect: "Elaborazione"
red: 0%
green: 0%
blue: 100%
- http_request.post:
url: !secret sede_toggle_url
headers:
X-API-KEY: !secret sede_api_key
Content-Type: application/json
json:
cardId: !lambda 'return id(card_id);'
hash: !lambda 'return id(card_hash);'
capture_response: true
on_response:
then:
- if:
condition:
lambda: return response->status_code == 200;
then:
- if:
condition:
lambda: return body == "true";
then:
- light.turn_on:
id: rgb_light
red: 0%
green: 100%
blue: 0%
effect: none
- globals.set:
id: sede_state
value: 'true'
else:
- light.turn_on:
id: rgb_light
red: 100%
green: 0%
blue: 0%
effect: none
- globals.set:
id: sede_state
value: 'false'
else:
- light.turn_on:
id: rgb_light
effect: "Errore"
- globals.set:
id: sede_state
value: 'false'
on_error:
then:
- light.turn_on:
id: rgb_light
effect: "Errore"
- globals.set:
id: sede_state
value: 'false'

i2c:
sda: GPIO5
Expand Down
Loading