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_SECRETfrom the admin dashboard at any time.
