Node-RED Context Watcher

Node-RED Context Watcher

This is a subflow node that permits you to watch a context variable and emit its value when it is set or modified in another location.

Important notes:

  • Read the built-in help on the Right Hand Sidebar
  • Only built in memory based context is supported
  • Only an object and/or its immediate properties can be watched.
  • If the object is replaced, the watcher will stop working
    • Injecting a blank msg or new configuration will repair it (see demo flow)

Screenshots

Adding a watcher

chrome_vPe13VFADn

Working Demo

chrome_RU0ciSH6f3

[{"id":"1ef1ab7a9b9d0221","type":"subflow","name":"Context Watcher","info":"Attaches an observer to an objects properties or a specific object property\r\nand outputs a message whenever it is set/modified.\r\n\r\n### Setup\r\n\r\n**Static configuration**\r\n* **Variable**: This is the name of the thing to watch (e.g. `global.sensors` or `flow.settings.name`)\r\n* **Default**: A default value to set if the object/property does not exist\r\n* **Mode**: Setting to chose whether to watch a specific property or the objects properties\r\n* **Delay**: How long to delay before applying the watcher. \r\n\r\n**Dynamic configuration**\r\n* leave the `variable` field blank and send a `msg.topic`.\r\n* If `msg.mode` is excluded, it will default to `\"propery\"` mode\r\n* If `msg.payload` is used as a default value if the context variable does not exist.\r\n* If `msg.value` is used to reinitialise the context variable (overrides the value in msg.payload`).\r\n\r\n**Notes**\r\n* If **Mode* is `\"property\"`, the variable in `msg.topic` must have 3 parts minimum (e.g. `global.obj.varname`)\r\n* If **Mode** is `\"object\"`, the variable in `msg.topic` must have 2 parts minimum (e.g. `global.obj`)\r\n* If the base object is directly modified or replaced, the watcher will stop working.\r\n\r\n### Inputs\r\n\r\n: topic (string)                 :  the path to the property (e.g. `global.sensors` or `flow.settings.name`)\r\n: *mode* (\"property\"|\"object\")   :  whether to watch a specific property or the objects properties\r\n: *payload* (Any)                :  a default value to set if the object/property does not exist (optional)\r\n: *value* (Any)                  :  an initial value to set on the object/property (optional)\r\n\r\n\r\n### Outputs\r\n\r\n: topic (string) : the object property name\r\n: payload (any) : the value set for the object property\r\n\r\n\r\n","category":"","in":[{"x":80,"y":140,"wires":[{"id":"0ddb0c890c5c1ee4"}]}],"out":[{"x":700,"y":140,"wires":[{"id":"28547d86c2ce4864","port":0}]}],"env":[{"name":"variable","type":"str","value":"global.myObj.myVar","ui":{"icon":"font-awesome/fa-pencil-square-o","label":{"en-US":"Variable"},"type":"input","opts":{"types":["str","env"]}}},{"name":"def","type":"json","value":"null","ui":{"icon":"font-awesome/fa-ellipsis-h","label":{"en-US":"Default"},"type":"input","opts":{"types":["str","num","bool","json","bin","env","conf-types"]}}},{"name":"delay","type":"num","value":"100","ui":{"icon":"font-awesome/fa-hourglass-1","label":{"en-US":"Init Delay (ms)"},"type":"spinner"}},{"name":"mode","type":"str","value":"property","ui":{"icon":"font-awesome/fa-question","label":{"en-US":"Watch Type"},"type":"select","opts":{"opts":[{"l":{"en-US":"Single Property"},"v":"property"},{"l":{"en-US":"All Properties"},"v":"object"}]}}}],"meta":{"module":"context-watcher","type":"context-watcher","version":"1.0.0","author":"Steve-Mcl","desc":"Watches a context variable and emits its value when it is set or modified","keywords":"node-red,context,watcher,observer","license":"MIT"},"color":"#C0DEED","icon":"node-red/watch.svg","status":{"x":700,"y":220,"wires":[{"id":"c0d8fba8479d5ca6","port":0}]}},{"id":"eed7f0f2b9f58317","type":"inject","z":"1ef1ab7a9b9d0221","name":"","props":[{"p":"topic","v":"variable","vt":"env"},{"p":"payload"},{"p":"delay","v":"delay","vt":"env"},{"p":"mode","v":"mode","vt":"env"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.5","topic":"","payload":"def","payloadType":"env","x":130,"y":60,"wires":[["5b3f37976ec04830"]]},{"id":"07f9f815accee96b","type":"delay","z":"1ef1ab7a9b9d0221","name":"","pauseType":"delayv","timeout":"1000","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":480,"y":60,"wires":[["28547d86c2ce4864"]]},{"id":"28547d86c2ce4864","type":"function","z":"1ef1ab7a9b9d0221","name":"Make Observable","func":"const watchMode = msg.mode\nconst currentWatch = flow.get('watch')\nconst currentWatchMode = flow.get('mode')\n\nflow.get('cleanup')?.()\n\nnode.status({fill:\"grey\",shape:\"ring\",text:\"not watching\"});\n// node.warn({_:\"check input start\", ...msg});\n\nif (!msg.topic) {\n    node.warn({_:\"msg.topic is empty\", msg});\n    return null // nothing to watch\n}\nif (msg.mode && !['property', 'object'].includes(msg.mode)) {\n    node.status({fill:\"red\",shape:\"ring\",text:'msg.mode must be \"object\" or \"property\"'})\n}\n\nmsg.mode = msg.mode || 'property'\nmsg.variable = msg.topic\nlet def = msg.value ?? msg.default\nlet forceSet = typeof msg.value !== \"undefined\"\n\nmsg.payload = makeObservable(msg.variable, def, msg.mode, forceSet)\n\nmsg.watching = {\n    name: flow.get('watch'),\n    mode: flow.get('mode'),\n}\nreturn msg;\n\n\nfunction parseWatch(variable, mode) {\n    let { store, parentPath, contextName, contextPath, objName, objPath, propName } = flow.get('parseWatchVar')(variable, mode)\n    return { store, parentPath, contextName, contextPath, objName, objPath, propName }\n}\n\n/**\n* make properties of `object` Observable\n* @param {string} name - key name to store `object` in `global` or `flow` context\n* @param {object} def  - the default value if underlying value is empty\n* @param {\"property\"|\"object\"} mode - watch mode\n* @param {boolean} forceSet - force the underlying value to the provided default\n*/\nfunction makeObservable(name, def, mode, forceSet) {\n\n    let parsed\n    try {\n        parsed = parseWatch(name, mode);\n    } catch (error) {\n        node.status({ fill: \"red\", shape: \"ring\", text: error.statusMessage || error.message })\n        throw error\n    };\n    let { store, parentPath, contextName, contextPath, objName, objPath, propName } = parsed;\n    // node.warn({ name, parentPath, contextName, contextPath, objName, objPath, propName })\n    let object = null\n    if (mode === 'object') {\n        object = store.get(contextPath)\n        if (forceSet || !object) {\n            store.set(contextPath, def ?? {})\n            object = store.get(contextPath)\n        }\n        observe(object, name)\n        store.set(contextPath, object);\n    } else {\n        object = store.get(objPath)\n        if (!object) {\n            // node.warn(`object ${objName} not found. setting to {} (property mode)`);\n            object = {}\n        }\n        if (forceSet) {\n            object = def\n        }\n        observeProperty(object, propName, def ?? null, name)\n        store.set(objPath, object);\n    }\n\n    flow.set('watch', name)\n    flow.set('mode', mode)\n\n    const status = `watching ${mode} ${name}`\n    node.status({ fill: \"green\", shape: \"dot\", text: status })\n\n    return store.get(contextPath);\n\n    function notify(name, value) {\n        // node.warn({ _: \"notify\", name, parentPath, contextName, contextPath, objName, objPath, propName })\n        var m = {\n            topic: name,\n            payload: value\n        }\n        node.send(m);\n    }\n\n    /**\n    * @param {object} obj - the object to make reactive\n    * @param {string} key - the key to observe\n    * @param {object} defValue - the default value if none exists\n    */\n    function observeProperty(obj, key, defValue, topic) {\n        // node.warn(`Checking object for existence of key ${key}`);\n        let val = obj[key] ?? defValue\n        if (key in obj && obj.hasOwnProperty(key)) {\n            // node.warn(`deleting object key ${key} which has value: ${val}`);\n            delete obj[key]\n            // node.warn({ _: `deleted object key ${key} from obj`, obj });\n        }\n        try {\n            Object.defineProperty(obj, key, {\n                get() {\n                    // node.warn(`getter called for '${topic}'`);\n                    return val;\n                },\n                set(newVal) {\n                    // node.warn(`setter called for '${topic}'`);\n                    val = newVal;\n                    notify(topic, val);\n                },\n                enumerable: true,\n                configurable: true\n            })\n        } catch (error) {\n            // node.warn(\"WHAAAAA Why!\");\n            throw error\n        };\n    }\n\n    /**\n    * @param {object} obj  the object to observe\n    */\n    function observe(obj, topic) {\n        for (let key in obj) {\n            if (obj.hasOwnProperty(key)) {\n                observeProperty(obj, key, obj?.[key], topic + '.' + key);\n            }\n        }\n    }\n}\n\n\n","outputs":1,"timeout":"","noerr":0,"initialize":"flow.set('cleanup', cleanup)\nflow.set('parseWatchVar', parseWatchVar)\n\nfunction cleanup() {\n    const currentWatch = flow.get('watch')\n    const currentWatchMode = flow.get('mode')\n    // node.warn({ _: \"cleanup ->\", currentWatch, currentWatchMode });\n    try {\n        if (currentWatchMode && currentWatch) {\n            let parsed\n            try {\n                parsed = parseWatchVar(currentWatch, currentWatchMode);\n                // node.warn({ _: \"cleanup -> parseWatchVar\", ...parsed });\n            } catch (error) {\n                // ignore\n                // node.warn({ _: \"cleanup -> parseWatchVar ERROR\", error });\n            };\n            let { store, contextPath, objPath, propName } = parsed;\n\n\n            if (currentWatchMode === 'object') {\n                let object = store.get(contextPath)\n                if (typeof object === 'object') {\n                    unobserve(object)\n                }\n            } else {\n                let object = store.get(objPath)\n                if (typeof object === 'object') {\n                    unobserveProperty(object, propName)\n                }\n            }\n        }\n    } finally {\n        flow.set('watch', undefined)\n        flow.set('mode', undefined)\n    }\n}\n\n\nfunction unobserveProperty(obj, key) {\n    let val = obj[key];\n    delete obj[key]\n    obj[key] = val\n}\n\n/**\n* @param {object} obj  the object to stop observing\n*/\nfunction unobserve(obj) {\n    for (let key in obj) {\n        if (obj.hasOwnProperty(key)) {\n            unobserveProperty(obj, key);\n        }\n    }\n}\n\nfunction parseWatchVar(variable, mode) {\n    let store = null\n    let nameParts = variable.split(\".\");\n    let invalidErrMsg = \"variable must be a property of an object in global or flow context. e.g. global.object.property or flow.object.property\"\n    let invalidStatusMsg = \"invalid variable name\"\n    let contextName = ''\n    let contextPath = ''\n    let propName = ''\n    let objName = ''\n    let parentPath = ''\n    if (mode === 'object') {\n        invalidErrMsg = \"variable must be an object property in global or flow context. e.g. global.object or flow.object\"\n        if (nameParts.length < 2) {\n            const e = new Error(invalidErrMsg, { cause: 'variable path too short' })\n            e.statusMessage = invalidStatusMsg\n            throw e\n        }\n    } else {\n        if (nameParts.length < 3) {\n            const e = new Error(invalidErrMsg, { cause: 'variable path too short' })\n            e.statusMessage = invalidStatusMsg\n            throw e\n        }\n    }\n    contextName = nameParts[0]\n    nameParts.shift();//drop the first item\n    contextPath = nameParts.join('.');\n    propName = nameParts.pop()\n    objName = nameParts.join('.')\n\n    switch (contextName) {\n        case \"flow\":\n            store = flow;\n            parentPath = '$parent'\n            break;\n        case \"global\":\n            store = global;\n            break;\n        default:\n            const e = new Error(invalidErrMsg, { cause: 'context must be global or flow' })\n            e.statusMessage = invalidStatusMsg\n            throw e\n    }\n    let objPath = parentPath ? [parentPath, objName].join('.') : objName\n    contextPath = parentPath ? [parentPath, contextPath].join('.') : contextPath\n\n    return { store, parentPath, contextName, contextPath, objName, objPath, propName }\n}","finalize":"\nflow.get('cleanup')?.()\nflow.set('cleanup', undefined)\nflow.set('parseWatchVar', undefined)","libs":[],"x":510,"y":140,"wires":[[]]},{"id":"c0d8fba8479d5ca6","type":"status","z":"1ef1ab7a9b9d0221","name":"","scope":["28547d86c2ce4864","3984433f.b0374c","da6b5e658ddb5d53"],"x":540,"y":220,"wires":[[]]},{"id":"5b3f37976ec04830","type":"switch","z":"1ef1ab7a9b9d0221","name":"","property":"topic","propertyType":"msg","rules":[{"t":"nempty"}],"checkall":"true","repair":false,"outputs":1,"x":310,"y":60,"wires":[["07f9f815accee96b"]]},{"id":"0ddb0c890c5c1ee4","type":"function","z":"1ef1ab7a9b9d0221","name":"Check input","func":"\nmsg.default = msg.default ?? env.get('def')\nmsg.mode = msg.mode ?? env.get('mode') \nmsg.topic = msg.topic ?? env.get('variable')\n\nreturn msg","outputs":1,"timeout":"","noerr":0,"initialize":"flow.set('cleanup', cleanup)\nflow.set('parseWatchVar', parseWatchVar)\n\nfunction cleanup() {\n    const currentWatch = flow.get('watch')\n    const currentWatchMode = flow.get('mode')\n    // node.warn({ _: \"cleanup ->\", currentWatch, currentWatchMode });\n    try {\n        if (currentWatchMode && currentWatch) {\n            let parsed\n            try {\n                parsed = parseWatchVar(currentWatch, currentWatchMode);\n                // node.warn({ _: \"cleanup -> parseWatchVar\", ...parsed });\n            } catch (error) {\n                // ignore\n                // node.warn({ _: \"cleanup -> parseWatchVar ERROR\", error });\n            };\n            let { store, contextPath, objPath, propName } = parsed;\n\n\n            if (currentWatchMode === 'object') {\n                let object = store.get(contextPath)\n                if (typeof object === 'object') {\n                    unobserve(object)\n                }\n            } else {\n                let object = store.get(objPath)\n                if (typeof object === 'object') {\n                    unobserveProperty(object, propName)\n                }\n            }\n        }\n    } finally {\n        flow.set('watch', undefined)\n        flow.set('mode', undefined)\n    }\n}\n\n\nfunction unobserveProperty(obj, key) {\n    let val = obj[key];\n    delete obj[key]\n    obj[key] = val\n}\n\n/**\n* @param {object} obj  the object to stop observing\n*/\nfunction unobserve(obj) {\n    for (let key in obj) {\n        if (obj.hasOwnProperty(key)) {\n            unobserveProperty(obj, key);\n        }\n    }\n}\n\nfunction parseWatchVar(variable, mode) {\n    let store = null\n    let nameParts = variable.split(\".\");\n    let invalidErrMsg = \"variable must be a property of an object in global or flow context. e.g. global.object.property or flow.object.property\"\n    let invalidStatusMsg = \"invalid variable name\"\n    let contextName = ''\n    let contextPath = ''\n    let propName = ''\n    let objName = ''\n    let parentPath = ''\n    if (mode === 'object') {\n        invalidErrMsg = \"variable must be an object property in global or flow context. e.g. global.object or flow.object\"\n        if (nameParts.length < 2) {\n            const e = new Error(invalidErrMsg, { cause: 'variable path too short' })\n            e.statusMessage = invalidStatusMsg\n            throw e\n        }\n    } else {\n        if (nameParts.length < 3) {\n            const e = new Error(invalidErrMsg, { cause: 'variable path too short' })\n            e.statusMessage = invalidStatusMsg\n            throw e\n        }\n    }\n    contextName = nameParts[0]\n    nameParts.shift();//drop the first item\n    contextPath = nameParts.join('.');\n    propName = nameParts.pop()\n    objName = nameParts.join('.')\n\n    switch (contextName) {\n        case \"flow\":\n            store = flow;\n            parentPath = '$parent'\n            break;\n        case \"global\":\n            store = global;\n            break;\n        default:\n            const e = new Error(invalidErrMsg, { cause: 'context must be global or flow' })\n            e.statusMessage = invalidStatusMsg\n            throw e\n    }\n    let objPath = parentPath ? [parentPath, objName].join('.') : objName\n    contextPath = parentPath ? [parentPath, contextPath].join('.') : contextPath\n\n    return { store, parentPath, contextName, contextPath, objName, objPath, propName }\n}","finalize":"\nflow.get('cleanup')?.()\nflow.set('cleanup', undefined)\nflow.set('parseWatchVar', undefined)","libs":[],"x":210,"y":140,"wires":[["28547d86c2ce4864"]]},{"id":"2383c95d6fb42e2f","type":"tab","label":"Context Watcher Demos","disabled":false,"info":"","env":[]},{"id":"e7762e5d50aa5276","type":"group","z":"2383c95d6fb42e2f","name":"DEMO 1","style":{"label":true},"nodes":["803fe60d2002f8eb","dc5dfaf1a09cfb48","be533efcc3eff1b1","2616cc34e9b2efc4","3bd8accdfafe5efa","cf5e593c6414d35e","968a48add4a13305","d9b0e9866d3ded16","ded0ad36c17a4e54","4686bc49c307c7c5","b7bd45ba40ace406","010b592ace4489e7"],"x":54,"y":99,"w":632,"h":342},{"id":"44a911618d55ad8b","type":"group","z":"2383c95d6fb42e2f","name":"DEMO 2","style":{"label":true},"nodes":["51816ddd4b9f4b85","2ba9128c4cd8eacb","022f9b1c5d114d20","d1326a318bf9a9c0","f9b046af10db2673","cfa73552e39d2bfe","5af6929e4b20c671","79431b290d341591","42047bb09e5091f1","721ec5aff3867f38","357e5e39e2308024","8c3471aaa811b668","1c20ab88c69eb909"],"x":54,"y":459,"w":632,"h":362},{"id":"9620f8962f1b184d","type":"comment","z":"2383c95d6fb42e2f","name":"Select a watcher and read built in help on the sidebar 👉","info":"","x":270,"y":60,"wires":[]},{"id":"803fe60d2002f8eb","type":"subflow:1ef1ab7a9b9d0221","z":"2383c95d6fb42e2f","g":"e7762e5d50aa5276","name":"global.myObj.myVar","env":[{"name":"def","value":"0","type":"num"}],"x":280,"y":180,"wires":[["dc5dfaf1a09cfb48"]]},{"id":"dc5dfaf1a09cfb48","type":"debug","z":"2383c95d6fb42e2f","g":"e7762e5d50aa5276","name":"watcher 1","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":560,"y":180,"wires":[]},{"id":"be533efcc3eff1b1","type":"inject","z":"2383c95d6fb42e2f","g":"e7762e5d50aa5276","name":"random","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"(\t    $minimum := 1;\t    $maximum := 10;\t    $round(($random() * ($maximum-$minimum)) + $minimum, 0)\t)","payloadType":"jsonata","x":150,"y":240,"wires":[["2616cc34e9b2efc4"]]},{"id":"2616cc34e9b2efc4","type":"change","z":"2383c95d6fb42e2f","g":"e7762e5d50aa5276","name":"","rules":[{"t":"set","p":"myObj.myVar","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":370,"y":240,"wires":[[]]},{"id":"3bd8accdfafe5efa","type":"comment","z":"2383c95d6fb42e2f","g":"e7762e5d50aa5276","name":"Watch single property of an object (static setup)","info":"","x":360,"y":140,"wires":[]},{"id":"cf5e593c6414d35e","type":"inject","z":"2383c95d6fb42e2f","g":"e7762e5d50aa5276","name":"","props":[{"p":"value","v":"{\"x\":1,\"y\":1,\"z\":1}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":150,"y":360,"wires":[["968a48add4a13305"]]},{"id":"968a48add4a13305","type":"change","z":"2383c95d6fb42e2f","g":"e7762e5d50aa5276","name":"directly modifies the myObj object (breaks watcher)","rules":[{"t":"set","p":"myObj","pt":"global","to":"{\"a\":1}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":360,"wires":[[]]},{"id":"d9b0e9866d3ded16","type":"comment","z":"2383c95d6fb42e2f","g":"e7762e5d50aa5276","name":"WARNING: Replacing the parent object will break the watcher","info":"","x":320,"y":320,"wires":[]},{"id":"ded0ad36c17a4e54","type":"inject","z":"2383c95d6fb42e2f","g":"e7762e5d50aa5276","name":"fix watcher (refresh)","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":190,"y":400,"wires":[["4686bc49c307c7c5"]]},{"id":"4686bc49c307c7c5","type":"link out","z":"2383c95d6fb42e2f","g":"e7762e5d50aa5276","name":"link out 7","mode":"link","links":["b7bd45ba40ace406"],"x":355,"y":400,"wires":[]},{"id":"b7bd45ba40ace406","type":"link in","z":"2383c95d6fb42e2f","g":"e7762e5d50aa5276","name":"link in 3","links":["4686bc49c307c7c5"],"x":95,"y":180,"wires":[["803fe60d2002f8eb"]]},{"id":"010b592ace4489e7","type":"comment","z":"2383c95d6fb42e2f","g":"e7762e5d50aa5276","name":"👈 Change value","info":"","x":580,"y":240,"wires":[]},{"id":"51816ddd4b9f4b85","type":"subflow:1ef1ab7a9b9d0221","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"dynamic","env":[{"name":"variable","value":"","type":"str"},{"name":"mode","value":"object","type":"str"}],"x":360,"y":540,"wires":[["2ba9128c4cd8eacb"]]},{"id":"2ba9128c4cd8eacb","type":"debug","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"watcher 2","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"topic & \" : \" & payload ","statusType":"jsonata","x":560,"y":540,"wires":[]},{"id":"022f9b1c5d114d20","type":"inject","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"","props":[{"p":"topic","vt":"str"},{"p":"value","v":"{\"x\":1,\"y\":1,\"z\":1}","vt":"json"},{"p":"mode","v":"object","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"global.myObj2","x":180,"y":540,"wires":[["51816ddd4b9f4b85"]]},{"id":"d1326a318bf9a9c0","type":"inject","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"random","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"(\t    $minimum := 1;\t    $maximum := 10;\t    $round(($random() * ($maximum-$minimum)) + $minimum, 0)\t)","payloadType":"jsonata","x":150,"y":620,"wires":[["f9b046af10db2673"]]},{"id":"f9b046af10db2673","type":"change","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"","rules":[{"t":"set","p":"myObj2.x","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":390,"y":620,"wires":[[]]},{"id":"cfa73552e39d2bfe","type":"change","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"","rules":[{"t":"set","p":"myObj2.y","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":390,"y":660,"wires":[[]]},{"id":"5af6929e4b20c671","type":"inject","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"random","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"(\t    $minimum := 1;\t    $maximum := 10;\t    $round(($random() * ($maximum-$minimum)) + $minimum, 0)\t)","payloadType":"jsonata","x":150,"y":660,"wires":[["cfa73552e39d2bfe"]]},{"id":"79431b290d341591","type":"inject","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"","props":[{"p":"value","v":"{\"x\":1,\"y\":1,\"z\":1}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":150,"y":780,"wires":[["42047bb09e5091f1"]]},{"id":"42047bb09e5091f1","type":"change","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"directly modifies the myObj2 object (breaks watcher)","rules":[{"t":"set","p":"myObj2","pt":"global","to":"{\"a\":1}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":440,"y":780,"wires":[[]]},{"id":"721ec5aff3867f38","type":"comment","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"WARNING: Replacing the watched object will break the watcher","info":"","x":310,"y":740,"wires":[]},{"id":"357e5e39e2308024","type":"comment","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"Watch all properties of an object (dynamic)","info":"","x":360,"y":500,"wires":[]},{"id":"8c3471aaa811b668","type":"comment","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"👈 Change value","info":"","x":580,"y":620,"wires":[]},{"id":"1c20ab88c69eb909","type":"comment","z":"2383c95d6fb42e2f","g":"44a911618d55ad8b","name":"👈 Change value","info":"","x":580,"y":660,"wires":[]}]

Flow Info

Created 3 weeks, 1 day ago
Updated 2 weeks, 6 days ago
Rating: not yet rated

Owner

Actions

Rate:

Node Types

Core
  • change (x5)
  • comment (x8)
  • debug (x2)
  • delay (x1)
  • function (x2)
  • inject (x8)
  • link in (x1)
  • link out (x1)
  • status (x1)
  • switch (x1)
Other
  • group (x2)
  • subflow (x1)
  • subflow:1ef1ab7a9b9d0221 (x2)
  • tab (x1)

Tags

  • context
  • watch
  • observer
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option