Operator docs

New sauna setup

Install a Raspberry Pi + relay + 12V maglock at the sauna, register the device in the admin dashboard, and paste the code below onto the Pi. The controller connects to Endless Summer Concepts West Coast over a persistent secure WebSocket — no port forwarding required.

1. Hardware

  • Raspberry Pi Zero 2 W (or Pi 4) on the site's WiFi or a 4G dongle
  • 5V optocoupler relay module
  • 12V DC magnetic lock (wired Normally Closed — power loss unlocks)
  • Dual-voltage power supply (5V for the Pi, 12V for the lock)
  • Hardwired physical exit button in-line with the 12V power feed

2. Wiring

Pi Pin 2  (5V)   → Relay VCC
Pi Pin 6  (GND)  → Relay GND
Pi Pin 11 (GPIO 17) → Relay IN

12V (+)  → Relay COM
Relay NC → Maglock (+)
12V (-)  → Maglock (-)   (in-line with exit button)

Using the NC (Normally Closed) contact means the door opens automatically on any power failure.

3. Register the device

In the admin dashboard, add a new sauna (or open an existing one) and click Generate device credentials. Copy the OSAUNA_DEVICE_ID and OSAUNA_SHARED_SECRET into the systemd unit below.

4. Pi control script

Save to /home/pi/sauna_control.py

import asyncio, json, os, websockets
from gpiozero import OutputDevice
from time import sleep

DEVICE_ID = os.environ["OSAUNA_DEVICE_ID"]
SHARED_SECRET = os.environ["OSAUNA_SHARED_SECRET"]
API_URL = f"wss://your-endless-summer-domain/api/public/ws/devices/{DEVICE_ID}"

# GPIO 17 → relay IN.  active_high=False for low-level-trigger modules.
relay = OutputDevice(17, active_high=False, initial_value=False)

def unlock_door(duration_minutes: int):
    print(f"Unlocking for {duration_minutes} minutes…")
    try:
        relay.on()                       # cut power to maglock → door open
        sleep(duration_minutes * 60)
    finally:
        relay.off()                      # re-engage maglock
        print("Locked.")

async def listen():
    headers = {"Authorization": f"Bearer {SHARED_SECRET}"}
    async with websockets.connect(API_URL, extra_headers=headers, ping_interval=30) as ws:
        print("Connected to Endless Summer Concepts West Coast.")
        while True:
            msg = await ws.recv()
            data = json.loads(msg)
            if data.get("event") == "PAYMENT_SUCCESS":
                minutes = int(data.get("duration_minutes", 30))
                loop = asyncio.get_event_loop()
                loop.run_in_executor(None, unlock_door, minutes)

while True:
    try:
        asyncio.run(listen())
    except Exception as e:
        print("Reconnecting in 5s:", e)
        sleep(5)

5. Run it forever

Save to /etc/systemd/system/endless-summer.service

[Unit]
Description=Endless Summer Concepts West Coast Door Access
After=network-online.target
Wants=network-online.target

[Service]
Environment=OSAUNA_DEVICE_ID=sauna_04_capetown
Environment=OSAUNA_SHARED_SECRET=REPLACE_ME
ExecStart=/usr/bin/python3 /home/pi/sauna_control.py
WorkingDirectory=/home/pi
Restart=always
RestartSec=5
User=pi

[Install]
WantedBy=multi-user.target
sudo apt update && sudo apt install -y python3-pip
pip3 install websockets gpiozero
sudo systemctl enable --now endless-summer.service

Security notes

  • The shared secret is validated on every connection with a timing-safe compare.
  • Pi never accepts inbound connections; all traffic is outbound WSS.
  • Rotate OSAUNA_SHARED_SECRET from the admin dashboard at any time.