ESP32 home automation powered by a fluent rule engine. Declarative rules for socket control based on time, light levels, phone presence, and solar production. Built with a clean DSL that reads like English. For about 30 Euro (or less, thanks to uncle Ali )
Wake up, light turns on if it's not light enough until you go to work.
At daytime, too much solar power? Then you turn on a heater (or cooler).
At some lux level, you turn on the main light, and at 6, the TV backlight flips on.
Between 23:00 / 24:00, randomly turn off a main light (or only when you're not at home).
But you're still watching TV, so the backlight turns off only if all other lights are off.
Or do you want sun up, sun down? This code has your back.
As it contains location-based sun up / down triggers.
And yes, it is NTP time synced, handling winter and summer time fine.
Some actions keep a switch for a duration under control.
While others allow for manual changes (or delayed actions).
| Overview | Schematic |
|---|---|
![]() |
How it looks from the website http(your ip addres):8080/index.html

(note in above picture it had no more archived data so week and month dont show)
Major rewrites, since esp32 has some limits on active sockets.
The startup discovery is a bit slower now, though socket exhaustion is solved now.
Also stability updates, and a rule fix! (bug OnCondition didnt skip but turned off).
A flexible rule-based automation system for ESP32 smart home control.
Rules evaluate conditions and time windows to control WiFi sockets automatically.
- Core Concepts
- Configuration
- Rule Actions
- Conditions
- Combinators
- Time Utilities
- System Functions
- Example Usage
| Decision | Meaning |
|---|---|
On |
Turn the socket on |
Off |
Turn the socket off |
Skip |
Don't change anything, let other rules decide |
Rules are evaluated in order. Later rules can override earlier ones if they return On or Off.
Time format: HH:MM. Duration parameters use seconds (600 = 5 minutes).
setLocation - Configure sun position calculations
setLocation(latitude, longitude, elevationMeters)Sets your geographic location for sunrise/sunset calculations. Use decimal degrees format (right-click in Google Maps to copy coordinates). Negative values for South latitude and West longitude. Elevation defaults to 0 and is optional.
SmartRuleSystem::setLocation(52.37, 4.90, 0); // Amsterdam
SmartRuleSystem::setLocation(46.82, 8.40, 1500); // Swiss Alpsperiod - Active during a time window
period(startTime, endTime, condition)Turns on if condition is met, returns skip if not. Automatically turns off in the last 2 minutes of the period.
Use case: Morning light that only activates when it's dark.
boolPeriod - On/Off based on condition in time window
boolPeriod(startTime, endTime, condition)Returns on when condition is true, off when false. Unlike period, this actively turns off when condition fails (no skip).
Use case: Light that follows a sensor throughout the entire period.
onAfter - Turn on after specified time
onAfter(timeStr, durationMins, condition)Turns on after the specified time if condition is met. Returns skip otherwise. The optional durationMins creates a time window.
Use case: Turn on evening lights after 18:00 when phone is home.
offAfter - Turn off after specified time
offAfter(timeStr, durationMins, condition)Turns off after the specified time if condition is met. Returns skip otherwise.
Use case: Force lights off after 23:30.
onCondition - Turn on when condition is true
onCondition(condition)Turns on when condition is true, skip otherwise. No time restrictions.
Use case: Simple condition-based activation without time constraints.
offCondition - Turn off when condition is true
offCondition(condition)Turns off when condition is true, skip otherwise.
Use case: Turn off when light level drops.
onConditionDelayed - Turn on after stable condition
onConditionDelayed(condition, delaySeconds)Turns on only after the condition has been continuously true for the specified delay.
Use case: Prevent flickering by requiring stable conditions before switching.
offConditionDelayed - Turn off after stable condition
offConditionDelayed(condition, delaySeconds)Turns off only after the condition has been continuously true for the specified delay.
Use case: Keep lights on briefly after motion stops.
delayedOnOff - Hysteresis control
delayedOnOff(startTime, endTime, onDelayMinutes, offDelayMinutes, condition)Hysteresis control within a time window. Waits onDelayMinutes before turning on and offDelayMinutes before turning off.
Use case: Prevent rapid switching when conditions fluctuate near thresholds.
solarHeaterControl - Solar-powered device control
solarHeaterControl(exportThreshold, importThreshold, minOnTime, minOffTime, extraCondition)Specialized rule for solar-powered devices. Turns on when exporting enough power, turns off when importing too much. Respects minimum on/off times to protect equipment.
Use case: Water heater that runs on excess solar production.
Light Sensor - lightBelow, lightAbove
Requires BH1750 sensor.
| Function | Description |
|---|---|
lightBelow(threshold) |
True when light level < threshold (lux) |
lightAbove(threshold) |
True when light level > threshold (lux) |
Temperature - temperatureAbove, temperatureBelow
Requires BME280 sensor. Returns false if sensor not found.
| Function | Description |
|---|---|
temperatureAbove(threshold) |
True when temperature > threshold (°C) |
temperatureBelow(threshold) |
True when temperature < threshold (°C) |
Humidity - humidityAbove, humidityBelow
Requires BME280 sensor. Returns false if sensor not found.
| Function | Description |
|---|---|
humidityAbove(threshold) |
True when relative humidity > threshold (%) |
humidityBelow(threshold) |
True when relative humidity < threshold (%) |
Air Pressure - pressureAbove, pressureBelow
Requires BME280 sensor. Returns false if sensor not found. Pressure is in hPa (hectopascal). Standard sea level pressure is 1013.25 hPa.
| Function | Description |
|---|---|
pressureAbove(threshold) |
True when pressure > threshold (hPa) |
pressureBelow(threshold) |
True when pressure < threshold (hPa) |
Phone Presence - phonePresent, phoneNotPresent
| Function | Description |
|---|---|
phonePresent() |
True when phone is detected on the network |
phoneNotPresent() |
True when phone is not detected |
Day of Week - isWorkday, isWeekend, isMonday, etc.
| Function | Description |
|---|---|
isWorkday() |
True Monday through Friday |
isWeekend() |
True Saturday and Sunday |
isMonday() |
True on Monday |
isTuesday() |
True on Tuesday |
isWednesday() |
True on Wednesday |
isThursday() |
True on Thursday |
isFriday() |
True on Friday |
isSaturday() |
True on Saturday |
isSunday() |
True on Sunday |
Sun Position - sunUp, sunDown, beforeSunrise, afterSunrise, beforeSunset, afterSunset
Requires setLocation() to be called at startup.
| Function | Description |
|---|---|
sunUp() |
True when sun is above the horizon |
sunDown() |
True when sun is below the horizon |
beforeSunrise(minutes) |
True during the X minutes before sunrise |
afterSunrise(minutes) |
True during the X minutes after sunrise |
beforeSunset(minutes) |
True during the X minutes before sunset |
afterSunset(minutes) |
True during the X minutes after sunset |
The before/after functions create a time window. For example, beforeSunset(30) is true starting 30 minutes before sunset and ending at sunset.
Power / Solar - powerSolarActive, powerProducing, powerConsuming, etc.
Requires P1 meter.
| Function | Description |
|---|---|
powerSolarActive() |
True when solar is producing |
powerProducing() |
True when exporting to grid |
powerConsuming() |
True when importing from grid |
powerProductionAbove(threshold) |
True when export > threshold (watts) |
powerProductionBelow(threshold) |
True when export < threshold (watts) |
Socket State - socketIsOn, socketIsOff
Check the state of other sockets. Socket numbers are 1-indexed.
| Function | Description |
|---|---|
socketIsOn(socketNumber) |
True if the specified socket is currently on |
socketIsOff(socketNumber) |
True if the specified socket is currently off |
Duration - hasBeenOnFor, hasBeenOffFor
Check how long a socket has been in its current state.
| Function | Description |
|---|---|
hasBeenOnFor(socketNumber, minutes) |
True if socket has been on for at least X minutes |
hasBeenOffFor(socketNumber, minutes) |
True if socket has been off for at least X minutes |
Time Window - timeWindowBetween, after
| Function | Description |
|---|---|
timeWindowBetween(start, end) |
True when current time is within the window |
after(time, duration) |
True after specified time (optional duration window) |
allOf - AND logic
allOf({condition1, condition2, ...})Returns true only if all conditions are true.
anyOf - OR logic
anyOf({condition1, condition2, ...})Returns true if any condition is true.
notOf - NOT logic
notOf(condition)Inverts a condition.
getSunriseTime / getSunsetTime - Get today's sun times
getSunriseTime() // Returns "HH:MM"
getSunsetTime() // Returns "HH:MM"Get today's sunrise or sunset time as a string. Compatible with other time functions.
rndTime - Random time offset
rndTime(baseTime, maxMinutes, extraSeed)Generates a random time offset from a base time. The randomness is consistent per day.
Use case: Simulate natural lighting patterns that vary slightly each day.
addMinutesToTime / addHoursToTime - Time arithmetic
addMinutesToTime(baseTime, minutesToAdd)
addHoursToTime(baseTime, hoursToAdd)Simple time arithmetic. Returns a new time string in "HH:MM" format.
getDailyRandom / getDailyRandom60 / getDailyRandom24 - Daily consistent randoms
getDailyRandom(index) // Returns 0-99
getDailyRandom60(index) // Returns 0-59
getDailyRandom24(index) // Returns 0-23Pre-generated random numbers that stay consistent throughout the day.
addRule - Register a rule
addRule(socketNumber, ruleName, evaluateFunction, timeWindow)Registers a new rule for a socket. Rules are evaluated in the order they are added.
update - Main loop function
update()Call this in your main loop. Polls physical states, evaluates all rules, and applies changes.
pollPhysicalStates - Refresh socket states
pollPhysicalStates()Manually refresh all socket states from the hardware. Called automatically by update().
getSocketState - Get socket state
getSocketState(socketIndex)Returns the physical state (true/false) of a socket by its index (0-indexed).
clearRules - Remove all rules
clearRules()Removes all registered rules.
This repo is based on a state machine to control all devices.
Similar to continuous flow coding as in a PLC.
Although the rule system has its own logic, which I show below.
And it can be easily extended to include a lot of rules.
Without conflicting with the main state machine loop code.
void setup() {
SmartRuleSystem::setLocation(52.37, 4.90, 0); // Amsterdam
setupRules();
}
// Note now with 8 wall sockets, timeouts may miss the rs.period(...) function to handle stuff
// A better way to code a period i now show in my below example, as demo code
// (the period function may be removed soon)
void setupRules() {
auto &rs = ruleSystem;
rs.addRule(1, "Good morning",
rs.onCondition(
rs.allOf({rs.lightBelow(9),
rs.isWorkday(),
rs.socketIsOff(1), //so.. we only fire this when needed !
rs.timeWindowBetween("07:10", "07:44")})));
// Morning OFF rule (07:45-08:00, workdays only)
rs.addRule(1, "Leave for car",
rs.offCondition(
rs.allOf({rs.isWorkday(),
rs.socketIsOn(1), //again only when needed !.
rs.timeWindowBetween("07:45", "08:00")})));
// Evening ON rule (weekdays)
rs.addRule(1, "Evening",
rs.onCondition(
rs.allOf({rs.lightBelow(7),
rs.isWorkday(),
rs.socketIsOff(1),
rs.timeWindowBetween("17:15", eveningEndTime)})));
// Evening OFF rule (weekdays)
rs.addRule(1, "Good night",
rs.offCondition(
rs.allOf({rs.isWorkday(),
rs.socketIsOn(1),
rs.timeWindowBetween(eveningEndTime,
rs.addMinutesToTime(eveningEndTime, 6))})));
| Component | Purpose | Required |
|---|---|---|
| ESP32 | Main controller | Yes |
| WiFi Sockets | Controllable devices (HomeWizard, etc.) | Yes |
| BH1750 | Light sensor | Optional |
| BME280 | Temperature/Humidity/Pressure | Optional |
| P1 Meter | Power monitoring (Dutch smart meter) | Optional |
| OLED Display (SH1106 128x64) | Status display | |
| (also futering a basic website) | ||
| Phone on static ip WiFi | Presence detection via ping | Optional |
Price estimate ~ 30 to 40 Euro's total.
So far this code controls 4 home connect switches, but you can code more.

