Custom firmware for the Rituals Perfume Genie diffuser (V1 and V2). Replaces the cloud-dependent Rituals firmware with fully local control via Home Assistant.
Community tested! Both ESP8266 (Rituals Genie V1/V2) and ESP32 versions are actively used by the community. Found an issue? Report it here.
ESP8266 Note: Versions v1.9.x include experimental features like NFC scent detection and various memory optimizations. If you experience stability issues on ESP8266, use v1.8.5 as the recommended stable version.
- Local Control - No cloud dependency, works offline
- Home Assistant Integration - MQTT auto-discovery
- NFC Scent Detection - Automatically detects Rituals scent cartridges (v1.9.0+, experimental on ESP8266)
- Timer Presets - 30, 60, 90, 120 minutes + continuous
- Interval Mode - Pulsing mode to save fragrance
- Night Mode - Auto-dim LED during configured hours
- Usage Statistics - Track total runtime
- OTA Updates - Wireless firmware updates via web interface
- Auto-Update - Checks GitHub for new releases, one-click install (ESP32)
- Web Interface - Configure WiFi, MQTT, passwords, and control the diffuser
- RGB LED Status - Visual feedback for device state
- Physical Buttons - Front and rear button support
- Download the latest release from Releases
- Flash using esptool or web flasher (see Installation section)
# Clone repository
git clone https://github.com/martijnrenkema/Rituals-diffuser.git
cd Rituals-diffuser
# Build for ESP32 (recommended)
pio run -e esp32dev
# Or build for ESP32-C3 SuperMini
pio run -e esp32c3_supermini
# Or build for ESP8266 (original chip)
pio run -e esp8266ℹ️ Supported Hardware
This firmware supports both Rituals Perfume Genie V1 and V2 (both contain ESP8266), as well as custom builds with ESP32 or ESP32-C3 SuperMini. Have a different hardware version? Feel free to try and share your feedback!
Connect your ESP32 DevKit to the Rituals Genie board:
| ESP32 GPIO | Genie Board | Wire Color | Function |
|---|---|---|---|
| GPIO25 | IO4 | Blue | Fan PWM speed control |
| GPIO26 | IO5/TP17 | Yellow | Fan tachometer (RPM) |
| GPIO27 | IO15 | Green | WS2812 RGB LED data |
| GPIO13 | SW2 | - | Front button (Connect) |
| GPIO14 | SW1 | - | Rear button (Reset) |
| GND | GND | Black | Ground |
| 3.3V | 3.3V | Red | Power |
RC522 NFC Reader (optional):
| ESP32 GPIO | RC522 Pin | Function |
|---|---|---|
| GPIO18 | SCK | SPI Clock (VSPI) |
| GPIO23 | MOSI | SPI Data Out |
| GPIO19 | MISO | SPI Data In |
| GPIO16 | SDA/CS | Chip Select |
| GPIO17 | RST | Reset |
⚠️ Important: Antenna PlacementWhen installing an ESP32 dev board inside the metal housing, position the board so the WiFi antenna points toward the nozzle opening. The metal enclosure acts as a Faraday cage, blocking WiFi signals. The nozzle opening is the only path for the signal to escape.
| GPIO | Function | Description |
|---|---|---|
| GPIO4 | Fan PWM | Speed control (blue wire) |
| GPIO5 | Fan Tacho | RPM feedback (yellow wire) |
| GPIO15 | LED | WS2812 RGB LED |
| GPIO16 | SW2 | Front/Connect button |
| GPIO3 | SW1 | Rear button (RX pin) |
| GPIO14 | SPI CLK | RC522 SCK (HSPI) |
| GPIO13 | SPI MOSI | RC522 MOSI (HSPI) |
| GPIO12 | SPI MISO | RC522 MISO (HSPI) |
| GPIO0 | RC522 CS | RC522 Chip Select |
| GPIO2 | RC522 RST | RC522 Reset |
NFC Reader (RC522): The Rituals Genie has a built-in RC522 NFC reader on the HSPI bus (GPIO12/13/14). Active after boot.
Compact alternative to the full ESP32 DevKit. Can be soldered directly to the original ESP8266 pads on the Rituals Genie board. Uses safe GPIO pins (ADC1 only, avoids strapping pins 2, 8, 9).
| GPIO | Function | Description |
|---|---|---|
| GPIO3 | Fan PWM | Speed control (ADC1, safe) |
| GPIO4 | Fan Tacho | RPM feedback (ADC1, interrupt) |
| GPIO10 | LED | WS2812 RGB LED |
| GPIO0 | SW2 | Front/Connect button |
| GPIO1 | SW1 | Rear button |
| GPIO6 | SPI CLK | RC522 SCK |
| GPIO7 | SPI MOSI | RC522 MOSI |
| GPIO20 | SPI MISO | RC522 MISO |
| GPIO5 | RC522 CS | RC522 Chip Select |
| GPIO21 | RC522 RST | RC522 Reset |
The ESP32-C3 SuperMini has native USB - no USB-to-serial chip needed. Serial output works directly via USB-C.
Before flashing, backup the original Rituals firmware:
# Find your serial port
ls /dev/cu.usbserial-*
# Backup (2MB flash for ESP8266)
esptool.py --port /dev/cu.usbserial-XXXX read_flash 0x00000 0x200000 rituals_backup.bin# For ESP32
pio run -e esp32dev -t upload --upload-port /dev/cu.usbserial-XXXX
pio run -e esp32dev -t uploadfs --upload-port /dev/cu.usbserial-XXXX
# For ESP8266
pio run -e esp8266 -t upload --upload-port /dev/cu.usbserial-XXXX
pio run -e esp8266 -t uploadfs --upload-port /dev/cu.usbserial-XXXX
⚠️ You must flash TWO files: firmware + filesystem (web interface)
Chip File Address ESP8266 firmware_esp8266.bin0x0ESP8266 littlefs_esp8266.bin0x1E0000ESP32 firmware_esp32.bin0x10000ESP32 spiffs_esp32.bin0x3D0000Web flashers don't work! Tools like ESPHome Flasher only flash to one address. Use
esptool.pyinstead.Install esptool:
pip install esptool(Windows/Mac/Linux guide)
# For ESP8266 - flash BOTH files:
esptool.py --port /dev/cu.usbserial-XXXX --chip esp8266 --baud 460800 \
write_flash 0x0 firmware_esp8266.bin 0x1E0000 littlefs_esp8266.bin
# For ESP32 - flash BOTH files:
esptool.py --port /dev/cu.usbserial-XXXX --chip esp32 --baud 460800 \
write_flash 0x10000 firmware_esp32.bin 0x3D0000 spiffs_esp32.binYou can also flash them separately:
# ESP8266
esptool.py --port /dev/cu.usbserial-XXXX --chip esp8266 write_flash 0x0 firmware_esp8266.bin
esptool.py --port /dev/cu.usbserial-XXXX --chip esp8266 write_flash 0x1E0000 littlefs_esp8266.bin
# ESP32
esptool.py --port /dev/cu.usbserial-XXXX --chip esp32 write_flash 0x10000 firmware_esp32.bin
esptool.py --port /dev/cu.usbserial-XXXX --chip esp32 write_flash 0x3D0000 spiffs_esp32.bin- Power on the device - LED will pulse orange (AP mode)
- Connect to WiFi network:
Rituals-Diffuser-XXXX - Password:
diffuser123 - Open browser:
http://192.168.4.1 - Configure your WiFi credentials
- Device restarts and connects to your network
- Find device IP in your router or use
rituals-diffuser.local - Open web interface
- Enter MQTT broker settings
- Device appears automatically in Home Assistant
Once installed, you can update wirelessly using one of these methods:
- Open web interface:
http://rituals-diffuser.localor device IP - Click "Firmware Update" at bottom
- Upload the firmware
.binfile - Upload the filesystem
.binfile (optional, for web interface updates) - Wait for restart
After the first serial flash, use OTA for subsequent updates:
# ESP8266 - Firmware
pio run -e esp8266_ota -t upload
# ESP8266 - Filesystem (LittleFS)
pio run -e esp8266_ota -t uploadfs
# ESP32 - Firmware
pio run -e esp32_ota -t upload
# ESP32 - Filesystem (SPIFFS)
pio run -e esp32_ota -t uploadfsRequirements:
- Device must be on same network as your computer
- Default hostname:
rituals-diffuser.local - Default OTA password:
diffuser-ota(configurable in web UI → Security) - OTA port: 3232 (ESP32) / 8266 (ESP8266)
Note: OTA updates don't require flash addresses - the ESP framework handles this automatically.
For firmware updates without PlatformIO:
# Download espota.py from Arduino ESP32/ESP8266 repository
python espota.py -i <device-ip> -p 3232 -a diffuser-ota -f firmware.binTip: For filesystem updates, use the web interface instead - it's easier than espota.py for SPIFFS/LittleFS.
The ESP32 uses a dual OTA partition scheme for safe updates:
| Partition | Address | Size | Purpose |
|---|---|---|---|
| app0 (ota_0) | 0x10000 | 1.9MB | OTA slot 0 |
| app1 (ota_1) | 0x1F0000 | 1.9MB | OTA slot 1 |
How it protects your device:
- New firmware is written to the inactive partition
- If write completes successfully, bootloader switches to new partition
- If update fails mid-write, old partition remains active → device keeps working
Note: ESP8266 has a single app partition (no rollback) due to its limited 2MB flash.
The device automatically appears in Home Assistant when MQTT auto-discovery is enabled. No manual configuration needed!
| Entity | Type | Description |
|---|---|---|
| Diffuser | Fan | On/off, speed 0-100%, timer presets |
| Interval Mode | Switch | Pulsing mode toggle |
| Interval On | Number | On-time (10-120 sec) |
| Interval Off | Number | Off-time (10-120 sec) |
| Time Left | Sensor | Remaining timer minutes |
| Fan RPM | Sensor | Current fan speed |
| WiFi Signal | Sensor | Signal strength (dBm) |
| Total Runtime | Sensor | Total device runtime (hours) |
| Scent | Sensor | Current fragrance name (v1.9.0+) |
| Cartridge Present | Binary Sensor | NFC cartridge detected (v1.9.0+) |
- 30 minutes
- 60 minutes
- 90 minutes
- 120 minutes
- Continuous
| Action | Function |
|---|---|
| Short press | Toggle fan on/off |
| Long press (3s) | Start AP mode for WiFi config |
| Action | Function |
|---|---|
| Short press | Restart device |
| Long press (3s) | Factory reset (clears all settings) |
| Color | Pattern | Status |
|---|---|---|
| Red | Blinking | Disconnected / Error |
| Cyan | Fast blink | Connecting to WiFi |
| Green | Solid | Fan running |
| Blue | Solid | Timer active |
| Blue | Slow breathing | Timer + Interval combined |
| Purple | Solid | Interval mode active |
| Orange | Pulsing | AP mode (WiFi config) |
| Purple | Fast blink | OTA update in progress |
| Function | Default Password | Changeable |
|---|---|---|
| WiFi AP | diffuser123 |
Yes |
| OTA Updates | diffuser-ota |
Yes |
Change passwords in web interface under "Security". Minimum 8 characters. Restart required after change.
Automatically dims the LED during specified hours:
- Configure start/end hour (0-23)
- Set dimmed brightness (0-100%)
- Enable/disable via web interface
- Long press front button (3s) to enter AP mode
- Connect to
Rituals-Diffuser-XXXX - Reconfigure WiFi settings
- Verify MQTT broker settings
- Check MQTT broker is reachable
- Power cycle the device
- Wait 30 seconds for discovery
- Ensure device is on same network
- Try using IP address instead of hostname
- Check port 3232 is not blocked
- Fallback: flash via serial connection
- Check wiring connections
- Go to Hardware Diagnostics in web interface
- Try "Test Cycle" to verify fan works
- Adjust Min PWM if fan needs higher starting voltage
First, verify you're in AP mode:
- LED should be orange pulsing
- WiFi network
Rituals-Diffuser-XXXXshould be visible - Password:
diffuser123
If AP mode won't start:
- AP mode only activates when:
- No WiFi credentials saved, OR
- WiFi connection fails 3x (takes ~90 seconds), OR
- Long press front button (3 seconds)
- Check serial log for
[WIFI] AP startedand[WIFI] AP IP: 192.168.4.1 - If
[WIFI] ERROR: Failed to start AP!appears, try factory reset (long press rear button)
If connected but page won't load:
- Use
http://192.168.4.1/(not https!) - Check your phone's WiFi details - Gateway should show
192.168.4.1 - Disable mobile data temporarily
- Try a different browser or device
If you see "Web interface files missing":
- You need to flash the filesystem (the web interface files)
- Download
littlefs_esp8266.bin(ESP8266) orspiffs_esp32.bin(ESP32) from the latest release - Flash via web interface (Firmware Update) or esptool:
# ESP8266: filesystem offset is 0x1E0000 esptool.py write_flash 0x1E0000 littlefs_esp8266.bin
Serial debug commands:
# Monitor serial output (115200 baud)
pio device monitor -b 115200
# Or with screen
screen /dev/ttyUSB0 115200Look for these log messages:
[WIFI] AP started: Rituals-Diffuser-XXXX- AP is running[WIFI] AP Password: diffuser123- Password being used[WIFI] AP IP: 192.168.4.1- IP address assigned[WEB] Server started on port 80- Web server ready[WIFI] DNS server started for captive portal- Captive portal active
├── src/
│ ├── main.cpp # Main entry point
│ ├── config.h # Pin definitions & settings
│ ├── fan_controller.* # Fan control, timer, interval
│ ├── led_controller.* # WS2812 RGB LED
│ ├── button_handler.* # Button input handling
│ ├── storage.* # Settings persistence
│ ├── wifi_manager.* # WiFi connection
│ ├── web_server.* # Web interface + OTA
│ ├── mqtt_handler.* # MQTT + HA discovery
│ └── ota_handler.* # ArduinoOTA
├── data/ # Web files (LittleFS on ESP8266, SPIFFS on ESP32)
│ ├── index.html
│ ├── update.html
│ ├── style.css
│ └── script.js
├── platformio.ini
└── README.md
- PlatformIO (VS Code extension or CLI)
- USB-to-Serial adapter
# Build firmware
pio run -e esp32dev # ESP32
pio run -e esp8266 # ESP8266
# Build filesystem
pio run -e esp32dev -t buildfs
pio run -e esp8266 -t buildfs
# Upload firmware
pio run -e esp32dev -t upload
pio run -e esp8266 -t upload
# Upload filesystem
pio run -e esp32dev -t uploadfs
pio run -e esp8266 -t uploadfs- PubSubClient - MQTT client
- ArduinoJson - JSON parsing
- ESPAsyncWebServer - Async web server
- FastLED - WS2812 LED control
- Based on research from the Home Assistant Community
- Inspired by Echnics/Perfume-Genie-ESPhome
- Thanks to @FredericMa for ESP8266 optimization contributions (PR #9)
MIT License - feel free to use and modify.
This project is not affiliated with Rituals Cosmetics. Use at your own risk. Modifying your device may void warranty.
ESP8266 RAM & Stability:
- Scent table moved to PROGMEM and duplicate settings copy removed: ~2 KB more free RAM on ESP8266 (static RAM 78.2% → 75.7%)
- New build flags for low-RAM ESP8266 builds:
-DESP8266_LITE=1(disables NFC + scent lookup, ~4 KB RAM saved), or individually-DENABLE_NFC=0/-DENABLE_SCENT=0 - MQTT reconnect backoff: failed broker connects retry at 5s → doubling up to 60s, so an offline broker no longer stalls the main loop (~3s block) every 5 seconds
- Fix
esp32c3_otabuild environment: missing MFRC522 dependency
Stability & Cleanup:
- MQTT:
removeDiscovery()now wipes all 13 entities (no more orphans in Home Assistant). Anonymous brokers work again (nullptr instead of empty user/pass). - Logger: streams JSON to
/api/logsinstead of buffering (saves ~3 KB heap on ESP8266). Save retries throttled after a failed write. - Update checker: ESP8266 retries once per hour after a failed first check. Low-heap floor raised to 18 KB.
- ESP8266 OTA upload uses
maxSketchSpaceso Update.begin no longer fails on slightly oversized multipart requests. - Fan: rejects speed/on/off commands during auto-calibration. Interval times clamped before storage (no more uint8 wrap).
- RFID: unknown cartridges show "Unknown cartridge" instead of raw bytes. Ambiguous hex matches logged.
- Night mode brightness changes apply immediately, no longer wait for the next day/night transition.
Audit Fixes:
- Fix MQTT speed/interval persistence: changes via Home Assistant now survive reboot
- Fix fan speed=0 invalid state: setting speed to 0 now correctly turns off the fan instead of leaving it "on" at 0%
- Fix update checker error state: error messages now stay visible until next check instead of being instantly cleared
- Fix OTA auto-prepare on page load: visiting /update.html no longer triggers safe mode automatically on ESP8266
- Remove broken
esp32c3_rfid_scanbuild environment (referenced non-existent source file) - Remove unused FastLED dependency from ESP8266 environments (ESP8266 uses NeoPixelBus)
- Fix ESP32 build error: invalid WiFi TX power constant (WIFI_POWER_20_5dBm → WIFI_POWER_19_5dBm)
ESP8266 RAM Optimization:
- Disabled ArduinoOTA background service on ESP8266 to free RAM - use web-based Safe Update mode instead
- ESP32 and ESP32-C3 retain ArduinoOTA (plenty of RAM available)
ESP8266 Update Checker & OTA Improvements - Firmware version now reliably appears in Home Assistant on ESP8266. Update UI enabled on all platforms. OTA upload page shows real-time progress.
Stream-based Update Checker:
- Replaced
getString()with direct JSON stream parsing (deserializeJsonfrom HTTP stream) - Avoids allocating ~10KB GitHub API response as String - critical for ESP8266 low-heap situations
- Increased BearSSL rx buffer from 512 to 1024 bytes for more reliable TLS
- Added
HTTP/1.0mode to force Content-Length headers (no chunked transfer)
Update UI on ESP8266:
- Update section now visible on ESP8266 web interface (was hidden)
- Added extra polling timeouts (10s, 15s) for slower BearSSL HTTPS checks
- Dynamic release URL from API response
- Added
release_urlanderrorfields to main/api/statusendpoint
OTA Upload Progress:
- Sync OTA page now uses XHR-based uploads with real-time progress bars
- Button disables during upload with "Do not interrupt!" warning
- Success/failure shown inline (no page reload)
- Removed PROGMEM success/fail HTML pages (saves flash)
ESP8266 Stability Overhaul - Major heap fragmentation fixes addressing crash reports (#8, #3). Thanks to @FredericMa for PR #9.
For older versions, see GitHub Releases.





