Implement Samsung FridgeOS-themed shell (PLAN.md §3.3) with inventory management, temperature controls, diagnostics, alerts, and other appliance commands. Add per-credential shell routing so static credentials can specify which shell to use via the `shell` config field, passed through ssh.Permissions.Extensions. Also extract shared ReadLine helper from bash to the shell package so both shells can reuse terminal input handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
349 lines
9.9 KiB
Go
349 lines
9.9 KiB
Go
package fridge
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.t-juice.club/torjus/oubliette/internal/shell"
|
|
)
|
|
|
|
const sessionTimeout = 5 * time.Minute
|
|
|
|
// FridgeShell emulates a Samsung Smart Fridge OS interface.
|
|
type FridgeShell struct{}
|
|
|
|
// NewFridgeShell returns a new FridgeShell instance.
|
|
func NewFridgeShell() *FridgeShell {
|
|
return &FridgeShell{}
|
|
}
|
|
|
|
func (f *FridgeShell) Name() string { return "fridge" }
|
|
func (f *FridgeShell) Description() string { return "Samsung Smart Fridge shell emulator" }
|
|
|
|
func (f *FridgeShell) Handle(ctx context.Context, sess *shell.SessionContext, rw io.ReadWriteCloser) error {
|
|
ctx, cancel := context.WithTimeout(ctx, sessionTimeout)
|
|
defer cancel()
|
|
|
|
state := newFridgeState()
|
|
|
|
// Boot banner.
|
|
fmt.Fprint(rw, bootBanner())
|
|
|
|
for {
|
|
if _, err := fmt.Fprint(rw, "FridgeOS> "); err != nil {
|
|
return nil
|
|
}
|
|
|
|
line, err := shell.ReadLine(ctx, rw)
|
|
if errors.Is(err, io.EOF) {
|
|
fmt.Fprint(rw, "logout\r\n")
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
|
|
result := state.dispatch(trimmed)
|
|
|
|
var output string
|
|
if result.output != "" {
|
|
output = result.output
|
|
output = strings.ReplaceAll(output, "\r\n", "\n")
|
|
output = strings.ReplaceAll(output, "\n", "\r\n")
|
|
fmt.Fprintf(rw, "%s\r\n", output)
|
|
}
|
|
|
|
// Log command and output to store.
|
|
if sess.Store != nil {
|
|
if err := sess.Store.AppendSessionLog(ctx, sess.SessionID, trimmed, output); err != nil {
|
|
return fmt.Errorf("append session log: %w", err)
|
|
}
|
|
}
|
|
|
|
if result.exit {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func bootBanner() string {
|
|
now := time.Now()
|
|
defrost := now.Add(-3*time.Hour - 22*time.Minute).Format("2006-01-02 15:04")
|
|
return fmt.Sprintf(`
|
|
_____ ____ ___ ____ ____ _____ ___ ____
|
|
| ___| _ \|_ _| _ \ / ___| ____/ _ \/ ___|
|
|
| |_ | |_) || || | | | | _| _|| | | \___ \
|
|
| _| | _ < | || |_| | |_| | |__| |_| |___) |
|
|
|_| |_| \_\___|____/ \____|_____\___/|____/
|
|
|
|
Samsung Smart Fridge OS v3.2.1 (FridgeOS-ARM)
|
|
Model: RF28R7351SR | Serial: SN-2847-FRDG-9182
|
|
Firmware: 3.2.1-stable | Last defrost: %s
|
|
|
|
Type 'help' for available commands.
|
|
|
|
`, defrost)
|
|
}
|
|
|
|
type fridgeState struct {
|
|
inventory []inventoryItem
|
|
fridgeF int // fridge temp in °F
|
|
freezerF int // freezer temp in °F
|
|
}
|
|
|
|
type inventoryItem struct {
|
|
name string
|
|
expiry string
|
|
}
|
|
|
|
type commandResult struct {
|
|
output string
|
|
exit bool
|
|
}
|
|
|
|
func newFridgeState() *fridgeState {
|
|
return &fridgeState{
|
|
inventory: []inventoryItem{
|
|
{"Whole Milk (1 gal)", time.Now().Add(48 * time.Hour).Format("2006-01-02")},
|
|
{"Eggs (dozen)", time.Now().Add(7 * 24 * time.Hour).Format("2006-01-02")},
|
|
{"Leftover Pizza (3 slices)", time.Now().Add(24 * time.Hour).Format("2006-01-02")},
|
|
{"Orange Juice", time.Now().Add(5 * 24 * time.Hour).Format("2006-01-02")},
|
|
{"Butter (unsalted)", time.Now().Add(30 * 24 * time.Hour).Format("2006-01-02")},
|
|
{"Mystery Tupperware", time.Now().Add(-14 * 24 * time.Hour).Format("2006-01-02")},
|
|
},
|
|
fridgeF: 37,
|
|
freezerF: 0,
|
|
}
|
|
}
|
|
|
|
func (s *fridgeState) dispatch(input string) commandResult {
|
|
parts := strings.Fields(input)
|
|
if len(parts) == 0 {
|
|
return commandResult{}
|
|
}
|
|
|
|
cmd := strings.ToLower(parts[0])
|
|
args := parts[1:]
|
|
|
|
switch cmd {
|
|
case "help":
|
|
return s.cmdHelp()
|
|
case "inventory":
|
|
return s.cmdInventory(args)
|
|
case "temp", "temperature":
|
|
return s.cmdTemp(args)
|
|
case "status":
|
|
return s.cmdStatus()
|
|
case "diagnostics":
|
|
return s.cmdDiagnostics()
|
|
case "alerts":
|
|
return s.cmdAlerts()
|
|
case "reboot":
|
|
return s.cmdReboot()
|
|
case "exit", "logout":
|
|
return commandResult{output: "Goodbye! Keep your food fresh!", exit: true}
|
|
default:
|
|
return commandResult{output: fmt.Sprintf("FridgeOS: unknown command '%s'. Type 'help' for available commands.", cmd)}
|
|
}
|
|
}
|
|
|
|
func (s *fridgeState) cmdHelp() commandResult {
|
|
help := `Available commands:
|
|
help - Show this help message
|
|
inventory - List fridge contents
|
|
inventory add <item> - Add item to inventory
|
|
inventory remove <item> - Remove item from inventory
|
|
temp - Show current temperatures
|
|
temp set <zone> <value> - Set temperature (zone: fridge|freezer)
|
|
status - Show system status
|
|
diagnostics - Run system diagnostics
|
|
alerts - Show active alerts
|
|
reboot - Reboot FridgeOS
|
|
exit / logout - Disconnect`
|
|
return commandResult{output: help}
|
|
}
|
|
|
|
func (s *fridgeState) cmdInventory(args []string) commandResult {
|
|
if len(args) == 0 || strings.ToLower(args[0]) == "list" {
|
|
return s.inventoryList()
|
|
}
|
|
|
|
sub := strings.ToLower(args[0])
|
|
switch sub {
|
|
case "add":
|
|
if len(args) < 2 {
|
|
return commandResult{output: "Usage: inventory add <item>"}
|
|
}
|
|
item := strings.Join(args[1:], " ")
|
|
return s.inventoryAdd(item)
|
|
case "remove":
|
|
if len(args) < 2 {
|
|
return commandResult{output: "Usage: inventory remove <item>"}
|
|
}
|
|
item := strings.Join(args[1:], " ")
|
|
return s.inventoryRemove(item)
|
|
default:
|
|
return commandResult{output: fmt.Sprintf("Unknown inventory subcommand '%s'. Try: list, add, remove", sub)}
|
|
}
|
|
}
|
|
|
|
func (s *fridgeState) inventoryList() commandResult {
|
|
if len(s.inventory) == 0 {
|
|
return commandResult{output: "Inventory is empty."}
|
|
}
|
|
var b strings.Builder
|
|
b.WriteString("=== Fridge Inventory ===\n")
|
|
b.WriteString(fmt.Sprintf("%-30s %s\n", "ITEM", "EXPIRES"))
|
|
b.WriteString(fmt.Sprintf("%-30s %s\n", "----", "-------"))
|
|
for _, item := range s.inventory {
|
|
b.WriteString(fmt.Sprintf("%-30s %s\n", item.name, item.expiry))
|
|
}
|
|
b.WriteString(fmt.Sprintf("\nTotal items: %d", len(s.inventory)))
|
|
return commandResult{output: b.String()}
|
|
}
|
|
|
|
func (s *fridgeState) inventoryAdd(item string) commandResult {
|
|
expiry := time.Now().Add(7 * 24 * time.Hour).Format("2006-01-02")
|
|
s.inventory = append(s.inventory, inventoryItem{name: item, expiry: expiry})
|
|
return commandResult{output: fmt.Sprintf("Added '%s' to inventory (expires: %s).", item, expiry)}
|
|
}
|
|
|
|
func (s *fridgeState) inventoryRemove(item string) commandResult {
|
|
lower := strings.ToLower(item)
|
|
for i, inv := range s.inventory {
|
|
if strings.ToLower(inv.name) == lower || strings.Contains(strings.ToLower(inv.name), lower) {
|
|
s.inventory = append(s.inventory[:i], s.inventory[i+1:]...)
|
|
return commandResult{output: fmt.Sprintf("Removed '%s' from inventory.", inv.name)}
|
|
}
|
|
}
|
|
return commandResult{output: fmt.Sprintf("Item '%s' not found in inventory.", item)}
|
|
}
|
|
|
|
func (s *fridgeState) cmdTemp(args []string) commandResult {
|
|
if len(args) == 0 {
|
|
return commandResult{output: fmt.Sprintf(
|
|
"=== Temperature Status ===\nFridge: %d°F (%.1f°C)\nFreezer: %d°F (%.1f°C)",
|
|
s.fridgeF, fToC(s.fridgeF), s.freezerF, fToC(s.freezerF),
|
|
)}
|
|
}
|
|
|
|
if strings.ToLower(args[0]) != "set" || len(args) < 3 {
|
|
return commandResult{output: "Usage: temp set <fridge|freezer> <value_in_F>"}
|
|
}
|
|
|
|
zone := strings.ToLower(args[1])
|
|
var val int
|
|
if _, err := fmt.Sscanf(args[2], "%d", &val); err != nil {
|
|
return commandResult{output: "Invalid temperature value. Must be an integer."}
|
|
}
|
|
|
|
switch zone {
|
|
case "fridge":
|
|
if val < 33 || val > 45 {
|
|
return commandResult{output: fmt.Sprintf("WARNING: Temperature %d°F is out of safe range (33-45°F). Setting rejected.", val)}
|
|
}
|
|
s.fridgeF = val
|
|
return commandResult{output: fmt.Sprintf("Fridge temperature set to %d°F (%.1f°C).", val, fToC(val))}
|
|
case "freezer":
|
|
if val < -10 || val > 10 {
|
|
return commandResult{output: fmt.Sprintf("WARNING: Temperature %d°F is out of safe range (-10 to 10°F). Setting rejected.", val)}
|
|
}
|
|
s.freezerF = val
|
|
return commandResult{output: fmt.Sprintf("Freezer temperature set to %d°F (%.1f°C).", val, fToC(val))}
|
|
default:
|
|
return commandResult{output: fmt.Sprintf("Unknown zone '%s'. Use 'fridge' or 'freezer'.", zone)}
|
|
}
|
|
}
|
|
|
|
func fToC(f int) float64 {
|
|
return float64(f-32) * 5.0 / 9.0
|
|
}
|
|
|
|
func (s *fridgeState) cmdStatus() commandResult {
|
|
status := `=== FridgeOS System Status ===
|
|
Compressor: Running
|
|
Door seal: OK
|
|
Ice maker: Active
|
|
Water filter: 82% remaining
|
|
|
|
WiFi: Connected (SmartHome-5G)
|
|
Signal: -42 dBm
|
|
Internal camera: Online (3 objects detected)
|
|
Voice assistant: Standby
|
|
TikTok recipes: Enabled
|
|
Spotify: "Chill Vibes" playlist paused
|
|
|
|
Energy rating: A++
|
|
Power: 127W
|
|
SmartHome Hub: Connected (12 devices)
|
|
|
|
Firmware: v3.2.1-stable
|
|
Update available: v3.3.0-beta`
|
|
return commandResult{output: status}
|
|
}
|
|
|
|
func (s *fridgeState) cmdDiagnostics() commandResult {
|
|
diag := `Running FridgeOS diagnostics...
|
|
|
|
[1/6] Compressor.............. OK
|
|
[2/6] Temperature sensors..... OK
|
|
[3/6] Door seal integrity..... OK
|
|
[4/6] Ice maker............... OK
|
|
[5/6] Network connectivity.... OK
|
|
[6/6] Internal camera......... OK
|
|
|
|
ALL SYSTEMS NOMINAL`
|
|
return commandResult{output: diag}
|
|
}
|
|
|
|
func (s *fridgeState) cmdAlerts() commandResult {
|
|
// Build dynamic alerts based on inventory.
|
|
var alerts []string
|
|
for _, item := range s.inventory {
|
|
expiry, err := time.Parse("2006-01-02", item.expiry)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
days := int(time.Until(expiry).Hours() / 24)
|
|
if days < 0 {
|
|
alerts = append(alerts, fmt.Sprintf("CRITICAL: %s expired %d day(s) ago!", item.name, -days))
|
|
} else if days <= 2 {
|
|
alerts = append(alerts, fmt.Sprintf("WARNING: %s expires in %d day(s)", item.name, days))
|
|
}
|
|
}
|
|
alerts = append(alerts,
|
|
"INFO: Ice maker: low water pressure detected",
|
|
"INFO: Firmware update available: v3.3.0-beta",
|
|
"INFO: TikTok recipe sync overdue (last sync: 3 days ago)",
|
|
)
|
|
|
|
var b strings.Builder
|
|
b.WriteString("=== Active Alerts ===\n")
|
|
for _, a := range alerts {
|
|
b.WriteString(a + "\n")
|
|
}
|
|
b.WriteString(fmt.Sprintf("\n%d alert(s) active", len(alerts)))
|
|
return commandResult{output: b.String()}
|
|
}
|
|
|
|
func (s *fridgeState) cmdReboot() commandResult {
|
|
reboot := `FridgeOS is rebooting...
|
|
|
|
Stopping services........... done
|
|
Saving inventory data....... done
|
|
Flushing temperature log.... done
|
|
Unmounting partitions....... done
|
|
|
|
Rebooting now. Goodbye!`
|
|
return commandResult{output: reboot, exit: true}
|
|
}
|