Example node-red-contrib-homekit-bridged lamp subflow
This is a subflow that will take zigbee2mqtt and node-red-contrib-homekit-bridged input and output mqtt and state for a homekit service.
This flow will work for both normal, colored (xy) or color temp bulbs. There is some configuration w.r.t. offline color changes, so far only IKEA Trådfri seems to support it, if you are using Trådfri bulbs you can set COLOR_QUEUE to false.
This flow requires zigbee2mqtt and node-red-contrib-homekit-bridged
[{"id":"a0439f79.20e49","type":"subflow","name":"router","info":"This flow will take both mqtt and homekit node output as input.\n\nIt will do the following\n- add a `tag` property to the msg\n - set to **homekit** for homekit messages\n - set to **mqtt** for mqtt messages\n- handle the special `/availability` topic\n - set `tag` to **mqtt**\n - move the value to `msg.payload.available`\n - convert value to true or false\n- mqtt publish failure\n - create new msg\n - set `tag` to **mqtt**\n - set `msg.payload.available` to false\n- route output based on tag ","category":"z2m_utils","in":[{"x":60,"y":100,"wires":[{"id":"9faa080a.9e506"}]}],"out":[{"x":280,"y":60,"wires":[{"id":"9faa080a.9e506","port":0}]},{"x":280,"y":140,"wires":[{"id":"9faa080a.9e506","port":1}]}],"env":[{"name":"MQTT_PREFIX","type":"str","value":"zigbee2mqtt"},{"name":"MQTT_DEVICE","type":"str","value":"<friendly_name>"}],"inputLabels":["mqtt+homekit"],"outputLabels":["mqtt","homekit"],"icon":"node-red/switch.png"},{"id":"9faa080a.9e506","type":"function","z":"a0439f79.20e49","name":"router","func":"// create topic filter\nvar mqtt_topic = env.get(\"MQTT_PREFIX\") + \"/\" + env.get(\"MQTT_DEVICE\");\nvar mqtt_topic_availability = mqtt_topic + \"/availability\";\nvar mqtt_topic_bridge = env.get(\"MQTT_PREFIX\") + \"/bridge/log\";\nvar mqtt_device = env.get(\"MQTT_DEVICE\");\n\n\n// decode json payload\nif (typeof msg.payload == \"string\") {\n try {\n msg.payload = JSON.parse(msg.payload);\n } catch (e) {\n // payload was not json\n }\n}\n\n// route messages\nif (msg.topic !== undefined) {\n // normal input\n if (msg.topic == mqtt_topic) {\n if ((msg.hap !== undefined) || (msg.payload.Identify === 1)) {\n msg.tag = \"homekit\";\n } else {\n msg.tag = \"mqtt\";\n }\n \n // availability changes\n } else if (msg.topic == mqtt_topic_bridge) {\n switch(msg.payload.type) {\n // NOTE: we failed to publish a state change\n // mark as unavailable if device name matches\n case \"zigbee_publish_error\":\n if (msg.payload.meta.friendly_name == mqtt_device) {\n return {\n \"topic\": mqtt_topic,\n \"tag\": \"mqtt\",\n \"payload\": {\n \"available\": false,\n },\n };\n }\n break;\n }\n } else if (msg.topic == mqtt_topic_availability) {\n return {\n \"topic\": mqtt_topic,\n \"tag\": \"mqtt\",\n \"payload\": {\n \"available\": (msg.payload == \"online\") ? true : false,\n },\n };\n }\n}\n\nreturn [\n (msg.tag == \"mqtt\") ? msg : null,\n (msg.tag == \"homekit\") ? msg : null,\n];","outputs":2,"noerr":0,"x":170,"y":100,"wires":[[],[]],"inputLabels":["mqtt+homekit"],"outputLabels":["mqtt","homekit"],"icon":"node-red/switch.png"},{"id":"9fee074b.ec4648","type":"subflow","name":"cache","info":"Input state will be stored on disk and passed along to the output.\n\nThe stored state can be retreived by sending 'restore' as msg payload.","category":"z2m_utils","in":[{"x":80,"y":100,"wires":[{"id":"5d63ddd9.a468a4"}]}],"out":[{"x":600,"y":100,"wires":[{"id":"ea196f26.16657","port":0}]}],"env":[{"name":"STATE_DIR","type":"str","value":"/tmp"},{"name":"STATE_NAME","type":"str","value":""}],"inputLabels":["state"],"outputLabels":["state"],"icon":"font-awesome/fa-floppy-o"},{"id":"e3b291a6.593008","type":"file","z":"9fee074b.ec4648","name":"store","filename":"","appendNewline":false,"createDir":true,"overwriteFile":"true","encoding":"utf8","x":290,"y":140,"wires":[["ea196f26.16657"]]},{"id":"94426ae1.0cd01","type":"file in","z":"9fee074b.ec4648","name":"load","filename":"","format":"utf8","chunk":false,"sendError":false,"encoding":"utf8","x":290,"y":60,"wires":[["ea196f26.16657"]]},{"id":"5d63ddd9.a468a4","type":"function","z":"9fee074b.ec4648","name":"","func":"var state_file = `${env.get(\"STATE_DIR\")}/${env.get(\"STATE_NAME\")}.persist`;\nvar msg_restore = null;\nvar msg_store = null;\nif (msg.topic == \"cache/load\") {\n msg_restore = {\n \"filename\": state_file, \n \"topic\": msg.topic,\n };\n} else if (msg.topic == \"cache/store\") {\n msg_store = {\n \"filename\": state_file, \n \"topic\": msg.topic,\n \"payload\": msg.payload,\n };\n}\nreturn [\n msg_restore,\n msg_store,\n];","outputs":2,"noerr":0,"x":175,"y":100,"wires":[["94426ae1.0cd01"],["e3b291a6.593008"]],"inputLabels":["state"],"outputLabels":["restore","store"],"icon":"node-red/split.png","l":false},{"id":"ea196f26.16657","type":"function","z":"9fee074b.ec4648","name":"","func":"if (msg.filename){\n delete msg.filename;\n}\n\nif (msg.error) {\n delete msg.error;\n msg.payload = {};\n} else {\n if (typeof msg.payload == \"string\") {\n try {\n msg.payload = JSON.parse(msg.payload);\n } catch (e) {\n // payload was not json\n }\n }\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":515,"y":100,"wires":[[]],"inputLabels":["state"],"outputLabels":["state"],"icon":"node-red/join.png","l":false},{"id":"33028345.91b6b4","type":"catch","z":"9fee074b.ec4648","name":"","scope":["94426ae1.0cd01"],"uncaught":false,"x":395,"y":100,"wires":[["ea196f26.16657"]],"l":false},{"id":"7884b31b.4f8f8c","type":"subflow","name":"lamp","info":"This subflow will take mqtt input and turn this\n into input a homekit node can use.\n \n**Do not forget to set the subject!**\n\nCurrent supported lamps:\n- IKEA Trådfri RGB bulbs\n- Philips Hue bulbs\n- Innr Bulbs\n\nHomekit Characteristics:\n```\n{\n \"On\": true,\n \"Brightness\": true,\n \"Hue\": true,\n \"Saturation\": true\n}\n```\n\nFlow Configuration:\n- COLOR_MODE:\n - NONE: ignore all color and color_temp messages\n - CWS: handle color messages\n - WS: handle color_temp mesages\n- COLOR_QUEUE\n - true: queue color changes when state=off\n - false: send color changes when state=off","category":"z2m_homekit","in":[{"x":60,"y":100,"wires":[{"id":"614c33b3.91a1a4"}]}],"out":[{"x":440,"y":60,"wires":[{"id":"dedd62d8.3472b","port":0}]},{"x":440,"y":140,"wires":[{"id":"d7094d95.4bdf4","port":0}]}],"env":[{"name":"MQTT_PREFIX","type":"str","value":"zigbee2mqtt"},{"name":"MQTT_DEVICE","type":"str","value":"<friendly_name>"},{"name":"STATE_DIR","type":"str","value":"/root/hkdb"},{"name":"COLOR_MODE","type":"str","value":"CWS"},{"name":"COLOR_QUEUE","type":"bool","value":"true"}],"inputLabels":["mqtt+homekit"],"outputLabels":["mqtt","homekit"],"icon":"node-red-contrib-homekit-bridged/homekit.png"},{"id":"dedd62d8.3472b","type":"link in","z":"7884b31b.4f8f8c","name":"mqtt","links":["fd84f3a1.0942f8"],"x":375,"y":80,"wires":[[]],"icon":"node-red/bridge.png"},{"id":"d7094d95.4bdf4","type":"link in","z":"7884b31b.4f8f8c","name":"homekit","links":["bf53a6e2.94ec3"],"x":375,"y":120,"wires":[[]],"icon":"node-red-contrib-homekit-bridged/homekit.png"},{"id":"a0747615.6b3e08","type":"comment","z":"7884b31b.4f8f8c","name":"TODO","info":"- swap WS/CSW\n - XY (get/set color_xy)\n - HS (get/set color_hs)\n - TEMP (get/set color_temp)\n- improve offline color\n - on homekit.color, don't update state.color\n - mqtt.color != state.color\n - discard homekit.color\n - update state.color\n - out.color = mqtt.color + homekit.color","x":90,"y":180,"wires":[]},{"id":"da78a427.c45a9","type":"function","z":"7884b31b.4f8f8c","name":"processor","func":"/*\n * processor\n * ----------------------------------------------------\n * A ticker is used to trigger the main event loop.\n * The ticker sets the pace of handling the flow state.\n */\n// variables\nvar state_base = `$parent.${env.get(\"MQTT_PREFIX\")}/${env.get(\"MQTT_DEVICE\")}`;\nvar sync_timeout = 5000;\nvar color_mode = env.get(\"COLOR_MODE\");\nvar color_queue = env.get(\"COLOR_QUEUE\");\nvar ret = {\n \"continue\": {\"reset\": true},\n \"cache\": null,\n \"mqtt\": null,\n \"homekit\": null,\n};\nvar rgb, hsv, xy;\n\n// helpers functions\nfunction stateGet(state_name, state_default=null) {\n return flow.get(`${state_base}/${state_name}`)||state_default;\n}\n\nfunction stateSet(state_name, state) {\n flow.set(`${state_base}/${state_name}`, state);\n}\n\nfunction stateGetAndSet(state_name, state, state_default=null) {\n var ret_state = stateGet(state_name, state_default);\n stateSet(state_name, state);\n return ret_state;\n}\n\n\nfunction stateUpdate(ret, update) {\n var current = stateGet(\"state\", {});\n var updated = Object.assign(current, update);\n \n stateSet(\"state\", updated);\n ret.cache = {\n \"topic\": \"cache/store\",\n \"payload\": updated,\n };\n}\n\nfunction homekitPublish(ret, prop, value) {\n // initialize ret.homekit\n if (ret.homekit === null) {\n ret.homekit = {\n \"topic\": `${env.get(\"MQTT_PREFIX\")}/${env.get(\"MQTT_DEVICE\")}`,\n \"payload\": {},\n };\n }\n \n // update payload\n ret.homekit.payload[prop] = value;\n}\n\nfunction mqttPublish(ret, prop, value) {\n // initialize ret.homekit\n if (ret.mqtt === null) {\n ret.mqtt = {\n \"topic\": `${env.get(\"MQTT_PREFIX\")}/${env.get(\"MQTT_DEVICE\")}/set`,\n \"payload\": {},\n };\n }\n \n // update payload\n ret.mqtt.payload[prop] = value;\n}\n\nfunction timestamp() {\n return Math.round((new Date()).getTime());\n}\n\nfunction cacheColorGet(mode, value) {\n // variables\n var cache, rgb, hsv;\n \n // passthru value\n if ((value !== null) && (value !== undefined)) return value;\n \n // lookup from state\n cache = stateGet(\"state\", {});\n if (cache.hasOwnProperty(\"color\")) {\n if (isColorXY(cache.color)) {\n rgb = xy2rgb(cache.color.x, cache.color.y);\n hsv = rgb2hsv(rgb.r, rgb.g, rgb.b);\n } else if (isColorHueSaturation(cache.color)) {\n hsv.h = cache.color.hue;\n hsv.s = cache.color.saturation;\n hsv.v = 100;\n }\n switch (mode) {\n case \"hue\":\n return hsv.h;\n case \"saturation\":\n return hsv.s;\n }\n }\n \n return null;\n}\n\nfunction isColorXY(color) {\n return ((color.x !== undefined) && (color.y !== undefined));\n}\n\nfunction isColorHueSaturation(color) {\n return ((color.hue !== undefined) && (color.saturation !== undefined));\n}\n\nfunction isColorRGB(color) {\n return ((color.r !== undefined) && (color.g !== undefined) && (color.b !== undefined));\n}\n\nfunction rgb2xy(red, green, blue) {\n // The RGB values should be between 0 and 1. So convert them.\n // The RGB color (255, 0, 100) becomes (1.0, 0.0, 0.39)\n red /= 255; green /= 255; blue /= 255;\n\n // Apply a gamma correction to the RGB values, which makes the color\n // more vivid and more the like the color displayed on the screen of your device\n red = (red > 0.04045) ? Math.pow((red + 0.055) / (1.0 + 0.055), 2.4) : (red / 12.92);\n green = (green > 0.04045) ? Math.pow((green + 0.055) / (1.0 + 0.055), 2.4) : (green / 12.92);\n blue = (blue > 0.04045) ? Math.pow((blue + 0.055) / (1.0 + 0.055), 2.4) : (blue / 12.92);\n\n // RGB values to XYZ using the Wide RGB D65 conversion formula\n const X = red * 0.664511 + green * 0.154324 + blue * 0.162028;\n const Y = red * 0.283881 + green * 0.668433 + blue * 0.047685;\n const Z = red * 0.000088 + green * 0.072310 + blue * 0.986039;\n\n // Calculate the xy values from the XYZ values\n let x = (X / (X + Y + Z)).toFixed(4);\n let y = (Y / (X + Y + Z)).toFixed(4);\n\n if (isNaN(x)) {\n x = 0;\n }\n\n if (isNaN(y)) {\n y = 0;\n }\n\n return {x: Number.parseFloat(x), y: Number.parseFloat(y)};\n}\n\nfunction xy2rgb(x, y, brightness){\n\t//Set to maximum brightness if no custom value was given (Not the slick ECMAScript 6 way for compatibility reasons)\n\tif (brightness === undefined) {\n\t\tbrightness = 254;\n\t}\n\n\tvar z = 1.0 - x - y;\n\tvar Y = (brightness / 254).toFixed(2);\n\tvar X = (Y / y) * x;\n\tvar Z = (Y / y) * z;\n\n\t//Convert to RGB using Wide RGB D65 conversion\n\tvar red \t= X * 1.656492 - Y * 0.354851 - Z * 0.255038;\n\tvar green \t= -X * 0.707196 + Y * 1.655397 + Z * 0.036152;\n\tvar blue \t= X * 0.051713 - Y * 0.121364 + Z * 1.011530;\n\n\t//If red, green or blue is larger than 1.0 set it back to the maximum of 1.0\n\tif (red > blue && red > green && red > 1.0) {\n\n\t\tgreen = green / red;\n\t\tblue = blue / red;\n\t\tred = 1.0;\n\t}\n\telse if (green > blue && green > red && green > 1.0) {\n\n\t\tred = red / green;\n\t\tblue = blue / green;\n\t\tgreen = 1.0;\n\t}\n\telse if (blue > red && blue > green && blue > 1.0) {\n\n\t\tred = red / blue;\n\t\tgreen = green / blue;\n\t\tblue = 1.0;\n\t}\n\n\t//Reverse gamma correction\n\tred \t= red <= 0.0031308 ? 12.92 * red : (1.0 + 0.055) * Math.pow(red, (1.0 / 2.4)) - 0.055;\n\tgreen \t= green <= 0.0031308 ? 12.92 * green : (1.0 + 0.055) * Math.pow(green, (1.0 / 2.4)) - 0.055;\n\tblue \t= blue <= 0.0031308 ? 12.92 * blue : (1.0 + 0.055) * Math.pow(blue, (1.0 / 2.4)) - 0.055;\n\n\n\t//Convert normalized decimal to decimal\n\tred \t= Math.round(red * 255);\n\tgreen \t= Math.round(green * 255);\n\tblue \t= Math.round(blue * 255);\n\n\tif (isNaN(red))\n\t\tred = 0;\n\n\tif (isNaN(green))\n\t\tgreen = 0;\n\n\tif (isNaN(blue))\n\t\tblue = 0;\n\n\treturn {\n r: red,\n g: green,\n b: blue,\n };\n}\n\nfunction rgb2hsv(r, g, b) {\n let rabs, gabs, babs, rr, gg, bb, h, s, v, diff, diffc, percentRoundFn;\n rabs = r / 255;\n gabs = g / 255;\n babs = b / 255;\n v = Math.max(rabs, gabs, babs);\n diff = v - Math.min(rabs, gabs, babs);\n diffc = c => (v - c) / 6 / diff + 1 / 2;\n percentRoundFn = num => Math.round(num * 100) / 100;\n if (diff === 0) {\n h = s = 0;\n } else {\n s = diff / v;\n rr = diffc(rabs);\n gg = diffc(gabs);\n bb = diffc(babs);\n\n if (rabs === v) {\n h = bb - gg;\n } else if (gabs === v) {\n h = (1 / 3) + rr - bb;\n } else if (babs === v) {\n h = (2 / 3) + gg - rr;\n }\n if (h < 0) {\n h += 1;\n }else if (h > 1) {\n h -= 1;\n }\n }\n return {\n h: Math.round(h * 360),\n s: percentRoundFn(s * 100),\n v: percentRoundFn(v * 100)\n };\n}\n\nfunction hsv2rgb(h, s, v = 100) {\n var RGB_MAX = 255;\n var HUE_MAX = 360;\n var SV_MAX = 100;\n \n if (typeof h === 'object') {\n const args = h\n h = args.h; s = args.s; v = args.v;\n }\n\n h = (h % 360 + 360) % 360;\n h = (h === HUE_MAX) ? 1 : (h % HUE_MAX / parseFloat(HUE_MAX) * 6)\n s = (s === SV_MAX) ? 1 : (s % SV_MAX / parseFloat(SV_MAX))\n v = (v === SV_MAX) ? 1 : (v % SV_MAX / parseFloat(SV_MAX))\n\n var i = Math.floor(h)\n var f = h - i\n var p = v * (1 - s)\n var q = v * (1 - f * s)\n var t = v * (1 - (1 - f) * s)\n var mod = i % 6\n var r = [v, q, p, p, t, v][mod]\n var g = [t, v, v, q, p, p][mod]\n var b = [p, p, t, v, v, q][mod]\n\n return {\n r: Math.floor(r * RGB_MAX),\n g: Math.floor(g * RGB_MAX),\n b: Math.floor(b * RGB_MAX),\n }\n}\n\nfunction xy2hsv(x, y, brightness) {\n var rgb = xy2rgb(x, y, brightness);\n return rgb2hsv(rgb.r, rgb.g, rgb.b);\n}\n\nfunction hsv2xy(h, s, v = 100) {\n var rgb = hsv2rgb(h, s, v);\n return rgb2xy(rgb.r, rgb.g, rgb.b);\n}\n\n// main logic controller\nif (stateGet(\"state\", null) === null) {\n // populate sync_state from cache\n ret.cache = {\"topic\": \"cache/load\"};\n}\n\nif (Object.keys(stateGet(\"queue_homekit\", {})).length > 0) {\n // process homekit change queue\n var homekit = stateGetAndSet(\"queue_homekit\", {}, {});\n \n if (!homekit.hasOwnProperty(\"Identify\")) {\n // convert homekit color to mqtt color\n if (color_mode.toUpperCase() == \"CWS\") {\n rgb = hsv = xy = null;\n hsv = {\"h\": null, \"s\": null, \"v\": 100};\n hsv.h = cacheColorGet(\"hue\", homekit.Hue);\n hsv.s = cacheColorGet(\"saturation\", homekit.Saturation);\n if (homekit.hasOwnProperty(\"Hue\") || homekit.hasOwnProperty(\"Saturation\")) {\n if ((typeof hsv.h == \"number\") && (typeof hsv.s == \"number\")) {\n xy = hsv2xy(hsv.h, hsv.s, hsv.v);\n if (\n (\n (color_queue) &&\n (\n (homekit.On || homekit.Brightness > 0) ||\n (stateGet(\"state\", {}).state == \"ON\")\n )\n ) || (\n (!color_queue) &&\n (\n (!homekit.hasOwnProperty(\"On\")) ||\n (homekit.On)\n )\n )\n ) {\n // publish color update\n mqttPublish(ret, \"color\", xy);\n } else {\n // requeue color update\n stateSet(\"queue_homekit\",\n Object.assign(\n {\"Hue\": hsv.h, \"Saturation\": hsv.s},\n stateGet(\"queue_homekit\", {})\n )\n );\n }\n } else {\n node.warn(`${node.id} - no cached color and partial update, skipping ...`);\n }\n }\n }\n \n // convert homekit to mqtt\n for (var hp in homekit) {\n var hv = homekit[hp];\n switch(hp) {\n case \"On\":\n mqttPublish(ret, \"state\", (hv ? \"ON\" : \"OFF\"));\n break;\n case \"Brightness\":\n mqttPublish(ret, \"brightness\", Math.round(Number(hv) * 2.55));\n break;\n case \"ColorTemperature\":\n if (color_mode.toUpperCase() == \"WS\") {\n mqttPublish(ret, \"color_temp\", hv);\n }\n break;\n }\n }\n \n // update cache\n if (ret.mqtt !== null) {\n stateSet(\"sync\", timestamp());\n stateUpdate(ret, ret.mqtt.payload);\n }\n } else {\n // identify bulb\n mqttPublish(ret, \"alert\", \"\");\n }\n}\n\nif (\n (Object.keys(stateGet(\"queue_mqtt\", {})).length > 0) && \n ((timestamp() - stateGet(\"sync\", sync_timeout)) >= sync_timeout)\n) {\n // process mqtt change queue\n var mqtt = stateGetAndSet(\"queue_mqtt\", {}, {});\n \n if (!mqtt.hasOwnProperty(\"available\") || mqtt.available) {\n // convert mqtt to homekit\n for (var mp in mqtt) {\n var mv = mqtt[mp];\n \n switch(mp) {\n case \"available\":\n delete mqtt.available;\n break;\n case \"state\":\n homekitPublish(ret,\"On\", (mv === \"ON\"));\n break;\n case \"brightness\":\n homekitPublish(ret, \"Brightness\", Math.round(Number(mv / 2.55)));\n break;\n case \"color_temp\":\n if (color_mode.toUpperCase() == \"WS\") {\n homekitPublish(ret, \"ColorTemperature\", mv);\n }\n break;\n case \"color\":\n if (color_mode.toUpperCase() == \"CWS\") {\n rgb = hsv = xy = null;\n if (isColorXY(mv)) {\n hsv = xy2hsv(mv.x, mv.y);\n } else if (isColorHueSaturation(mv)) {\n hsv = {\"h\": mv.hue, \"s\": mv.saturation, \"v\": 100};\n } else if (isColorRGB(mv)) {\n hsv = rgb2hsv(mv.r, mv.g, mv.b);\n }\n if (hsv !== null) {\n var hkq = stateGet(\"queue_homekit\", {});\n \n if (JSON.stringify(mv) !== JSON.stringify(stateGet(\"state\", {}).color)) {\n delete hkq.Hue;\n delete hkq.Saturation;\n stateSet(\"queue_homekit\", hkq);\n }\n \n if (!hkq.hasOwnProperty(\"Hue\") && !hkq.hasOwnProperty(\"Saturation\")) {\n homekitPublish(ret, \"Hue\", hsv.h);\n homekitPublish(ret, \"Saturation\", hsv.s);\n }\n }\n }\n break;\n }\n }\n \n // update cache\n stateUpdate(ret, mqtt);\n } else {\n // mark bulb offline\n homekitPublish(ret, \"On\", \"NO_RESPONSE\");\n }\n}\n\n// publish messages\nreturn [\n ret.continue,\n ret.cache,\n ret.mqtt,\n ret.homekit,\n];","outputs":4,"noerr":0,"x":300,"y":300,"wires":[["c737fdf5.dc70f8"],["cbc3b1ab.d95d"],["fd84f3a1.0942f8"],["bf53a6e2.94ec3"]],"inputLabels":["tick"],"outputLabels":["continue","cache","mqtt","homekit"],"icon":"font-awesome/fa-cogs"},{"id":"258bc2d4.04dbd6","type":"inject","z":"7884b31b.4f8f8c","name":"ticker","topic":"core/tick","payload":"","payloadType":"date","repeat":"0.5","crontab":"","once":true,"onceDelay":"1","x":90,"y":260,"wires":[["fdfa8a63.a9e5a8"]],"icon":"node-red/timer.png"},{"id":"cbc3b1ab.d95d","type":"subflow:9fee074b.ec4648","z":"7884b31b.4f8f8c","name":"processor::cache","env":[{"name":"STATE_DIR","type":"env","value":"STATE_DIR"},{"name":"STATE_NAME","type":"env","value":"MQTT_DEVICE"}],"x":435,"y":280,"wires":[["7b13f1ab.a84a38"]],"l":false},{"id":"fdfa8a63.a9e5a8","type":"rbe","z":"7884b31b.4f8f8c","name":"processor::pause","func":"rbe","gap":"","start":"","inout":"out","property":"topic","x":195,"y":300,"wires":[["da78a427.c45a9"]],"icon":"font-awesome/fa-pause-circle","l":false},{"id":"c737fdf5.dc70f8","type":"link out","z":"7884b31b.4f8f8c","name":"processor::continue","links":["99d74bd9.3a4938"],"x":435,"y":240,"wires":[],"icon":"font-awesome/fa-play-circle"},{"id":"99d74bd9.3a4938","type":"link in","z":"7884b31b.4f8f8c","name":"processor","links":["c737fdf5.dc70f8"],"x":55,"y":300,"wires":[["fdfa8a63.a9e5a8"]]},{"id":"7b13f1ab.a84a38","type":"function","z":"7884b31b.4f8f8c","name":"state::load","func":"/*\n * state::load\n * ----------------------------------------------------\n * update in memory state on load\n */\n// variables\nvar state_base = `$parent.${env.get(\"MQTT_PREFIX\")}/${env.get(\"MQTT_DEVICE\")}`\n\nif (msg.topic == \"cache/load\") {\n flow.set(`${state_base}/state`, {});\n flow.set(`${state_base}/queue_mqtt`, msg.payload);\n}\n\nreturn null;","outputs":1,"noerr":0,"x":550,"y":280,"wires":[[]],"icon":"font-awesome/fa-database"},{"id":"9350885e.a6ca88","type":"link in","z":"7884b31b.4f8f8c","name":"queue","links":["a5446b09.442388"],"x":55,"y":360,"wires":[["ad099bfe.bb698"]],"icon":"font-awesome/fa-list"},{"id":"ad099bfe.bb698","type":"function","z":"7884b31b.4f8f8c","name":"queue","func":"/*\n * queue\n * ----------------------------------------------------\n * queue mqtt/homekit changes for processor\n */\n// variables\nvar state_base = `$parent.${env.get(\"MQTT_PREFIX\")}/${env.get(\"MQTT_DEVICE\")}`\nvar queue = flow.get(`${state_base}/queue_${msg.tag}`)||{};\n\nfor (var property in msg.payload) {\n queue[property] = msg.payload[property];\n}\n\nflow.set(`${state_base}/queue_${msg.tag}`, queue);\nreturn null;","outputs":1,"noerr":0,"x":290,"y":360,"wires":[[]],"icon":"font-awesome/fa-code-fork"},{"id":"fd84f3a1.0942f8","type":"link out","z":"7884b31b.4f8f8c","name":"processor::mqtt","links":["dedd62d8.3472b"],"x":435,"y":320,"wires":[],"icon":"node-red/bridge.png"},{"id":"bf53a6e2.94ec3","type":"link out","z":"7884b31b.4f8f8c","name":"processor::homekit","links":["d7094d95.4bdf4"],"x":435,"y":360,"wires":[],"icon":"node-red-contrib-homekit-bridged/homekit.png"},{"id":"a5446b09.442388","type":"link out","z":"7884b31b.4f8f8c","name":"queue","links":["9350885e.a6ca88"],"x":275,"y":100,"wires":[],"icon":"font-awesome/fa-list"},{"id":"614c33b3.91a1a4","type":"subflow:a0439f79.20e49","z":"7884b31b.4f8f8c","name":"","env":[{"name":"MQTT_PREFIX","type":"env","value":"MQTT_PREFIX"},{"name":"MQTT_DEVICE","type":"env","value":"MQTT_DEVICE"}],"x":150,"y":100,"wires":[["a5446b09.442388"],["a5446b09.442388"]]},{"id":"fdf0223a.77625","type":"subflow:7884b31b.4f8f8c","z":"8d327434.7855a","name":"Desk Lamp - Handler","env":[{"name":"MQTT_DEVICE","type":"str","value":"bedroom/desk_lamp"}],"x":260,"y":140,"wires":[["2e0bce9f.ac8caa"],["d6aacaac.34c8a"]]}]