Categories
Stuff

Rademacher Bridge zerlegt

Es musste ein alter Gurtwickler nach Jahren harter Arbeit ersetzt werden, der Motor röchelte schon nur noch 🙂 Nachdem ich kürzlich schon einen Rademacher “RolloTron Basis DuoFern 1200” angeschaut und für ok befunden habe, habe ich fix noch einen gekauft, zusammen mit der Bridge. Und was soll ich sagen, die Bridge ist für DIY tatsächlich ausgesprochen gut geeignet!

Hardware

Interessanterweise ist nämlich ein Raspberry Pi Zero verbaut worden, in der Version ohne Wifi – daher kann die Bridge auch nur per Ethernet angebunden werden. Wenn unbedingt notwendig könnte man Wifi nun vermutlich dranbasteln, entweder indem man einen USB Wifi Stick verwendet, oder gleich durch Austausch des Raspi. Vielleicht ein Projekt für später 🙂

Die Elektronik auf der Zusatzplatine enthält kaum Überraschungen, im wesentlichen werkelt hier nur ein KSZ 8851 SNL für den Ethernetport, und ein NRF 905 für den Funk. (Der große scheinbar leere Teil der Platine gegenüber vom Raspi ist die Antenne – sehr gut diese möglichst weit vom Raspi entfernt zu positionieren!)

Doch es gibt noch einen dritten Chip, dieser ist nur beschriftet mit “104GKA 742FZ00” (0en könnten auch Os sein, und 1er könnten auch große I oder kleine l sein, der Font ist nicht ganz klar). Dazu finde ich keine Informationen oder Datenblätter, ich vermute dies ist ein kleiner Custom Microcontroller, welcher die DuoFern Verschlüsselung handhabt. Er schein jedenfalls in der Schaltung zwischen Raspi und Funkchip zu sitzen, im Detail habe ich das aber nicht untersucht.

Software

Die 16GB SD Karte habe ich natürlich an einem anderen Rechner ein wenig durchgesehen. Interessanterweise werden nur etwa 6,7GB verwendet, der Rest ist nicht einmal partitioniert. Die vier Partitionen sind:

  1. /boot, verwendet den U-Boot Bootloader
  2. Root-FS / für normalen Betrieb
  3. Root-FS / für “Recovery” – einfach ein Minimalsystem das die SD-Karte updaten kann
  4. /data, Ablage von Konfigurations- und Nutzerdaten mittels OverlayFS

Das verwendete Linux ist ein radikal abgespecktes Debian-Derivat, im Endeffekt läuft einfach nur ein nginx, ein mosquitto, und eine Reihe Java-basierte Server für die Homepilot Funktionalität. Nicht viel anders als z.B. eine Hue Bridge.

Im Prinzip wird auch ein SSH Server (Dropbear) gestartet, doch ist dieser nicht im normalen Betrieb erreichbar, und der root Account hat eh kein Passwort. Etwas merkwürdig ist dies alles aber schon, dazu evtl. später mehr.

Betrieb

Die Bridge holt sich über DHCP eine IP und ist nach ein oder zwei Minuten im Netz (ausschließlich) per http zu erreichen. Das Web-UI zeigt zu Anfang einige Fehlermeldungen, nach der Registrierung eines Gurtwicklers verschwanden diese aber.

Zuerst dachte ich, man braucht unbedingt die Mobile App um die Bridge zu betreiben, da dies in der Schnellstart-Doku so dargestellt ist. Die Homepilot App verlangt aber eine Anmeldung mit lauter persönlichen Daten und unter Abzeichnung einer ewig langen AGB, das ist jetzt echt nicht cool von Rademacher, da mache ich nicht mit, ich will deren Cloud doch überhaupt nicht verwenden. Die Bridge funktioniert auch ohne die App problemlos.

Anmelden der Gurtwickler geht problemlos, der Zustand ist in der Web-UI zu sehen und die Geräte auch von dort zu steuern. Zu meiner Überraschung und Freude stellt sich das Protokoll der Web-UI zwischen Browser und Server als einfache REST-API ohne Verschlüsselung oder andere Komplikationen dar. Sehr gut! \o/

Abfrage des Zustands aller Geräte, die aktuelle Position des Rolladens ist in “statusesMap.Position”:

GET http://192.168.1.100/v4/devices?devtype=Actuator
->
{"response":"get_visible_devices","devices":[{"description":"Ihre Gerätebeschreibung","deviceGroup":2,"did":1,"hasErrors":0,"iconSetInverted":0,"iconSet":{"k":"iconset15"},"messages":[],"name":"Gurtwickler","properties":{"closingContact":3,"dawn":3,"dusk":3,"motion":3,"rain":3,"smartphone":3,"smoke":3,"sun":3,"temperature":3,"time":3,"trigger":3,"warning":3,"wind":3},"statusValid":true,"statusesMap":{"Manuellbetrieb":0,"Position":100},"visible":true,"deviceNumber":"12345","uid":"12345_1","voiceControlledBy":"Alexa,Google","origin":"HomePilot"}]}

Um einen Gurtwickler z.B. hoch zu fahren wird dieser Befehl verwendet:

{"name":"POS_UP_CMD"}
->
PUT http://192.168.1.100/devices/1

Das lässt sich alles gut in den Chrome Developer Tools nachvollziehen. Die Web-UI pollt dabei alle 5 Sekunden den Zustand aller Geräte.

Automatisierung mit Node-Red

Ich bin seit einiger Zeit dabei meine Heimautomatisierung auf Node-Red umzustellen. Aufgrund der einfachen API ist das Einbinden der DuoFern Bridge einfach, z.B.:

Der HTTP Request ist schlicht ein GET auf die o.g. URL, dann wird payload.devices extrahiert, und nach dem Split topic auf die JSONata Expression "node-red/state/shutter/" & payload.name und payload auf payload.statusesMap.Position gesetzt.

Kompletter Flow zum Import:

[{"id":"c8b52443.ebd668","type":"http request","z":"7c7fbbe3.c93334","name":"get Homepilot Actuators","method":"GET","ret":"obj","paytoqs":"ignore","url":"http://192.168.5.84/v4/devices?devtype=Actuator","tls":"","persist":true,"proxy":"","authType":"","x":330,"y":100,"wires":[["554cb107.e498b"]]},{"id":"197a0837.8f5378","type":"inject","z":"7c7fbbe3.c93334","name":"every 10s","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"10","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":130,"y":100,"wires":[["c8b52443.ebd668"]]},{"id":"fbfbe102.faed","type":"split","z":"7c7fbbe3.c93334","name":"","splt":"\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":690,"y":100,"wires":[["54561ee1.d4a4a"]]},{"id":"554cb107.e498b","type":"change","z":"7c7fbbe3.c93334","name":"extract devices","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.devices","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":540,"y":100,"wires":[["fbfbe102.faed"]]},{"id":"54561ee1.d4a4a","type":"change","z":"7c7fbbe3.c93334","name":"extract name and position","rules":[{"t":"set","p":"topic","pt":"msg","to":"\"node-red/state/shutter/\" & payload.name","tot":"jsonata"},{"t":"set","p":"payload","pt":"msg","to":"payload.statusesMap.Position","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":180,"wires":[["2dadb18f.485a5e"]]},{"id":"2dadb18f.485a5e","type":"rbe","z":"7c7fbbe3.c93334","name":"changed?","func":"rbe","gap":"","start":"","inout":"out","property":"payload","x":720,"y":180,"wires":[["8e95cd87.c3616"]]},{"id":"b980972.e76f068","type":"mqtt out","z":"7c7fbbe3.c93334","name":"retain in mqtt","topic":"","qos":"0","retain":"true","broker":"15113c55.7e1ad4","x":990,"y":180,"wires":[]},{"id":"8e95cd87.c3616","type":"json","z":"7c7fbbe3.c93334","name":"","property":"payload","action":"str","pretty":false,"x":850,"y":180,"wires":[["b980972.e76f068"]]},{"id":"15113c55.7e1ad4","type":"mqtt-broker","name":"mqtt","broker":"192.168.5.55","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"node-red/status","birthQos":"0","birthRetain":"true","birthPayload":"online","closeTopic":"node-red/status","closeQos":"0","closeRetain":"true","closePayload":"offline","willTopic":"node-red/status","willQos":"0","willRetain":"true","willPayload":"offline"}]

Zum Steuern wird der entsprechende Befehl per HTTP PUT gesendet, das lässt sich leicht testen:

Beispiel-Flow:

[{"id":"9a664c2c.2850f","type":"inject","z":"7c7fbbe3.c93334","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":360,"wires":[["73147993.9bb628"]]},{"id":"82bdbdfd.1611a","type":"http request","z":"7c7fbbe3.c93334","name":"-> Rolladen Kinderzimmer","method":"PUT","ret":"txt","paytoqs":"ignore","url":"http://192.168.5.84/devices/1","tls":"","persist":false,"proxy":"","authType":"","x":550,"y":400,"wires":[[]]},{"id":"73147993.9bb628","type":"change","z":"7c7fbbe3.c93334","name":"POS_DOWN_CMD","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"name\":\"POS_DOWN_CMD\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":310,"y":360,"wires":[["82bdbdfd.1611a"]]},{"id":"90dea158.bf2be","type":"change","z":"7c7fbbe3.c93334","name":"POS_UP_CMD","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"name\":\"POS_UP_CMD\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":300,"y":400,"wires":[["82bdbdfd.1611a"]]},{"id":"9801853d.7dc9c8","type":"change","z":"7c7fbbe3.c93334","name":"STOP_CMD","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"name\":\"STOP_CMD\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":290,"y":440,"wires":[["82bdbdfd.1611a"]]},{"id":"7444ae38.d4d86","type":"inject","z":"7c7fbbe3.c93334","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":400,"wires":[["90dea158.bf2be"]]},{"id":"54598784.398fa8","type":"inject","z":"7c7fbbe3.c93334","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":440,"wires":[["9801853d.7dc9c8"]]}]

Das ganze habe ich in mein Dashboard eingebunden, dazu habe ich im Netz einen interessanten Trick gefunden den Rolladen-Zustand mit einem SVG dynamisch darzustellen, und dies mit einem Popup-Kontextmenu verbunden, hier z.B. ganz geschlossen und ich habe das Symbol angetippt um das Menu zu öffnen:

Der entsprechende Flow basiert auf dem oben generierten MQTT Status, und sehr viel HTML/CSS/JS/SVG:

Kompletter Flow zum Import in Node Red:

[{"id":"ecf7b07a.89c4d","type":"ui_template","z":"c84d2e4e.6fecf","group":"915ba942.2c5bf8","name":"Rolladen","order":2,"width":1,"height":1,"format":"<div class=\"rollade__container\" ng-click=\"send({x:$event.clientX,y:$event.clientY})\">\n    <svg class=\"rollade__fenster\" width=\"100%\" height=\"100%\" viewBox=\"0 0 170 188\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xml:space=\"preserve\" xmlns:serif=\"http://www.serif.com/\" style=\"fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;\">\n        <g transform=\"matrix(1,0,0,1,-15.909,-45.6212)\">\n            <g transform=\"matrix(0.989904,0,0,0.992491,4.03539,0.837446)\">\n                <path d=\"M180.329,56.389C180.329,51.839 176.626,48.145 172.064,48.145L23.291,48.145C18.729,48.145 15.025,51.839 15.025,56.389L15.025,222.454C15.025,227.004 18.729,230.698 23.291,230.698L172.064,230.698C176.626,230.698 180.329,227.004 180.329,222.454L180.329,56.389Z\" />\n            </g>\n            <g transform=\"matrix(1,0,0,1.38253,3.04599,-37.9811)\">\n                <path d=\"M97.589,64.432L97.589,191.286\" />\n            </g>\n            <g transform=\"matrix(1,0,0,1,2.84135,56.1777)\">\n                <path d=\"M18.74,83.189L177.463,84.189\" />\n            </g>\n        </g>\n    </svg>\n    <svg class=\"rollade__rollade\" style=\"fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;top: calc((-1) * {{msg.payload}}%);\" width=\"100%\" height=\"100%\" viewBox=\"0 0 181 190\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xml:space=\"preserve\" xmlns:serif=\"http://www.serif.com/\">\n        <g transform=\"matrix(1.35281,0,0,1.35281,-45.3041,-102.109)\">\n            <path class=\"bg\" d=\"M167.134,82.161C167.134,78.473 164.14,75.479 160.452,75.479L40.171,75.479C36.483,75.479 33.489,78.473 33.489,82.161L33.489,208.93C33.489,212.618 36.483,215.612 40.171,215.612L160.452,215.612C164.14,215.612 167.134,212.618 167.134,208.93L167.134,82.161Z\"/>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,-9)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,1)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,11)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,21)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,31)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,41)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,51)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,61)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,71)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,81)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,91)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,101)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n            <g transform=\"matrix(1.69574,0,0,1,18.3547,111)\">\n                <path class=\"line\" d=\"M13.383,95.652L84.46,95.652\" />\n            </g>\n        </g>\n    </svg>\n</div>","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":false,"templateScope":"local","x":820,"y":2480,"wires":[["1d646b43.9bcf25"]]},{"id":"31af164f.9f07ca","type":"mqtt in","z":"c84d2e4e.6fecf","name":"","topic":"node-red/state/shutter/Kinderzimmer Seitenfenster","qos":"2","datatype":"auto","broker":"15113c55.7e1ad4","x":250,"y":2480,"wires":[["6ce5e2fb.d46b6c"]]},{"id":"4ddbec36.18dfa4","type":"change","z":"c84d2e4e.6fecf","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"100 - payload","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":660,"y":2480,"wires":[["ecf7b07a.89c4d"]]},{"id":"6ce5e2fb.d46b6c","type":"json","z":"c84d2e4e.6fecf","name":"","property":"payload","action":"obj","pretty":false,"x":510,"y":2480,"wires":[["4ddbec36.18dfa4"]]},{"id":"a643ae90.71294","type":"ui_template","z":"c84d2e4e.6fecf","group":"c0e1c6ff.af3868","name":"Rolladen Style","order":3,"width":0,"height":0,"format":"<style>\n        :root {\n            --rollade-color: #097479;\n            --rollade-width: 32px;\n        }\n        .rollade__container {\n            width: var(--rollade-width);\n            overflow: hidden;\n            position: relative;\n        }\n        .rollade__fenster {\n            position: relative;\n            overflow: hidden;\n            z-index: 1;\n        }\n        .rollade__fenster path {\n            stroke: var(--rollade-color);\n            fill:none!important;\n            stroke-width:6px;\n        }\n        .rollade__rollade {\n            position: absolute;\n            width: 100%;\n            height: 100%;\n            left: 0px;\n            z-index: 10;\n            transition: all 1s;\n        }\n        .rollade__rollade path.line {\n            stroke: white;\n            fill: none;\n            stroke-width: 1.5px;\n        }\n        .rollade__rollade path.bg {\n            fill: var(--rollade-color);\n        }\n        \n</style>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"global","x":140,"y":2440,"wires":[[]]},{"id":"1d646b43.9bcf25","type":"ui_context_menu","z":"c84d2e4e.6fecf","group":"915ba942.2c5bf8","order":5,"width":0,"height":-1,"fontSize":"20","inputPositionXField":"x","inputPositionXType":"msg","inputPositionYField":"y","inputPositionYType":"msg","outputField":"payload","inputMenuField":"menu","inputMenuType":"fixed","menuItems":[{"id":"","icon":"","label":"auf","topic":"","payload":"POS_UP_CMD","payloadType":"str","visible":true,"enabled":true},{"id":"","icon":"","label":"zu","topic":"","payload":"POS_DOWN_CMD","payloadType":"str","visible":true,"enabled":true},{"id":"","icon":"","label":"Stop","topic":"","payload":"STOP_CMD","payloadType":"str","visible":true,"enabled":true}],"colors":"theme","textColor":"#000000","backgroundColor":"#ffffff","borderColor":"#626262","intervalLength":"10","intervalUnit":"secs","startTimerAtOpen":true,"startTimerAtLeave":false,"stopTimerAtEnter":false,"name":"","x":440,"y":2540,"wires":[["d727a854.e7e268"]]},{"id":"d727a854.e7e268","type":"change","z":"c84d2e4e.6fecf","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"name\":payload}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":640,"y":2540,"wires":[["77911e99.f0cf9"]]},{"id":"77911e99.f0cf9","type":"http request","z":"c84d2e4e.6fecf","name":"-> Rolladen Kinderzimmer","method":"PUT","ret":"txt","paytoqs":"ignore","url":"http://192.168.5.84/devices/1","tls":"","persist":false,"proxy":"","authType":"","x":870,"y":2540,"wires":[[]]},{"id":"915ba942.2c5bf8","type":"ui_group","name":"OG: Kinder","tab":"f36ba877.ed9c98","order":5,"disp":true,"width":6,"collapse":false},{"id":"15113c55.7e1ad4","type":"mqtt-broker","name":"mqtt","broker":"192.168.5.55","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"node-red/status","birthQos":"0","birthRetain":"true","birthPayload":"online","closeTopic":"node-red/status","closeQos":"0","closeRetain":"true","closePayload":"offline","willTopic":"node-red/status","willQos":"0","willRetain":"true","willPayload":"offline"},{"id":"c0e1c6ff.af3868","type":"ui_group","name":"Verschiedenes","tab":"a517c876.a6dfc8","order":1,"disp":true,"width":"6","collapse":false},{"id":"f36ba877.ed9c98","type":"ui_tab","name":"Fenster","icon":"mi-business","order":3,"disabled":false,"hidden":false},{"id":"a517c876.a6dfc8","type":"ui_tab","name":"Home","icon":"mi-home","order":1,"disabled":false,"hidden":false}]