Hi,
I wanted to be able to manage my RFY covers’ position but they only support UP/DOWN/STOP commands, so I came up with some node-red automation to do it.
It’s based on the time needed by each cover to go all the way up or all the way down.
This doesn’t work if you’re using remotes to manage your covers. I didn’t yet test if it was possible to trap the remote message and use it.
Hass
What you need for each cover is:
- The actual cover:
cover:
- platform: rfxtrx
automatic_add: False
signal_repetitions: 1
devices:
SOMEID:
name: "RFY Cover 1"
- An MQTT virtual cover that supports positions:
- name: "Cover Room 1"
platform: mqtt
command_topic: 'hass/room/cover/1/in'
set_position_topic: 'hass/room/cover/1/position'
position_topic: 'hass/room/cover/1/state'
retain: false
optimistic: false
This is for each cover that you want to manage.
Nodered
Prerequisites
You’ll need to install some nodes before this works
- node-red-contrib-home-assistant-websocket
You’ll also need to install an mqtt server (eg: mosquitto
)
The flow
Then import this flow into node-red:
[{"id":"5cafdd9.eae5724","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"bd6441.09020bc","type":"mqtt out","z":"5cafdd9.eae5724","name":"hass/+/cover/+/state","topic":"","qos":"","retain":"true","broker":"b2ce6188.0fb15","x":720,"y":480,"wires":[]},{"id":"3b2db43e.8bfc1c","type":"mqtt in","z":"5cafdd9.eae5724","name":"","topic":"hass/+/cover/+/in","qos":"2","broker":"b2ce6188.0fb15","x":80,"y":540,"wires":[["625d1eb4.3948f"]]},{"id":"625d1eb4.3948f","type":"function","z":"5cafdd9.eae5724","name":"Manage Manual Position","func":"function up (entityId) {\n return {\n payload: {\n service: \"open_cover\",\n data: {\n entity_id: entityId\n }\n }\n };\n}\n\nfunction down (entityId) {\n return {\n payload: {\n service: \"close_cover\",\n data: {\n entity_id: entityId\n }\n }\n };\n}\n\nfunction stop (entityId) {\n return {\n payload: {\n service: \"stop_cover\",\n data: {\n entity_id: entityId\n }\n }\n };\n}\n\nvar idx = msg.topic.split(\"/\", 4).join(\"/\");\nvar topic = msg.topic.split(\"/\")[4];\n\nvar blinds = flow.get('blindsHASS');\nvar cover = blinds[idx];\n\nvar now = Date.now();\n\nif (msg.hasOwnProperty('forceStop') || msg.hasOwnProperty('positionStop')) {\n if (msg.lastUpdate != cover.lastUpdate) {\n return null;\n }\n}\n\nif (msg.payload == \"OPEN\") {\n var waitTime = ((100 - cover.value) / 100 * cover.upTime + 1) * 1000\n var wait = {\n topic: msg.topic,\n lastUpdate: now,\n delay: waitTime,\n forceStop: true,\n payload: \"STOP\"\n }\n node.send([null, wait, null, up(cover.idxHW)]);\n} else if (msg.payload == \"CLOSE\") {\n var waitTime = ((cover.value) / 100 * cover.downTime + 1) * 1000\n var wait = {\n topic: msg.topic,\n lastUpdate: now,\n delay: waitTime,\n forceStop: true,\n payload: \"STOP\"\n }\n node.send([null, wait, null, down(cover.idxHW)]);\n} else if (msg.payload == \"STOP\" && cover.lastAction != \"STOP\" && !msg.hasOwnProperty('forceStop')) {\n node.send([null, null, null, stop(cover.idxHW)]);\n}\n\nif (msg.hasOwnProperty('positionStop')) {\n cover.lastAction = msg.payload;\n cover.lastUpdate = now;\n cover.value = msg.position;\n return { topic: idx + '/state', payload: msg.position };\n}\n\nif (cover.lastAction == \"OPEN\") {\n if ((now - cover.lastUpdate) / 1000 > cover.upTime) {\n cover.lastUpdate = now;\n cover.lastAction = msg.payload;\n cover.value = 100;\n return { topic: idx + '/state', payload: 100 };\n }\n cover.value = cover.value + (((now - cover.lastUpdate) / 1000) / cover.upTime * 100) | 0;\n if (cover.value > 100) { cover.value = 100 }\n node.send({ topic: idx + '/state', payload: cover.value });\n} else if (cover.lastAction == \"CLOSE\") {\n if ((now - cover.lastUpdate) / 1000 > cover.downTime) {\n cover.lastUpdate = now;\n cover.lastAction = msg.payload;\n cover.value = 0;\n return { topic: idx + '/state', payload: 0 };\n }\n cover.value = cover.value - (((now - cover.lastUpdate) / 1000) / cover.upTime * 100) | 0;\n if (cover.value < 0) { cover.value = 0; }\n node.send({ topic: idx + '/state', payload: cover.value });\n}\ncover.lastAction = msg.payload;\ncover.lastUpdate = now;\n\n\n\nreturn [null, null, blinds];","outputs":"4","noerr":0,"x":350,"y":540,"wires":[["860529bf.6559f8","bd6441.09020bc"],["860529bf.6559f8","a9f49c9b.afbe9"],["860529bf.6559f8"],["c0c885fa.9f1d18"]],"outputLabels":["state","stop wait","blinds","hass request"]},{"id":"860529bf.6559f8","type":"debug","z":"5cafdd9.eae5724","name":"","active":false,"console":"false","complete":"true","x":670,"y":540,"wires":[]},{"id":"a9f49c9b.afbe9","type":"delay","z":"5cafdd9.eae5724","name":"If no stop is received","pauseType":"delayv","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":360,"y":640,"wires":[["625d1eb4.3948f","7be9faa8.b4c6b4"]]},{"id":"7be9faa8.b4c6b4","type":"debug","z":"5cafdd9.eae5724","name":"","active":false,"console":"false","complete":"true","x":670,"y":640,"wires":[]},{"id":"69dbb978.2390e8","type":"debug","z":"5cafdd9.eae5724","name":"","active":false,"console":"false","complete":"true","x":850,"y":580,"wires":[]},{"id":"732de4c2.cc2e4c","type":"function","z":"5cafdd9.eae5724","name":"Manage Blinds","func":"function up (entityId) {\n return {\n payload: {\n service: \"open_cover\",\n data: {\n entity_id: entityId\n }\n }\n };\n}\n\nfunction down (entityId) {\n return {\n payload: {\n service: \"close_cover\",\n data: {\n entity_id: entityId\n }\n }\n };\n}\n\nfunction stop (entityId) {\n return {\n payload: {\n service: \"stop_cover\",\n data: {\n entity_id: entityId\n }\n }\n };\n}\n\n\nvar idx = msg.topic.split(\"/\", 4).join(\"/\");\nvar topic = msg.topic.split(\"/\")[4];\n\nvar blinds = flow.get('blindsHASS');\nvar cover = blinds[idx];\n\nvar now = Date.now();\n\nif (cover.lastAction == \"OPEN\") {\n if ((now - cover.lastUpdate) / 1000 > cover.upTime) {\n cover.lastUpdate = now;\n cover.lastAction = msg.payload;\n cover.value = 100;\n node.send([{ topic: idx + '/state', payload: 100 }, null]);\n }\n cover.value = cover.value + (((now - cover.lastUpdate) / 1000) / cover.upTime * 100) | 0;\n if (cover.value > 100) { cover.value = 100 }\n node.send({ topic: idx + '/state', payload: cover.value });\n} else if (cover.lastAction == \"CLOSE\") {\n if ((now - cover.lastUpdate) / 1000 > cover.downTime) {\n cover.lastUpdate = now;\n cover.lastAction = msg.payload;\n cover.value = 0;\n node.send([{ topic: idx + '/state', payload: 0 }, null]);\n }\n cover.value = cover.value - (((now - cover.lastUpdate) / 1000) / cover.upTime * 100) | 0;\n if (cover.value < 0) { cover.value = 0; }\n node.send({ topic: idx + '/state', payload: cover.value });\n}\ncover.lastUpdate = now;\n\nwantedVal = Number(msg.payload);\nif (wantedVal == cover.value) {\n return null;\n}\nttw = 0;\nif (cover.value > wantedVal) { // Cover down\n delta = cover.value - wantedVal;\n ttw = cover.downTime / 100 * delta;\n localMsg = down(cover.idxHW);\n cover.lastAction = \"CLOSE\";\n} else if (cover.value < wantedVal) { // Cover Up\n delta = wantedVal - cover.value;\n ttw = cover.upTime / 100 * delta;\n localMsg = up(cover.idxHW);\n cover.lastAction = \"OPEN\";\n}\nif (wantedVal === 0 || wantedVal == 100) {\n stopMsg = null;\n ttw += 2;\n}\nstopMsg = {\n topic: msg.topic,\n payload: \"STOP\",\n lastUpdate: now,\n delay: ttw * 1000,\n positionStop: true,\n position: wantedVal\n};\nif (wantedVal === 0 || wantedVal == 100) {\n stopMsg.forceStop = true;\n}\n\nnode.send([null, localMsg, stopMsg]);\nreturn null;","outputs":"3","noerr":0,"x":340,"y":340,"wires":[["829ed290.7e5d6","b6521a54.7e9e08"],["21dfa073.240ff","b2bdb169.2aa9"],["50fd4094.c5234","bd978c3d.efc75"]],"outputLabels":["state","hass request","stop"]},{"id":"50fd4094.c5234","type":"delay","z":"5cafdd9.eae5724","name":"Delay stop","pauseType":"delayv","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":350,"y":440,"wires":[["625d1eb4.3948f"]]},{"id":"9a638973.cc1628","type":"mqtt in","z":"5cafdd9.eae5724","name":"","topic":"hass/+/cover/+/position","qos":"2","broker":"b2ce6188.0fb15","x":100,"y":340,"wires":[["732de4c2.cc2e4c"]]},{"id":"829ed290.7e5d6","type":"debug","z":"5cafdd9.eae5724","name":"","active":false,"console":"false","complete":"true","x":670,"y":260,"wires":[]},{"id":"21dfa073.240ff","type":"debug","z":"5cafdd9.eae5724","name":"","active":false,"console":"false","complete":"true","x":670,"y":360,"wires":[]},{"id":"bd978c3d.efc75","type":"debug","z":"5cafdd9.eae5724","name":"","active":false,"console":"false","complete":"true","x":670,"y":420,"wires":[]},{"id":"b6521a54.7e9e08","type":"mqtt out","z":"5cafdd9.eae5724","name":"hass/+/cover/+/state","topic":"","qos":"","retain":"","broker":"b2ce6188.0fb15","x":720,"y":220,"wires":[]},{"id":"da7146b2.8fee58","type":"function","z":"5cafdd9.eae5724","name":"Init JSON","func":"if (flow.get('blindsHASS') === undefined || msg.payload === 'reset') {\n flow.set('blindsHASS', undefined);\n flow.set('blindsHASS', {\n 'hass/room/cover/1': { // Volet salon 1\n lastAction: \"STOP\",\n lastUpdate: 0,\n value: 0,\n idxHW: \"cover.rfy_cover_1\",\n entityID: \"cover.cover_room_1\",\n upTime: 28.7,\n downTime: 26.5,\n }\n });\n}\nreturn [{go: true}, flow.get('blindsHASS')];","outputs":"2","noerr":0,"x":180,"y":120,"wires":[["6d6ea286.d3addc"],["4bd99ca6.2fbfa4"]]},{"id":"f43f7d6c.193ec","type":"inject","z":"5cafdd9.eae5724","name":"Init","topic":"","payload":"","payloadType":"date","repeat":"60","crontab":"","once":true,"x":90,"y":60,"wires":[["da7146b2.8fee58"]]},{"id":"6d6ea286.d3addc","type":"function","z":"5cafdd9.eae5724","name":"Create Blinds List","func":"blinds = flow.get('blindsHASS');\nvar msgs = [];\nfor (var prop in blinds) {\n if (!blinds.hasOwnProperty(prop)) {\n //The current property is not a direct property of p\n continue;\n }\n var toget = {\n idx: prop,\n entityID: blinds[prop].entityID,\n payload: {\n entity_id: blinds[prop].entityID\n }\n }\n if (toget.entityID) {\n msgs.push(toget);\n }\n}\nnode.send([msgs]);","outputs":1,"noerr":0,"x":310,"y":60,"wires":[["8989d9e6.64aa08"]]},{"id":"9508e019.1ddda","type":"debug","z":"5cafdd9.eae5724","name":"","active":false,"console":"false","complete":"true","x":830,"y":60,"wires":[]},{"id":"29c33da4.204312","type":"function","z":"5cafdd9.eae5724","name":"Extract Value","func":"blinds = flow.get('blindsHASS');\nblinds[msg.idx].value = msg.data.attributes.current_position | 0;\n\nreturn blinds;","outputs":1,"noerr":0,"x":670,"y":60,"wires":[["9508e019.1ddda"]]},{"id":"30a76119.a91e2e","type":"comment","z":"5cafdd9.eae5724","name":"HASS","info":"","x":60,"y":20,"wires":[]},{"id":"4bd99ca6.2fbfa4","type":"debug","z":"5cafdd9.eae5724","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":330,"y":120,"wires":[]},{"id":"8989d9e6.64aa08","type":"api-current-state","z":"5cafdd9.eae5724","name":"Get State","server":"186c7670.558e3a","halt_if":"","halt_if_type":"","halt_if_compare":"is","override_topic":true,"override_payload":true,"override_data":true,"entity_id":"","state_type":"str","outputs":1,"x":500,"y":120,"wires":[["8f5ad89a.869ec8","29c33da4.204312"]]},{"id":"8f5ad89a.869ec8","type":"debug","z":"5cafdd9.eae5724","name":"","active":false,"console":"false","complete":"true","x":670,"y":120,"wires":[]},{"id":"b2bdb169.2aa9","type":"api-call-service","z":"5cafdd9.eae5724","name":"","server":"186c7670.558e3a","service_domain":"cover","service":"","data":"","mergecontext":"","x":690,"y":320,"wires":[["227f7aa0.f26ef6"]]},{"id":"c0c885fa.9f1d18","type":"api-call-service","z":"5cafdd9.eae5724","name":"","server":"186c7670.558e3a","service_domain":"cover","service":"","data":"","mergecontext":"","x":690,"y":580,"wires":[["69dbb978.2390e8"]]},{"id":"227f7aa0.f26ef6","type":"debug","z":"5cafdd9.eae5724","name":"","active":false,"console":"false","complete":"true","x":850,"y":320,"wires":[]},{"id":"9995a6f6.aabc48","type":"inject","z":"5cafdd9.eae5724","name":"Reset","topic":"","payload":"reset","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":"","x":90,"y":180,"wires":[["da7146b2.8fee58"]]},{"id":"b2ce6188.0fb15","type":"mqtt-broker","z":"","name":"docker-mqtt","broker":"1.2.3.4","port":"1883","tls":"7d764641.872898","clientid":"","usetls":true,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"186c7670.558e3a","type":"server","z":"","name":"hass","legacy":false},{"id":"7d764641.872898","type":"tls-config","z":"","name":"","cert":"","key":"","ca":"","certname":"","keyname":"","caname":"","verifyservercert":false}]
You’ll need to edit the configuration node named hass
(in one of the blue node) and the mqtt-server configuration (named docker-mqtt
)
You’ll then want to modify the node called Init JSON
to match your setup.
if (flow.get('blindsHASS') === undefined || msg.payload === 'reset') {
flow.set('blindsHASS', undefined);
flow.set('blindsHASS', { // You can have as many covers as needed here, just add it to the json map
'hass/room/cover/1': { // The topic prefix used by your mqtt cover, see above
lastAction: "STOP",
lastUpdate: 0,
value: 0,
idxHW: "cover.rfy_cover_1", // The device name of the actual RFY cover
entityID: "cover.cover_room_1", // The device name of the virtual cover
upTime: 28.7, // The time needed by the cover to go all the way up in seconds, be accurate
downTime: 26.5, // The time needed by the cover to go all the way down in seconds, be accurate
},
'hass/room/cover/2': { // Exemple for a second cover
[...]
}
});
}
return [{go: true}, flow.get('blindsHASS')];
Apply you flow and add your MQTT virtual cover device to your frontend, you’ll now be able to manage the cover position from within Home Assistant.