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
Working Demo
[{"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":[]}]