A dynamic state machine node based on the Finite state machine model.
The dsm
node is a pure Node.js package for Node Red, it doesn't use any external libraries. The dsm behaviour is defined in a JSON format and can be optianally set/modified programmatically e.g. using an inject node.
A finite-state machine (FSM) is an abstract machine that can be in exactly one of a finite number of states at any given time. The FSM can change from one state to another in response to some external inputs; the change from one state to another is called a transition. A FSM is defined by a list of its states, its initial state, and the conditions for each transition.
The dsm
properties can be defined in the edit dsm node
window or by an input JSON object:
topic: "set"
payload:
{
"triggerInput": "topic",
"stateOutput": "current",
"preStateOutput": "previous",
"globalOutput": "my_global_var",
"flowOutput": "my_flow_var",
"preState": null,
"currentState": "state1",
"states": {
"state1": {
"transition2": "state2"
},
"state2": {
"transition1": "state1",
"transition3": "state3"
},
"state3": {
"transition1": "state1",
"transition2": "state2"
}
}
}
triggerInput
, stateOutput
, preStateOutput
, globalOutput
and flowOutput
can be omitted.
triggerInput
and stateOutput
default topic
.
preStateOutput
default preState
globalOutput
and flowOutput
default false
.
If globalOutput
or flowlOutput
is defined, the corresponding variable is additionally set.
When the node receives a trigger with a transition it will change from one state to another:
topic: "transition"
payload: any
In addition to states and transitions a dsm
can also contain arbitrary data and methods.
{
"stateOutput": "payload",
"currentState": "one_state",
"states": {
"recursive": "one_state"
},
"data": {
"delay": 2000
},
"methods": {
"setDelay": "sm.data.delay = msg.payload;",
"getDelay": "msg.delay = sm.data.delay;",
"default": "msg.payload = 'method undefined'"
}
}
Methods can be any valid javascript statements. If the method name is the same as the transition name, both the transition and the method are processed.
The method default
is executed when no method is found in the methods list.
To simplify editing methods can be defined as an array of comma separated strings:
"onTransition" : [
"if (sm.data.indoor > sm.data.outdoor) {",
"msg.payload = 'indoor > outdoor';",
"} else if (sm.data.indoor < sm.data.outdoor) {",
"msg.payload = 'indoor < outdoor';",
"} else {",
"msg.payload = 'indoor = outdoor';",
"}"
]
init
onBeforeTransition
onTransition
onAfterTransition
status
defines an arbitrary status
The general purpose methods are executed independently even without states.
The init
method is executed when the dsm is deployed respectively when the dsm is set. init
can be used to initialize values.
}
"methods": {
"onTransition": "msg.payload = {'sid': global.get('xiaomi_name')[msg.payload]};",
"status": {
"fill": "green",
"shape": "ring",
"text": {
"get": "JSON.stringify(msg.payload)"
}
}
}
}
To simplify the configuration build-in methods are available.
- setData
- getData
- timer
- resetTimer
- watchdog
{
"data": {
"temperature": 0,
"humidity": 0,
"pressure": 0,
"co2": 0
},
"methods": {
"temperature": {
"name": "setData"
},
"humidity": {
"name": "setData"
},
"pressure": {
"name": "setData"
},
"co2": {
"name": "setData"
},
"getData": {
"name": "getData"
}
}
}
{
"data": {
"delay1": 2000,
"delay2": 3000
},
"methods": {
"timer1": {
"name": "timer",
"param": "delay1"
},
"timer2": {
"name": "timer",
"param": "delay2",
"do": "node.status({});"
},
"timer3": {
"name": "timer",
"param": 5000,
"send": "toilet window timeout"
},
"resetTimer3": {
"name": "resetTimer",
"param": "timer3"
},
"watchdog1": {
"name": "watchdog",
"param": 5000,
"send": {
"get": "msg.payload.name+' timeout';"
}
}
}
}
timer
and watchdog
param in milliseconds.
do
executes a statement.
send
sets msg.payload - either to a string or when using get
to the result of a statement - and send the msg.
{
"currentState": "closed",
"states": {
"opened": {
"close": "closed",
"stop": "stopped"
},
"closed": {
"open": "opened",
"stop": "stopped"
},
"stopped": {
"open": "opened",
"close": "closed"
}
}
}
[{"id":"8b02669f.7ddd98","type":"debug","z":"57ef434a.058a6c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":430,"y":1680,"wires":[]},{"id":"68c0e2d5.3b798c","type":"inject","z":"57ef434a.058a6c","name":"open","topic":"open","payload":"your open payload","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":90,"y":1640,"wires":[["3b0c292.2da33d6"]]},{"id":"fae17b11.e48a88","type":"inject","z":"57ef434a.058a6c","name":"close","topic":"close","payload":"your close payload","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":90,"y":1680,"wires":[["3b0c292.2da33d6"]]},{"id":"3b0c292.2da33d6","type":"dsm","z":"57ef434a.058a6c","name":"door","sm_config":"{\n \"currentState\": \"closed\",\n \"states\": {\n \"opened\": {\n \"close\": \"closed\",\n \"stop\": \"stopped\"\n },\n \"closed\": {\n \"open\": \"opened\",\n \"stop\": \"stopped\"\n },\n \"stopped\": {\n \"open\": \"opened\",\n \"close\": \"closed\"\n }\n }\n}","x":260,"y":1680,"wires":[["8b02669f.7ddd98"]]},{"id":"caac460e.2942c8","type":"inject","z":"57ef434a.058a6c","name":"stop","topic":"stop","payload":"your stop payload","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":90,"y":1720,"wires":[["3b0c292.2da33d6"]]}]
{
"stateOutput": "payload",
"currentState": "closed",
"states": {
"opened": {
"click": "closing"
},
"closed": {
"click": "opening"
},
"opening": {
"limit_switch": "opened",
"click": "opening_stopped"
},
"closing": {
"limit_switch": "closed",
"click": "closing_stopped"
},
"opening_stopped": {
"click": "closing"
},
"closing_stopped": {
"click": "opening"
}
}
}
[{"id":"1501e603.fdbeba","type":"dsm","z":"57ef434a.058a6c","name":"garage door","sm_config":"{\n \"stateOutput\": \"payload\",\n \"globalOutput\": \"garage_door_state\",\n \"currentState\": \"closed\",\n \"states\": {\n \"opened\": {\n \"click\": \"closing\"\n },\n \"closed\": {\n \"click\": \"opening\"\n },\n \"opening\": {\n \"limit_switch\": \"opened\",\n \"click\": \"opening_stopped\"\n },\n \"closing\": {\n \"limit_switch\": \"closed\",\n \"click\": \"closing_stopped\"\n },\n \"opening_stopped\": {\n \"click\": \"closing\"\n },\n \"closing_stopped\": {\n \"click\": \"opening\"\n }\n }\n}","x":260,"y":1240,"wires":[["2125cea7.329542"]]},{"id":"4744a8a3.28aec8","type":"inject","z":"57ef434a.058a6c","name":"click","topic":"click","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":90,"y":1240,"wires":[["1501e603.fdbeba"]]},{"id":"379cd28e.34076e","type":"link in","z":"57ef434a.058a6c","name":"to timer","links":["4c183a20.342304"],"x":115,"y":1280,"wires":[["1501e603.fdbeba"]]},{"id":"4c183a20.342304","type":"link out","z":"57ef434a.058a6c","name":"from dsm","links":["379cd28e.34076e"],"x":535,"y":1340,"wires":[]},{"id":"5a467385.1c0bcc","type":"comment","z":"57ef434a.058a6c","name":"motor simulation","info":"","x":440,"y":1200,"wires":[]},{"id":"2125cea7.329542","type":"change","z":"57ef434a.058a6c","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload = \"opening\" or payload = \"closing\" ? payload : \"reset\"","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":440,"y":1240,"wires":[["23d5eb3e.3f4fa4"]]},{"id":"23d5eb3e.3f4fa4","type":"trigger","z":"57ef434a.058a6c","op1":"","op2":"limit_switch","op1type":"nul","op2type":"str","duration":"3","extend":false,"units":"s","reset":"reset","bytopic":"topic","name":"","x":320,"y":1340,"wires":[["77a5a197.e301e"]]},{"id":"77a5a197.e301e","type":"change","z":"57ef434a.058a6c","name":"topic","rules":[{"t":"set","p":"topic","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":450,"y":1340,"wires":[["4c183a20.342304"]]}]
For more examples have a look at the Wiki