Using state machines to make smart switches usable

Anyone who's tried to automate their home lights has encountered the same issues. Someone flips the switch and turns off your smart bulb. Someone tries to turn the light off only for it to turn straight back on because a door somewhere in the room is still open. Someone walks into the room and enjoys the lights turning on automatically only for them to switch off a few moments later because all the doors are closed.

Well, with the power of Home Assistant, Node-RED, and graph theory, we can make your family stop cursing your home automations.


Setup

This guide assumes you have HA and Node-RED working and are familiar with their basic usage.

This workflow is intended for smart switches, not smart bulbs

Smart switches have several advantages:

  • They remain powered at all times, even in their off state
  • They can operate the lights even when HA or the network aren't available
  • When manually operated, they turn the lights on or off immediately, without waiting for HA to issue commands first
  • They can operate many bulbs at once, simplifying the workflow for the kinds of large rooms where this automation is most useful

The subflow

So here's the problem: how do we know what the people inside the room want to do?

If they open a door or you detect motion, then it's obvious enough that they want the lights on. But what if they go into the room, close all the doors, and just sit there for a long time? What if someone wants to leave a door open when they exit the room but still have the lights off?

The solution: use a state machine to prefer what the switch says is happening over what the presence detection sensors say is happening.

Don't worry if that looks unfamiliar. Essentially, we have three states our lights can be in:

  1. Lights are off
  2. Lights are on because a switch requested it
  3. Lights are on because a sensor requested it

The "state machine" part means we can only ever be in one of those states and we can control which states are allowed to move to which other states.

The secret sauce is that if we get into the "Lights On By Switch" state (that is, a person physically turned a switch on), no sensor can turns the lights off. We can only get back to the "Lights Off" state by a person turning a switch off (or by reaching our "Force Off Time" escape hatch detailed below). So the room basically acts like there's no automation if a human manually flips a switch.

However, if someone merely opens a door or causes a motion detector to trigger, we'll go into the "Lights On By Sensor" state. When in that state, the lights can turn off either by a person manually turning a switch off or by no presence sensor being "On" for some reasonable amount of time. Thus, if no one physically touches a switch, the room behaves completely automatically.

You can download the json file below or copy the json text. Then import it into Node-RED:

  • Right click the empty space inside a blank flow of Node-RED
  • Select Insert > Import
  • Paste the json or pick the json file

JSON text version:

[{"id":"a907d30771c7cd59","type":"subflow","name":"room lights manager","info":"","category":"","in":[],"out":[{"x":1030,"y":240,"wires":[{"id":"ed791c79b9e54ecc","port":0}]},{"x":1410,"y":140,"wires":[{"id":"ba1cdcd1e411f916","port":0}]},{"x":1410,"y":200,"wires":[{"id":"5032e850fc8bed82","port":0}]}],"env":[{"name":"switchGroup","type":"str","value":"","ui":{"icon":"font-awesome/fa-toggle-on","label":{"en-US":"Switch Group"},"type":"input","opts":{"types":["str","env"]}}},{"name":"sensorGroup","type":"str","value":"","ui":{"icon":"font-awesome/fa-eye","label":{"en-US":"Sensor Group"},"type":"input","opts":{"types":["str","env"]}}},{"name":"sensorOffTime","type":"num","value":"300","ui":{"icon":"font-awesome/fa-clock-o","label":{"en-US":"Sensor Off Time (s)"},"type":"input","opts":{"types":["num","env"]}}},{"name":"forceOffTime","type":"num","value":"3600","ui":{"icon":"font-awesome/fa-clock-o","label":{"en-US":"Force Off Time (s)"},"type":"input","opts":{"types":["num","env"]}}},{"name":"sensorPollingInterval","type":"num","value":"5","ui":{"icon":"font-awesome/fa-clock-o","label":{"en-US":"Sensor Polling Interval (s)"},"type":"input","opts":{"types":["num","env"]}}}],"meta":{},"color":"#DDAA99","outputLabels":["Current State","Lights Turned On","Lights Turned Off"],"status":{"x":1000,"y":60,"wires":[{"id":"ed791c79b9e54ecc","port":0}]}},{"id":"ed791c79b9e54ecc","type":"state-machine","z":"a907d30771c7cd59","name":"","triggerProperty":"topic","triggerPropertyType":"msg","stateProperty":"topic","statePropertyType":"msg","initialDelay":"5","persistOnReload":true,"outputStateChangeOnly":true,"throwException":false,"states":["Lights Off","Lights On By Sensor","Lights On By Switch"],"transitions":[{"name":"Sensor Trigger","from":"Lights Off","to":"Lights On By Sensor"},{"name":"Switch Trigger","from":"Lights Off","to":"Lights On By Switch"},{"name":"Sensors Off","from":"Lights On By Sensor","to":"Lights Off"},{"name":"Switch Off","from":"Lights On By Switch","to":"Lights Off"},{"name":"Force Off","from":"Lights On By Sensor","to":"Lights Off"},{"name":"Force Off","from":"Lights On By Switch","to":"Lights Off"},{"name":"Force Off","from":"Lights Off","to":"Lights Off"}],"x":840,"y":160,"wires":[["819c1b2f2b3e80b9"]]},{"id":"1611e6eb5e67dd76","type":"change","z":"a907d30771c7cd59","name":"Sensor Trigger","rules":[{"t":"set","p":"topic","pt":"msg","to":"Sensor Trigger","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":620,"y":200,"wires":[["ed791c79b9e54ecc"]]},{"id":"58f65b41c4625c99","type":"change","z":"a907d30771c7cd59","name":"Switch Trigger","rules":[{"t":"set","p":"topic","pt":"msg","to":"Switch Trigger","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":620,"y":40,"wires":[["ed791c79b9e54ecc"]]},{"id":"953d804e8da5e7eb","type":"change","z":"a907d30771c7cd59","name":"Force Off","rules":[{"t":"set","p":"topic","pt":"msg","to":"Force Off","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":640,"y":120,"wires":[["ed791c79b9e54ecc"]]},{"id":"2ff6aef172aa0b86","type":"change","z":"a907d30771c7cd59","name":"Sensors Off","rules":[{"t":"set","p":"topic","pt":"msg","to":"Sensors Off","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":630,"y":260,"wires":[["ed791c79b9e54ecc"]]},{"id":"819c1b2f2b3e80b9","type":"switch","z":"a907d30771c7cd59","name":"","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"Lights On By Switch","vt":"str"},{"t":"eq","v":"Lights On By Sensor","vt":"str"},{"t":"eq","v":"Lights Off","vt":"str"}],"checkall":"true","repair":false,"outputs":3,"x":1010,"y":160,"wires":[["ba1cdcd1e411f916"],["ba1cdcd1e411f916"],["5032e850fc8bed82"]]},{"id":"5032e850fc8bed82","type":"api-call-service","z":"a907d30771c7cd59","name":"Turn Switch Off","server":"28ff6982.bca986","version":7,"debugenabled":false,"action":"switch.turn_off","floorId":[],"areaId":[],"deviceId":[],"entityId":["${switchGroup}"],"labelId":[],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":false,"domain":"switch","service":"turn_off","x":1200,"y":200,"wires":[[]]},{"id":"ba1cdcd1e411f916","type":"api-call-service","z":"a907d30771c7cd59","name":"Turn Switch On","server":"28ff6982.bca986","version":7,"debugenabled":false,"action":"switch.turn_on","floorId":[],"areaId":[],"deviceId":[],"entityId":["${switchGroup}"],"labelId":[],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":false,"domain":"switch","service":"turn_on","x":1200,"y":140,"wires":[[]]},{"id":"a0fab82beff4dd35","type":"trigger-state","z":"a907d30771c7cd59","name":"Any Sensor Triggered","server":"28ff6982.bca986","version":5,"inputs":0,"outputs":2,"exposeAsEntityConfig":"","entities":{"entity":["${sensorGroup}"],"substring":[],"regex":[]},"debugEnabled":false,"constraints":[{"targetType":"this_entity","targetValue":"","propertyType":"current_state","propertyValue":"new_state.state","comparatorType":"is","comparatorValueDatatype":"bool","comparatorValue":"true"}],"customOutputs":[],"outputInitially":false,"stateType":"habool","enableInput":false,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"x":360,"y":220,"wires":[["1611e6eb5e67dd76"],[]]},{"id":"3e0b7845a27391f1","type":"api-current-state","z":"a907d30771c7cd59","name":"Sensors off for specified time","server":"28ff6982.bca986","version":3,"outputs":2,"halt_if":"false","halt_if_type":"bool","halt_if_compare":"is","entity_id":"${sensorGroup}","state_type":"habool","blockInputOverrides":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"}],"for":"$env('sensorOffTime')","forType":"jsonata","forUnits":"seconds","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":360,"y":280,"wires":[["2ff6aef172aa0b86"],[]]},{"id":"950e06ecf0f46281","type":"inject","z":"a907d30771c7cd59","name":"Poll Sensors","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"${sensorPollingInterval}","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":220,"wires":[["3e0b7845a27391f1","cd5ebb8a754ca111"]]},{"id":"3602f8c3cc64b5c6","type":"trigger-state","z":"a907d30771c7cd59","name":"Switch Turned On","server":"28ff6982.bca986","version":5,"inputs":0,"outputs":2,"exposeAsEntityConfig":"","entities":{"entity":["${switchGroup}"],"substring":[],"regex":[]},"debugEnabled":false,"constraints":[{"targetType":"this_entity","targetValue":"","propertyType":"current_state","propertyValue":"new_state.state","comparatorType":"is","comparatorValueDatatype":"bool","comparatorValue":"true"},{"targetType":"this_entity","targetValue":"","propertyType":"previous_state","propertyValue":"old_state.state","comparatorType":"is","comparatorValueDatatype":"bool","comparatorValue":"false"}],"customOutputs":[],"outputInitially":false,"stateType":"str","enableInput":false,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"x":370,"y":40,"wires":[["58f65b41c4625c99"],[]]},{"id":"4458492bad2ae1d0","type":"trigger-state","z":"a907d30771c7cd59","name":"Switch Turned Off","server":"28ff6982.bca986","version":5,"inputs":0,"outputs":2,"exposeAsEntityConfig":"","entities":{"entity":["${switchGroup}"],"substring":[],"regex":[]},"debugEnabled":false,"constraints":[{"targetType":"this_entity","targetValue":"","propertyType":"current_state","propertyValue":"new_state.state","comparatorType":"is","comparatorValueDatatype":"bool","comparatorValue":"false"},{"targetType":"this_entity","targetValue":"","propertyType":"previous_state","propertyValue":"old_state.state","comparatorType":"is","comparatorValueDatatype":"bool","comparatorValue":"true"}],"customOutputs":[],"outputInitially":false,"stateType":"str","enableInput":false,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"x":370,"y":100,"wires":[["953d804e8da5e7eb"],[]]},{"id":"cd5ebb8a754ca111","type":"api-current-state","z":"a907d30771c7cd59","name":"Switch On For Max Time","server":"28ff6982.bca986","version":3,"outputs":2,"halt_if":"true","halt_if_type":"bool","halt_if_compare":"is","entity_id":"${switchGroup}","state_type":"habool","blockInputOverrides":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"}],"for":"$env('forceOffTime')","forType":"jsonata","forUnits":"seconds","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":350,"y":160,"wires":[["953d804e8da5e7eb"],[]]},{"id":"28ff6982.bca986","type":"server","name":"Home Assistant","version":5,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":false,"cacheJson":false,"heartbeat":false,"heartbeatInterval":"30","areaSelector":"friendlyName","deviceSelector":"friendlyName","entitySelector":"friendlyName","statusSeparator":"at: ","statusYear":"hidden","statusMonth":"short","statusDay":"numeric","statusHourCycle":"h23","statusTimeFormat":"h:m","enableGlobalContextStore":true}]

Groupings

What you'll want to do is create groupings for your switches and sensors by room. For instance, if you want this workflow to manage the lights in your garage:

Create a binary sensor group for all of the presence sensors in your garage. This can include door sensors, tilt sensors (for the vehicle door), motion sensors, mmw sensors, BT location sensors, etc. Anything that would tell you, "a person is in or moving through this room right now".

Set it so that the group is "On" if any of the included devices are "On".

An example binary sensor group that will be "On" if any of the sensors are "On"

Create another group for all of the switches that operate the lights in the garage. Instead of a binary sensor group, this would be a switch group. This will allow you to pass all of the switches as one switch into the state machine, providing consistent behavior without the problem of out-of-sync switch states. If your room only has one switch, you can skip this step.


Wiring it up

When you have the subflow imported into Node-RED, you can add it to a regular flow and open the properties:

  • Switch Group: The name of the group you made for switches in the room.
  • Sensor Group: The name of the group you made for all of the presence detection sensors in the room.
  • Sensor Off Time: The time (in seconds) to wait for the sensor group to be "Off" before trying to turn the lights off.
  • Force Off Time: The time (in seconds) to wait before forcing the lights off. This is an escape hatch for the occasion that the sensors are "On" for a very long time (eg: if a door is left open for hours).
  • Sensor Polling Interval: The delay (in seconds) between reading the sensor group state. This is just to throttle how many resources Node-RED uses.

And that's it. You don't have to wire any input or outputs. The room will now behave much more intelligently all on its own.

If you introduce more sensors or switches in the future, you simply add them to the corresponding group and the subflow will use them automatically.