Node-redでEchonetlite単機能照明（Panasonic アドバンスリンクプラス）を制御する

:::note
[改良版](https://qiita.com/fiddler/items/e8c80ea5f08a09188ca6)を投稿しました。
:::

#前提・謝辞
この記事では@fujitaさんの[node-red用の送受信node](https://qiita.com/fujitah/items/ef9b6a6cd0bc331bce6b)を利用しています。
[アドバンスリンクプラス](https://www2.panasonic.biz/ls/densetsu/haisen/switch_concent/advance/lineup/linkplus/)の施工には電気工事士の資格が必要です。
無線アダプタのipアドレスは固定する必要があります。
電文の理解は@miyazawa_shiさんの[記事](https://qiita.com/miyazawa_shi/items/725bc5eb6590be72970d)を参照させていただきました。
node-redでのスイッチとしてdashboardを利用しています。
#フロー
![スクリーンショット 2021-10-31 2021-10-31(日)13.04.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2231449/b9d9e17e-8b55-2f8f-8779-5dd1ac26c9a2.png)
初回記事からアップデートしています。
#やっていること
dashboardスイッチのon/offに応じてEDTとデバイスのインスタンス設定、さらにipアドレス等共通のパラメータを設定してudp送信しています。
スイッチの状態が変化するとudpでアナウンスされますのでそれを受信してインスタンス毎に分岐させてdashboardスイッチを変えています。
~~dashboardスイッチのon/offを繰り返すと一時的に状態がずれてしまうので受信したかどうかを判定してループしないようにしてあります。~~
→passthroughしない設定にしました。
injectを利用してスイッチの状態を最初に反映させるようにしてあります。
#はまった点
無線アダプタが単機能照明(0291)であることに気づかず最初一般照明(0290)で動かないと悩んでいました。
無線アダプタはSetC(61)で電文を作ると、一旦今の状態をアナウンスする
例；onなら一回offをudp送信してから変更後のoff送信
してしまうみたいなのでSetI(60)を利用しています。
#Flow json
```
[{"id":"e6bad3a9.e94f6","type":"tab","label":"echonet lite","disabled":false,"info":""},{"id":"ba456875.11dc7","type":"function","z":"e6bad3a9.e94f6","name":"Parse EL1","func":"// Parse ECHONET Lite packet\n// Assumption: OPC = 1\n\n//  OUTPUT:\n//  msg.elr.ip      // ip address (String)\n//  msg.elr.ehd     // EHD (Int)\n//  msg.elr.tid     // TID (Int)\n//  msg.elr.seoj    // SEOJ ([Int:device, Int:instance])\n//  msg.elr.deoj    // DEOJ ([Int:device, Int:instance])\n//  msg.elr.esv     // ESV (Int)\n//  msg.elr.opc     // OPC (Int)\n//  msg.elr.epc     // EPC (Int)\n//  msg.elr.pdc     // PDC (Int)\n//  msg.elr.edt     // EDT ([Int]) or [](no EDT data)\n\nconst buffer = msg.payload;\nmsg.elr = null;\n\nif (buffer.length >= 14) {\n  const ehd_buffer = buffer.slice(0, 2);\n  const tid_buffer = buffer.slice(2, 4);\n  const seoj_device_buffer = buffer.slice(4, 6);\n  const seoj_instance_buffer = buffer.slice(6, 7);\n  const deoj_device_buffer = buffer.slice(7, 9);\n  const deoj_instance_buffer = buffer.slice(9, 10);\n  const esv_buffer = buffer.slice(10, 11);\n  const opc_buffer = buffer.slice(11, 12);\n  const epc_buffer = buffer.slice(12, 13);\n  const pdc_buffer = buffer.slice(13, 14);\n\n  const ehd = ehd_buffer.readUInt16BE(0);\n  const tid = tid_buffer.readUInt16BE(0);\n  const seoj_device = seoj_device_buffer.readUInt16BE(0);\n  const seoj_instance = seoj_instance_buffer.readUInt8(0);\n  const seoj = [seoj_device, seoj_instance];\n  const deoj_device = deoj_device_buffer.readUInt16BE(0);\n  const deoj_instance = deoj_instance_buffer.readUInt8(0);\n  const deoj = [deoj_device, deoj_instance];\n  const esv = esv_buffer.readUInt8(0);\n  const opc = opc_buffer.readUInt8(0);\n  const epc = epc_buffer.readUInt8(0);\n  const pdc = pdc_buffer.readUInt8(0);\n  let edt = [];\n  \n  if (buffer.length >= 15) {\n    const edt_buffer = buffer.slice(14);\n    for (var i = 0; i < edt_buffer.length; i++) {\n        edt[i] = edt_buffer.readUInt8(i);\n    }\n  }\n\n  // ECHONET Lite header check\n  if (ehd == 0x1081) {\n    msg.elr = {\n        \"ip\"            : msg.ip,\n        \"ehd\"           : ehd,\n        \"tid\"           : tid,\n        \"seoj\"          : seoj,\n        \"deoj\"          : deoj,\n        \"esv\"           : esv,\n        \"opc\"           : opc,\n        \"epc\"           : epc,\n        \"pdc\"           : pdc,\n        \"edt\"           : edt\n    };\n  }\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":270,"y":480,"wires":[["109f7084.9e5a9f"]]},{"id":"109f7084.9e5a9f","type":"function","z":"e6bad3a9.e94f6","name":"filter lightStatus","func":"//  msg.elr.ip      // ip address (String)\n//  msg.elr.ehd     // EHD (Int)\n//  msg.elr.tid     // TID (Int)\n//  msg.elr.seoj    // SEOJ ([Int:device, Int:instance])\n//  msg.elr.deoj    // DEOJ ([Int:device, Int:instance])\n//  msg.elr.esv     // ESV (Int)\n//  msg.elr.opc     // OPC (Int)\n//  msg.elr.epc     // EPC (Int)\n//  msg.elr.pdc     // PDC (Int)\n//  msg.elr.edt     // EDT ([Int]) or [](no EDT data)\n\n// 照明 EOJ=0x0291, 電源状態 EPC=0x80, off = 0x31 on = 0x30\nif ((msg.elr.seoj[0] == 0x0291) && (msg.elr.epc == 0x80)) {\n    msg.payload = Boolean (msg.elr.edt[0]-0x31);\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":440,"y":480,"wires":[["56532baf.7e954c"]]},{"id":"bb3b2488.e0bd98","type":"udp in","z":"e6bad3a9.e94f6","name":"EL receive","iface":"","port":"3610","ipv":"udp4","multicast":"true","group":"224.0.23.0","datatype":"buffer","x":100,"y":480,"wires":[["ba456875.11dc7"]]},{"id":"56532baf.7e954c","type":"switch","z":"e6bad3a9.e94f6","name":"SW sorting","property":"elr.seoj[1]","propertyType":"msg","rules":[{"t":"eq","v":"1","vt":"num"},{"t":"eq","v":"2","vt":"num"},{"t":"eq","v":"3","vt":"str"},{"t":"eq","v":"4","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":5,"x":110,"y":180,"wires":[["f60a8e89.5dde78"],["8d958ea2.4c9008"],["ca063506.0e609"],["8f705f8f.e355d"],[]]},{"id":"f60a8e89.5dde78","type":"ui_switch","z":"e6bad3a9.e94f6","name":"","label":"switch","tooltip":"","group":"f13025dc.ef0068","order":1,"width":0,"height":0,"passthru":false,"decouple":"true","topic":"topic","topicType":"msg","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"x":310,"y":60,"wires":[["81d64817.8d0ee8"]]},{"id":"81d64817.8d0ee8","type":"change","z":"e6bad3a9.e94f6","name":"EL set","rules":[{"t":"change","p":"payload","pt":"msg","from":"true","fromt":"bool","to":"0x30","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"false","fromt":"bool","to":"0x31","tot":"str"},{"t":"set","p":"els.num","pt":"msg","to":"1","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":60,"wires":[["5e771cab.ba816c"]]},{"id":"5e771cab.ba816c","type":"function","z":"e6bad3a9.e94f6","name":"EL set","func":"//  msg.els.ip_string   // ip address (String)\n//  msg.els.tid         // TID (Int)\n//  msg.els.seoj        // SEOJ ([Int:device, Int:instance])\n//  msg.els.deoj        // DEOJ ([Int:device, Int:instance])\n//  msg.els.esv         // ESV (Int)\n//  msg.els.epc         // EPC (Int)\n//  msg.els.edt_int     // EDT ([Int])\n\nmsg.els = {\n    \"ip\" : \"xxx.xxx.xxx.xxx\",\n    \"tid\" : 0,\n    \"seoj\" : [0x05ff,1],\n    \"deoj\" : [0x0291,msg.els.num],\n    \"esv\" : 0x60,\n    \"epc\" : 0x80,\n    \"edt\" : [msg.payload]\n};\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":90,"y":420,"wires":[["822bb038.6749b"]]},{"id":"84a2a9e4.cbc818","type":"udp out","z":"e6bad3a9.e94f6","name":"","addr":"","iface":"","port":"3610","ipv":"udp4","outport":"","base64":false,"multicast":"multi","x":420,"y":420,"wires":[]},{"id":"822bb038.6749b","type":"function","z":"e6bad3a9.e94f6","name":"Send EL1","func":"// Send ECHONET Lite packet\n// Assumption: OPC = 1\n//\n//  Usage:\n//      Input following data\n//\n//  msg.els.ip      // ip address (String)\n//  msg.els.tid     // TID (Int)\n//  msg.els.seoj    // SEOJ ([Int:device, Int:instance])\n//  msg.els.deoj    // DEOJ ([Int:device, Int:instance])\n//  msg.els.esv     // ESV (Int)\n//  msg.els.epc     // EPC (Int)\n//  msg.els.edt     // EDT ([Int])\n//          // in case of no EDT data, no edt or []\n\nconst tid_buffer = Buffer.allocUnsafe(2);\nlet   seoj_buffer = Buffer.allocUnsafe(3);\nconst seoj = msg.els.seoj;\nlet   deoj_buffer = Buffer.allocUnsafe(3);\nconst deoj = msg.els.deoj;\nconst esv_buffer = Buffer.allocUnsafe(1);\nconst opc_buffer = Buffer.from([1]);\nconst epc_buffer = Buffer.allocUnsafe(1);\nconst pdc_buffer = Buffer.allocUnsafe(1);\nlet edt = msg.els.edt;\n\ntid_buffer.writeUInt16BE(msg.els.tid, 0);\nesv_buffer.writeUInt8(msg.els.esv, 0);\nepc_buffer.writeUInt8(msg.els.epc, 0);\n\nseoj_buffer.writeUInt16BE(seoj[0], 0);\nseoj_buffer.writeUInt8(seoj[1], 2);\ndeoj_buffer.writeUInt16BE(deoj[0], 0);\ndeoj_buffer.writeUInt8(deoj[1], 2);\n\n// edt[]からedt_bufferを作成\nif ((typeof edt) == \"undefined\") {\n    edt = [];\n}\n\nconst edt_buffer = Buffer.allocUnsafe(edt.length);\nfor (var i = 0; i < edt.length; i++) {\n    edt_buffer.writeUInt8(edt[i], i);\n}\n\npdc_buffer.writeUInt8(edt_buffer.length, 0);\n\n// Create ECHONET Lite packet\nconst el_buffer = Buffer.from([\n\t0x10, 0x81,\n\ttid_buffer[0], tid_buffer[1],\n\tseoj_buffer[0], seoj_buffer[1], seoj_buffer[2],\n\tdeoj_buffer[0], deoj_buffer[1], deoj_buffer[2],\n\tesv_buffer[0], opc_buffer[0], epc_buffer[0], pdc_buffer[0]]);\n\n// add EDT data\nif (edt_buffer.length !== 0) {\n    msg.payload = Buffer.concat([el_buffer, edt_buffer], \n        (el_buffer.length + edt_buffer.length));\n} else {\n    msg.payload = el_buffer;\n}\n\nmsg.ip = msg.els.ip;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":260,"y":420,"wires":[["84a2a9e4.cbc818"]]},{"id":"8d958ea2.4c9008","type":"ui_switch","z":"e6bad3a9.e94f6","name":"","label":"switch","tooltip":"","group":"f13025dc.ef0068","order":1,"width":0,"height":0,"passthru":false,"decouple":"true","topic":"topic","topicType":"msg","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"x":310,"y":140,"wires":[["f8bf218e.e14e88"]]},{"id":"f8bf218e.e14e88","type":"change","z":"e6bad3a9.e94f6","name":"EL set","rules":[{"t":"change","p":"payload","pt":"msg","from":"true","fromt":"bool","to":"0x30","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"false","fromt":"bool","to":"0x31","tot":"str"},{"t":"set","p":"els.num","pt":"msg","to":"2","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":140,"wires":[["5e771cab.ba816c"]]},{"id":"ca063506.0e609","type":"ui_switch","z":"e6bad3a9.e94f6","name":"","label":"switch","tooltip":"","group":"f13025dc.ef0068","order":1,"width":0,"height":0,"passthru":false,"decouple":"true","topic":"topic","topicType":"msg","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"x":310,"y":220,"wires":[["5d2b6442.840274"]]},{"id":"5d2b6442.840274","type":"change","z":"e6bad3a9.e94f6","name":"EL set","rules":[{"t":"change","p":"payload","pt":"msg","from":"true","fromt":"bool","to":"0x30","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"false","fromt":"bool","to":"0x31","tot":"str"},{"t":"set","p":"els.num","pt":"msg","to":"3","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":220,"wires":[["5e771cab.ba816c"]]},{"id":"8f705f8f.e355d","type":"ui_switch","z":"e6bad3a9.e94f6","name":"","label":"switch","tooltip":"","group":"f13025dc.ef0068","order":1,"width":0,"height":0,"passthru":false,"decouple":"true","topic":"topic","topicType":"msg","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"x":310,"y":300,"wires":[["d94cf168.c9a7c"]]},{"id":"d94cf168.c9a7c","type":"change","z":"e6bad3a9.e94f6","name":"EL set","rules":[{"t":"change","p":"payload","pt":"msg","from":"true","fromt":"bool","to":"0x30","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"false","fromt":"bool","to":"0x31","tot":"str"},{"t":"set","p":"els.num","pt":"msg","to":"4","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":300,"wires":[["5e771cab.ba816c"]]},{"id":"4692771.0852108","type":"inject","z":"e6bad3a9.e94f6","name":"init","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"","topic":"","payload":"","payloadType":"date","x":90,"y":540,"wires":[["bf42422a.6d8bd"]]},{"id":"bf42422a.6d8bd","type":"function","z":"e6bad3a9.e94f6","name":"EL get","func":"//  msg.els.ip_string   // ip address (String)\n//  msg.els.tid         // TID (Int)\n//  msg.els.seoj        // SEOJ ([Int:device, Int:instance])\n//  msg.els.deoj        // DEOJ ([Int:device, Int:instance])\n//  msg.els.esv         // ESV (Int)\n//  msg.els.epc         // EPC (Int)\n//  msg.els.edt_int     // EDT ([Int])\n\nmsg.els = {\n    \"ip\" : \"xxx.xxx.xxx.xxx\",\n    \"tid\" : 5,\n    \"seoj\" : [0x05ff,1],\n    \"deoj\" : [0x0291,0],\n    \"esv\" : 0x62,\n    \"epc\" : 0x80,\n    \"edt\" : [0x30]\n};\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":270,"y":540,"wires":[["822bb038.6749b"]]},{"id":"f13025dc.ef0068","type":"ui_group","z":"","name":"buttons","tab":"67ec7f3f.c65bb8","order":6,"disp":false,"width":1,"collapse":false},{"id":"67ec7f3f.c65bb8","type":"ui_tab","z":"","name":"ホーム","icon":"dashboard","disabled":false,"hidden":false}]
```